From a64fa9770f2a8851cd3a382f30847178b5456a61 Mon Sep 17 00:00:00 2001 From: WorldTeacher Date: Wed, 3 Sep 2025 12:32:05 +0200 Subject: [PATCH 01/40] refactor searchBook to allow for regex matches --- src/backend/database.py | 81 +++++++++++++++++++++++++---------------- 1 file changed, 50 insertions(+), 31 deletions(-) diff --git a/src/backend/database.py b/src/backend/database.py index e2e5d03..32b60c6 100644 --- a/src/backend/database.py +++ b/src/backend/database.py @@ -1,6 +1,7 @@ import datetime import json import os +import re import sqlite3 as sql import sys import tempfile @@ -382,49 +383,67 @@ class Database: """ return self.query_db("SELECT id FROM media ORDER BY id DESC", one=True)[0] - def searchBook(self, data: dict[str, str]) -> list[tuple[BookData, int]]: + def searchBook( + self, data: dict[str, str] + ) -> Optional[list[tuple["BookData", int, int]]]: """ - Search a book in the database based on the sent data. + Search a book in the database using regex against signature/title. Args: - data (dict[str, str]): A dictionary containing the data to be searched for. The dictionary can contain the following: - - signature: The signature of the book - - title: The title of the book + data: may contain: + - "signature": regex to match against BookData.signature + - "title": regex to match against BookData.title Returns: - list[tuple[BookData, int]]: A list of tuples containing the wrapped Metadata and the id of the book + list of (BookData, app_id, prof_id) tuples, or None if invalid args """ - rdata = self.query_db("SELECT * FROM media WHERE deleted=0") - # log.debug(rdata, len(rdata)) + + # Determine mode (kept compatible with your original logic) mode = 0 - if len(data) == 1: - if "signature" in data.keys(): - mode = 1 - elif "title" in data.keys(): - mode = 2 - elif len(data) == 2: + if len(data) == 1 and "signature" in data: + mode = 1 + elif len(data) == 1 and "title" in data: + mode = 2 + elif len(data) == 2 and "signature" in data and "title" in data: mode = 3 else: return None - ret = [] - for book in rdata: - bookdata = BookData().from_string(book[1]) - app_id = book[2] - prof_id = book[3] + + def _compile(expr: str) -> re.Pattern: + try: + return re.compile(expr, re.IGNORECASE | re.UNICODE) + except re.error: + # If user provided a broken regex, treat it as a literal + return re.compile(re.escape(expr), re.IGNORECASE | re.UNICODE) + + sig_re = _compile(data["signature"]) if mode in (1, 3) else None + title_re = _compile(data["title"]) if mode in (2, 3) else None + + # Fetch candidates once + rows = self.query_db("SELECT * FROM media WHERE deleted=0") + + results: list[tuple["BookData", int, int]] = [] + for row in rows: + bookdata = BookData().from_string( + row[1] + ) # assumes row[1] is the serialized bookdata + app_id = row[2] + prof_id = row[3] + + sig_val = getattr(bookdata, "signature", None) or "" + title_val = getattr(bookdata, "title", None) or "" + if mode == 1: - if data["signature"] in bookdata.signature: - ret.append((bookdata, app_id, prof_id)) + if sig_re.search(sig_val): + results.append((bookdata, app_id, prof_id)) elif mode == 2: - if data["title"] in bookdata.title: - ret.append((bookdata, app_id, prof_id)) - elif mode == 3: - if ( - data["signature"] in bookdata.signature - and data["title"] in bookdata.title - ): - ret.append((bookdata, app_id, prof_id)) - # log.debug(ret) - return ret + if title_re.search(title_val): + results.append((bookdata, app_id, prof_id)) + else: # mode == 3 + if sig_re.search(sig_val) and title_re.search(title_val): + results.append((bookdata, app_id, prof_id)) + + return results def setAvailability(self, book_id: str, available: str): """ -- 2.49.1 From b577a69dadc8b7c38dcaaaf6365e48b072a55d2e Mon Sep 17 00:00:00 2001 From: WorldTeacher Date: Wed, 3 Sep 2025 12:32:42 +0200 Subject: [PATCH 02/40] add a progressbar to the statusbar which displays the search progress on searchstat page for signature update --- src/ui/userInterface.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/ui/userInterface.py b/src/ui/userInterface.py index c7847f3..ff40cf8 100644 --- a/src/ui/userInterface.py +++ b/src/ui/userInterface.py @@ -270,6 +270,9 @@ class Ui(Ui_Semesterapparat): self.player = QMediaPlayer() self.audio_output = QAudioOutput() + self.status_progress = QtWidgets.QProgressBar() + self.statusBar.addWidget(self.status_progress) + self.status_progress.hide() self.valid_check_semester.clicked.connect(self.display_valid_semester) # type:ignore def create_doc(self): @@ -370,6 +373,15 @@ class Ui(Ui_Semesterapparat): self.calendarWidget.setMessages([data]) self.calendarWidget.updateCells() + def status_bar_progress(self, current: int, total: int): + self.status_progress.setRange(0, total) + self.status_progress.setValue(current) + if current == total: + self.status_progress.hide() + self.status_progress.setValue(0) + else: + self.status_progress.show() + def tabW1_changed(self): if self.tabWidget.currentIndex() == 1: # Statistics stats_layout = self.search_statistics.layout() @@ -384,6 +396,7 @@ class Ui(Ui_Semesterapparat): statistics.apparat_open.connect(self.open_apparat) statistics.refreshSignal.connect(self.update_apparat_list) statistics.updateCalendar.connect(self.update_calendar) + statistics.status_update.connect(self.status_bar_progress) stats_layout.addWidget(statistics) # #log.debug("searchpage") @@ -1658,7 +1671,7 @@ class Ui(Ui_Semesterapparat): for row in range(self.tableWidget_apparat_media.rowCount()) ] # type: ignore prof_id = self.db.getProfId(self.profdata) # type: ignore - app_id = self.active_apparat + app_id = self.db.getId(self.app_name.text()) # type: ignore books: List[Tuple[int, BookData]] = [] for signature in signatures: book = self.db.getBookBasedOnSignature( @@ -1667,7 +1680,7 @@ class Ui(Ui_Semesterapparat): prof_id=prof_id, ) book_id = self.db.getBookIdBasedOnSignature( - self.active_apparat, + app_id, prof_id, signature, ) -- 2.49.1 From 373257864f5c972579ae8e0ff017699b3346a836 Mon Sep 17 00:00:00 2001 From: WorldTeacher Date: Wed, 3 Sep 2025 12:33:52 +0200 Subject: [PATCH 03/40] update searchpage, add updater thread and emit signal for progress bar add context menu and related actions --- src/ui/widgets/searchPage.py | 61 +++- .../search_statistic_page_ui.py | 340 +++++++----------- 2 files changed, 184 insertions(+), 217 deletions(-) diff --git a/src/ui/widgets/searchPage.py b/src/ui/widgets/searchPage.py index 6463419..814efdb 100644 --- a/src/ui/widgets/searchPage.py +++ b/src/ui/widgets/searchPage.py @@ -7,9 +7,10 @@ from PySide6.QtCore import Signal from src import LOG_DIR from src.backend import Database, Semester -from src.logic import Prof, custom_sort, sort_semesters_list +from src.logic import BookData, Prof, custom_sort, sort_semesters_list from src.ui.dialogs import ApparatExtendDialog, Mail_Dialog, ReminderDialog from src.ui.widgets import DataQtGraph, StatusWidget +from src.ui.widgets.signature_update import UpdaterThread from .widget_sources.search_statistic_page_ui import Ui_Dialog @@ -19,7 +20,6 @@ log.add(sys.stdout, level="INFO") log.add(f"{LOG_DIR}/application.log", rotation="1 MB", retention="10 days") - class MyComboBox(QtWidgets.QComboBox): def __init__(self, parent=None): super().__init__(parent) @@ -30,6 +30,7 @@ class SearchStatisticPage(QtWidgets.QDialog, Ui_Dialog): reloadSignal = Signal() refreshSignal = Signal() updateCalendar = Signal(int, list) + status_update = Signal(int, int) def __init__(self): log.info("SearchStatisticPage started") @@ -72,8 +73,54 @@ class SearchStatisticPage(QtWidgets.QDialog, Ui_Dialog): self.semester = Semester().value self.search_by_signature.returnPressed.connect(self.search_book) self.search_by_title.returnPressed.connect(self.search_book) + # add context menu to the book_search_result + self.book_search_result.setContextMenuPolicy( + QtCore.Qt.ContextMenuPolicy.CustomContextMenu + ) + self.book_search_result.customContextMenuRequested.connect( + self.book_search_result_context_menu + ) self.populate_tab() + def book_search_result_context_menu(self, position): + menu = QtWidgets.QMenu() + open_apparat_action = menu.addAction("Semesterapparat öffnen") + open_apparat_action.triggered.connect(self.open_selected_apparat) + update_books_action = menu.addAction("Bücher aktualisieren") + update_books_action.triggered.connect(self.update_books) + menu.exec(self.book_search_result.mapToGlobal(position)) + + def open_selected_apparat(self): + selected_rows = self.book_search_result.selectionModel().selectedRows() + if len(selected_rows) == 0: + return + row = selected_rows[0].row() + apparat_text = self.book_search_result.item(row, 2).text() + apparat_number = apparat_text.split(" (")[0] + self.apparat_open.emit(apparat_number) + + def update_books(self): + # update all books in the book_search_result + self.updater = UpdaterThread() + books = [] + for row in range(self.book_search_result.rowCount()): + signature = self.book_search_result.item(row, 1).text() + booksdata = self.db.query_db( + "SELECT id, bookdata from media WHERE bookdata LIKE ?", + (f'%"{signature}"%',), + one=True, + ) + books.append( + {"id": booksdata[0], "bookdata": BookData().from_string(booksdata[1])} + ) + self.updater.books = books # type: ignore + self.updater.currtot.connect(self.status_update_emit) + self.updater.finished.connect(self.updater.deleteLater) + self.updater.start() + + def status_update_emit(self, current: int, total: int): + self.status_update.emit(current, total) + def mass_extend_apparats(self): extend = ApparatExtendDialog() extend.exec() @@ -84,13 +131,13 @@ class SearchStatisticPage(QtWidgets.QDialog, Ui_Dialog): if self.tableWidget.cellWidget(i, 0).isChecked(): app_name = self.tableWidget.item(i, 1).text() app_id = self.db.getApparatId(app_name) - self.db.setNewSemesterDate(app_id, data["semester"], data["dauerapp"]) + self.db.setNewSemesterDate( + app_id, data["semester"], data["dauerapp"] + ) # remove the row self.tableWidget.removeRow(i) self.refreshSignal.emit() - - def restore_apparat(self): selected_rows = self.tableWidget.selectionModel().selectedRows() apparats = [] @@ -193,7 +240,7 @@ class SearchStatisticPage(QtWidgets.QDialog, Ui_Dialog): 2, QtWidgets.QTableWidgetItem( self.db.fetch_one( - "SELECT semesterapparat.appnr || ' (' || semesterapparat.name || ')' AS formatted_result from semesterapparat WHERE semesterapparat.appnr = ?", + "SELECT semesterapparat.appnr || ' (' || semesterapparat.name || ')' AS formatted_result from semesterapparat WHERE semesterapparat.id = ?", (book[1],), )[0], ), @@ -510,7 +557,7 @@ class SearchStatisticPage(QtWidgets.QDialog, Ui_Dialog): QtWidgets.QMessageBox.information( self, "Information", - f"Der Semesterapparat wurde bereits gelöscht und kann nicht angezeigt werden.", + "Der Semesterapparat wurde bereits gelöscht und kann nicht angezeigt werden.", ) if parent_depth == 1: # person selected case - open all apparats from this person in the tableWidget diff --git a/src/ui/widgets/widget_sources/search_statistic_page_ui.py b/src/ui/widgets/widget_sources/search_statistic_page_ui.py index 207dd58..8a49705 100644 --- a/src/ui/widgets/widget_sources/search_statistic_page_ui.py +++ b/src/ui/widgets/widget_sources/search_statistic_page_ui.py @@ -8,137 +8,126 @@ ## WARNING! All changes made in this file will be lost when recompiling UI file! ################################################################################ -from PySide6.QtCore import QCoreApplication, Qt -from PySide6.QtWidgets import ( - QAbstractItemView, - QCheckBox, - QComboBox, - QFrame, - QGridLayout, - QHBoxLayout, - QLabel, - QLayout, - QLineEdit, - QPushButton, - QSizePolicy, - QSpacerItem, - QStackedWidget, - QTableWidget, - QTableWidgetItem, - QTabWidget, - QVBoxLayout, - QWidget, -) - +from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale, + QMetaObject, QObject, QPoint, QRect, + QSize, QTime, QUrl, Qt) +from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor, + QFont, QFontDatabase, QGradient, QIcon, + QImage, QKeySequence, QLinearGradient, QPainter, + QPalette, QPixmap, QRadialGradient, QTransform) +from PySide6.QtWidgets import (QAbstractItemView, QApplication, QCheckBox, QComboBox, + QDialog, QFrame, QGridLayout, QHBoxLayout, + QHeaderView, QLabel, QLayout, QLineEdit, + QPushButton, QSizePolicy, QSpacerItem, QStackedWidget, + QTabWidget, QTableWidget, QTableWidgetItem, QVBoxLayout, + QWidget) class Ui_Dialog(object): def setupUi(self, Dialog): if not Dialog.objectName(): - Dialog.setObjectName("Dialog") + Dialog.setObjectName(u"Dialog") Dialog.resize(1244, 767) self.verticalLayout = QVBoxLayout(Dialog) - self.verticalLayout.setObjectName("verticalLayout") + self.verticalLayout.setObjectName(u"verticalLayout") self.tabWidget_2 = QTabWidget(Dialog) - self.tabWidget_2.setObjectName("tabWidget_2") + self.tabWidget_2.setObjectName(u"tabWidget_2") self.tabWidget_2.setMaximumSize(QSize(16777215, 250)) self.tabWidget_2.setFocusPolicy(Qt.ClickFocus) self.tabWidget_2.setTabPosition(QTabWidget.North) self.tabWidget_2.setTabShape(QTabWidget.Rounded) self.tab_3 = QWidget() - self.tab_3.setObjectName("tab_3") + self.tab_3.setObjectName(u"tab_3") self.horizontalLayout_2 = QHBoxLayout(self.tab_3) - self.horizontalLayout_2.setObjectName("horizontalLayout_2") + self.horizontalLayout_2.setObjectName(u"horizontalLayout_2") self.gridLayout_3 = QGridLayout() - self.gridLayout_3.setObjectName("gridLayout_3") + self.gridLayout_3.setObjectName(u"gridLayout_3") self.box_semester = QComboBox(self.tab_3) - self.box_semester.setObjectName("box_semester") + self.box_semester.setObjectName(u"box_semester") self.box_semester.setEditable(True) self.gridLayout_3.addWidget(self.box_semester, 0, 3, 1, 1) self.label_18 = QLabel(self.tab_3) - self.label_18.setObjectName("label_18") + self.label_18.setObjectName(u"label_18") self.gridLayout_3.addWidget(self.label_18, 2, 2, 1, 1) self.box_fach = QComboBox(self.tab_3) - self.box_fach.setObjectName("box_fach") + self.box_fach.setObjectName(u"box_fach") self.box_fach.setEditable(True) self.gridLayout_3.addWidget(self.box_fach, 2, 1, 1, 1) self.label_15 = QLabel(self.tab_3) - self.label_15.setObjectName("label_15") + self.label_15.setObjectName(u"label_15") self.gridLayout_3.addWidget(self.label_15, 3, 0, 1, 1) self.label_11 = QLabel(self.tab_3) - self.label_11.setObjectName("label_11") + self.label_11.setObjectName(u"label_11") self.gridLayout_3.addWidget(self.label_11, 1, 0, 1, 1) - self.verticalSpacer_3 = QSpacerItem( - 20, 40, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding - ) + self.verticalSpacer_3 = QSpacerItem(20, 40, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding) self.gridLayout_3.addItem(self.verticalSpacer_3, 4, 0, 1, 1) self.label_7 = QLabel(self.tab_3) - self.label_7.setObjectName("label_7") + self.label_7.setObjectName(u"label_7") self.gridLayout_3.addWidget(self.label_7, 0, 0, 1, 1) self.label_17 = QLabel(self.tab_3) - self.label_17.setObjectName("label_17") + self.label_17.setObjectName(u"label_17") self.gridLayout_3.addWidget(self.label_17, 0, 2, 1, 1) self.box_appnrs = QComboBox(self.tab_3) - self.box_appnrs.setObjectName("box_appnrs") + self.box_appnrs.setObjectName(u"box_appnrs") self.box_appnrs.setEditable(True) self.gridLayout_3.addWidget(self.box_appnrs, 0, 1, 1, 1) self.box_dauerapp = QComboBox(self.tab_3) - self.box_dauerapp.setObjectName("box_dauerapp") + self.box_dauerapp.setObjectName(u"box_dauerapp") self.gridLayout_3.addWidget(self.box_dauerapp, 2, 3, 1, 1) self.box_person = QComboBox(self.tab_3) - self.box_person.setObjectName("box_person") + self.box_person.setObjectName(u"box_person") self.box_person.setEditable(True) self.gridLayout_3.addWidget(self.box_person, 1, 1, 1, 1) self.box_erstellsemester = QComboBox(self.tab_3) - self.box_erstellsemester.setObjectName("box_erstellsemester") + self.box_erstellsemester.setObjectName(u"box_erstellsemester") self.box_erstellsemester.setEditable(True) self.gridLayout_3.addWidget(self.box_erstellsemester, 1, 3, 1, 1) self.label_19 = QLabel(self.tab_3) - self.label_19.setObjectName("label_19") + self.label_19.setObjectName(u"label_19") self.gridLayout_3.addWidget(self.label_19, 1, 2, 1, 1) self.label_16 = QLabel(self.tab_3) - self.label_16.setObjectName("label_16") + self.label_16.setObjectName(u"label_16") self.gridLayout_3.addWidget(self.label_16, 2, 0, 1, 1) self.check_deletable = QCheckBox(self.tab_3) - self.check_deletable.setObjectName("check_deletable") + self.check_deletable.setObjectName(u"check_deletable") self.check_deletable.setFocusPolicy(Qt.StrongFocus) self.gridLayout_3.addWidget(self.check_deletable, 3, 1, 1, 1) self.btn_search = QPushButton(self.tab_3) - self.btn_search.setObjectName("btn_search") + self.btn_search.setObjectName(u"btn_search") self.gridLayout_3.addWidget(self.btn_search, 5, 0, 1, 1) self.db_err_message = QLabel(self.tab_3) - self.db_err_message.setObjectName("db_err_message") + self.db_err_message.setObjectName(u"db_err_message") self.gridLayout_3.addWidget(self.db_err_message, 5, 1, 1, 1) @@ -150,9 +139,7 @@ class Ui_Dialog(object): self.horizontalLayout_2.addLayout(self.gridLayout_3) - self.horizontalSpacer = QSpacerItem( - 40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum - ) + self.horizontalSpacer = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum) self.horizontalLayout_2.addItem(self.horizontalSpacer) @@ -160,60 +147,58 @@ class Ui_Dialog(object): self.horizontalLayout_2.setStretch(1, 1) self.tabWidget_2.addTab(self.tab_3, "") self.tab_4 = QWidget() - self.tab_4.setObjectName("tab_4") + self.tab_4.setObjectName(u"tab_4") self.horizontalLayout_3 = QHBoxLayout(self.tab_4) - self.horizontalLayout_3.setObjectName("horizontalLayout_3") + self.horizontalLayout_3.setObjectName(u"horizontalLayout_3") self.gridLayout = QGridLayout() - self.gridLayout.setObjectName("gridLayout") + self.gridLayout.setObjectName(u"gridLayout") self.search_by_signature = QLineEdit(self.tab_4) - self.search_by_signature.setObjectName("search_by_signature") + self.search_by_signature.setObjectName(u"search_by_signature") self.search_by_signature.setFocusPolicy(Qt.ClickFocus) self.search_by_signature.setClearButtonEnabled(True) self.gridLayout.addWidget(self.search_by_signature, 0, 1, 1, 1) self.label_25 = QLabel(self.tab_4) - self.label_25.setObjectName("label_25") + self.label_25.setObjectName(u"label_25") self.gridLayout.addWidget(self.label_25, 0, 0, 1, 1) - self.verticalSpacer = QSpacerItem( - 20, 40, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding - ) + self.verticalSpacer = QSpacerItem(20, 40, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding) self.gridLayout.addItem(self.verticalSpacer, 5, 0, 1, 1) self.search_by_title = QLineEdit(self.tab_4) - self.search_by_title.setObjectName("search_by_title") + self.search_by_title.setObjectName(u"search_by_title") self.search_by_title.setFocusPolicy(Qt.ClickFocus) self.search_by_title.setClearButtonEnabled(True) self.gridLayout.addWidget(self.search_by_title, 1, 1, 1, 1) self.label_26 = QLabel(self.tab_4) - self.label_26.setObjectName("label_26") + self.label_26.setObjectName(u"label_26") self.gridLayout.addWidget(self.label_26, 1, 0, 1, 1) self.horizontalLayout_4 = QHBoxLayout() - self.horizontalLayout_4.setObjectName("horizontalLayout_4") + self.horizontalLayout_4.setObjectName(u"horizontalLayout_4") self.label = QLabel(self.tab_4) - self.label.setObjectName("label") + self.label.setObjectName(u"label") self.horizontalLayout_4.addWidget(self.label) self.no_result = QLabel(self.tab_4) - self.no_result.setObjectName("no_result") + self.no_result.setObjectName(u"no_result") self.horizontalLayout_4.addWidget(self.no_result) + self.gridLayout.addLayout(self.horizontalLayout_4, 3, 1, 1, 1) + self.horizontalLayout_3.addLayout(self.gridLayout) - self.horizontalSpacer_2 = QSpacerItem( - 40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum - ) + self.horizontalSpacer_2 = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum) self.horizontalLayout_3.addItem(self.horizontalSpacer_2) @@ -224,64 +209,59 @@ class Ui_Dialog(object): self.verticalLayout.addWidget(self.tabWidget_2) self.verticalLayout_3 = QVBoxLayout() - self.verticalLayout_3.setObjectName("verticalLayout_3") + self.verticalLayout_3.setObjectName(u"verticalLayout_3") self.verticalLayout_3.setSizeConstraint(QLayout.SetDefaultConstraint) self.stackedWidget_4 = QStackedWidget(Dialog) - self.stackedWidget_4.setObjectName("stackedWidget_4") - sizePolicy = QSizePolicy( - QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding - ) + self.stackedWidget_4.setObjectName(u"stackedWidget_4") + sizePolicy = QSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth( - self.stackedWidget_4.sizePolicy().hasHeightForWidth() - ) + sizePolicy.setHeightForWidth(self.stackedWidget_4.sizePolicy().hasHeightForWidth()) self.stackedWidget_4.setSizePolicy(sizePolicy) self.stackedWidget_4.setFrameShape(QFrame.StyledPanel) self.stackedWidget_4.setFrameShadow(QFrame.Raised) self.apparatResult = QWidget() - self.apparatResult.setObjectName("apparatResult") + self.apparatResult.setObjectName(u"apparatResult") self.horizontalLayout = QHBoxLayout(self.apparatResult) - self.horizontalLayout.setObjectName("horizontalLayout") + self.horizontalLayout.setObjectName(u"horizontalLayout") self.app_results = QWidget(self.apparatResult) - self.app_results.setObjectName("app_results") + self.app_results.setObjectName(u"app_results") self.verticalLayout_6 = QVBoxLayout(self.app_results) - self.verticalLayout_6.setObjectName("verticalLayout_6") + self.verticalLayout_6.setObjectName(u"verticalLayout_6") self.verticalLayout_4 = QVBoxLayout() - self.verticalLayout_4.setObjectName("verticalLayout_4") + self.verticalLayout_4.setObjectName(u"verticalLayout_4") self.horizontalLayout_7 = QHBoxLayout() - self.horizontalLayout_7.setObjectName("horizontalLayout_7") + self.horizontalLayout_7.setObjectName(u"horizontalLayout_7") self.verticalLayout_5 = QVBoxLayout() - self.verticalLayout_5.setObjectName("verticalLayout_5") + self.verticalLayout_5.setObjectName(u"verticalLayout_5") self.horizontalLayout_7.addLayout(self.verticalLayout_5) self.btn_del_select_apparats = QPushButton(self.app_results) - self.btn_del_select_apparats.setObjectName("btn_del_select_apparats") + self.btn_del_select_apparats.setObjectName(u"btn_del_select_apparats") self.btn_del_select_apparats.setFocusPolicy(Qt.StrongFocus) self.horizontalLayout_7.addWidget(self.btn_del_select_apparats) self.btn_notify_for_deletion = QPushButton(self.app_results) - self.btn_notify_for_deletion.setObjectName("btn_notify_for_deletion") + self.btn_notify_for_deletion.setObjectName(u"btn_notify_for_deletion") self.horizontalLayout_7.addWidget(self.btn_notify_for_deletion) self.btn_extendSelection = QPushButton(self.app_results) - self.btn_extendSelection.setObjectName("btn_extendSelection") + self.btn_extendSelection.setObjectName(u"btn_extendSelection") self.horizontalLayout_7.addWidget(self.btn_extendSelection) - self.horizontalSpacer_5 = QSpacerItem( - 40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum - ) + self.horizontalSpacer_5 = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum) self.horizontalLayout_7.addItem(self.horizontalSpacer_5) + self.verticalLayout_4.addLayout(self.horizontalLayout_7) self.tableWidget = QTableWidget(self.app_results) - if self.tableWidget.columnCount() < 5: + if (self.tableWidget.columnCount() < 5): self.tableWidget.setColumnCount(5) __qtablewidgetitem = QTableWidgetItem() self.tableWidget.setHorizontalHeaderItem(0, __qtablewidgetitem) @@ -293,33 +273,35 @@ class Ui_Dialog(object): self.tableWidget.setHorizontalHeaderItem(3, __qtablewidgetitem3) __qtablewidgetitem4 = QTableWidgetItem() self.tableWidget.setHorizontalHeaderItem(4, __qtablewidgetitem4) - self.tableWidget.setObjectName("tableWidget") + self.tableWidget.setObjectName(u"tableWidget") self.tableWidget.setFocusPolicy(Qt.NoFocus) self.tableWidget.setContextMenuPolicy(Qt.CustomContextMenu) self.tableWidget.setEditTriggers(QAbstractItemView.NoEditTriggers) self.tableWidget.setGridStyle(Qt.NoPen) self.tableWidget.setSortingEnabled(True) self.tableWidget.horizontalHeader().setStretchLastSection(True) - self.tableWidget.verticalHeader().setProperty("showSortIndicator", True) + self.tableWidget.verticalHeader().setProperty(u"showSortIndicator", True) self.verticalLayout_4.addWidget(self.tableWidget) + self.verticalLayout_6.addLayout(self.verticalLayout_4) + self.horizontalLayout.addWidget(self.app_results) self.stats = QFrame(self.apparatResult) - self.stats.setObjectName("stats") + self.stats.setObjectName(u"stats") self.verticalLayout_8 = QVBoxLayout(self.stats) - self.verticalLayout_8.setObjectName("verticalLayout_8") + self.verticalLayout_8.setObjectName(u"verticalLayout_8") self.tabWidget_3 = QTabWidget(self.stats) - self.tabWidget_3.setObjectName("tabWidget_3") + self.tabWidget_3.setObjectName(u"tabWidget_3") self.statistic_table = QWidget() - self.statistic_table.setObjectName("statistic_table") + self.statistic_table.setObjectName(u"statistic_table") self.verticalLayout_7 = QVBoxLayout(self.statistic_table) - self.verticalLayout_7.setObjectName("verticalLayout_7") + self.verticalLayout_7.setObjectName(u"verticalLayout_7") self.statistics_table = QTableWidget(self.statistic_table) - if self.statistics_table.columnCount() < 3: + if (self.statistics_table.columnCount() < 3): self.statistics_table.setColumnCount(3) __qtablewidgetitem5 = QTableWidgetItem() self.statistics_table.setHorizontalHeaderItem(0, __qtablewidgetitem5) @@ -327,10 +309,8 @@ class Ui_Dialog(object): self.statistics_table.setHorizontalHeaderItem(1, __qtablewidgetitem6) __qtablewidgetitem7 = QTableWidgetItem() self.statistics_table.setHorizontalHeaderItem(2, __qtablewidgetitem7) - self.statistics_table.setObjectName("statistics_table") - sizePolicy.setHeightForWidth( - self.statistics_table.sizePolicy().hasHeightForWidth() - ) + self.statistics_table.setObjectName(u"statistics_table") + sizePolicy.setHeightForWidth(self.statistics_table.sizePolicy().hasHeightForWidth()) self.statistics_table.setSizePolicy(sizePolicy) self.statistics_table.setMaximumSize(QSize(16777215, 16777215)) self.statistics_table.setFocusPolicy(Qt.NoFocus) @@ -340,35 +320,36 @@ class Ui_Dialog(object): self.statistics_table.horizontalHeader().setCascadingSectionResizes(True) self.statistics_table.horizontalHeader().setMinimumSectionSize(40) self.statistics_table.horizontalHeader().setDefaultSectionSize(80) - self.statistics_table.horizontalHeader().setProperty("showSortIndicator", True) + self.statistics_table.horizontalHeader().setProperty(u"showSortIndicator", True) self.statistics_table.horizontalHeader().setStretchLastSection(False) self.statistics_table.verticalHeader().setStretchLastSection(True) self.verticalLayout_7.addWidget(self.statistics_table) self.dataLayout = QHBoxLayout() - self.dataLayout.setObjectName("dataLayout") + self.dataLayout.setObjectName(u"dataLayout") self.verticalLayout_7.addLayout(self.dataLayout) self.tabWidget_3.addTab(self.statistic_table, "") self.graph_table = QWidget() - self.graph_table.setObjectName("graph_table") + self.graph_table.setObjectName(u"graph_table") self.tabWidget_3.addTab(self.graph_table, "") self.verticalLayout_8.addWidget(self.tabWidget_3) + self.horizontalLayout.addWidget(self.stats) self.stackedWidget_4.addWidget(self.apparatResult) self.bookresult = QWidget() - self.bookresult.setObjectName("bookresult") + self.bookresult.setObjectName(u"bookresult") sizePolicy.setHeightForWidth(self.bookresult.sizePolicy().hasHeightForWidth()) self.bookresult.setSizePolicy(sizePolicy) self.verticalLayout_2 = QVBoxLayout(self.bookresult) - self.verticalLayout_2.setObjectName("verticalLayout_2") + self.verticalLayout_2.setObjectName(u"verticalLayout_2") self.book_search_result = QTableWidget(self.bookresult) - if self.book_search_result.columnCount() < 3: + if (self.book_search_result.columnCount() < 3): self.book_search_result.setColumnCount(3) __qtablewidgetitem8 = QTableWidgetItem() self.book_search_result.setHorizontalHeaderItem(0, __qtablewidgetitem8) @@ -376,7 +357,7 @@ class Ui_Dialog(object): self.book_search_result.setHorizontalHeaderItem(1, __qtablewidgetitem9) __qtablewidgetitem10 = QTableWidgetItem() self.book_search_result.setHorizontalHeaderItem(2, __qtablewidgetitem10) - self.book_search_result.setObjectName("book_search_result") + self.book_search_result.setObjectName(u"book_search_result") self.book_search_result.setFrameShadow(QFrame.Plain) self.book_search_result.setEditTriggers(QAbstractItemView.NoEditTriggers) self.book_search_result.setAlternatingRowColors(True) @@ -384,11 +365,9 @@ class Ui_Dialog(object): self.book_search_result.horizontalHeader().setCascadingSectionResizes(True) self.book_search_result.horizontalHeader().setMinimumSectionSize(100) self.book_search_result.horizontalHeader().setDefaultSectionSize(200) - self.book_search_result.horizontalHeader().setProperty( - "showSortIndicator", True - ) + self.book_search_result.horizontalHeader().setProperty(u"showSortIndicator", True) self.book_search_result.horizontalHeader().setStretchLastSection(True) - self.book_search_result.verticalHeader().setProperty("showSortIndicator", False) + self.book_search_result.verticalHeader().setProperty(u"showSortIndicator", False) self.verticalLayout_2.addWidget(self.book_search_result) @@ -396,9 +375,10 @@ class Ui_Dialog(object): self.verticalLayout_3.addWidget(self.stackedWidget_4) + self.verticalLayout.addLayout(self.verticalLayout_3) - # if QT_CONFIG(shortcut) +#if QT_CONFIG(shortcut) self.label_18.setBuddy(self.box_dauerapp) self.label_15.setBuddy(self.check_deletable) self.label_11.setBuddy(self.box_person) @@ -408,7 +388,7 @@ class Ui_Dialog(object): self.label_16.setBuddy(self.box_fach) self.label_25.setBuddy(self.search_by_signature) self.label_26.setBuddy(self.search_by_title) - # endif // QT_CONFIG(shortcut) +#endif // QT_CONFIG(shortcut) QWidget.setTabOrder(self.box_appnrs, self.box_person) QWidget.setTabOrder(self.box_person, self.box_fach) QWidget.setTabOrder(self.box_fach, self.check_deletable) @@ -426,118 +406,58 @@ class Ui_Dialog(object): self.stackedWidget_4.setCurrentIndex(0) self.tabWidget_3.setCurrentIndex(0) - QMetaObject.connectSlotsByName(Dialog) + QMetaObject.connectSlotsByName(Dialog) # setupUi def retranslateUi(self, Dialog): - Dialog.setWindowTitle(QCoreApplication.translate("Dialog", "Dialog", None)) - self.label_18.setText(QCoreApplication.translate("Dialog", "Dauerapp:", None)) - self.label_15.setText( - QCoreApplication.translate("Dialog", "L\u00f6schbar", None) - ) - self.label_11.setText(QCoreApplication.translate("Dialog", "Person:", None)) - self.label_7.setText(QCoreApplication.translate("Dialog", "Appnr.:", None)) - self.label_17.setText( - QCoreApplication.translate("Dialog", "Endsemester:", None) - ) - self.label_19.setText( - QCoreApplication.translate("Dialog", "Erstellsemester:", None) - ) - self.label_16.setText(QCoreApplication.translate("Dialog", "Fach:", None)) + Dialog.setWindowTitle(QCoreApplication.translate("Dialog", u"Dialog", None)) + self.label_18.setText(QCoreApplication.translate("Dialog", u"Dauerapp:", None)) + self.label_15.setText(QCoreApplication.translate("Dialog", u"L\u00f6schbar", None)) + self.label_11.setText(QCoreApplication.translate("Dialog", u"Person:", None)) + self.label_7.setText(QCoreApplication.translate("Dialog", u"Appnr.:", None)) + self.label_17.setText(QCoreApplication.translate("Dialog", u"Endsemester:", None)) + self.label_19.setText(QCoreApplication.translate("Dialog", u"Erstellsemester:", None)) + self.label_16.setText(QCoreApplication.translate("Dialog", u"Fach:", None)) self.check_deletable.setText("") - self.btn_search.setText(QCoreApplication.translate("Dialog", "Suchen", None)) + self.btn_search.setText(QCoreApplication.translate("Dialog", u"Suchen", None)) self.db_err_message.setText("") - self.tabWidget_2.setTabText( - self.tabWidget_2.indexOf(self.tab_3), - QCoreApplication.translate("Dialog", "Statistik", None), - ) - # if QT_CONFIG(statustip) - self.search_by_signature.setStatusTip( - QCoreApplication.translate( - "Dialog", "Trunkierung mit * am Ende unterst\u00fctzt", None - ) - ) - # endif // QT_CONFIG(statustip) - self.label_25.setText(QCoreApplication.translate("Dialog", "Signatur", None)) - self.label_26.setText(QCoreApplication.translate("Dialog", "Titel", None)) - self.label.setText( - QCoreApplication.translate("Dialog", "Suche mit Enter starten", None) - ) + self.tabWidget_2.setTabText(self.tabWidget_2.indexOf(self.tab_3), QCoreApplication.translate("Dialog", u"Statistik", None)) +#if QT_CONFIG(statustip) + self.search_by_signature.setStatusTip(QCoreApplication.translate("Dialog", u"Trunkierung mit * am Ende unterst\u00fctzt", None)) +#endif // QT_CONFIG(statustip) + self.label_25.setText(QCoreApplication.translate("Dialog", u"Signatur", None)) + self.label_26.setText(QCoreApplication.translate("Dialog", u"Titel", None)) + self.label.setText(QCoreApplication.translate("Dialog", u"Suche mit Enter starten", None)) self.no_result.setText("") - self.tabWidget_2.setTabText( - self.tabWidget_2.indexOf(self.tab_4), - QCoreApplication.translate("Dialog", "Suchen", None), - ) - self.btn_del_select_apparats.setText( - QCoreApplication.translate("Dialog", "Ausgew\u00e4hlte L\u00f6schen", None) - ) - # if QT_CONFIG(statustip) - self.btn_notify_for_deletion.setStatusTip( - QCoreApplication.translate( - "Dialog", - "Zeigt f\u00fcr jeden ausgew\u00e4hlten Apparat eine eMail-Vorlage an", - None, - ) - ) - # endif // QT_CONFIG(statustip) - self.btn_notify_for_deletion.setText( - QCoreApplication.translate( - "Dialog", "Ausgew\u00e4hlte Benachrichtigen", None - ) - ) - self.btn_extendSelection.setText( - QCoreApplication.translate( - "Dialog", "Ausgew\u00e4hlte Verl\u00e4ngern", None - ) - ) + self.tabWidget_2.setTabText(self.tabWidget_2.indexOf(self.tab_4), QCoreApplication.translate("Dialog", u"Suchen", None)) + self.btn_del_select_apparats.setText(QCoreApplication.translate("Dialog", u"Ausgew\u00e4hlte L\u00f6schen", None)) +#if QT_CONFIG(statustip) + self.btn_notify_for_deletion.setStatusTip(QCoreApplication.translate("Dialog", u"Zeigt f\u00fcr jeden ausgew\u00e4hlten Apparat eine eMail-Vorlage an", None)) +#endif // QT_CONFIG(statustip) + self.btn_notify_for_deletion.setText(QCoreApplication.translate("Dialog", u"Ausgew\u00e4hlte Benachrichtigen", None)) + self.btn_extendSelection.setText(QCoreApplication.translate("Dialog", u"Ausgew\u00e4hlte Verl\u00e4ngern", None)) ___qtablewidgetitem = self.tableWidget.horizontalHeaderItem(1) - ___qtablewidgetitem.setText( - QCoreApplication.translate("Dialog", "Apparatsname", None) - ) + ___qtablewidgetitem.setText(QCoreApplication.translate("Dialog", u"Apparatsname", None)); ___qtablewidgetitem1 = self.tableWidget.horizontalHeaderItem(2) - ___qtablewidgetitem1.setText( - QCoreApplication.translate("Dialog", "Apparatsnummer", None) - ) + ___qtablewidgetitem1.setText(QCoreApplication.translate("Dialog", u"Apparatsnummer", None)); ___qtablewidgetitem2 = self.tableWidget.horizontalHeaderItem(3) - ___qtablewidgetitem2.setText( - QCoreApplication.translate("Dialog", "Person", None) - ) + ___qtablewidgetitem2.setText(QCoreApplication.translate("Dialog", u"Person", None)); ___qtablewidgetitem3 = self.tableWidget.horizontalHeaderItem(4) - ___qtablewidgetitem3.setText(QCoreApplication.translate("Dialog", "Fach", None)) + ___qtablewidgetitem3.setText(QCoreApplication.translate("Dialog", u"Fach", None)); ___qtablewidgetitem4 = self.statistics_table.horizontalHeaderItem(0) - ___qtablewidgetitem4.setText( - QCoreApplication.translate("Dialog", "Semester", None) - ) + ___qtablewidgetitem4.setText(QCoreApplication.translate("Dialog", u"Semester", None)); ___qtablewidgetitem5 = self.statistics_table.horizontalHeaderItem(1) - ___qtablewidgetitem5.setText( - QCoreApplication.translate("Dialog", "Zugang", None) - ) + ___qtablewidgetitem5.setText(QCoreApplication.translate("Dialog", u"Zugang", None)); ___qtablewidgetitem6 = self.statistics_table.horizontalHeaderItem(2) - ___qtablewidgetitem6.setText( - QCoreApplication.translate("Dialog", "Abgang", None) - ) - self.tabWidget_3.setTabText( - self.tabWidget_3.indexOf(self.statistic_table), - QCoreApplication.translate("Dialog", "Tabelle", None), - ) - self.tabWidget_3.setTabText( - self.tabWidget_3.indexOf(self.graph_table), - QCoreApplication.translate( - "Dialog", "Erstellte und gel\u00f6schte Semesterapparate", None - ), - ) + ___qtablewidgetitem6.setText(QCoreApplication.translate("Dialog", u"Abgang", None)); + self.tabWidget_3.setTabText(self.tabWidget_3.indexOf(self.statistic_table), QCoreApplication.translate("Dialog", u"Tabelle", None)) + self.tabWidget_3.setTabText(self.tabWidget_3.indexOf(self.graph_table), QCoreApplication.translate("Dialog", u"Erstellte und gel\u00f6schte Semesterapparate", None)) ___qtablewidgetitem7 = self.book_search_result.horizontalHeaderItem(0) - ___qtablewidgetitem7.setText( - QCoreApplication.translate("Dialog", "Titel", None) - ) + ___qtablewidgetitem7.setText(QCoreApplication.translate("Dialog", u"Titel", None)); ___qtablewidgetitem8 = self.book_search_result.horizontalHeaderItem(1) - ___qtablewidgetitem8.setText( - QCoreApplication.translate("Dialog", "Signatur", None) - ) + ___qtablewidgetitem8.setText(QCoreApplication.translate("Dialog", u"Signatur", None)); ___qtablewidgetitem9 = self.book_search_result.horizontalHeaderItem(2) - ___qtablewidgetitem9.setText( - QCoreApplication.translate("Dialog", "Apparat", None) - ) - + ___qtablewidgetitem9.setText(QCoreApplication.translate("Dialog", u"Apparat", None)); # retranslateUi + -- 2.49.1 From 1f34442397b856c510d613170d0a9850ff2f4700 Mon Sep 17 00:00:00 2001 From: WorldTeacher Date: Mon, 8 Sep 2025 10:34:34 +0200 Subject: [PATCH 04/40] rework catalogue wrapper to split entries based on space div --- src/backend/catalogue.py | 101 ++++++++++++++++++++++++--------------- 1 file changed, 62 insertions(+), 39 deletions(-) diff --git a/src/backend/catalogue.py b/src/backend/catalogue.py index 4f72ec1..439972d 100644 --- a/src/backend/catalogue.py +++ b/src/backend/catalogue.py @@ -1,12 +1,13 @@ +import sys +from datetime import datetime + +import loguru import requests from bs4 import BeautifulSoup +from src import LOG_DIR from src.logic import BookData as Book -from datetime import datetime -import sys -import loguru -from src import LOG_DIR 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" @@ -20,6 +21,8 @@ log.add( rotation="1 day", retention="1 month", ) + + class Catalogue: def __init__(self, timeout=5): self.timeout = timeout @@ -57,45 +60,65 @@ class Catalogue: log.info(f"Searching for term: {searchterm}") links = self.get_book_links(searchterm) + print(links) for link in links: result = self.search(link) # in result search for class col-xs-12 rds-dl RDS_LOCATION # if found, return text of href soup = BeautifulSoup(result, "html.parser") - location = soup.find_all("div", class_="col-xs-12 rds-dl RDS_LOCATION") - for loc in location: - if f"1. OG Semesterapparat" in loc.text: - title = ( - soup.find("div", class_="headline text") - .text.replace("\n", "") - .strip() + + # Optional (unchanged): title and ppn if you need them + title_el = soup.find("div", class_="headline text") + title = title_el.get_text(strip=True) if title_el else None + + ppn_el = soup.find( + "div", class_="col-xs-12 col-md-5 col-lg-4 rds-dl-head RDS_PPN" + ) + ppn = ppn_el.get_text(strip=True) if ppn_el else None + + signature = None + + panel = soup.select_one("div.panel-body") + if panel: + # Collect the RDS_* blocks in order, using the 'space' divs as separators + groups = [] + cur = {} + for node in panel.select( + "div.rds-dl.RDS_SIGNATURE, div.rds-dl.RDS_STATUS, div.rds-dl.RDS_LOCATION, div.col-xs-12.space" + ): + classes = node.get("class", []) + # Separator between entries + if "space" in classes: + if cur: + groups.append(cur) + cur = {} + continue + + # Read the value from the corresponding panel cell + val_el = node.select_one(".rds-dl-panel") + val = ( + val_el.get_text(" ", strip=True) + if val_el + else node.get_text(" ", strip=True) ) - ppn = soup.find( - "div", class_="col-xs-12 col-md-5 col-lg-4 rds-dl-head RDS_PPN" - ) - signature = soup.find( - "div", class_="col-xs-12 rds-dl RDS_SIGNATURE" - ) - if signature: - signature = ( - signature.find_next("div") - .find_next("div") - .text.replace("\n", "") - .strip() + + if "RDS_SIGNATURE" in classes: + cur["signature"] = val + elif "RDS_STATUS" in classes: + cur["status"] = val + elif "RDS_LOCATION" in classes: + cur["location"] = val + + if cur: # append the last group if not followed by a space + groups.append(cur) + + # Find the signature for the entry whose location mentions "Semesterapparat" + for g in groups: + loc = g.get("location", "").lower() + if "semesterapparat" in loc: + signature = g.get("signature") + return Book( + title=title, + ppn=ppn, + signature=signature, ) - # use ppn to find the next div and extract the text - if ppn: - ppn = ppn.find_next("div").text.replace("\n", "").strip() - else: - ppn = None - isbn = soup.find( - "div", class_="col-xs-12 col-md-5 col-lg-4 rds-dl-head RDS_ISBN" - ) - if isbn: - isbn = isbn.find_next("div").find_next("div").text - else: - isbn = None - return Book( - title=title, ppn=ppn, signature=signature, isbn=isbn, link=link - ) - return False -- 2.49.1 From 6f21c22d221da790b0fb359d42156514ef94a446 Mon Sep 17 00:00:00 2001 From: WorldTeacher Date: Mon, 8 Sep 2025 10:34:59 +0200 Subject: [PATCH 05/40] UI: add eta label --- src/ui/semesterapparat_ui.ui | 7 +++++++ src/ui/semesterapparat_ui_ui.py | 6 ++++++ 2 files changed, 13 insertions(+) diff --git a/src/ui/semesterapparat_ui.ui b/src/ui/semesterapparat_ui.ui index cca8956..859d005 100644 --- a/src/ui/semesterapparat_ui.ui +++ b/src/ui/semesterapparat_ui.ui @@ -354,6 +354,13 @@ + + + + + + + diff --git a/src/ui/semesterapparat_ui_ui.py b/src/ui/semesterapparat_ui_ui.py index b01c144..f30675d 100644 --- a/src/ui/semesterapparat_ui_ui.py +++ b/src/ui/semesterapparat_ui_ui.py @@ -218,6 +218,11 @@ class Ui_MainWindow(object): self.avail_layout.addWidget(self.progressBar) + self.label_eta = QLabel(self.gridLayoutWidget_2) + self.label_eta.setObjectName(u"label_eta") + + self.avail_layout.addWidget(self.label_eta) + self.horizontalLayout_5.addLayout(self.avail_layout) @@ -878,6 +883,7 @@ class Ui_MainWindow(object): self.label_info.setText(QCoreApplication.translate("MainWindow", u"Medien werden hinzugef\u00fcgt", None)) self.progress_label.setText(QCoreApplication.translate("MainWindow", u"Medium x/y", None)) self.label_20.setText(QCoreApplication.translate("MainWindow", u"Medien werden gepr\u00fcft", None)) + self.label_eta.setText("") self.avail_status.setText(QCoreApplication.translate("MainWindow", u"TextLabel", None)) self.automation_add_selected_books.setText(QCoreApplication.translate("MainWindow", u"Ausgew\u00e4hlte als verf\u00fcgbar markieren", None)) ___qtablewidgetitem6 = self.tableWidget_apparat_media.horizontalHeaderItem(0) -- 2.49.1 From 7be9dba9ca4fd42dfe1113531af2b05514d8acfe Mon Sep 17 00:00:00 2001 From: WorldTeacher Date: Mon, 8 Sep 2025 10:35:29 +0200 Subject: [PATCH 06/40] refresh displayed table entities after signatures were checked --- src/ui/widgets/searchPage.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ui/widgets/searchPage.py b/src/ui/widgets/searchPage.py index 814efdb..2948a77 100644 --- a/src/ui/widgets/searchPage.py +++ b/src/ui/widgets/searchPage.py @@ -116,6 +116,7 @@ class SearchStatisticPage(QtWidgets.QDialog, Ui_Dialog): self.updater.books = books # type: ignore self.updater.currtot.connect(self.status_update_emit) self.updater.finished.connect(self.updater.deleteLater) + self.updater.finished.connect(self.search_book) self.updater.start() def status_update_emit(self, current: int, total: int): -- 2.49.1 From e934a2b3f1b9418b4cd98c64fb3e301da8619852 Mon Sep 17 00:00:00 2001 From: WorldTeacher Date: Mon, 8 Sep 2025 10:35:47 +0200 Subject: [PATCH 07/40] add ppn search --- src/logic/webrequest.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/logic/webrequest.py b/src/logic/webrequest.py index e545cf5..d593bf7 100644 --- a/src/logic/webrequest.py +++ b/src/logic/webrequest.py @@ -80,6 +80,12 @@ class WebRequest: response = requests.get(PPN_URL.format(searchterm), timeout=self.timeout) return response.text + @sleep_and_retry + @limits(calls=RATE_LIMIT, period=RATE_PERIOD) + def search_ppn(self, ppn: str) -> str: + response = requests.get(API_URL.format(ppn), timeout=self.timeout) + return response.text + def get_book_links(self, searchterm: str) -> list[str]: response: str = self.search_book(searchterm) # type:ignore soup = BeautifulSoup(response, "html.parser") -- 2.49.1 From 1ee7901d49a4f9b5e0014f41706a4ce7acac1505 Mon Sep 17 00:00:00 2001 From: WorldTeacher Date: Mon, 8 Sep 2025 10:36:18 +0200 Subject: [PATCH 08/40] add eta, items/s signals, calculate values --- src/backend/thread_neweditions.py | 37 ++++++++++++++++++++++++++++--- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/src/backend/thread_neweditions.py b/src/backend/thread_neweditions.py index 45e662f..245c091 100644 --- a/src/backend/thread_neweditions.py +++ b/src/backend/thread_neweditions.py @@ -5,6 +5,7 @@ from datetime import datetime from math import ceil from queue import Empty, Queue from typing import List, Optional, Set, Union +from time import monotonic # <-- NEW import loguru from PySide6.QtCore import QThread, Signal @@ -49,13 +50,10 @@ def _same_book(a: BookData, b: BookData) -> bool: # strong title match required; then author if available; then year if available if ta and tb and ta == tb: - # if both have authors, require match if aa and ab and aa == ab: - # if both have year, require match if ya and yb: return ya == yb return True - # if one/both authors missing, allow title (+year if both present) if ya and yb: return ya == yb return True @@ -116,6 +114,10 @@ class NewEditionCheckerThread(QThread): total_entries_signal = Signal(int) resultsSignal = Signal(list) # list[tuple[BookData, list[BookData]]] + # NEW: metrics signals + rateSignal = Signal(float) # items per second ("it/s") + etaSignal = Signal(int) # seconds remaining (-1 when unknown) + def __init__(self, entries: Optional[list["BookData"]] = None, parent=None): super().__init__(parent) self.entries: list["BookData"] = entries if entries is not None else [] @@ -194,6 +196,11 @@ class NewEditionCheckerThread(QThread): for entry in response if not (_norm_isbns(entry.isbn) & _norm_isbns(book.isbn)) ] + response = [ + entry + for entry in response + if book.publisher in entry.publisher + ] if not response: return None @@ -221,8 +228,14 @@ class NewEditionCheckerThread(QThread): total = len(self.entries) self.total_entries_signal.emit(total) + # start timer for metrics + t0 = monotonic() + if total == 0: log.debug("No entries to process.") + # emit metrics (zero work) + self.rateSignal.emit(0.0) + self.etaSignal.emit(0) self.resultsSignal.emit([]) return @@ -255,9 +268,27 @@ class NewEditionCheckerThread(QThread): processed += int(payload) self.updateSignal.emit(processed, total) self.updateProgress.emit(processed, total) + + # ---- NEW: compute & emit metrics ---- + elapsed = max(1e-9, monotonic() - t0) + rate = processed / elapsed # items per second + remaining = max(0, total - processed) + eta_sec = int(round(remaining / rate)) if rate > 0 else -1 + + self.rateSignal.emit(rate) + # clamp negative just in case + self.etaSignal.emit(max(0, eta_sec) if eta_sec >= 0 else -1) + # ------------------------------------- + elif kind == "result": self.results.append(payload) elif kind == "done": finished_workers += 1 + # Final metrics on completion + elapsed_total = max(1e-9, monotonic() - t0) + final_rate = total / elapsed_total + self.rateSignal.emit(final_rate) + self.etaSignal.emit(0) + self.resultsSignal.emit(self.results) -- 2.49.1 From fd6684cc4702f61eb84763b8a1c4618400acb33c Mon Sep 17 00:00:00 2001 From: WorldTeacher Date: Mon, 8 Sep 2025 10:36:57 +0200 Subject: [PATCH 09/40] add label setter and clearer --- src/ui/userInterface.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/ui/userInterface.py b/src/ui/userInterface.py index ff40cf8..3347e6b 100644 --- a/src/ui/userInterface.py +++ b/src/ui/userInterface.py @@ -253,6 +253,7 @@ class Ui(Ui_Semesterapparat): self.mail_thread = None self.autoGrabber = None self.newEditionChecker = NewEditionCheckerThread() + self.elsatab.setLayout(QtWidgets.QVBoxLayout()) self.search_statistics.setLayout(QtWidgets.QVBoxLayout()) @@ -275,6 +276,9 @@ class Ui(Ui_Semesterapparat): self.status_progress.hide() self.valid_check_semester.clicked.connect(self.display_valid_semester) # type:ignore + def update_eta(self, eta: str): + self.label_eta.setText(f"Bitte warten... (ETA: {eta})") + def create_doc(self): log.debug("Creating document") # open DocumentPrintDialog @@ -1549,6 +1553,10 @@ class Ui(Ui_Semesterapparat): self.newEditionChecker.entries = books self.newEditionChecker.finished.connect(self.newEditionChecker.reset) + self.newEditionChecker.finished.connect(self.reset_eta) + + self.newEditionChecker.etaSignal.connect(self.update_eta) + self.progressBar.setMaximum(len(books)) self.newEditionChecker.updateSignal.connect(self.update_status) @@ -1580,6 +1588,9 @@ class Ui(Ui_Semesterapparat): ) self.mail_thread.show() + def reset_eta(self): + self.label_eta.setText("") + def reminder(self): log.info("Opening reminder dialog") reminder = ReminderDialog() -- 2.49.1 From 1263faa23f944ebec2b198366f3758805e64919e Mon Sep 17 00:00:00 2001 From: WorldTeacher Date: Wed, 17 Sep 2025 14:27:12 +0200 Subject: [PATCH 10/40] delete files --- ...hner (alexander.kirchner@ph-freiburg.de).eml | 54 -- ...on zur Auflösung des Semesterapparates.eml | 18 - .../dialogs/dialog_sources/Ui_app_status.py | 22 - .../dialog_sources/Ui_apparat_extend.py | 83 ---- .../dialog_sources/Ui_confirm_extend.py | 37 -- .../dialog_sources/Ui_edit_bookdata.py | 127 ----- .../dialog_sources/Ui_elsa_add_table_entry.py | 470 ------------------ .../Ui_elsa_generate_citation.py | 83 ---- .../Ui_elsa_generator_confirm.py | 129 ----- src/ui/dialogs/dialog_sources/Ui_login.py | 53 -- .../dialogs/dialog_sources/Ui_mail_preview.py | 137 ----- .../dialogs/dialog_sources/Ui_medianadder.py | 385 -------------- .../Ui_newMailTemplateDesigner.py | 181 ------- .../dialog_sources/Ui_parsed_titles.py | 91 ---- src/ui/dialogs/dialog_sources/Ui_reminder.py | 46 -- src/ui/dialogs/dialog_sources/Ui_settings.py | 341 ------------- 16 files changed, 2257 deletions(-) delete mode 100644 mail_vorlagen/Information bezüglich der Auflösung des Semesterapparates {AppNr} - Alexander Kirchner (alexander.kirchner@ph-freiburg.de).eml delete mode 100644 mail_vorlagen/Information zur Auflösung des Semesterapparates.eml delete mode 100644 src/ui/dialogs/dialog_sources/Ui_app_status.py delete mode 100644 src/ui/dialogs/dialog_sources/Ui_apparat_extend.py delete mode 100644 src/ui/dialogs/dialog_sources/Ui_confirm_extend.py delete mode 100644 src/ui/dialogs/dialog_sources/Ui_edit_bookdata.py delete mode 100644 src/ui/dialogs/dialog_sources/Ui_elsa_add_table_entry.py delete mode 100644 src/ui/dialogs/dialog_sources/Ui_elsa_generate_citation.py delete mode 100644 src/ui/dialogs/dialog_sources/Ui_elsa_generator_confirm.py delete mode 100644 src/ui/dialogs/dialog_sources/Ui_login.py delete mode 100644 src/ui/dialogs/dialog_sources/Ui_mail_preview.py delete mode 100644 src/ui/dialogs/dialog_sources/Ui_medianadder.py delete mode 100644 src/ui/dialogs/dialog_sources/Ui_newMailTemplateDesigner.py delete mode 100644 src/ui/dialogs/dialog_sources/Ui_parsed_titles.py delete mode 100644 src/ui/dialogs/dialog_sources/Ui_reminder.py delete mode 100644 src/ui/dialogs/dialog_sources/Ui_settings.py diff --git a/mail_vorlagen/Information bezüglich der Auflösung des Semesterapparates {AppNr} - Alexander Kirchner (alexander.kirchner@ph-freiburg.de).eml b/mail_vorlagen/Information bezüglich der Auflösung des Semesterapparates {AppNr} - Alexander Kirchner (alexander.kirchner@ph-freiburg.de).eml deleted file mode 100644 index 9bc91f4..0000000 --- a/mail_vorlagen/Information bezüglich der Auflösung des Semesterapparates {AppNr} - Alexander Kirchner (alexander.kirchner@ph-freiburg.de).eml +++ /dev/null @@ -1,54 +0,0 @@ -Message-ID: <987b46cf-2d8b-4a27-acb3-c50f61d3d85d@ph-freiburg.de> -Date: Tue, 31 Oct 2023 11:38:34 +0100 -MIME-Version: 1.0 -User-Agent: Mozilla Thunderbird -From: Alexander Kirchner -Subject: =?UTF-8?Q?Information_bez=C3=BCglich_der_Aufl=C3=B6sung_des_Semeste?= - =?UTF-8?Q?rapparates_=7BAppNr=7D?= -Content-Language: de-DE -X-Mozilla-Draft-Info: internal/draft; vcard=0; receipt=0; DSN=0; uuencode=0; - attachmentreminder=0; deliveryformat=0 -X-Identity-Key: id1 -Fcc: imap://aky547@imap.ph-freiburg.de/INBOX/Sent -Content-Type: text/html; charset=UTF-8 -Content-Transfer-Encoding: 8bit - - - - - - - -

Sehr geehrte/r {Profname},
-

-


-

- auf die E-Mail bezüglich der Auflösung oder Verlängerung der - Semesterapparate haben wir von Ihnen keine Rückmeldung erhalten. - Deshalb gehen wir davon aus, dass der Apparat aufgelöst werden kann. - Die Medien, die im Apparat aufgestellt waren, werden nun wieder - regulär ausleihbar und sind dann an ihren Standorten bei den Fächern - zu finden.
-
- Falls Sie den Apparat erneut, oder einen neuen Apparat anlegen - wollen, können Sie mir das ausgefüllte Formular zur Einrichtung des - Apparates (https://www.ph-freiburg.de/bibliothek/lernen/semesterapparate/info-lehrende-sem.html) - zukommen lassen. Im Falle einer Verlängerung des Apparates reicht - eine Antwort auf diese Mail. -


-

-

Bei Fragen können Sie sich jederzeit an mich wenden.
-

-


-

-
-- 
-Freundliche Grüße
-
-Alexander Kirchner
-
-
-Bibliothek der Pädagogischen Hochschule Freiburg
-Tel. 0761/682-778
- - diff --git a/mail_vorlagen/Information zur Auflösung des Semesterapparates.eml b/mail_vorlagen/Information zur Auflösung des Semesterapparates.eml deleted file mode 100644 index 45ca60f..0000000 --- a/mail_vorlagen/Information zur Auflösung des Semesterapparates.eml +++ /dev/null @@ -1,18 +0,0 @@ -Subject: Information zur Auflösung des Semesterapparates {AppNr} - {Appname} -MIME-Version: 1.0 -Content-Type: text/html; charset="UTF-8" -Content-Transfer-Encoding: 8bit - - - - - - -

{greeting}

-


-

Ihr Semesterapparat "{Appname} ({AppNr})" wurde wie besprochen aufgelöst.

-

Die Medien sind von nun an wieder in den Regalen zu finden.

-


-

-- 

-

{signature}

-

\ No newline at end of file diff --git a/src/ui/dialogs/dialog_sources/Ui_app_status.py b/src/ui/dialogs/dialog_sources/Ui_app_status.py deleted file mode 100644 index 7dcc51b..0000000 --- a/src/ui/dialogs/dialog_sources/Ui_app_status.py +++ /dev/null @@ -1,22 +0,0 @@ -# Form implementation generated from reading ui file 'c:\Users\aky547\GitHub\SemesterapparatsManager\src\ui\dialogs\dialog_sources\app_status.ui' -# -# Created by: PySide6 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 PySide6 import QtCore - - -class Ui_Form(object): - def setupUi(self, Form): - Form.setObjectName("Form") - Form.resize(300, 500) - - self.retranslateUi(Form) - QtCore.QMetaObject.connectSlotsByName(Form) - - def retranslateUi(self, Form): - _translate = QtCore.QCoreApplication.translate - Form.setWindowTitle(_translate("Form", "Form")) diff --git a/src/ui/dialogs/dialog_sources/Ui_apparat_extend.py b/src/ui/dialogs/dialog_sources/Ui_apparat_extend.py deleted file mode 100644 index 828d1a8..0000000 --- a/src/ui/dialogs/dialog_sources/Ui_apparat_extend.py +++ /dev/null @@ -1,83 +0,0 @@ -# Form implementation generated from reading ui file 'c:\Users\aky547\GitHub\SemesterapparatsManager\src\ui\dialogs\dialog_sources\apparat_extend.ui' -# -# Created by: PySide6 UI code generator 6.7.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 PySide6 import QtCore, QtGui, QtWidgets - - -class Ui_Dialog(object): - def setupUi(self, Dialog): - Dialog.setObjectName("Dialog") - Dialog.resize(388, 103) - sizePolicy = QtWidgets.QSizePolicy( - QtWidgets.QSizePolicy.Policy.Fixed, QtWidgets.QSizePolicy.Policy.Fixed - ) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(Dialog.sizePolicy().hasHeightForWidth()) - Dialog.setSizePolicy(sizePolicy) - Dialog.setMinimumSize(QtCore.QSize(388, 103)) - Dialog.setMaximumSize(QtCore.QSize(388, 103)) - self.buttonBox = QtWidgets.QDialogButtonBox(parent=Dialog) - self.buttonBox.setGeometry(QtCore.QRect(290, 30, 81, 241)) - self.buttonBox.setOrientation(QtCore.Qt.Orientation.Vertical) - self.buttonBox.setStandardButtons( - QtWidgets.QDialogButtonBox.StandardButton.Abort - | QtWidgets.QDialogButtonBox.StandardButton.Save - ) - self.buttonBox.setObjectName("buttonBox") - self.label = QtWidgets.QLabel(parent=Dialog) - self.label.setGeometry(QtCore.QRect(10, 0, 281, 31)) - sizePolicy = QtWidgets.QSizePolicy( - QtWidgets.QSizePolicy.Policy.Fixed, QtWidgets.QSizePolicy.Policy.Fixed - ) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.label.sizePolicy().hasHeightForWidth()) - self.label.setSizePolicy(sizePolicy) - font = QtGui.QFont() - font.setPointSize(10) - self.label.setFont(font) - self.label.setObjectName("label") - self.frame = QtWidgets.QFrame(parent=Dialog) - self.frame.setGeometry(QtCore.QRect(10, 30, 241, 41)) - self.frame.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel) - self.frame.setFrameShadow(QtWidgets.QFrame.Shadow.Raised) - self.frame.setObjectName("frame") - self.line = QtWidgets.QFrame(parent=self.frame) - self.line.setGeometry(QtCore.QRect(120, 0, 3, 61)) - self.line.setFrameShape(QtWidgets.QFrame.Shape.VLine) - self.line.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken) - self.line.setObjectName("line") - self.rad_sommer = QtWidgets.QRadioButton(parent=self.frame) - self.rad_sommer.setGeometry(QtCore.QRect(10, 10, 82, 21)) - self.rad_sommer.setObjectName("rad_sommer") - self.rad_winter = QtWidgets.QRadioButton(parent=self.frame) - self.rad_winter.setGeometry(QtCore.QRect(140, 10, 82, 21)) - self.rad_winter.setObjectName("rad_winter") - self.sem_year = QtWidgets.QLineEdit(parent=Dialog) - self.sem_year.setGeometry(QtCore.QRect(10, 70, 121, 20)) - self.sem_year.setObjectName("sem_year") - self.dauerapp = QtWidgets.QCheckBox(parent=Dialog) - self.dauerapp.setGeometry(QtCore.QRect(150, 70, 111, 21)) - self.dauerapp.setObjectName("dauerapp") - - self.retranslateUi(Dialog) - self.buttonBox.accepted.connect(Dialog.accept) # type: ignore - self.buttonBox.rejected.connect(Dialog.reject) # type: ignore - QtCore.QMetaObject.connectSlotsByName(Dialog) - - def retranslateUi(self, Dialog): - _translate = QtCore.QCoreApplication.translate - Dialog.setWindowTitle(_translate("Dialog", "Dialog")) - self.label.setText( - _translate("Dialog", "Bis wann soll der Apparat verlängert werden?") - ) - self.rad_sommer.setText(_translate("Dialog", "Sommer")) - self.rad_winter.setText(_translate("Dialog", "Winter")) - self.sem_year.setPlaceholderText(_translate("Dialog", "2023")) - self.dauerapp.setText(_translate("Dialog", "Dauerapparat")) diff --git a/src/ui/dialogs/dialog_sources/Ui_confirm_extend.py b/src/ui/dialogs/dialog_sources/Ui_confirm_extend.py deleted file mode 100644 index 72ef74f..0000000 --- a/src/ui/dialogs/dialog_sources/Ui_confirm_extend.py +++ /dev/null @@ -1,37 +0,0 @@ -# Form implementation generated from reading ui file 'c:\Users\aky547\GitHub\SemesterapparatsManager\src\ui\dialogs\dialog_sources\confirm_extend.ui' -# -# Created by: PySide6 UI code generator 6.7.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 PySide6 import QtCore, QtWidgets - - -class Ui_extend_confirm(object): - def setupUi(self, extend_confirm): - extend_confirm.setObjectName("extend_confirm") - extend_confirm.resize(380, 97) - self.horizontalLayout = QtWidgets.QHBoxLayout(extend_confirm) - self.horizontalLayout.setObjectName("horizontalLayout") - self.textEdit = QtWidgets.QTextEdit(parent=extend_confirm) - self.textEdit.setObjectName("textEdit") - self.horizontalLayout.addWidget(self.textEdit) - self.buttonBox = QtWidgets.QDialogButtonBox(parent=extend_confirm) - self.buttonBox.setOrientation(QtCore.Qt.Orientation.Vertical) - self.buttonBox.setStandardButtons( - QtWidgets.QDialogButtonBox.StandardButton.Cancel - | QtWidgets.QDialogButtonBox.StandardButton.Ok - ) - self.buttonBox.setObjectName("buttonBox") - self.horizontalLayout.addWidget(self.buttonBox) - - self.retranslateUi(extend_confirm) - self.buttonBox.accepted.connect(extend_confirm.accept) # type: ignore - self.buttonBox.rejected.connect(extend_confirm.reject) # type: ignore - QtCore.QMetaObject.connectSlotsByName(extend_confirm) - - def retranslateUi(self, extend_confirm): - _translate = QtCore.QCoreApplication.translate - extend_confirm.setWindowTitle(_translate("extend_confirm", "Dialog")) diff --git a/src/ui/dialogs/dialog_sources/Ui_edit_bookdata.py b/src/ui/dialogs/dialog_sources/Ui_edit_bookdata.py deleted file mode 100644 index 381cd43..0000000 --- a/src/ui/dialogs/dialog_sources/Ui_edit_bookdata.py +++ /dev/null @@ -1,127 +0,0 @@ -# Form implementation generated from reading ui file 'c:\Users\aky547\GitHub\SemesterapparatsManager\src\ui\dialogs\dialog_sources\edit_bookdata.ui' -# -# Created by: PySide6 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 PySide6 import QtCore, QtGui, QtWidgets - - -class Ui_Dialog(object): - def setupUi(self, Dialog): - Dialog.setObjectName("Dialog") - Dialog.resize(448, 572) - self.buttonBox = QtWidgets.QDialogButtonBox(parent=Dialog) - self.buttonBox.setGeometry(QtCore.QRect(260, 530, 161, 32)) - self.buttonBox.setOrientation(QtCore.Qt.Orientation.Horizontal) - self.buttonBox.setStandardButtons( - QtWidgets.QDialogButtonBox.StandardButton.Cancel - | QtWidgets.QDialogButtonBox.StandardButton.Ok - ) - self.buttonBox.setObjectName("buttonBox") - self.gridLayoutWidget = QtWidgets.QWidget(parent=Dialog) - self.gridLayoutWidget.setGeometry(QtCore.QRect(0, 0, 441, 531)) - self.gridLayoutWidget.setObjectName("gridLayoutWidget") - self.gridLayout = QtWidgets.QGridLayout(self.gridLayoutWidget) - self.gridLayout.setSizeConstraint( - QtWidgets.QLayout.SizeConstraint.SetDefaultConstraint - ) - self.gridLayout.setContentsMargins(0, 0, 0, 0) - self.gridLayout.setObjectName("gridLayout") - self.label_10 = QtWidgets.QLabel(parent=self.gridLayoutWidget) - self.label_10.setObjectName("label_10") - self.gridLayout.addWidget(self.label_10, 10, 1, 1, 1) - self.label = QtWidgets.QLabel(parent=self.gridLayoutWidget) - self.label.setObjectName("label") - self.gridLayout.addWidget(self.label, 0, 1, 1, 1) - self.label_9 = QtWidgets.QLabel(parent=self.gridLayoutWidget) - self.label_9.setObjectName("label_9") - self.gridLayout.addWidget(self.label_9, 9, 1, 1, 1) - self.label_8 = QtWidgets.QLabel(parent=self.gridLayoutWidget) - self.label_8.setObjectName("label_8") - self.gridLayout.addWidget(self.label_8, 8, 1, 1, 1) - self.label_12 = QtWidgets.QLabel(parent=self.gridLayoutWidget) - self.label_12.setObjectName("label_12") - self.gridLayout.addWidget(self.label_12, 6, 1, 1, 1) - self.line_edition = QtWidgets.QLineEdit(parent=self.gridLayoutWidget) - self.line_edition.setObjectName("line_edition") - self.gridLayout.addWidget(self.line_edition, 2, 2, 1, 1) - self.label_3 = QtWidgets.QLabel(parent=self.gridLayoutWidget) - self.label_3.setObjectName("label_3") - self.gridLayout.addWidget(self.label_3, 2, 1, 1, 1) - self.label_4 = QtWidgets.QLabel(parent=self.gridLayoutWidget) - self.label_4.setObjectName("label_4") - self.gridLayout.addWidget(self.label_4, 3, 1, 1, 1) - self.line_link = QtWidgets.QLineEdit(parent=self.gridLayoutWidget) - self.line_link.setCursor(QtGui.QCursor(QtCore.Qt.CursorShape.ArrowCursor)) - self.line_link.setReadOnly(True) - self.line_link.setObjectName("line_link") - self.gridLayout.addWidget(self.line_link, 6, 2, 1, 1) - self.label_5 = QtWidgets.QLabel(parent=self.gridLayoutWidget) - self.label_5.setObjectName("label_5") - self.gridLayout.addWidget(self.label_5, 4, 1, 1, 1) - self.label_7 = QtWidgets.QLabel(parent=self.gridLayoutWidget) - self.label_7.setObjectName("label_7") - self.gridLayout.addWidget(self.label_7, 7, 1, 1, 1) - self.label_6 = QtWidgets.QLabel(parent=self.gridLayoutWidget) - self.label_6.setObjectName("label_6") - self.gridLayout.addWidget(self.label_6, 5, 1, 1, 1) - self.label_2 = QtWidgets.QLabel(parent=self.gridLayoutWidget) - self.label_2.setObjectName("label_2") - self.gridLayout.addWidget(self.label_2, 1, 1, 1, 1) - spacerItem = QtWidgets.QSpacerItem( - 5, - 20, - QtWidgets.QSizePolicy.Policy.Fixed, - QtWidgets.QSizePolicy.Policy.Minimum, - ) - self.gridLayout.addItem(spacerItem, 8, 0, 1, 1) - self.line_title = QtWidgets.QLineEdit(parent=self.gridLayoutWidget) - self.line_title.setObjectName("line_title") - self.gridLayout.addWidget(self.line_title, 0, 2, 1, 1) - self.line_signature = QtWidgets.QLineEdit(parent=self.gridLayoutWidget) - self.line_signature.setObjectName("line_signature") - self.gridLayout.addWidget(self.line_signature, 1, 2, 1, 1) - self.line_author = QtWidgets.QLineEdit(parent=self.gridLayoutWidget) - self.line_author.setObjectName("line_author") - self.gridLayout.addWidget(self.line_author, 3, 2, 1, 1) - self.line_lang = QtWidgets.QLineEdit(parent=self.gridLayoutWidget) - self.line_lang.setObjectName("line_lang") - self.gridLayout.addWidget(self.line_lang, 8, 2, 1, 1) - self.line_ppn = QtWidgets.QLineEdit(parent=self.gridLayoutWidget) - self.line_ppn.setObjectName("line_ppn") - self.gridLayout.addWidget(self.line_ppn, 5, 2, 1, 1) - self.line_isbn = QtWidgets.QLineEdit(parent=self.gridLayoutWidget) - self.line_isbn.setObjectName("line_isbn") - self.gridLayout.addWidget(self.line_isbn, 7, 2, 1, 1) - self.line_year = QtWidgets.QLineEdit(parent=self.gridLayoutWidget) - self.line_year.setObjectName("line_year") - self.gridLayout.addWidget(self.line_year, 9, 2, 1, 1) - self.line_pages = QtWidgets.QLineEdit(parent=self.gridLayoutWidget) - self.line_pages.setObjectName("line_pages") - self.gridLayout.addWidget(self.line_pages, 10, 2, 1, 1) - self.line_publisher = QtWidgets.QLineEdit(parent=self.gridLayoutWidget) - self.line_publisher.setObjectName("line_publisher") - self.gridLayout.addWidget(self.line_publisher, 4, 2, 1, 1) - - self.retranslateUi(Dialog) - self.buttonBox.accepted.connect(Dialog.accept) # type: ignore - self.buttonBox.rejected.connect(Dialog.reject) # type: ignore - QtCore.QMetaObject.connectSlotsByName(Dialog) - - def retranslateUi(self, Dialog): - _translate = QtCore.QCoreApplication.translate - Dialog.setWindowTitle(_translate("Dialog", "Dialog")) - self.label_10.setText(_translate("Dialog", "Seiten")) - self.label.setText(_translate("Dialog", "Titel")) - self.label_9.setText(_translate("Dialog", "Jahr")) - self.label_8.setText(_translate("Dialog", "Sprache")) - self.label_12.setText(_translate("Dialog", "Link")) - self.label_3.setText(_translate("Dialog", "Auflage")) - self.label_4.setText(_translate("Dialog", "Autor")) - self.label_5.setText(_translate("Dialog", "Herausgeber")) - self.label_7.setText(_translate("Dialog", "ISBN(s)")) - self.label_6.setText(_translate("Dialog", "PPN")) - self.label_2.setText(_translate("Dialog", "Signatur")) diff --git a/src/ui/dialogs/dialog_sources/Ui_elsa_add_table_entry.py b/src/ui/dialogs/dialog_sources/Ui_elsa_add_table_entry.py deleted file mode 100644 index a393508..0000000 --- a/src/ui/dialogs/dialog_sources/Ui_elsa_add_table_entry.py +++ /dev/null @@ -1,470 +0,0 @@ -# Form implementation generated from reading ui file 'c:\Users\aky547\GitHub\SemesterapparatsManager\src\ui\dialogs\dialog_sources\elsa_add_table_entry.ui' -# -# Created by: PySide6 UI code generator 6.7.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 PySide6 import QtCore, QtWidgets - - -class Ui_Dialog(object): - def setupUi(self, Dialog): - Dialog.setObjectName("Dialog") - Dialog.resize(529, 482) - self.verticalLayout = QtWidgets.QVBoxLayout(Dialog) - self.verticalLayout.setObjectName("verticalLayout") - self.groupBox = QtWidgets.QGroupBox(parent=Dialog) - self.groupBox.setFlat(True) - self.groupBox.setCheckable(False) - self.groupBox.setObjectName("groupBox") - self.gridLayout_4 = QtWidgets.QGridLayout(self.groupBox) - self.gridLayout_4.setObjectName("gridLayout_4") - spacerItem = QtWidgets.QSpacerItem( - 40, - 20, - QtWidgets.QSizePolicy.Policy.Expanding, - QtWidgets.QSizePolicy.Policy.Minimum, - ) - self.gridLayout_4.addItem(spacerItem, 0, 3, 1, 1) - self.btn_mono = QtWidgets.QRadioButton(parent=self.groupBox) - self.btn_mono.setChecked(False) - self.btn_mono.setObjectName("btn_mono") - self.gridLayout_4.addWidget(self.btn_mono, 0, 0, 1, 1) - self.btn_zs = QtWidgets.QRadioButton(parent=self.groupBox) - self.btn_zs.setObjectName("btn_zs") - self.gridLayout_4.addWidget(self.btn_zs, 0, 2, 1, 1) - self.btn_hg = QtWidgets.QRadioButton(parent=self.groupBox) - self.btn_hg.setObjectName("btn_hg") - self.gridLayout_4.addWidget(self.btn_hg, 0, 1, 1, 1) - self.verticalLayout.addWidget(self.groupBox) - self.horizontalLayout_2 = QtWidgets.QHBoxLayout() - self.horizontalLayout_2.setObjectName("horizontalLayout_2") - self.label_2 = QtWidgets.QLabel(parent=Dialog) - self.label_2.setObjectName("label_2") - self.horizontalLayout_2.addWidget(self.label_2) - self.searchIdent = QtWidgets.QLineEdit(parent=Dialog) - self.searchIdent.setObjectName("searchIdent") - self.horizontalLayout_2.addWidget(self.searchIdent) - self.btn_search = QtWidgets.QPushButton(parent=Dialog) - self.btn_search.setObjectName("btn_search") - self.horizontalLayout_2.addWidget(self.btn_search) - spacerItem1 = QtWidgets.QSpacerItem( - 40, - 20, - QtWidgets.QSizePolicy.Policy.Expanding, - QtWidgets.QSizePolicy.Policy.Minimum, - ) - self.horizontalLayout_2.addItem(spacerItem1) - self.make_quote = QtWidgets.QPushButton(parent=Dialog) - self.make_quote.setObjectName("make_quote") - self.horizontalLayout_2.addWidget(self.make_quote) - self.verticalLayout.addLayout(self.horizontalLayout_2) - self.stackedWidget = QtWidgets.QStackedWidget(parent=Dialog) - self.stackedWidget.setObjectName("stackedWidget") - self.mono = QtWidgets.QWidget() - self.mono.setObjectName("mono") - self.gridLayout_2 = QtWidgets.QGridLayout(self.mono) - self.gridLayout_2.setObjectName("gridLayout_2") - self.label = QtWidgets.QLabel(parent=self.mono) - self.label.setObjectName("label") - self.gridLayout_2.addWidget(self.label, 0, 0, 1, 1) - self.book_author = QtWidgets.QLineEdit(parent=self.mono) - self.book_author.setObjectName("book_author") - self.gridLayout_2.addWidget(self.book_author, 0, 1, 1, 1) - self.label_3 = QtWidgets.QLabel(parent=self.mono) - self.label_3.setObjectName("label_3") - self.gridLayout_2.addWidget(self.label_3, 1, 0, 1, 1) - self.book_year = QtWidgets.QLineEdit(parent=self.mono) - self.book_year.setObjectName("book_year") - self.gridLayout_2.addWidget(self.book_year, 1, 1, 1, 1) - self.label_4 = QtWidgets.QLabel(parent=self.mono) - self.label_4.setObjectName("label_4") - self.gridLayout_2.addWidget(self.label_4, 2, 0, 1, 1) - self.book_edition = QtWidgets.QLineEdit(parent=self.mono) - self.book_edition.setObjectName("book_edition") - self.gridLayout_2.addWidget(self.book_edition, 2, 1, 1, 1) - self.label_5 = QtWidgets.QLabel(parent=self.mono) - self.label_5.setObjectName("label_5") - self.gridLayout_2.addWidget(self.label_5, 3, 0, 1, 1) - self.book_title = QtWidgets.QLineEdit(parent=self.mono) - self.book_title.setObjectName("book_title") - self.gridLayout_2.addWidget(self.book_title, 3, 1, 1, 1) - self.label_6 = QtWidgets.QLabel(parent=self.mono) - self.label_6.setObjectName("label_6") - self.gridLayout_2.addWidget(self.label_6, 4, 0, 1, 1) - self.book_place = QtWidgets.QLineEdit(parent=self.mono) - self.book_place.setObjectName("book_place") - self.gridLayout_2.addWidget(self.book_place, 4, 1, 1, 1) - self.label_7 = QtWidgets.QLabel(parent=self.mono) - self.label_7.setObjectName("label_7") - self.gridLayout_2.addWidget(self.label_7, 5, 0, 1, 1) - self.book_publisher = QtWidgets.QLineEdit(parent=self.mono) - self.book_publisher.setObjectName("book_publisher") - self.gridLayout_2.addWidget(self.book_publisher, 5, 1, 1, 1) - self.label_8 = QtWidgets.QLabel(parent=self.mono) - self.label_8.setObjectName("label_8") - self.gridLayout_2.addWidget(self.label_8, 6, 0, 1, 1) - self.book_signature = QtWidgets.QLineEdit(parent=self.mono) - self.book_signature.setObjectName("book_signature") - self.gridLayout_2.addWidget(self.book_signature, 6, 1, 1, 1) - self.label_9 = QtWidgets.QLabel(parent=self.mono) - self.label_9.setObjectName("label_9") - self.gridLayout_2.addWidget(self.label_9, 7, 0, 1, 1) - self.book_pages = QtWidgets.QLineEdit(parent=self.mono) - self.book_pages.setObjectName("book_pages") - self.gridLayout_2.addWidget(self.book_pages, 7, 1, 1, 1) - self.page_warn_2 = QtWidgets.QToolButton(parent=self.mono) - self.page_warn_2.setText("") - self.page_warn_2.setAutoRaise(True) - self.page_warn_2.setObjectName("page_warn_2") - self.gridLayout_2.addWidget(self.page_warn_2, 7, 2, 1, 1) - self.label_29 = QtWidgets.QLabel(parent=self.mono) - self.label_29.setObjectName("label_29") - self.gridLayout_2.addWidget(self.label_29, 8, 0, 1, 1) - self.book_isbn = QtWidgets.QLineEdit(parent=self.mono) - self.book_isbn.setObjectName("book_isbn") - self.gridLayout_2.addWidget(self.book_isbn, 8, 1, 1, 1) - self.stackedWidget.addWidget(self.mono) - self.hg = QtWidgets.QWidget() - self.hg.setObjectName("hg") - self.gridLayout_3 = QtWidgets.QGridLayout(self.hg) - self.gridLayout_3.setObjectName("gridLayout_3") - self.hg_editor = QtWidgets.QLineEdit(parent=self.hg) - self.hg_editor.setObjectName("hg_editor") - self.gridLayout_3.addWidget(self.hg_editor, 4, 1, 1, 1) - self.label_26 = QtWidgets.QLabel(parent=self.hg) - self.label_26.setObjectName("label_26") - self.gridLayout_3.addWidget(self.label_26, 7, 0, 1, 1) - self.hg_edition = QtWidgets.QLineEdit(parent=self.hg) - self.hg_edition.setObjectName("hg_edition") - self.gridLayout_3.addWidget(self.hg_edition, 2, 1, 1, 1) - self.label_20 = QtWidgets.QLabel(parent=self.hg) - self.label_20.setObjectName("label_20") - self.gridLayout_3.addWidget(self.label_20, 1, 0, 1, 1) - self.label_24 = QtWidgets.QLabel(parent=self.hg) - self.label_24.setObjectName("label_24") - self.gridLayout_3.addWidget(self.label_24, 3, 0, 1, 1) - self.label_27 = QtWidgets.QLabel(parent=self.hg) - self.label_27.setObjectName("label_27") - self.gridLayout_3.addWidget(self.label_27, 8, 0, 1, 1) - self.label_28 = QtWidgets.QLabel(parent=self.hg) - self.label_28.setObjectName("label_28") - self.gridLayout_3.addWidget(self.label_28, 9, 0, 1, 1) - self.label_23 = QtWidgets.QLabel(parent=self.hg) - self.label_23.setObjectName("label_23") - self.gridLayout_3.addWidget(self.label_23, 5, 0, 1, 1) - self.label_21 = QtWidgets.QLabel(parent=self.hg) - self.label_21.setObjectName("label_21") - self.gridLayout_3.addWidget(self.label_21, 2, 0, 1, 1) - self.hg_pages = QtWidgets.QLineEdit(parent=self.hg) - self.hg_pages.setObjectName("hg_pages") - self.gridLayout_3.addWidget(self.hg_pages, 8, 1, 1, 1) - self.label_19 = QtWidgets.QLabel(parent=self.hg) - self.label_19.setObjectName("label_19") - self.gridLayout_3.addWidget(self.label_19, 0, 0, 1, 1) - self.hg_signature = QtWidgets.QLineEdit(parent=self.hg) - self.hg_signature.setObjectName("hg_signature") - self.gridLayout_3.addWidget(self.hg_signature, 9, 1, 1, 1) - self.label_30 = QtWidgets.QLabel(parent=self.hg) - self.label_30.setObjectName("label_30") - self.gridLayout_3.addWidget(self.label_30, 10, 0, 1, 1) - self.label_25 = QtWidgets.QLabel(parent=self.hg) - self.label_25.setObjectName("label_25") - self.gridLayout_3.addWidget(self.label_25, 6, 0, 1, 1) - self.hg_year = QtWidgets.QLineEdit(parent=self.hg) - self.hg_year.setObjectName("hg_year") - self.gridLayout_3.addWidget(self.hg_year, 1, 1, 1, 1) - self.label_22 = QtWidgets.QLabel(parent=self.hg) - self.label_22.setObjectName("label_22") - self.gridLayout_3.addWidget(self.label_22, 4, 0, 1, 1) - self.hg_title = QtWidgets.QLineEdit(parent=self.hg) - self.hg_title.setObjectName("hg_title") - self.gridLayout_3.addWidget(self.hg_title, 5, 1, 1, 1) - self.hg_chaptertitle = QtWidgets.QLineEdit(parent=self.hg) - self.hg_chaptertitle.setObjectName("hg_chaptertitle") - self.gridLayout_3.addWidget(self.hg_chaptertitle, 3, 1, 1, 1) - self.hg_author = QtWidgets.QLineEdit(parent=self.hg) - self.hg_author.setObjectName("hg_author") - self.gridLayout_3.addWidget(self.hg_author, 0, 1, 1, 1) - self.hg_isbn = QtWidgets.QLineEdit(parent=self.hg) - self.hg_isbn.setObjectName("hg_isbn") - self.gridLayout_3.addWidget(self.hg_isbn, 10, 1, 1, 1) - self.hg_publisher = QtWidgets.QLineEdit(parent=self.hg) - self.hg_publisher.setObjectName("hg_publisher") - self.gridLayout_3.addWidget(self.hg_publisher, 7, 1, 1, 1) - self.hg_place = QtWidgets.QLineEdit(parent=self.hg) - self.hg_place.setObjectName("hg_place") - self.gridLayout_3.addWidget(self.hg_place, 6, 1, 1, 1) - self.page_warn_3 = QtWidgets.QToolButton(parent=self.hg) - self.page_warn_3.setText("") - self.page_warn_3.setAutoRaise(True) - self.page_warn_3.setObjectName("page_warn_3") - self.gridLayout_3.addWidget(self.page_warn_3, 8, 2, 1, 1) - self.stackedWidget.addWidget(self.hg) - self.zs = QtWidgets.QWidget() - self.zs.setObjectName("zs") - self.gridLayout = QtWidgets.QGridLayout(self.zs) - self.gridLayout.setObjectName("gridLayout") - self.label_10 = QtWidgets.QLabel(parent=self.zs) - self.label_10.setObjectName("label_10") - self.gridLayout.addWidget(self.label_10, 0, 0, 1, 1) - self.zs_publisher = QtWidgets.QLineEdit(parent=self.zs) - self.zs_publisher.setObjectName("zs_publisher") - self.gridLayout.addWidget(self.zs_publisher, 6, 1, 1, 1) - self.zs_place = QtWidgets.QLineEdit(parent=self.zs) - self.zs_place.setObjectName("zs_place") - self.gridLayout.addWidget(self.zs_place, 5, 1, 1, 1) - self.label_14 = QtWidgets.QLabel(parent=self.zs) - self.label_14.setObjectName("label_14") - self.gridLayout.addWidget(self.label_14, 4, 0, 1, 1) - self.label_11 = QtWidgets.QLabel(parent=self.zs) - self.label_11.setObjectName("label_11") - self.gridLayout.addWidget(self.label_11, 1, 0, 1, 1) - self.zs_year = QtWidgets.QLineEdit(parent=self.zs) - self.zs_year.setObjectName("zs_year") - self.gridLayout.addWidget(self.zs_year, 1, 1, 1, 1) - self.label_17 = QtWidgets.QLabel(parent=self.zs) - self.label_17.setObjectName("label_17") - self.gridLayout.addWidget(self.label_17, 7, 0, 1, 1) - self.label_16 = QtWidgets.QLabel(parent=self.zs) - self.label_16.setObjectName("label_16") - self.gridLayout.addWidget(self.label_16, 6, 0, 1, 1) - self.zs_issue = QtWidgets.QLineEdit(parent=self.zs) - self.zs_issue.setObjectName("zs_issue") - self.gridLayout.addWidget(self.zs_issue, 2, 1, 1, 1) - self.zs_chapter_title = QtWidgets.QLineEdit(parent=self.zs) - self.zs_chapter_title.setObjectName("zs_chapter_title") - self.gridLayout.addWidget(self.zs_chapter_title, 3, 1, 1, 1) - self.zs_isbn = QtWidgets.QLineEdit(parent=self.zs) - self.zs_isbn.setObjectName("zs_isbn") - self.gridLayout.addWidget(self.zs_isbn, 9, 1, 1, 1) - self.label_12 = QtWidgets.QLabel(parent=self.zs) - self.label_12.setObjectName("label_12") - self.gridLayout.addWidget(self.label_12, 2, 0, 1, 1) - self.label_31 = QtWidgets.QLabel(parent=self.zs) - self.label_31.setObjectName("label_31") - self.gridLayout.addWidget(self.label_31, 9, 0, 1, 1) - self.label_15 = QtWidgets.QLabel(parent=self.zs) - self.label_15.setObjectName("label_15") - self.gridLayout.addWidget(self.label_15, 5, 0, 1, 1) - self.zs_signature = QtWidgets.QLineEdit(parent=self.zs) - self.zs_signature.setObjectName("zs_signature") - self.gridLayout.addWidget(self.zs_signature, 8, 1, 1, 1) - self.zs_pages = QtWidgets.QLineEdit(parent=self.zs) - self.zs_pages.setObjectName("zs_pages") - self.gridLayout.addWidget(self.zs_pages, 7, 1, 1, 1) - self.label_13 = QtWidgets.QLabel(parent=self.zs) - self.label_13.setObjectName("label_13") - self.gridLayout.addWidget(self.label_13, 3, 0, 1, 1) - self.label_18 = QtWidgets.QLabel(parent=self.zs) - self.label_18.setObjectName("label_18") - self.gridLayout.addWidget(self.label_18, 8, 0, 1, 1) - self.zs_author = QtWidgets.QLineEdit(parent=self.zs) - self.zs_author.setObjectName("zs_author") - self.gridLayout.addWidget(self.zs_author, 0, 1, 1, 1) - self.zs_title = QtWidgets.QLineEdit(parent=self.zs) - self.zs_title.setObjectName("zs_title") - self.gridLayout.addWidget(self.zs_title, 4, 1, 1, 1) - self.page_warn = QtWidgets.QToolButton(parent=self.zs) - self.page_warn.setText("") - self.page_warn.setAutoRaise(True) - self.page_warn.setObjectName("page_warn") - self.gridLayout.addWidget(self.page_warn, 7, 2, 1, 1) - self.stackedWidget.addWidget(self.zs) - self.page = QtWidgets.QWidget() - self.page.setLayoutDirection(QtCore.Qt.LayoutDirection.LeftToRight) - self.page.setObjectName("page") - self.gridLayout_5 = QtWidgets.QGridLayout(self.page) - self.gridLayout_5.setObjectName("gridLayout_5") - self.label_32 = QtWidgets.QLabel(parent=self.page) - self.label_32.setObjectName("label_32") - self.gridLayout_5.addWidget(self.label_32, 0, 0, 1, 1) - spacerItem2 = QtWidgets.QSpacerItem( - 20, - 40, - QtWidgets.QSizePolicy.Policy.Minimum, - QtWidgets.QSizePolicy.Policy.Expanding, - ) - self.gridLayout_5.addItem(spacerItem2, 7, 0, 1, 1) - self.file_desc_edit = QtWidgets.QTextEdit(parent=self.page) - self.file_desc_edit.setReadOnly(True) - self.file_desc_edit.setObjectName("file_desc_edit") - self.gridLayout_5.addWidget(self.file_desc_edit, 6, 0, 1, 1) - self.label_34 = QtWidgets.QLabel(parent=self.page) - self.label_34.setObjectName("label_34") - self.gridLayout_5.addWidget(self.label_34, 3, 0, 1, 1) - self.filename_edit = QtWidgets.QTextEdit(parent=self.page) - self.filename_edit.setReadOnly(True) - self.filename_edit.setObjectName("filename_edit") - self.gridLayout_5.addWidget(self.filename_edit, 1, 0, 1, 1) - self.label_33 = QtWidgets.QLabel(parent=self.page) - self.label_33.setObjectName("label_33") - self.gridLayout_5.addWidget(self.label_33, 5, 0, 1, 1) - self.ilias_filename = QtWidgets.QTextEdit(parent=self.page) - self.ilias_filename.setReadOnly(True) - self.ilias_filename.setObjectName("ilias_filename") - self.gridLayout_5.addWidget(self.ilias_filename, 4, 0, 1, 1) - self.verticalLayout_2 = QtWidgets.QVBoxLayout() - self.verticalLayout_2.setObjectName("verticalLayout_2") - spacerItem3 = QtWidgets.QSpacerItem( - 20, - 40, - QtWidgets.QSizePolicy.Policy.Minimum, - QtWidgets.QSizePolicy.Policy.Expanding, - ) - self.verticalLayout_2.addItem(spacerItem3) - self.copy_filename = QtWidgets.QToolButton(parent=self.page) - self.copy_filename.setLayoutDirection(QtCore.Qt.LayoutDirection.LeftToRight) - self.copy_filename.setAutoFillBackground(False) - self.copy_filename.setObjectName("copy_filename") - self.verticalLayout_2.addWidget(self.copy_filename) - self.filename_edit_label = QtWidgets.QLabel(parent=self.page) - self.filename_edit_label.setText("") - self.filename_edit_label.setObjectName("filename_edit_label") - self.verticalLayout_2.addWidget(self.filename_edit_label) - spacerItem4 = QtWidgets.QSpacerItem( - 20, - 40, - QtWidgets.QSizePolicy.Policy.Minimum, - QtWidgets.QSizePolicy.Policy.Expanding, - ) - self.verticalLayout_2.addItem(spacerItem4) - self.gridLayout_5.addLayout(self.verticalLayout_2, 1, 1, 1, 1) - self.verticalLayout_3 = QtWidgets.QVBoxLayout() - self.verticalLayout_3.setObjectName("verticalLayout_3") - spacerItem5 = QtWidgets.QSpacerItem( - 20, - 40, - QtWidgets.QSizePolicy.Policy.Minimum, - QtWidgets.QSizePolicy.Policy.Expanding, - ) - self.verticalLayout_3.addItem(spacerItem5) - self.copy_ilias_filename = QtWidgets.QToolButton(parent=self.page) - self.copy_ilias_filename.setObjectName("copy_ilias_filename") - self.verticalLayout_3.addWidget(self.copy_ilias_filename) - self.ilias_filename_label = QtWidgets.QLabel(parent=self.page) - self.ilias_filename_label.setText("") - self.ilias_filename_label.setObjectName("ilias_filename_label") - self.verticalLayout_3.addWidget(self.ilias_filename_label) - spacerItem6 = QtWidgets.QSpacerItem( - 20, - 40, - QtWidgets.QSizePolicy.Policy.Minimum, - QtWidgets.QSizePolicy.Policy.Expanding, - ) - self.verticalLayout_3.addItem(spacerItem6) - self.gridLayout_5.addLayout(self.verticalLayout_3, 4, 1, 1, 1) - self.verticalLayout_4 = QtWidgets.QVBoxLayout() - self.verticalLayout_4.setObjectName("verticalLayout_4") - spacerItem7 = QtWidgets.QSpacerItem( - 20, - 40, - QtWidgets.QSizePolicy.Policy.Minimum, - QtWidgets.QSizePolicy.Policy.Expanding, - ) - self.verticalLayout_4.addItem(spacerItem7) - self.copy_qoute = QtWidgets.QToolButton(parent=self.page) - self.copy_qoute.setObjectName("copy_qoute") - self.verticalLayout_4.addWidget(self.copy_qoute) - self.file_desc_edit_label = QtWidgets.QLabel(parent=self.page) - self.file_desc_edit_label.setText("") - self.file_desc_edit_label.setObjectName("file_desc_edit_label") - self.verticalLayout_4.addWidget(self.file_desc_edit_label) - spacerItem8 = QtWidgets.QSpacerItem( - 20, - 40, - QtWidgets.QSizePolicy.Policy.Minimum, - QtWidgets.QSizePolicy.Policy.Expanding, - ) - self.verticalLayout_4.addItem(spacerItem8) - self.gridLayout_5.addLayout(self.verticalLayout_4, 6, 1, 1, 1) - self.stackedWidget.addWidget(self.page) - self.verticalLayout.addWidget(self.stackedWidget) - self.horizontalLayout = QtWidgets.QHBoxLayout() - self.horizontalLayout.setObjectName("horizontalLayout") - self.buttonBox = QtWidgets.QDialogButtonBox(parent=Dialog) - self.buttonBox.setStandardButtons( - QtWidgets.QDialogButtonBox.StandardButton.Cancel - | QtWidgets.QDialogButtonBox.StandardButton.Discard - | QtWidgets.QDialogButtonBox.StandardButton.Ok - ) - self.buttonBox.setObjectName("buttonBox") - self.horizontalLayout.addWidget(self.buttonBox) - self.retryButton = QtWidgets.QPushButton(parent=Dialog) - self.retryButton.setObjectName("retryButton") - self.horizontalLayout.addWidget(self.retryButton) - self.verticalLayout.addLayout(self.horizontalLayout) - - self.retranslateUi(Dialog) - self.stackedWidget.setCurrentIndex(3) - QtCore.QMetaObject.connectSlotsByName(Dialog) - - def retranslateUi(self, Dialog): - _translate = QtCore.QCoreApplication.translate - Dialog.setWindowTitle(_translate("Dialog", "Dialog")) - self.groupBox.setTitle(_translate("Dialog", "Medientyp?")) - self.btn_mono.setText(_translate("Dialog", "Monografie")) - self.btn_zs.setText(_translate("Dialog", "Zeitschrift")) - self.btn_hg.setText(_translate("Dialog", "Herausgeberwerk")) - self.label_2.setText(_translate("Dialog", "Identifikator")) - self.btn_search.setText(_translate("Dialog", "Suchen")) - self.make_quote.setToolTip( - _translate("Dialog", "Zuerst die Seitenzahl anpassen") - ) - self.make_quote.setText(_translate("Dialog", "Zitat erstellen")) - self.label.setText(_translate("Dialog", "Autor(en)\n Nachname, Vorname")) - self.book_author.setToolTip( - _translate("Dialog", "Bei mehreren Autoren mit ; trennen") - ) - self.label_3.setText(_translate("Dialog", "Jahr")) - self.label_4.setText(_translate("Dialog", "Auflage")) - self.label_5.setText(_translate("Dialog", "Titel")) - self.label_6.setText(_translate("Dialog", "Ort")) - self.label_7.setText(_translate("Dialog", "Verlag")) - self.label_8.setText(_translate("Dialog", "Signatur")) - self.label_9.setText(_translate("Dialog", "Seiten")) - self.book_pages.setPlaceholderText( - _translate("Dialog", "Seitenanzahl des Mediums, zum zitieren ändern!") - ) - self.label_29.setText(_translate("Dialog", "ISBN")) - self.hg_editor.setToolTip( - _translate("Dialog", "Bei mehreren Autoren mit ; trennen") - ) - self.label_26.setText(_translate("Dialog", "Verlag")) - self.label_20.setText(_translate("Dialog", "Jahr")) - self.label_24.setText(_translate("Dialog", "Beitragstitel")) - self.label_27.setText(_translate("Dialog", "Seiten")) - self.label_28.setText(_translate("Dialog", "Signatur")) - self.label_23.setText(_translate("Dialog", "Titel des Werkes")) - self.label_21.setText(_translate("Dialog", "Auflage")) - self.label_19.setText(_translate("Dialog", "Autor(en)\nNachname, Vorname")) - self.label_30.setText(_translate("Dialog", "ISBN")) - self.label_25.setText(_translate("Dialog", "Ort")) - self.label_22.setText( - _translate("Dialog", "Herausgebername(n)\nNachname, Vorname") - ) - self.hg_author.setToolTip( - _translate("Dialog", "Bei mehreren Autoren mit ; trennen") - ) - self.label_10.setText(_translate("Dialog", "Autor(en)\nNachname, Vorname")) - self.label_14.setText(_translate("Dialog", "Name der Zeitschrift")) - self.label_11.setText(_translate("Dialog", "Jahr")) - self.label_17.setText(_translate("Dialog", "Seiten")) - self.label_16.setText(_translate("Dialog", "Verlag")) - self.label_12.setText(_translate("Dialog", "Heft")) - self.label_31.setText(_translate("Dialog", "ISSN")) - self.label_15.setText(_translate("Dialog", "Ort")) - self.label_13.setText(_translate("Dialog", "Artikeltitel")) - self.label_18.setText(_translate("Dialog", "Signatur")) - self.zs_author.setToolTip( - _translate("Dialog", "Bei mehreren Autoren mit ; trennen") - ) - self.label_32.setText(_translate("Dialog", "Dateiname")) - self.label_34.setText(_translate("Dialog", "ILIAS Name")) - self.label_33.setText(_translate("Dialog", "ILIAS Dateibeschreibung")) - self.copy_filename.setText(_translate("Dialog", "Kopieren")) - self.copy_ilias_filename.setText(_translate("Dialog", "Kopieren")) - self.copy_qoute.setText(_translate("Dialog", "Kopieren")) - self.retryButton.setText(_translate("Dialog", "Wiederholen")) diff --git a/src/ui/dialogs/dialog_sources/Ui_elsa_generate_citation.py b/src/ui/dialogs/dialog_sources/Ui_elsa_generate_citation.py deleted file mode 100644 index 43944f4..0000000 --- a/src/ui/dialogs/dialog_sources/Ui_elsa_generate_citation.py +++ /dev/null @@ -1,83 +0,0 @@ -# Form implementation generated from reading ui file 'c:\Users\aky547\GitHub\SemesterapparatsManager\src\ui\dialogs\dialog_sources\elsa_generate_citation.ui' -# -# Created by: PySide6 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 PySide6 import QtCore, QtGui, QtWidgets - - -class Ui_Dialog(object): - def setupUi(self, Dialog): - Dialog.setObjectName("Dialog") - Dialog.resize(564, 517) - self.verticalLayout_2 = QtWidgets.QVBoxLayout(Dialog) - self.verticalLayout_2.setObjectName("verticalLayout_2") - self.select_type = QtWidgets.QFrame(parent=Dialog) - self.select_type.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel) - self.select_type.setFrameShadow(QtWidgets.QFrame.Shadow.Raised) - self.select_type.setObjectName("select_type") - self.verticalLayout = QtWidgets.QVBoxLayout(self.select_type) - self.verticalLayout.setObjectName("verticalLayout") - self.radio_mono = QtWidgets.QRadioButton(parent=self.select_type) - self.radio_mono.setObjectName("radio_mono") - self.verticalLayout.addWidget(self.radio_mono) - self.radio_zs = QtWidgets.QRadioButton(parent=self.select_type) - self.radio_zs.setObjectName("radio_zs") - self.verticalLayout.addWidget(self.radio_zs) - self.radio_hg = QtWidgets.QRadioButton(parent=self.select_type) - self.radio_hg.setObjectName("radio_hg") - self.verticalLayout.addWidget(self.radio_hg) - spacerItem = QtWidgets.QSpacerItem( - 20, - 40, - QtWidgets.QSizePolicy.Policy.Minimum, - QtWidgets.QSizePolicy.Policy.Expanding, - ) - self.verticalLayout.addItem(spacerItem) - self.verticalLayout_2.addWidget(self.select_type) - self.check = QtWidgets.QGroupBox(parent=Dialog) - font = QtGui.QFont() - font.setBold(True) - self.check.setFont(font) - self.check.setObjectName("check") - self.verticalLayout_3 = QtWidgets.QVBoxLayout(self.check) - self.verticalLayout_3.setObjectName("verticalLayout_3") - self.citation_style_result = QtWidgets.QStackedWidget(parent=self.check) - font = QtGui.QFont() - font.setBold(False) - self.citation_style_result.setFont(font) - self.citation_style_result.setObjectName("citation_style_result") - self.monografie = QtWidgets.QWidget() - self.monografie.setObjectName("monografie") - self.citation_style_result.addWidget(self.monografie) - self.zsaufsatz = QtWidgets.QWidget() - self.zsaufsatz.setObjectName("zsaufsatz") - self.citation_style_result.addWidget(self.zsaufsatz) - self.herausgeberwerk = QtWidgets.QWidget() - self.herausgeberwerk.setObjectName("herausgeberwerk") - self.citation_style_result.addWidget(self.herausgeberwerk) - self.verticalLayout_3.addWidget(self.citation_style_result) - self.pushButton = QtWidgets.QPushButton(parent=self.check) - self.pushButton.setObjectName("pushButton") - self.verticalLayout_3.addWidget( - self.pushButton, 0, QtCore.Qt.AlignmentFlag.AlignRight - ) - self.verticalLayout_2.addWidget(self.check) - self.verticalLayout_2.setStretch(0, 20) - self.verticalLayout_2.setStretch(1, 80) - - self.retranslateUi(Dialog) - self.citation_style_result.setCurrentIndex(2) - QtCore.QMetaObject.connectSlotsByName(Dialog) - - def retranslateUi(self, Dialog): - _translate = QtCore.QCoreApplication.translate - Dialog.setWindowTitle(_translate("Dialog", "Dialog")) - self.radio_mono.setText(_translate("Dialog", "Monografie")) - self.radio_zs.setText(_translate("Dialog", "Zeitschriftenaufsatz")) - self.radio_hg.setText(_translate("Dialog", "Herausgeberwerk")) - self.check.setTitle(_translate("Dialog", "Daten")) - self.pushButton.setText(_translate("Dialog", "Bestätigen")) diff --git a/src/ui/dialogs/dialog_sources/Ui_elsa_generator_confirm.py b/src/ui/dialogs/dialog_sources/Ui_elsa_generator_confirm.py deleted file mode 100644 index c56c6f3..0000000 --- a/src/ui/dialogs/dialog_sources/Ui_elsa_generator_confirm.py +++ /dev/null @@ -1,129 +0,0 @@ -# Form implementation generated from reading ui file 'c:\Users\aky547\GitHub\SemesterapparatsManager\src\ui\dialogs\dialog_sources\elsa_generator_confirm.ui' -# -# Created by: PySide6 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 PySide6 import QtCore, QtGui, QtWidgets - - -class Ui_Dialog(object): - def setupUi(self, Dialog): - Dialog.setObjectName("Dialog") - Dialog.resize(530, 210) - sizePolicy = QtWidgets.QSizePolicy( - QtWidgets.QSizePolicy.Policy.Fixed, QtWidgets.QSizePolicy.Policy.Fixed - ) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(Dialog.sizePolicy().hasHeightForWidth()) - Dialog.setSizePolicy(sizePolicy) - Dialog.setMaximumSize(QtCore.QSize(530, 210)) - self.horizontalLayout = QtWidgets.QHBoxLayout(Dialog) - self.horizontalLayout.setObjectName("horizontalLayout") - self.groupBox = QtWidgets.QGroupBox(parent=Dialog) - font = QtGui.QFont() - font.setBold(True) - self.groupBox.setFont(font) - self.groupBox.setObjectName("groupBox") - self.gridLayout = QtWidgets.QGridLayout(self.groupBox) - self.gridLayout.setObjectName("gridLayout") - self.label = QtWidgets.QLabel(parent=self.groupBox) - font = QtGui.QFont() - font.setBold(False) - self.label.setFont(font) - self.label.setObjectName("label") - self.gridLayout.addWidget(self.label, 1, 0, 1, 1) - self.bookauthor = QtWidgets.QLineEdit(parent=self.groupBox) - font = QtGui.QFont() - font.setBold(False) - self.bookauthor.setFont(font) - self.bookauthor.setObjectName("bookauthor") - self.gridLayout.addWidget(self.bookauthor, 5, 1, 1, 1) - self.book_title = QtWidgets.QLineEdit(parent=self.groupBox) - font = QtGui.QFont() - font.setBold(False) - self.book_title.setFont(font) - self.book_title.setObjectName("book_title") - self.gridLayout.addWidget(self.book_title, 3, 1, 1, 1) - self.label_5 = QtWidgets.QLabel(parent=self.groupBox) - font = QtGui.QFont() - font.setBold(False) - self.label_5.setFont(font) - self.label_5.setObjectName("label_5") - self.gridLayout.addWidget(self.label_5, 5, 0, 1, 1) - self.pages = QtWidgets.QLineEdit(parent=self.groupBox) - font = QtGui.QFont() - font.setBold(False) - self.pages.setFont(font) - self.pages.setObjectName("pages") - self.gridLayout.addWidget(self.pages, 4, 1, 1, 1) - self.label_2 = QtWidgets.QLabel(parent=self.groupBox) - font = QtGui.QFont() - font.setBold(False) - self.label_2.setFont(font) - self.label_2.setObjectName("label_2") - self.gridLayout.addWidget(self.label_2, 2, 0, 1, 1) - self.label_3 = QtWidgets.QLabel(parent=self.groupBox) - font = QtGui.QFont() - font.setBold(False) - self.label_3.setFont(font) - self.label_3.setObjectName("label_3") - self.gridLayout.addWidget(self.label_3, 3, 0, 1, 1) - self.label_4 = QtWidgets.QLabel(parent=self.groupBox) - font = QtGui.QFont() - font.setBold(False) - self.label_4.setFont(font) - self.label_4.setObjectName("label_4") - self.gridLayout.addWidget(self.label_4, 4, 0, 1, 1) - self.chapter_title = QtWidgets.QLineEdit(parent=self.groupBox) - font = QtGui.QFont() - font.setBold(False) - self.chapter_title.setFont(font) - self.chapter_title.setObjectName("chapter_title") - self.gridLayout.addWidget(self.chapter_title, 1, 1, 1, 1) - self.chapter_authors = QtWidgets.QLineEdit(parent=self.groupBox) - font = QtGui.QFont() - font.setBold(False) - self.chapter_authors.setFont(font) - self.chapter_authors.setObjectName("chapter_authors") - self.gridLayout.addWidget(self.chapter_authors, 2, 1, 1, 1) - self.label_6 = QtWidgets.QLabel(parent=self.groupBox) - font = QtGui.QFont() - font.setBold(False) - self.label_6.setFont(font) - self.label_6.setObjectName("label_6") - self.gridLayout.addWidget(self.label_6, 0, 1, 1, 1) - self.horizontalLayout.addWidget(self.groupBox) - self.buttonBox = QtWidgets.QDialogButtonBox(parent=Dialog) - self.buttonBox.setLayoutDirection(QtCore.Qt.LayoutDirection.LeftToRight) - self.buttonBox.setOrientation(QtCore.Qt.Orientation.Vertical) - self.buttonBox.setStandardButtons( - QtWidgets.QDialogButtonBox.StandardButton.Cancel - | QtWidgets.QDialogButtonBox.StandardButton.Ok - ) - self.buttonBox.setCenterButtons(False) - self.buttonBox.setObjectName("buttonBox") - self.horizontalLayout.addWidget(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) - - def retranslateUi(self, Dialog): - _translate = QtCore.QCoreApplication.translate - Dialog.setWindowTitle(_translate("Dialog", "Dialog")) - self.groupBox.setTitle(_translate("Dialog", "Angaben korrekt?")) - self.label.setText(_translate("Dialog", "Kapiteltitel")) - self.label_5.setText(_translate("Dialog", "Herausgebername")) - self.label_2.setText(_translate("Dialog", "Autor(en)")) - self.label_3.setText(_translate("Dialog", "Buchtitel")) - self.label_4.setText(_translate("Dialog", "Seite(n)")) - self.label_6.setText( - _translate( - "Dialog", "Hier können fehlerhafte / fehlende Daten geändert werden" - ) - ) diff --git a/src/ui/dialogs/dialog_sources/Ui_login.py b/src/ui/dialogs/dialog_sources/Ui_login.py deleted file mode 100644 index deb1a62..0000000 --- a/src/ui/dialogs/dialog_sources/Ui_login.py +++ /dev/null @@ -1,53 +0,0 @@ -# Form implementation generated from reading ui file 'c:\Users\aky547\GitHub\SemesterapparatsManager\src\ui\dialogs\dialog_sources\login.ui' -# -# Created by: PySide6 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 PySide6 import QtCore, QtGui, QtWidgets - - -class Ui_Dialog(object): - def setupUi(self, Dialog): - Dialog.setObjectName("Dialog") - Dialog.resize(218, 190) - icon = QtGui.QIcon() - icon.addPixmap( - QtGui.QPixmap(":/icons/resources/1f510.svg"), - QtGui.QIcon.Mode.Normal, - QtGui.QIcon.State.Off, - ) - Dialog.setWindowIcon(icon) - self.label = QtWidgets.QLabel(parent=Dialog) - self.label.setGeometry(QtCore.QRect(20, 40, 71, 21)) - self.label.setObjectName("label") - self.lineEdit = QtWidgets.QLineEdit(parent=Dialog) - self.lineEdit.setGeometry(QtCore.QRect(80, 40, 113, 21)) - self.lineEdit.setObjectName("lineEdit") - self.label_2 = QtWidgets.QLabel(parent=Dialog) - self.label_2.setGeometry(QtCore.QRect(20, 80, 71, 21)) - self.label_2.setObjectName("label_2") - self.lineEdit_2 = QtWidgets.QLineEdit(parent=Dialog) - self.lineEdit_2.setGeometry(QtCore.QRect(80, 80, 113, 21)) - self.lineEdit_2.setInputMethodHints(QtCore.Qt.InputMethodHint.ImhSensitiveData) - self.lineEdit_2.setClearButtonEnabled(True) - self.lineEdit_2.setObjectName("lineEdit_2") - self.login_button = QtWidgets.QPushButton(parent=Dialog) - self.login_button.setGeometry(QtCore.QRect(30, 140, 76, 32)) - self.login_button.setObjectName("login_button") - self.cancel_button = QtWidgets.QPushButton(parent=Dialog) - self.cancel_button.setGeometry(QtCore.QRect(120, 140, 76, 32)) - self.cancel_button.setObjectName("cancel_button") - - self.retranslateUi(Dialog) - QtCore.QMetaObject.connectSlotsByName(Dialog) - - def retranslateUi(self, Dialog): - _translate = QtCore.QCoreApplication.translate - Dialog.setWindowTitle(_translate("Dialog", "Login")) - self.label.setText(_translate("Dialog", "Username")) - self.label_2.setText(_translate("Dialog", "Password")) - self.login_button.setText(_translate("Dialog", "Login")) - self.cancel_button.setText(_translate("Dialog", "Cancel")) diff --git a/src/ui/dialogs/dialog_sources/Ui_mail_preview.py b/src/ui/dialogs/dialog_sources/Ui_mail_preview.py deleted file mode 100644 index e852ebf..0000000 --- a/src/ui/dialogs/dialog_sources/Ui_mail_preview.py +++ /dev/null @@ -1,137 +0,0 @@ -# Form implementation generated from reading ui file 'c:\Users\aky547\GitHub\SemesterapparatsManager\src\ui\dialogs\dialog_sources\mail_preview.ui' -# -# Created by: PySide6 UI code generator 6.7.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 PySide6 import QtCore, QtGui, QtWidgets - - -class Ui_eMailPreview(object): - def setupUi(self, eMailPreview): - eMailPreview.setObjectName("eMailPreview") - eMailPreview.resize(700, 668) - icon = QtGui.QIcon() - icon.addPixmap( - QtGui.QPixmap( - "c:\\Users\\aky547\\GitHub\\SemesterapparatsManager\\src\\ui\\dialogs\\dialog_sources\\../../../../../../icons/email.svg" - ), - QtGui.QIcon.Mode.Normal, - QtGui.QIcon.State.Off, - ) - eMailPreview.setWindowIcon(icon) - self.gridLayout_2 = QtWidgets.QGridLayout(eMailPreview) - self.gridLayout_2.setObjectName("gridLayout_2") - self.gridLayout = QtWidgets.QGridLayout() - self.gridLayout.setObjectName("gridLayout") - self.prof_name = QtWidgets.QLineEdit(parent=eMailPreview) - self.prof_name.setObjectName("prof_name") - self.gridLayout.addWidget(self.prof_name, 2, 2, 1, 1) - self.newTemplate = QtWidgets.QPushButton(parent=eMailPreview) - self.newTemplate.setAutoFillBackground(False) - self.newTemplate.setText("") - self.newTemplate.setIconSize(QtCore.QSize(24, 24)) - self.newTemplate.setAutoDefault(True) - self.newTemplate.setDefault(False) - self.newTemplate.setFlat(False) - self.newTemplate.setObjectName("newTemplate") - self.gridLayout.addWidget(self.newTemplate, 0, 3, 1, 1) - self.comboBox = QtWidgets.QComboBox(parent=eMailPreview) - self.comboBox.setObjectName("comboBox") - self.gridLayout.addWidget(self.comboBox, 0, 2, 1, 1) - self.mail_header = QtWidgets.QLineEdit(parent=eMailPreview) - self.mail_header.setObjectName("mail_header") - self.gridLayout.addWidget(self.mail_header, 3, 2, 1, 1) - self.label_6 = QtWidgets.QLabel(parent=eMailPreview) - self.label_6.setObjectName("label_6") - self.gridLayout.addWidget(self.label_6, 4, 0, 1, 1) - self.mail_body = QtWidgets.QTextEdit(parent=eMailPreview) - self.mail_body.setObjectName("mail_body") - self.gridLayout.addWidget(self.mail_body, 5, 2, 1, 1) - self.label_2 = QtWidgets.QLabel(parent=eMailPreview) - self.label_2.setObjectName("label_2") - self.gridLayout.addWidget(self.label_2, 2, 0, 1, 1) - self.mail_name = QtWidgets.QLineEdit(parent=eMailPreview) - self.mail_name.setObjectName("mail_name") - self.gridLayout.addWidget(self.mail_name, 1, 2, 1, 1) - self.label_5 = QtWidgets.QLabel(parent=eMailPreview) - self.label_5.setObjectName("label_5") - self.gridLayout.addWidget(self.label_5, 0, 0, 1, 1) - self.label_4 = QtWidgets.QLabel(parent=eMailPreview) - self.label_4.setObjectName("label_4") - self.gridLayout.addWidget(self.label_4, 3, 0, 1, 1) - self.horizontalLayout_3 = QtWidgets.QHBoxLayout() - self.horizontalLayout_3.setObjectName("horizontalLayout_3") - self.gender_male = QtWidgets.QRadioButton(parent=eMailPreview) - self.gender_male.setObjectName("gender_male") - self.horizontalLayout_3.addWidget(self.gender_male) - self.gender_female = QtWidgets.QRadioButton(parent=eMailPreview) - self.gender_female.setObjectName("gender_female") - self.horizontalLayout_3.addWidget(self.gender_female) - self.gender_non = QtWidgets.QRadioButton(parent=eMailPreview) - self.gender_non.setObjectName("gender_non") - self.horizontalLayout_3.addWidget(self.gender_non) - spacerItem = QtWidgets.QSpacerItem( - 40, - 20, - QtWidgets.QSizePolicy.Policy.Expanding, - QtWidgets.QSizePolicy.Policy.Minimum, - ) - self.horizontalLayout_3.addItem(spacerItem) - self.gridLayout.addLayout(self.horizontalLayout_3, 4, 2, 1, 1) - self.label_3 = QtWidgets.QLabel(parent=eMailPreview) - self.label_3.setAlignment( - QtCore.Qt.AlignmentFlag.AlignLeading - | QtCore.Qt.AlignmentFlag.AlignLeft - | QtCore.Qt.AlignmentFlag.AlignTop - ) - self.label_3.setObjectName("label_3") - self.gridLayout.addWidget(self.label_3, 5, 0, 1, 1) - self.label = QtWidgets.QLabel(parent=eMailPreview) - self.label.setObjectName("label") - self.gridLayout.addWidget(self.label, 1, 0, 1, 1) - self.horizontalLayout_2 = QtWidgets.QHBoxLayout() - self.horizontalLayout_2.setObjectName("horizontalLayout_2") - spacerItem1 = QtWidgets.QSpacerItem( - 40, - 20, - QtWidgets.QSizePolicy.Policy.Expanding, - QtWidgets.QSizePolicy.Policy.Minimum, - ) - self.horizontalLayout_2.addItem(spacerItem1) - self.btn_okay = QtWidgets.QPushButton(parent=eMailPreview) - self.btn_okay.setStatusTip("") - self.btn_okay.setObjectName("btn_okay") - self.horizontalLayout_2.addWidget(self.btn_okay) - self.buttonBox = QtWidgets.QDialogButtonBox(parent=eMailPreview) - self.buttonBox.setOrientation(QtCore.Qt.Orientation.Horizontal) - self.buttonBox.setStandardButtons( - QtWidgets.QDialogButtonBox.StandardButton.Cancel - ) - self.buttonBox.setCenterButtons(True) - self.buttonBox.setObjectName("buttonBox") - self.horizontalLayout_2.addWidget(self.buttonBox) - self.gridLayout.addLayout(self.horizontalLayout_2, 6, 2, 1, 1) - self.gridLayout_2.addLayout(self.gridLayout, 0, 0, 1, 1) - - self.retranslateUi(eMailPreview) - self.buttonBox.accepted.connect(eMailPreview.accept) # type: ignore - self.buttonBox.rejected.connect(eMailPreview.reject) # type: ignore - QtCore.QMetaObject.connectSlotsByName(eMailPreview) - - def retranslateUi(self, eMailPreview): - _translate = QtCore.QCoreApplication.translate - eMailPreview.setWindowTitle(_translate("eMailPreview", "eMail Voransicht")) - self.label_6.setText(_translate("eMailPreview", "Anrede")) - self.label_2.setText(_translate("eMailPreview", "Prof")) - self.label_5.setText(_translate("eMailPreview", "Art")) - self.label_4.setText(_translate("eMailPreview", "Betreff")) - self.gender_male.setText(_translate("eMailPreview", "M")) - self.gender_female.setText(_translate("eMailPreview", "W")) - self.gender_non.setText(_translate("eMailPreview", "Divers")) - self.label_3.setText(_translate("eMailPreview", "Mail")) - self.label.setText(_translate("eMailPreview", "eMail")) - self.btn_okay.setWhatsThis(_translate("eMailPreview", "test")) - self.btn_okay.setText(_translate("eMailPreview", "Senden")) diff --git a/src/ui/dialogs/dialog_sources/Ui_medianadder.py b/src/ui/dialogs/dialog_sources/Ui_medianadder.py deleted file mode 100644 index db20569..0000000 --- a/src/ui/dialogs/dialog_sources/Ui_medianadder.py +++ /dev/null @@ -1,385 +0,0 @@ -# Form implementation generated from reading ui file 'c:\Users\aky547\GitHub\SemesterapparatsManager\src\ui\dialogs\dialog_sources\medianadder.ui' -# -# Created by: PySide6 UI code generator 6.7.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 PySide6 import QtCore, QtGui, QtWidgets - - -class Ui_Dialog(object): - def setupUi(self, Dialog): - Dialog.setObjectName("Dialog") - Dialog.resize(620, 481) - icon = QtGui.QIcon() - icon.addPixmap( - QtGui.QPixmap(":/icons/resources/2795.svg"), - QtGui.QIcon.Mode.Normal, - QtGui.QIcon.State.Off, - ) - Dialog.setWindowIcon(icon) - self.label = QtWidgets.QLabel(parent=Dialog) - self.label.setGeometry(QtCore.QRect(20, 10, 47, 21)) - self.label.setObjectName("label") - self.label_2 = QtWidgets.QLabel(parent=Dialog) - self.label_2.setGeometry(QtCore.QRect(20, 40, 47, 21)) - self.label_2.setObjectName("label_2") - self.comboBox = QtWidgets.QComboBox(parent=Dialog) - self.comboBox.setGeometry(QtCore.QRect(70, 40, 69, 22)) - self.comboBox.setObjectName("comboBox") - self.comboBox.addItem("") - self.comboBox.addItem("") - self.comboBox.addItem("") - self.comboBox.addItem("") - self.lineEdit = QtWidgets.QLineEdit(parent=Dialog) - self.lineEdit.setGeometry(QtCore.QRect(70, 10, 113, 20)) - self.lineEdit.setObjectName("lineEdit") - self.label_3 = QtWidgets.QLabel(parent=Dialog) - self.label_3.setGeometry(QtCore.QRect(20, 90, 47, 21)) - self.label_3.setObjectName("label_3") - self.widget = QtWidgets.QWidget(parent=Dialog) - self.widget.setGeometry(QtCore.QRect(330, 90, 281, 381)) - self.widget.setObjectName("widget") - self.horizontalLayout = QtWidgets.QHBoxLayout(self.widget) - self.horizontalLayout.setContentsMargins(0, 0, 0, 0) - self.horizontalLayout.setObjectName("horizontalLayout") - self.tableWidget = QtWidgets.QTableWidget(parent=self.widget) - self.tableWidget.setEnabled(True) - self.tableWidget.setFocusPolicy(QtCore.Qt.FocusPolicy.NoFocus) - self.tableWidget.setAutoFillBackground(False) - self.tableWidget.setLineWidth(0) - self.tableWidget.setMidLineWidth(0) - self.tableWidget.setVerticalScrollBarPolicy( - QtCore.Qt.ScrollBarPolicy.ScrollBarAlwaysOff - ) - self.tableWidget.setHorizontalScrollBarPolicy( - QtCore.Qt.ScrollBarPolicy.ScrollBarAlwaysOff - ) - self.tableWidget.setSizeAdjustPolicy( - QtWidgets.QAbstractScrollArea.SizeAdjustPolicy.AdjustToContents - ) - self.tableWidget.setEditTriggers( - QtWidgets.QAbstractItemView.EditTrigger.NoEditTriggers - ) - self.tableWidget.setAlternatingRowColors(True) - self.tableWidget.setSelectionMode( - QtWidgets.QAbstractItemView.SelectionMode.NoSelection - ) - self.tableWidget.setTextElideMode(QtCore.Qt.TextElideMode.ElideMiddle) - self.tableWidget.setObjectName("tableWidget") - self.tableWidget.setColumnCount(4) - self.tableWidget.setRowCount(11) - item = QtWidgets.QTableWidgetItem() - self.tableWidget.setVerticalHeaderItem(0, item) - item = QtWidgets.QTableWidgetItem() - self.tableWidget.setVerticalHeaderItem(1, item) - item = QtWidgets.QTableWidgetItem() - self.tableWidget.setVerticalHeaderItem(2, item) - item = QtWidgets.QTableWidgetItem() - self.tableWidget.setVerticalHeaderItem(3, item) - item = QtWidgets.QTableWidgetItem() - self.tableWidget.setVerticalHeaderItem(4, item) - item = QtWidgets.QTableWidgetItem() - self.tableWidget.setVerticalHeaderItem(5, item) - item = QtWidgets.QTableWidgetItem() - self.tableWidget.setVerticalHeaderItem(6, item) - item = QtWidgets.QTableWidgetItem() - self.tableWidget.setVerticalHeaderItem(7, item) - item = QtWidgets.QTableWidgetItem() - self.tableWidget.setVerticalHeaderItem(8, item) - item = QtWidgets.QTableWidgetItem() - self.tableWidget.setVerticalHeaderItem(9, item) - item = QtWidgets.QTableWidgetItem() - self.tableWidget.setVerticalHeaderItem(10, item) - item = QtWidgets.QTableWidgetItem() - self.tableWidget.setHorizontalHeaderItem(0, item) - item = QtWidgets.QTableWidgetItem() - self.tableWidget.setHorizontalHeaderItem(1, item) - item = QtWidgets.QTableWidgetItem() - self.tableWidget.setHorizontalHeaderItem(2, item) - item = QtWidgets.QTableWidgetItem() - self.tableWidget.setHorizontalHeaderItem(3, item) - item = QtWidgets.QTableWidgetItem() - self.tableWidget.setItem(0, 0, item) - item = QtWidgets.QTableWidgetItem() - self.tableWidget.setItem(0, 1, item) - item = QtWidgets.QTableWidgetItem() - self.tableWidget.setItem(0, 2, item) - item = QtWidgets.QTableWidgetItem() - self.tableWidget.setItem(0, 3, item) - item = QtWidgets.QTableWidgetItem() - self.tableWidget.setItem(1, 0, item) - item = QtWidgets.QTableWidgetItem() - self.tableWidget.setItem(1, 1, item) - item = QtWidgets.QTableWidgetItem() - self.tableWidget.setItem(1, 2, item) - item = QtWidgets.QTableWidgetItem() - self.tableWidget.setItem(1, 3, item) - item = QtWidgets.QTableWidgetItem() - self.tableWidget.setItem(2, 0, item) - item = QtWidgets.QTableWidgetItem() - self.tableWidget.setItem(2, 1, item) - item = QtWidgets.QTableWidgetItem() - self.tableWidget.setItem(2, 2, item) - item = QtWidgets.QTableWidgetItem() - self.tableWidget.setItem(2, 3, item) - item = QtWidgets.QTableWidgetItem() - self.tableWidget.setItem(3, 0, item) - item = QtWidgets.QTableWidgetItem() - self.tableWidget.setItem(3, 1, item) - item = QtWidgets.QTableWidgetItem() - self.tableWidget.setItem(3, 2, item) - item = QtWidgets.QTableWidgetItem() - self.tableWidget.setItem(3, 3, item) - item = QtWidgets.QTableWidgetItem() - self.tableWidget.setItem(4, 0, item) - item = QtWidgets.QTableWidgetItem() - self.tableWidget.setItem(4, 1, item) - item = QtWidgets.QTableWidgetItem() - self.tableWidget.setItem(4, 2, item) - item = QtWidgets.QTableWidgetItem() - self.tableWidget.setItem(4, 3, item) - item = QtWidgets.QTableWidgetItem() - self.tableWidget.setItem(5, 0, item) - item = QtWidgets.QTableWidgetItem() - self.tableWidget.setItem(5, 1, item) - item = QtWidgets.QTableWidgetItem() - self.tableWidget.setItem(5, 2, item) - item = QtWidgets.QTableWidgetItem() - self.tableWidget.setItem(5, 3, item) - item = QtWidgets.QTableWidgetItem() - self.tableWidget.setItem(6, 0, item) - item = QtWidgets.QTableWidgetItem() - self.tableWidget.setItem(6, 1, item) - item = QtWidgets.QTableWidgetItem() - self.tableWidget.setItem(6, 2, item) - item = QtWidgets.QTableWidgetItem() - self.tableWidget.setItem(6, 3, item) - item = QtWidgets.QTableWidgetItem() - self.tableWidget.setItem(7, 0, item) - item = QtWidgets.QTableWidgetItem() - self.tableWidget.setItem(7, 1, item) - item = QtWidgets.QTableWidgetItem() - self.tableWidget.setItem(7, 2, item) - item = QtWidgets.QTableWidgetItem() - self.tableWidget.setItem(7, 3, item) - item = QtWidgets.QTableWidgetItem() - self.tableWidget.setItem(8, 0, item) - item = QtWidgets.QTableWidgetItem() - self.tableWidget.setItem(8, 1, item) - item = QtWidgets.QTableWidgetItem() - self.tableWidget.setItem(8, 2, item) - item = QtWidgets.QTableWidgetItem() - self.tableWidget.setItem(8, 3, item) - item = QtWidgets.QTableWidgetItem() - self.tableWidget.setItem(9, 0, item) - item = QtWidgets.QTableWidgetItem() - self.tableWidget.setItem(9, 1, item) - item = QtWidgets.QTableWidgetItem() - self.tableWidget.setItem(9, 2, item) - item = QtWidgets.QTableWidgetItem() - self.tableWidget.setItem(9, 3, item) - item = QtWidgets.QTableWidgetItem() - self.tableWidget.setItem(10, 0, item) - item = QtWidgets.QTableWidgetItem() - self.tableWidget.setItem(10, 1, item) - item = QtWidgets.QTableWidgetItem() - self.tableWidget.setItem(10, 2, item) - item = QtWidgets.QTableWidgetItem() - self.tableWidget.setItem(10, 3, item) - self.tableWidget.horizontalHeader().setDefaultSectionSize(45) - self.horizontalLayout.addWidget(self.tableWidget) - self.listWidget = QtWidgets.QListWidget(parent=Dialog) - self.listWidget.setGeometry(QtCore.QRect(10, 110, 281, 321)) - self.listWidget.setContextMenuPolicy( - QtCore.Qt.ContextMenuPolicy.CustomContextMenu - ) - self.listWidget.setObjectName("listWidget") - self.label_4 = QtWidgets.QLabel(parent=Dialog) - self.label_4.setGeometry(QtCore.QRect(330, 50, 181, 21)) - self.label_4.setObjectName("label_4") - self.label_5 = QtWidgets.QLabel(parent=Dialog) - self.label_5.setGeometry(QtCore.QRect(200, 90, 41, 21)) - self.label_5.setObjectName("label_5") - self.list_amount = QtWidgets.QLabel(parent=Dialog) - self.list_amount.setGeometry(QtCore.QRect(240, 90, 47, 21)) - self.list_amount.setObjectName("list_amount") - self.horizontalLayoutWidget = QtWidgets.QWidget(parent=Dialog) - self.horizontalLayoutWidget.setGeometry(QtCore.QRect(10, 440, 160, 31)) - self.horizontalLayoutWidget.setObjectName("horizontalLayoutWidget") - self.horizontalLayout_2 = QtWidgets.QHBoxLayout(self.horizontalLayoutWidget) - self.horizontalLayout_2.setContentsMargins(0, 0, 0, 0) - self.horizontalLayout_2.setObjectName("horizontalLayout_2") - self.btn_save = QtWidgets.QPushButton(parent=self.horizontalLayoutWidget) - self.btn_save.setFocusPolicy(QtCore.Qt.FocusPolicy.NoFocus) - self.btn_save.setObjectName("btn_save") - self.horizontalLayout_2.addWidget(self.btn_save) - self.btn_cancel = QtWidgets.QPushButton(parent=self.horizontalLayoutWidget) - self.btn_cancel.setFocusPolicy(QtCore.Qt.FocusPolicy.NoFocus) - self.btn_cancel.setObjectName("btn_cancel") - self.horizontalLayout_2.addWidget(self.btn_cancel) - self.check_use_any_book = QtWidgets.QCheckBox(parent=Dialog) - self.check_use_any_book.setGeometry(QtCore.QRect(20, 70, 141, 20)) - self.check_use_any_book.setObjectName("check_use_any_book") - self.check_use_exact_signature = QtWidgets.QCheckBox(parent=Dialog) - self.check_use_exact_signature.setGeometry(QtCore.QRect(165, 70, 121, 20)) - self.check_use_exact_signature.setObjectName("check_use_exact_signature") - - self.retranslateUi(Dialog) - QtCore.QMetaObject.connectSlotsByName(Dialog) - Dialog.setTabOrder(self.lineEdit, self.comboBox) - Dialog.setTabOrder(self.comboBox, self.listWidget) - Dialog.setTabOrder(self.listWidget, self.tableWidget) - - def retranslateUi(self, Dialog): - _translate = QtCore.QCoreApplication.translate - Dialog.setWindowTitle(_translate("Dialog", "Medien")) - self.label.setText(_translate("Dialog", "Signatur")) - self.label_2.setText(_translate("Dialog", "Modus")) - self.comboBox.setItemText(0, _translate("Dialog", "ARRAY")) - self.comboBox.setItemText(1, _translate("Dialog", "BibTeX")) - self.comboBox.setItemText(2, _translate("Dialog", "COinS")) - self.comboBox.setItemText(3, _translate("Dialog", "RIS")) - self.lineEdit.setPlaceholderText(_translate("Dialog", "Signatur / ISBN")) - self.label_3.setText(_translate("Dialog", "Queue")) - item = self.tableWidget.verticalHeaderItem(0) - item.setText(_translate("Dialog", "PPN")) - item = self.tableWidget.verticalHeaderItem(1) - item.setText(_translate("Dialog", "Signatur")) - item = self.tableWidget.verticalHeaderItem(2) - item.setText(_translate("Dialog", "Autor")) - item = self.tableWidget.verticalHeaderItem(3) - item.setText(_translate("Dialog", "ISBN")) - item = self.tableWidget.verticalHeaderItem(4) - item.setText(_translate("Dialog", "Jahr")) - item = self.tableWidget.verticalHeaderItem(5) - item.setText(_translate("Dialog", "Auflage")) - item = self.tableWidget.verticalHeaderItem(6) - item.setText(_translate("Dialog", "Sprache")) - item = self.tableWidget.verticalHeaderItem(7) - item.setText(_translate("Dialog", "Herausgeber")) - item = self.tableWidget.verticalHeaderItem(8) - item.setText(_translate("Dialog", "Seiten")) - item = self.tableWidget.verticalHeaderItem(9) - item.setText(_translate("Dialog", "Titel")) - item = self.tableWidget.verticalHeaderItem(10) - item.setText(_translate("Dialog", "Link")) - item = self.tableWidget.horizontalHeaderItem(0) - item.setText(_translate("Dialog", "Array")) - item = self.tableWidget.horizontalHeaderItem(1) - item.setText(_translate("Dialog", "BibTeX")) - item = self.tableWidget.horizontalHeaderItem(2) - item.setText(_translate("Dialog", "COinS")) - item = self.tableWidget.horizontalHeaderItem(3) - item.setText(_translate("Dialog", "RIS")) - __sortingEnabled = self.tableWidget.isSortingEnabled() - self.tableWidget.setSortingEnabled(False) - item = self.tableWidget.item(0, 0) - item.setText(_translate("Dialog", "1")) - item = self.tableWidget.item(0, 1) - item.setText(_translate("Dialog", "0")) - item = self.tableWidget.item(0, 2) - item.setText(_translate("Dialog", "1")) - item = self.tableWidget.item(0, 3) - item.setText(_translate("Dialog", "1")) - item = self.tableWidget.item(1, 0) - item.setText(_translate("Dialog", "1")) - item = self.tableWidget.item(1, 1) - item.setText(_translate("Dialog", "1")) - item = self.tableWidget.item(1, 2) - item.setText(_translate("Dialog", "0")) - item = self.tableWidget.item(1, 3) - item.setText(_translate("Dialog", "1")) - item = self.tableWidget.item(2, 0) - item.setText(_translate("Dialog", "1")) - item = self.tableWidget.item(2, 1) - item.setText(_translate("Dialog", "1")) - item = self.tableWidget.item(2, 2) - item.setText(_translate("Dialog", "1")) - item = self.tableWidget.item(2, 3) - item.setText(_translate("Dialog", "1")) - item = self.tableWidget.item(3, 0) - item.setText(_translate("Dialog", "1")) - item = self.tableWidget.item(3, 1) - item.setText(_translate("Dialog", "1")) - item = self.tableWidget.item(3, 2) - item.setText(_translate("Dialog", "1")) - item = self.tableWidget.item(3, 3) - item.setText(_translate("Dialog", "1")) - item = self.tableWidget.item(4, 0) - item.setText(_translate("Dialog", "1")) - item = self.tableWidget.item(4, 1) - item.setText(_translate("Dialog", "1")) - item = self.tableWidget.item(4, 2) - item.setText(_translate("Dialog", "1")) - item = self.tableWidget.item(4, 3) - item.setText(_translate("Dialog", "1")) - item = self.tableWidget.item(5, 0) - item.setText(_translate("Dialog", "1")) - item = self.tableWidget.item(5, 1) - item.setText(_translate("Dialog", "1")) - item = self.tableWidget.item(5, 2) - item.setText(_translate("Dialog", "1")) - item = self.tableWidget.item(5, 3) - item.setText(_translate("Dialog", "1")) - item = self.tableWidget.item(6, 0) - item.setText(_translate("Dialog", "1")) - item = self.tableWidget.item(6, 1) - item.setText(_translate("Dialog", "1")) - item = self.tableWidget.item(6, 2) - item.setText(_translate("Dialog", "0")) - item = self.tableWidget.item(6, 3) - item.setText(_translate("Dialog", "1")) - item = self.tableWidget.item(7, 0) - item.setText(_translate("Dialog", "1")) - item = self.tableWidget.item(7, 1) - item.setText(_translate("Dialog", "1")) - item = self.tableWidget.item(7, 2) - item.setText(_translate("Dialog", "1")) - item = self.tableWidget.item(7, 3) - item.setText(_translate("Dialog", "1")) - item = self.tableWidget.item(8, 0) - item.setText(_translate("Dialog", "1")) - item = self.tableWidget.item(8, 1) - item.setText(_translate("Dialog", "1")) - item = self.tableWidget.item(8, 2) - item.setText(_translate("Dialog", "1")) - item = self.tableWidget.item(8, 3) - item.setText(_translate("Dialog", "1")) - item = self.tableWidget.item(9, 0) - item.setText(_translate("Dialog", "1")) - item = self.tableWidget.item(9, 1) - item.setText(_translate("Dialog", "1")) - item = self.tableWidget.item(9, 2) - item.setText(_translate("Dialog", "1")) - item = self.tableWidget.item(9, 3) - item.setText(_translate("Dialog", "1")) - item = self.tableWidget.item(10, 0) - item.setText(_translate("Dialog", "1")) - item = self.tableWidget.item(10, 1) - item.setText(_translate("Dialog", "0")) - item = self.tableWidget.item(10, 2) - item.setText(_translate("Dialog", "1")) - item = self.tableWidget.item(10, 3) - item.setText(_translate("Dialog", "1")) - self.tableWidget.setSortingEnabled(__sortingEnabled) - self.label_4.setText(_translate("Dialog", "Belegbare Felder per Anbieter")) - self.label_5.setText(_translate("Dialog", "Anzahl:")) - self.list_amount.setText(_translate("Dialog", "0")) - self.btn_save.setText(_translate("Dialog", "Ok")) - self.btn_cancel.setText(_translate("Dialog", "Abbrechen")) - self.check_use_any_book.setToolTip( - _translate( - "Dialog", - "Verwendet ein zufälliges Buch des Datensatzes, nützlich wenn das Buch noch nicht im Apparat ist", - ) - ) - self.check_use_any_book.setText(_translate("Dialog", "Jedes Buch verwenden")) - self.check_use_exact_signature.setToolTip( - _translate( - "Dialog", "Verwendet die eingegebene Signatur für die Suche von Daten" - ) - ) - self.check_use_exact_signature.setText(_translate("Dialog", "Exakte Signatur")) diff --git a/src/ui/dialogs/dialog_sources/Ui_newMailTemplateDesigner.py b/src/ui/dialogs/dialog_sources/Ui_newMailTemplateDesigner.py deleted file mode 100644 index ee2e682..0000000 --- a/src/ui/dialogs/dialog_sources/Ui_newMailTemplateDesigner.py +++ /dev/null @@ -1,181 +0,0 @@ -# Form implementation generated from reading ui file 'c:\Users\aky547\GitHub\SemesterapparatsManager\src\ui\dialogs\dialog_sources\newMailTemplateDesigner.ui' -# -# Created by: PySide6 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 PySide6 import QtCore, QtWidgets - - -class Ui_Dialog(object): - def setupUi(self, Dialog): - Dialog.setObjectName("Dialog") - Dialog.resize(689, 572) - self.verticalLayout_2 = QtWidgets.QVBoxLayout(Dialog) - self.verticalLayout_2.setObjectName("verticalLayout_2") - self.verticalLayout = QtWidgets.QVBoxLayout() - self.verticalLayout.setObjectName("verticalLayout") - self.horizontalLayout_2 = QtWidgets.QHBoxLayout() - self.horizontalLayout_2.setObjectName("horizontalLayout_2") - self.bold = QtWidgets.QPushButton(parent=Dialog) - self.bold.setFocusPolicy(QtCore.Qt.FocusPolicy.NoFocus) - self.bold.setCheckable(True) - self.bold.setObjectName("bold") - self.horizontalLayout_2.addWidget(self.bold) - self.italic = QtWidgets.QPushButton(parent=Dialog) - self.italic.setFocusPolicy(QtCore.Qt.FocusPolicy.NoFocus) - self.italic.setCheckable(True) - self.italic.setObjectName("italic") - self.horizontalLayout_2.addWidget(self.italic) - self.underlined = QtWidgets.QPushButton(parent=Dialog) - self.underlined.setFocusPolicy(QtCore.Qt.FocusPolicy.NoFocus) - self.underlined.setCheckable(True) - self.underlined.setObjectName("underlined") - self.horizontalLayout_2.addWidget(self.underlined) - self.fontBox = QtWidgets.QFontComboBox(parent=Dialog) - self.fontBox.setFocusPolicy(QtCore.Qt.FocusPolicy.NoFocus) - self.fontBox.setObjectName("fontBox") - self.horizontalLayout_2.addWidget(self.fontBox) - self.fontSize = QtWidgets.QComboBox(parent=Dialog) - self.fontSize.setFocusPolicy(QtCore.Qt.FocusPolicy.NoFocus) - self.fontSize.setObjectName("fontSize") - self.fontSize.addItem("") - self.fontSize.addItem("") - self.fontSize.addItem("") - self.fontSize.addItem("") - self.fontSize.addItem("") - self.fontSize.addItem("") - self.fontSize.addItem("") - self.fontSize.addItem("") - self.fontSize.addItem("") - self.fontSize.addItem("") - self.fontSize.addItem("") - self.fontSize.addItem("") - self.fontSize.addItem("") - self.fontSize.addItem("") - self.fontSize.addItem("") - self.horizontalLayout_2.addWidget(self.fontSize) - spacerItem = QtWidgets.QSpacerItem( - 40, - 20, - QtWidgets.QSizePolicy.Policy.Expanding, - QtWidgets.QSizePolicy.Policy.Minimum, - ) - self.horizontalLayout_2.addItem(spacerItem) - self.verticalLayout.addLayout(self.horizontalLayout_2) - self.horizontalLayout_4 = QtWidgets.QHBoxLayout() - self.horizontalLayout_4.setObjectName("horizontalLayout_4") - self.verticalLayout.addLayout(self.horizontalLayout_4) - self.gridLayout = QtWidgets.QGridLayout() - self.gridLayout.setObjectName("gridLayout") - self.label = QtWidgets.QLabel(parent=Dialog) - self.label.setObjectName("label") - self.gridLayout.addWidget(self.label, 0, 0, 1, 1) - self.placeholder_list = QtWidgets.QComboBox(parent=Dialog) - self.placeholder_list.setFocusPolicy(QtCore.Qt.FocusPolicy.NoFocus) - self.placeholder_list.setSizeAdjustPolicy( - QtWidgets.QComboBox.SizeAdjustPolicy.AdjustToContents - ) - self.placeholder_list.setObjectName("placeholder_list") - self.placeholder_list.addItem("") - self.placeholder_list.addItem("") - self.placeholder_list.addItem("") - self.placeholder_list.addItem("") - self.placeholder_list.addItem("") - self.placeholder_list.addItem("") - self.gridLayout.addWidget(self.placeholder_list, 1, 0, 1, 1) - self.label_2 = QtWidgets.QLabel(parent=Dialog) - self.label_2.setObjectName("label_2") - self.gridLayout.addWidget(self.label_2, 0, 1, 1, 1) - self.lineEdit = QtWidgets.QLineEdit(parent=Dialog) - self.lineEdit.setEnabled(True) - self.lineEdit.setFocusPolicy(QtCore.Qt.FocusPolicy.NoFocus) - self.lineEdit.setFrame(False) - self.lineEdit.setReadOnly(True) - self.lineEdit.setObjectName("lineEdit") - self.gridLayout.addWidget(self.lineEdit, 1, 1, 1, 1) - self.insertPlaceholder = QtWidgets.QPushButton(parent=Dialog) - self.insertPlaceholder.setFocusPolicy(QtCore.Qt.FocusPolicy.NoFocus) - self.insertPlaceholder.setObjectName("insertPlaceholder") - self.gridLayout.addWidget(self.insertPlaceholder, 1, 2, 1, 1) - self.verticalLayout.addLayout(self.gridLayout) - self.label_3 = QtWidgets.QLabel(parent=Dialog) - self.label_3.setObjectName("label_3") - self.verticalLayout.addWidget(self.label_3) - self.subject = QtWidgets.QLineEdit(parent=Dialog) - self.subject.setObjectName("subject") - self.verticalLayout.addWidget(self.subject) - self.templateEdit = QtWidgets.QTextEdit(parent=Dialog) - self.templateEdit.setObjectName("templateEdit") - self.verticalLayout.addWidget(self.templateEdit) - self.horizontalLayout_3 = QtWidgets.QHBoxLayout() - self.horizontalLayout_3.setObjectName("horizontalLayout_3") - self.testTemplate = QtWidgets.QPushButton(parent=Dialog) - self.testTemplate.setObjectName("testTemplate") - self.horizontalLayout_3.addWidget(self.testTemplate) - spacerItem1 = QtWidgets.QSpacerItem( - 40, - 20, - QtWidgets.QSizePolicy.Policy.Expanding, - QtWidgets.QSizePolicy.Policy.Minimum, - ) - self.horizontalLayout_3.addItem(spacerItem1) - self.verticalLayout.addLayout(self.horizontalLayout_3) - self.verticalLayout_2.addLayout(self.verticalLayout) - 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.Save - ) - self.buttonBox.setObjectName("buttonBox") - self.verticalLayout_2.addWidget(self.buttonBox) - - self.retranslateUi(Dialog) - self.fontSize.setCurrentIndex(1) - QtCore.QMetaObject.connectSlotsByName(Dialog) - Dialog.setTabOrder(self.subject, self.templateEdit) - Dialog.setTabOrder(self.templateEdit, self.testTemplate) - Dialog.setTabOrder(self.testTemplate, self.insertPlaceholder) - Dialog.setTabOrder(self.insertPlaceholder, self.lineEdit) - Dialog.setTabOrder(self.lineEdit, self.fontSize) - Dialog.setTabOrder(self.fontSize, self.placeholder_list) - Dialog.setTabOrder(self.placeholder_list, self.fontBox) - - def retranslateUi(self, Dialog): - _translate = QtCore.QCoreApplication.translate - Dialog.setWindowTitle(_translate("Dialog", "Dialog")) - self.bold.setText(_translate("Dialog", "Fett")) - self.italic.setText(_translate("Dialog", "Kursiv")) - self.underlined.setText(_translate("Dialog", "Unterstrichen")) - self.fontSize.setItemText(0, _translate("Dialog", "8")) - self.fontSize.setItemText(1, _translate("Dialog", "9")) - self.fontSize.setItemText(2, _translate("Dialog", "11")) - self.fontSize.setItemText(3, _translate("Dialog", "12")) - self.fontSize.setItemText(4, _translate("Dialog", "14")) - self.fontSize.setItemText(5, _translate("Dialog", "16")) - self.fontSize.setItemText(6, _translate("Dialog", "18")) - self.fontSize.setItemText(7, _translate("Dialog", "20")) - self.fontSize.setItemText(8, _translate("Dialog", "22")) - self.fontSize.setItemText(9, _translate("Dialog", "24")) - self.fontSize.setItemText(10, _translate("Dialog", "26")) - self.fontSize.setItemText(11, _translate("Dialog", "28")) - self.fontSize.setItemText(12, _translate("Dialog", "36")) - self.fontSize.setItemText(13, _translate("Dialog", "48")) - self.fontSize.setItemText(14, _translate("Dialog", "76")) - self.label.setText(_translate("Dialog", "Platzhalter")) - self.placeholder_list.setItemText(0, _translate("Dialog", "«Anrede»")) - self.placeholder_list.setItemText(1, _translate("Dialog", "«ApparatsName»")) - self.placeholder_list.setItemText(2, _translate("Dialog", "«ApparatsFach»")) - self.placeholder_list.setItemText(3, _translate("Dialog", "«ApparatsNummer»")) - self.placeholder_list.setItemText(4, _translate("Dialog", "«DozentName»")) - self.placeholder_list.setItemText(5, _translate("Dialog", "«Signatur»")) - self.label_2.setText(_translate("Dialog", "Beschreibung")) - self.insertPlaceholder.setText( - _translate("Dialog", "An aktiver Position einfügen") - ) - self.label_3.setText(_translate("Dialog", "Betreff")) - self.testTemplate.setText(_translate("Dialog", "Template testen")) diff --git a/src/ui/dialogs/dialog_sources/Ui_parsed_titles.py b/src/ui/dialogs/dialog_sources/Ui_parsed_titles.py deleted file mode 100644 index bf60db7..0000000 --- a/src/ui/dialogs/dialog_sources/Ui_parsed_titles.py +++ /dev/null @@ -1,91 +0,0 @@ -# Form implementation generated from reading ui file 'c:\Users\aky547\GitHub\SemesterapparatsManager\src\ui\dialogs\dialog_sources\parsed_titles.ui' -# -# Created by: PySide6 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 PySide6 import QtCore, QtGui, QtWidgets - - -class Ui_Form(object): - def setupUi(self, Form): - Form.setObjectName("Form") - Form.resize(402, 316) - self.frame = QtWidgets.QFrame(parent=Form) - self.frame.setGeometry(QtCore.QRect(10, 10, 381, 41)) - self.frame.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel) - self.frame.setFrameShadow(QtWidgets.QFrame.Shadow.Raised) - self.frame.setObjectName("frame") - self.horizontalLayoutWidget = QtWidgets.QWidget(parent=self.frame) - self.horizontalLayoutWidget.setGeometry(QtCore.QRect(0, 0, 381, 41)) - self.horizontalLayoutWidget.setObjectName("horizontalLayoutWidget") - self.horizontalLayout = QtWidgets.QHBoxLayout(self.horizontalLayoutWidget) - self.horizontalLayout.setContentsMargins(0, 0, 0, 0) - self.horizontalLayout.setObjectName("horizontalLayout") - self.label = QtWidgets.QLabel(parent=self.horizontalLayoutWidget) - self.label.setObjectName("label") - self.horizontalLayout.addWidget(self.label) - self.count = QtWidgets.QLabel(parent=self.horizontalLayoutWidget) - font = QtGui.QFont() - font.setBold(True) - font.setWeight(75) - self.count.setFont(font) - self.count.setTextFormat(QtCore.Qt.TextFormat.PlainText) - self.count.setObjectName("count") - self.horizontalLayout.addWidget(self.count) - self.label_2 = QtWidgets.QLabel(parent=self.horizontalLayoutWidget) - self.label_2.setObjectName("label_2") - self.horizontalLayout.addWidget(self.label_2) - spacerItem = QtWidgets.QSpacerItem( - 40, - 20, - QtWidgets.QSizePolicy.Policy.Expanding, - QtWidgets.QSizePolicy.Policy.Minimum, - ) - self.horizontalLayout.addItem(spacerItem) - self.frame_2 = QtWidgets.QFrame(parent=Form) - self.frame_2.setGeometry(QtCore.QRect(10, 80, 381, 201)) - self.frame_2.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel) - self.frame_2.setFrameShadow(QtWidgets.QFrame.Shadow.Raised) - self.frame_2.setObjectName("frame_2") - self.horizontalLayoutWidget_2 = QtWidgets.QWidget(parent=self.frame_2) - self.horizontalLayoutWidget_2.setGeometry(QtCore.QRect(0, 10, 381, 191)) - self.horizontalLayoutWidget_2.setObjectName("horizontalLayoutWidget_2") - self.horizontalLayout_2 = QtWidgets.QHBoxLayout(self.horizontalLayoutWidget_2) - self.horizontalLayout_2.setContentsMargins(0, 0, 0, 0) - self.horizontalLayout_2.setObjectName("horizontalLayout_2") - self.listWidget = QtWidgets.QListWidget(parent=self.horizontalLayoutWidget_2) - self.listWidget.setObjectName("listWidget") - self.horizontalLayout_2.addWidget(self.listWidget) - self.listWidget_done = QtWidgets.QListWidget( - parent=self.horizontalLayoutWidget_2 - ) - self.listWidget_done.setObjectName("listWidget_done") - self.horizontalLayout_2.addWidget(self.listWidget_done) - self.progressBar = QtWidgets.QProgressBar(parent=Form) - self.progressBar.setGeometry(QtCore.QRect(10, 60, 381, 23)) - self.progressBar.setProperty("value", 24) - self.progressBar.setObjectName("progressBar") - self.buttonBox = QtWidgets.QDialogButtonBox(parent=Form) - self.buttonBox.setGeometry(QtCore.QRect(230, 290, 156, 23)) - self.buttonBox.setStandardButtons( - QtWidgets.QDialogButtonBox.StandardButton.Cancel - | QtWidgets.QDialogButtonBox.StandardButton.Ok - ) - self.buttonBox.setObjectName("buttonBox") - self.toolButton = QtWidgets.QToolButton(parent=Form) - self.toolButton.setGeometry(QtCore.QRect(20, 290, 25, 19)) - self.toolButton.setObjectName("toolButton") - - self.retranslateUi(Form) - QtCore.QMetaObject.connectSlotsByName(Form) - - def retranslateUi(self, Form): - _translate = QtCore.QCoreApplication.translate - Form.setWindowTitle(_translate("Form", "Form")) - self.label.setText(_translate("Form", "Es wurden")) - self.count.setText(_translate("Form", "0")) - self.label_2.setText(_translate("Form", "Signaturen gefunden.")) - self.toolButton.setText(_translate("Form", "...")) diff --git a/src/ui/dialogs/dialog_sources/Ui_reminder.py b/src/ui/dialogs/dialog_sources/Ui_reminder.py deleted file mode 100644 index 5f62089..0000000 --- a/src/ui/dialogs/dialog_sources/Ui_reminder.py +++ /dev/null @@ -1,46 +0,0 @@ -# Form implementation generated from reading ui file 'c:\Users\aky547\GitHub\SemesterapparatsManager\src\ui\dialogs\dialog_sources\reminder.ui' -# -# Created by: PySide6 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 PySide6 import QtCore, QtWidgets - - -class Ui_Erinnerung(object): - def setupUi(self, Erinnerung): - Erinnerung.setObjectName("Erinnerung") - Erinnerung.resize(358, 308) - self.buttonBox = QtWidgets.QDialogButtonBox(parent=Erinnerung) - self.buttonBox.setGeometry(QtCore.QRect(190, 270, 161, 32)) - self.buttonBox.setOrientation(QtCore.Qt.Orientation.Horizontal) - self.buttonBox.setStandardButtons( - QtWidgets.QDialogButtonBox.StandardButton.Cancel - | QtWidgets.QDialogButtonBox.StandardButton.Ok - ) - self.buttonBox.setObjectName("buttonBox") - self.message_box = QtWidgets.QTextEdit(parent=Erinnerung) - self.message_box.setGeometry(QtCore.QRect(10, 60, 341, 201)) - self.message_box.setObjectName("message_box") - self.label = QtWidgets.QLabel(parent=Erinnerung) - self.label.setGeometry(QtCore.QRect(10, 30, 61, 21)) - self.label.setObjectName("label") - self.label_2 = QtWidgets.QLabel(parent=Erinnerung) - self.label_2.setGeometry(QtCore.QRect(120, 30, 81, 21)) - self.label_2.setObjectName("label_2") - self.dateEdit = QtWidgets.QDateEdit(parent=Erinnerung) - self.dateEdit.setGeometry(QtCore.QRect(210, 30, 141, 22)) - self.dateEdit.setObjectName("dateEdit") - - self.retranslateUi(Erinnerung) - self.buttonBox.accepted.connect(Erinnerung.accept) # type: ignore - self.buttonBox.rejected.connect(Erinnerung.reject) # type: ignore - QtCore.QMetaObject.connectSlotsByName(Erinnerung) - - def retranslateUi(self, Erinnerung): - _translate = QtCore.QCoreApplication.translate - Erinnerung.setWindowTitle(_translate("Erinnerung", "Dialog")) - self.label.setText(_translate("Erinnerung", "Nachricht:")) - self.label_2.setText(_translate("Erinnerung", "Erinnerung am:")) diff --git a/src/ui/dialogs/dialog_sources/Ui_settings.py b/src/ui/dialogs/dialog_sources/Ui_settings.py deleted file mode 100644 index f6cdc11..0000000 --- a/src/ui/dialogs/dialog_sources/Ui_settings.py +++ /dev/null @@ -1,341 +0,0 @@ -# Form implementation generated from reading ui file 'c:\Users\aky547\GitHub\SemesterapparatsManager\src\ui\dialogs\dialog_sources\settings.ui' -# -# Created by: PySide6 UI code generator 6.7.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 PySide6 import QtCore, QtGui, QtWidgets - - -class Ui_Dialog(object): - def setupUi(self, Dialog): - Dialog.setObjectName("Dialog") - Dialog.setWindowModality(QtCore.Qt.WindowModality.NonModal) - Dialog.resize(651, 679) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.MinimumExpanding, QtWidgets.QSizePolicy.Policy.MinimumExpanding) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(Dialog.sizePolicy().hasHeightForWidth()) - Dialog.setSizePolicy(sizePolicy) - self.verticalLayout = QtWidgets.QVBoxLayout(Dialog) - self.verticalLayout.setObjectName("verticalLayout") - self.toolBox = QtWidgets.QToolBox(parent=Dialog) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.MinimumExpanding, QtWidgets.QSizePolicy.Policy.MinimumExpanding) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.toolBox.sizePolicy().hasHeightForWidth()) - self.toolBox.setSizePolicy(sizePolicy) - self.toolBox.setInputMethodHints(QtCore.Qt.InputMethodHint.ImhNone) - self.toolBox.setObjectName("toolBox") - self.page_1 = QtWidgets.QWidget() - self.page_1.setGeometry(QtCore.QRect(0, 0, 633, 511)) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.MinimumExpanding, QtWidgets.QSizePolicy.Policy.MinimumExpanding) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.page_1.sizePolicy().hasHeightForWidth()) - self.page_1.setSizePolicy(sizePolicy) - self.page_1.setObjectName("page_1") - self.gridLayout_3 = QtWidgets.QGridLayout(self.page_1) - self.gridLayout_3.setObjectName("gridLayout_3") - self.db_name = QtWidgets.QLineEdit(parent=self.page_1) - self.db_name.setObjectName("db_name") - self.gridLayout_3.addWidget(self.db_name, 0, 1, 1, 1) - self.label_5 = QtWidgets.QLabel(parent=self.page_1) - self.label_5.setObjectName("label_5") - self.gridLayout_3.addWidget(self.label_5, 0, 0, 1, 1) - self.db_path = QtWidgets.QLineEdit(parent=self.page_1) - self.db_path.setEnabled(False) - self.db_path.setObjectName("db_path") - self.gridLayout_3.addWidget(self.db_path, 1, 1, 1, 1) - self.label_12 = QtWidgets.QLabel(parent=self.page_1) - self.label_12.setObjectName("label_12") - self.gridLayout_3.addWidget(self.label_12, 2, 0, 1, 1) - self.label_11 = QtWidgets.QLabel(parent=self.page_1) - self.label_11.setObjectName("label_11") - self.gridLayout_3.addWidget(self.label_11, 1, 0, 1, 1) - self.tb_set_save_path = QtWidgets.QToolButton(parent=self.page_1) - self.tb_set_save_path.setObjectName("tb_set_save_path") - self.gridLayout_3.addWidget(self.tb_set_save_path, 2, 2, 1, 1) - self.tb_select_db = QtWidgets.QToolButton(parent=self.page_1) - self.tb_select_db.setObjectName("tb_select_db") - self.gridLayout_3.addWidget(self.tb_select_db, 0, 2, 1, 1) - self.save_path = QtWidgets.QLineEdit(parent=self.page_1) - self.save_path.setObjectName("save_path") - self.gridLayout_3.addWidget(self.save_path, 2, 1, 1, 1) - spacerItem = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Expanding) - self.gridLayout_3.addItem(spacerItem, 3, 1, 1, 1) - self.toolBox.addItem(self.page_1, "") - self.page_2 = QtWidgets.QWidget() - self.page_2.setGeometry(QtCore.QRect(0, 0, 633, 511)) - self.page_2.setObjectName("page_2") - self.gridLayout = QtWidgets.QGridLayout(self.page_2) - self.gridLayout.setObjectName("gridLayout") - self.zotero_library_type = QtWidgets.QLineEdit(parent=self.page_2) - self.zotero_library_type.setObjectName("zotero_library_type") - self.gridLayout.addWidget(self.zotero_library_type, 2, 2, 1, 1) - self.zotero_library_id = QtWidgets.QLineEdit(parent=self.page_2) - self.zotero_library_id.setObjectName("zotero_library_id") - self.gridLayout.addWidget(self.zotero_library_id, 1, 2, 1, 1) - self.label_4 = QtWidgets.QLabel(parent=self.page_2) - self.label_4.setObjectName("label_4") - self.gridLayout.addWidget(self.label_4, 2, 0, 1, 1) - self.label_3 = QtWidgets.QLabel(parent=self.page_2) - self.label_3.setObjectName("label_3") - self.gridLayout.addWidget(self.label_3, 1, 0, 1, 1) - self.zotero_api_key = QtWidgets.QLineEdit(parent=self.page_2) - self.zotero_api_key.setInputMethodHints(QtCore.Qt.InputMethodHint.ImhHiddenText|QtCore.Qt.InputMethodHint.ImhSensitiveData) - self.zotero_api_key.setObjectName("zotero_api_key") - self.gridLayout.addWidget(self.zotero_api_key, 0, 2, 1, 1) - self.label_2 = QtWidgets.QLabel(parent=self.page_2) - self.label_2.setObjectName("label_2") - self.gridLayout.addWidget(self.label_2, 0, 0, 1, 1) - self.toggle_api_visibility = QtWidgets.QToolButton(parent=self.page_2) - self.toggle_api_visibility.setText("") - self.toggle_api_visibility.setObjectName("toggle_api_visibility") - self.gridLayout.addWidget(self.toggle_api_visibility, 0, 3, 1, 1) - spacerItem1 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Expanding) - self.gridLayout.addItem(spacerItem1, 3, 2, 1, 1) - self.toolBox.addItem(self.page_2, "") - self.page_3 = QtWidgets.QWidget() - self.page_3.setGeometry(QtCore.QRect(0, 0, 633, 511)) - self.page_3.setObjectName("page_3") - self.horizontalLayout_2 = QtWidgets.QHBoxLayout(self.page_3) - self.horizontalLayout_2.setObjectName("horizontalLayout_2") - self.email_settings = QtWidgets.QTabWidget(parent=self.page_3) - self.email_settings.setObjectName("email_settings") - self.email_settingsPage1_2 = QtWidgets.QWidget() - self.email_settingsPage1_2.setObjectName("email_settingsPage1_2") - self.horizontalLayout_4 = QtWidgets.QHBoxLayout(self.email_settingsPage1_2) - self.horizontalLayout_4.setObjectName("horizontalLayout_4") - self.gridLayout_2 = QtWidgets.QGridLayout() - self.gridLayout_2.setObjectName("gridLayout_2") - self.use_username_smtp_login = QtWidgets.QCheckBox(parent=self.email_settingsPage1_2) - self.use_username_smtp_login.setTristate(False) - self.use_username_smtp_login.setObjectName("use_username_smtp_login") - self.gridLayout_2.addWidget(self.use_username_smtp_login, 4, 1, 1, 1) - self.label_6 = QtWidgets.QLabel(parent=self.email_settingsPage1_2) - self.label_6.setObjectName("label_6") - self.gridLayout_2.addWidget(self.label_6, 1, 0, 1, 1) - self.smtp_port = QtWidgets.QLineEdit(parent=self.email_settingsPage1_2) - self.smtp_port.setInputMethodHints(QtCore.Qt.InputMethodHint.ImhDigitsOnly|QtCore.Qt.InputMethodHint.ImhPreferNumbers) - self.smtp_port.setClearButtonEnabled(True) - self.smtp_port.setObjectName("smtp_port") - self.gridLayout_2.addWidget(self.smtp_port, 1, 1, 1, 1) - self.label_7 = QtWidgets.QLabel(parent=self.email_settingsPage1_2) - self.label_7.setObjectName("label_7") - self.gridLayout_2.addWidget(self.label_7, 2, 0, 1, 1) - self.sender_email = QtWidgets.QLineEdit(parent=self.email_settingsPage1_2) - self.sender_email.setInputMethodHints(QtCore.Qt.InputMethodHint.ImhEmailCharactersOnly) - self.sender_email.setClearButtonEnabled(True) - self.sender_email.setObjectName("sender_email") - self.gridLayout_2.addWidget(self.sender_email, 2, 1, 1, 1) - self.mail_username = QtWidgets.QLineEdit(parent=self.email_settingsPage1_2) - self.mail_username.setClearButtonEnabled(True) - self.mail_username.setObjectName("mail_username") - self.gridLayout_2.addWidget(self.mail_username, 3, 1, 1, 1) - self.label_9 = QtWidgets.QLabel(parent=self.email_settingsPage1_2) - self.label_9.setText("") - self.label_9.setObjectName("label_9") - self.gridLayout_2.addWidget(self.label_9, 7, 0, 1, 1) - self.password = QtWidgets.QLineEdit(parent=self.email_settingsPage1_2) - self.password.setInputMethodHints(QtCore.Qt.InputMethodHint.ImhHiddenText|QtCore.Qt.InputMethodHint.ImhSensitiveData) - self.password.setClearButtonEnabled(True) - self.password.setObjectName("password") - self.gridLayout_2.addWidget(self.password, 5, 1, 1, 1) - self.smtp_address = QtWidgets.QLineEdit(parent=self.email_settingsPage1_2) - self.smtp_address.setClearButtonEnabled(True) - self.smtp_address.setObjectName("smtp_address") - self.gridLayout_2.addWidget(self.smtp_address, 0, 1, 1, 1) - self.label = QtWidgets.QLabel(parent=self.email_settingsPage1_2) - self.label.setObjectName("label") - self.gridLayout_2.addWidget(self.label, 0, 0, 1, 1) - self.label_10 = QtWidgets.QLabel(parent=self.email_settingsPage1_2) - self.label_10.setObjectName("label_10") - self.gridLayout_2.addWidget(self.label_10, 5, 0, 1, 1) - self.togglePassword = QtWidgets.QPushButton(parent=self.email_settingsPage1_2) - self.togglePassword.setFocusPolicy(QtCore.Qt.FocusPolicy.NoFocus) - self.togglePassword.setText("") - self.togglePassword.setObjectName("togglePassword") - self.gridLayout_2.addWidget(self.togglePassword, 5, 2, 1, 1) - self.label_8 = QtWidgets.QLabel(parent=self.email_settingsPage1_2) - self.label_8.setObjectName("label_8") - self.gridLayout_2.addWidget(self.label_8, 3, 0, 1, 1) - self.label_13 = QtWidgets.QLabel(parent=self.email_settingsPage1_2) - self.label_13.setObjectName("label_13") - self.gridLayout_2.addWidget(self.label_13, 6, 0, 1, 1) - self.printermail = QtWidgets.QLineEdit(parent=self.email_settingsPage1_2) - self.printermail.setObjectName("printermail") - self.gridLayout_2.addWidget(self.printermail, 6, 1, 1, 1) - self.horizontalLayout_4.addLayout(self.gridLayout_2) - self.email_settings.addTab(self.email_settingsPage1_2, "") - self.email_settingsPage2_2 = QtWidgets.QWidget() - self.email_settingsPage2_2.setObjectName("email_settingsPage2_2") - self.verticalLayout_3 = QtWidgets.QVBoxLayout(self.email_settingsPage2_2) - self.verticalLayout_3.setObjectName("verticalLayout_3") - self.verticalLayout_2 = QtWidgets.QVBoxLayout() - self.verticalLayout_2.setObjectName("verticalLayout_2") - self.horizontalLayout_3 = QtWidgets.QHBoxLayout() - self.horizontalLayout_3.setObjectName("horizontalLayout_3") - spacerItem2 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum) - self.horizontalLayout_3.addItem(spacerItem2) - self.bold = QtWidgets.QPushButton(parent=self.email_settingsPage2_2) - self.bold.setCheckable(True) - self.bold.setObjectName("bold") - self.horizontalLayout_3.addWidget(self.bold) - self.italic = QtWidgets.QPushButton(parent=self.email_settingsPage2_2) - self.italic.setCheckable(True) - self.italic.setObjectName("italic") - self.horizontalLayout_3.addWidget(self.italic) - self.underscore = QtWidgets.QPushButton(parent=self.email_settingsPage2_2) - self.underscore.setCheckable(True) - self.underscore.setObjectName("underscore") - self.horizontalLayout_3.addWidget(self.underscore) - spacerItem3 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum) - self.horizontalLayout_3.addItem(spacerItem3) - self.verticalLayout_2.addLayout(self.horizontalLayout_3) - self.horizontalLayout = QtWidgets.QHBoxLayout() - self.horizontalLayout.setObjectName("horizontalLayout") - self.fontComboBox = QtWidgets.QFontComboBox(parent=self.email_settingsPage2_2) - self.fontComboBox.setObjectName("fontComboBox") - self.horizontalLayout.addWidget(self.fontComboBox) - self.font_size = QtWidgets.QComboBox(parent=self.email_settingsPage2_2) - self.font_size.setObjectName("font_size") - self.font_size.addItem("") - self.font_size.addItem("") - self.font_size.addItem("") - self.font_size.addItem("") - self.font_size.addItem("") - self.font_size.addItem("") - self.font_size.addItem("") - self.font_size.addItem("") - self.font_size.addItem("") - self.font_size.addItem("") - self.font_size.addItem("") - self.font_size.addItem("") - self.font_size.addItem("") - self.font_size.addItem("") - self.font_size.addItem("") - self.horizontalLayout.addWidget(self.font_size) - spacerItem4 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum) - self.horizontalLayout.addItem(spacerItem4) - self.verticalLayout_2.addLayout(self.horizontalLayout) - self.verticalLayout_3.addLayout(self.verticalLayout_2) - self.editSignature = QtWidgets.QTextEdit(parent=self.email_settingsPage2_2) - self.editSignature.setObjectName("editSignature") - self.verticalLayout_3.addWidget(self.editSignature) - self.debug = QtWidgets.QPushButton(parent=self.email_settingsPage2_2) - self.debug.setObjectName("debug") - self.verticalLayout_3.addWidget(self.debug) - self.email_settings.addTab(self.email_settingsPage2_2, "") - self.horizontalLayout_2.addWidget(self.email_settings) - self.toolBox.addItem(self.page_3, "") - self.page_4 = QtWidgets.QWidget() - self.page_4.setGeometry(QtCore.QRect(0, 0, 633, 511)) - self.page_4.setObjectName("page_4") - self.verticalLayout_4 = QtWidgets.QVBoxLayout(self.page_4) - self.verticalLayout_4.setObjectName("verticalLayout_4") - self.groupBox = QtWidgets.QGroupBox(parent=self.page_4) - font = QtGui.QFont() - font.setPointSize(12) - font.setBold(True) - self.groupBox.setFont(font) - self.groupBox.setObjectName("groupBox") - self.verticalLayout_5 = QtWidgets.QVBoxLayout(self.groupBox) - self.verticalLayout_5.setObjectName("verticalLayout_5") - self.scrollArea_3 = QtWidgets.QScrollArea(parent=self.groupBox) - self.scrollArea_3.setWidgetResizable(True) - self.scrollArea_3.setObjectName("scrollArea_3") - self.scrollAreaWidgetContents_3 = QtWidgets.QWidget() - self.scrollAreaWidgetContents_3.setGeometry(QtCore.QRect(0, 0, 593, 201)) - self.scrollAreaWidgetContents_3.setObjectName("scrollAreaWidgetContents_3") - self.verticalLayout_7 = QtWidgets.QVBoxLayout(self.scrollAreaWidgetContents_3) - self.verticalLayout_7.setObjectName("verticalLayout_7") - self.gridLayout_4 = QtWidgets.QGridLayout() - self.gridLayout_4.setObjectName("gridLayout_4") - self.verticalLayout_7.addLayout(self.gridLayout_4) - self.scrollArea_3.setWidget(self.scrollAreaWidgetContents_3) - self.verticalLayout_5.addWidget(self.scrollArea_3) - self.verticalLayout_4.addWidget(self.groupBox) - self.scrollArea_2 = QtWidgets.QScrollArea(parent=self.page_4) - self.scrollArea_2.setWidgetResizable(True) - self.scrollArea_2.setObjectName("scrollArea_2") - self.scrollAreaWidgetContents_2 = QtWidgets.QWidget() - self.scrollAreaWidgetContents_2.setGeometry(QtCore.QRect(0, 0, 613, 241)) - self.scrollAreaWidgetContents_2.setObjectName("scrollAreaWidgetContents_2") - self.verticalLayout_6 = QtWidgets.QVBoxLayout(self.scrollAreaWidgetContents_2) - self.verticalLayout_6.setObjectName("verticalLayout_6") - self.vertical_icons = QtWidgets.QVBoxLayout() - self.vertical_icons.setObjectName("vertical_icons") - self.verticalLayout_6.addLayout(self.vertical_icons) - self.scrollArea_2.setWidget(self.scrollAreaWidgetContents_2) - self.verticalLayout_4.addWidget(self.scrollArea_2) - self.toolBox.addItem(self.page_4, "") - self.verticalLayout.addWidget(self.toolBox) - self.buttonBox = QtWidgets.QDialogButtonBox(parent=Dialog) - self.buttonBox.setOrientation(QtCore.Qt.Orientation.Horizontal) - self.buttonBox.setStandardButtons(QtWidgets.QDialogButtonBox.StandardButton.Cancel|QtWidgets.QDialogButtonBox.StandardButton.Ok) - self.buttonBox.setObjectName("buttonBox") - self.verticalLayout.addWidget(self.buttonBox) - self.label_5.setBuddy(self.db_name) - self.label_12.setBuddy(self.save_path) - self.label_11.setBuddy(self.db_path) - - self.retranslateUi(Dialog) - self.toolBox.setCurrentIndex(2) - self.email_settings.setCurrentIndex(0) - self.buttonBox.accepted.connect(Dialog.accept) # type: ignore - self.buttonBox.rejected.connect(Dialog.reject) # type: ignore - QtCore.QMetaObject.connectSlotsByName(Dialog) - - def retranslateUi(self, Dialog): - _translate = QtCore.QCoreApplication.translate - Dialog.setWindowTitle(_translate("Dialog", "Dialog")) - self.db_name.setText(_translate("Dialog", "sap.db")) - self.label_5.setToolTip(_translate("Dialog", "

Name der Datenbank, welche verwendet werden soll. Muss auf .db enden

")) - self.label_5.setText(_translate("Dialog", "Datenbankname")) - self.label_12.setToolTip(_translate("Dialog", "Pfad, an dem heruntergeladene Dateien gespeichert werden sollen")) - self.label_12.setText(_translate("Dialog", "Temporäre Dateien")) - self.label_11.setText(_translate("Dialog", "Datenbankpfad")) - self.tb_set_save_path.setText(_translate("Dialog", "...")) - self.tb_select_db.setText(_translate("Dialog", "...")) - self.toolBox.setItemText(self.toolBox.indexOf(self.page_1), _translate("Dialog", "Datenbank")) - self.label_4.setText(_translate("Dialog", "Bibliothekstyp")) - self.label_3.setText(_translate("Dialog", "Bibliotheks-ID")) - self.label_2.setText(_translate("Dialog", "API Key")) - self.toolBox.setItemText(self.toolBox.indexOf(self.page_2), _translate("Dialog", "Zotero")) - self.use_username_smtp_login.setStatusTip(_translate("Dialog", "Anklicken, wenn Nutzername benötigt wird, um sich beim Server anzumelden")) - self.use_username_smtp_login.setText(_translate("Dialog", "Nutzername zum\n" -" Anmelden verwenden")) - self.label_6.setText(_translate("Dialog", "Port")) - self.label_7.setText(_translate("Dialog", "Sender-eMail")) - self.mail_username.setStatusTip(_translate("Dialog", "Kürzel, von der Hochschule vergeben, bsp: Aky547")) - self.label.setText(_translate("Dialog", "SMTP-Server")) - self.label_10.setText(_translate("Dialog", "Passwort")) - self.label_8.setText(_translate("Dialog", "Nutzername")) - self.label_13.setText(_translate("Dialog", "Printmail")) - self.email_settings.setTabText(self.email_settings.indexOf(self.email_settingsPage1_2), _translate("Dialog", "Allgemeines")) - self.bold.setText(_translate("Dialog", "Fett")) - self.italic.setText(_translate("Dialog", "Kursiv")) - self.underscore.setText(_translate("Dialog", "Unterstrichen")) - self.font_size.setItemText(0, _translate("Dialog", "8")) - self.font_size.setItemText(1, _translate("Dialog", "9")) - self.font_size.setItemText(2, _translate("Dialog", "11")) - self.font_size.setItemText(3, _translate("Dialog", "12")) - self.font_size.setItemText(4, _translate("Dialog", "14")) - self.font_size.setItemText(5, _translate("Dialog", "16")) - self.font_size.setItemText(6, _translate("Dialog", "18")) - self.font_size.setItemText(7, _translate("Dialog", "20")) - self.font_size.setItemText(8, _translate("Dialog", "22")) - self.font_size.setItemText(9, _translate("Dialog", "24")) - self.font_size.setItemText(10, _translate("Dialog", "26")) - self.font_size.setItemText(11, _translate("Dialog", "28")) - self.font_size.setItemText(12, _translate("Dialog", "36")) - self.font_size.setItemText(13, _translate("Dialog", "48")) - self.font_size.setItemText(14, _translate("Dialog", "72")) - self.debug.setText(_translate("Dialog", "Debug")) - self.email_settings.setTabText(self.email_settings.indexOf(self.email_settingsPage2_2), _translate("Dialog", "Signatur")) - self.toolBox.setItemText(self.toolBox.indexOf(self.page_3), _translate("Dialog", "e-Mail")) - self.groupBox.setTitle(_translate("Dialog", "Farben")) - self.toolBox.setItemText(self.toolBox.indexOf(self.page_4), _translate("Dialog", "Icons")) -- 2.49.1 From 247db562b1122f4e63f02fee02ccd1d85c7d3915 Mon Sep 17 00:00:00 2001 From: WorldTeacher Date: Wed, 17 Sep 2025 14:47:24 +0200 Subject: [PATCH 11/40] add sound: error --- src/sounds/error.mp3 | Bin 0 -> 51827 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/sounds/error.mp3 diff --git a/src/sounds/error.mp3 b/src/sounds/error.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..6b4487ff46c03a883ded41b2ad0bb2830b2dbde6 GIT binary patch literal 51827 zcmeIbc~n#99`C&~ClDYB5q1J%fDi)$?hGIxY7z!P12TzV(IyN56cq$54z-z>!;BgR zHGoC1)Cp9yR1;<~AczB6+oC8AwHBp=R;w?bx97a~UF-gF&N{hkUGB{j*vTe4U)K75 z^U3ddo;+*s4{t{j0gGt>NCf}@3jk&`0D#T@9G%^reaMvp08+>g3E;T1AZV?z{m&dQ z`tv_?<^P($eRzA_h|qtVwvnO#*1C5#Li zAUh*N1~75%N*Eb3Kz2rk3}E8il`t}7fb5J68NkH3D`8~F0NEKCGJuJ5SHj4U0kShP zWB?QAu7r^x17v4p$N(nJT?r#Y2FT9HkO54byAno*43M3XAp@8=cO{Gr86Z0&Lk2K$ z?n-z!JEofu`t<)G&Aor@By= z=-A*SI~eGQD7Ve}C1q>}Uqw6b@FjIuA~LmhFFkG2zFOILOr-9_Z`u=lCsfj&MSU73 zLA?2d`#6)c_2N*4tCwY1udAi6Oce)lIWaf2emQX^$RwY=4~G*bKQiP|GJ7_KkL{ml zQ(&c*RJm`9nce-z*%Zj65UWfm7{VViUs02N;iwbE_0F)mx?EgbuMo=c4c|+C-?#Nk zkeY0ZQ-}F#`w!x+M1!&>>Yjx{@AmuzywWeWrL~xmegBJ<0!xD1?)?>I9tHSxKNIYa zq3q#ns+aw_ugi<#Q+PK0D8dc$)&0;Kvk6CYWIF520Hgg~3af`VsE7N-@SXJ(QUcFG z3d{d9c81-^UE(+$<7X~{Hj{dX?Rx^Cl zEme)%u1;^oVhMtvE2f_HL;Df_WZNO1`G35s_+i0^+4k4lrvq!zn-Cz8*nA3Fu%}VS2p6vGOI-VJG6c>#T&Rk!U9#I1 z>%efYcJ;AeP`&qzc(i)|YDv7gP2uw`x{Q*Xfb*<2=Jx@;TR1IsU9tolynfh;{mH{0 zACOBTH6ren!tTK8P>T!8O{7=M#5Y0LaELFb9h@ z3Zg0WYQ!asd4L&&mGlNoDlbdiuctRl(9{!6o40-Vqjd^NXGrYMkB$s{i)`hLS~%oO zDt)1kam}{38uDWxap|q+DH>Q6qJm%Ma>c~UJFf_~+OkB{AH8X)oKjgU+_cP#ss*S+ z57Kv3w~jO=v(!rS_*Vr5{t|nO{Jy|8R*kPqq869q-{k8IAQ^Bnaw!aMZ|_}O8Tk8l zY8W|rCBFbE0ZhTpGl{QQ_1Zuhl{(NF54W$7Hl0<}bbe&$Ir8oIV)v`HH@E+NMv}$# z%8}5`J_CO<)R2@JoKqQC%{=rKK0`t*3}Y->Oc_8XhI?1^Fy@ogCmio( z>6@I%aRx?`;OVUuZ{(fa*~TLxxLh31a`A%@urg zs;AKHyKfoiL$BT28S8pj|LpmTp{@VJHraubnD%`~cZ+H0*0Y={+Vu5=>}6)kcJ4&; zlflV2l^HPM<)`gl)kdlhv51B?(SLC2?tCQNF64YTI$5uv2M^sX;*^zX(<;|Vi(@=j z#J;;ZlCb_L50~j;S(oev3Guw)V( zIXT8v=-;pA=tMf5J+`iY$}$i%U$&exJreAb$O_K$lPDroB{>)g+pZeVP2%wt$S#(w zT-$P8blP5tmXmbV`EdS>`#hQbDH>j@m~^Mf;B8hYp&)}h-l|J!b1o1%n6h@c_UaN4 zW#|eKjTr}sXyQ^#m&Ot&$6vJYOeh;qvUJNwZkh2d)h|c7!Q^}g4wk8O1)!0tp87Mg z0daa4qS37Giis|>r4E@BCN4p18)W&pxfZtSy(CfDYjB^xDT{#ZX{dOkj$(-UL^%Sx z)VlWI*y9{Jq<(^j(kJ?1QULF6g@^l8W_%NI;fpV3Nt*eys;V+Qo68nWr@y4(sZe(7 zH&f2fEu9~Hl(*txn(JAUrMM{<&q`?VdBmjufx$DJS zrJLf@sy&kr6>pD^b9_f&5cciCUkM@i1{ZJ!LzX zBHh04S(bRm?Jy{isFcWc#=b5T5Zn(DVNJ)LFxhPw>(nQ#5qT&F09u6-gmei$% zw0Y`(godvbhx~FTq|~OAWP`7y1}fM*^5DdT`~2FSa`m7!n2lG9JYRgmL1>|l?Z>qvDLe!QeuY7U7hMz=AVt$)LYdN>t}-Qb%VwWwPIc2S#0E%@ zCZkZQiF3ICj9~)M%9ve*EHX+VRUiS<3{NK`XhkjleQ){ju}I`_6zJT@SXdfG*F*&HiQOr{kLz-zl1{dVk&e zc~4Zeeuu}uS;h?9+4=Y7008kTNKrE_z?*`at}JTddXS4zcOVhcC@R#7XQc^{I5Z}I zQcIBo-UV7jl@~y5?}La1p_{@nIL=XNQ}H5!x2hbHimfNFd=agAb;l%X+rPv zll$n>a)G6ah3v~ZMDspPa4OpITh+z-u&>AstF6m=$L2-l0?nrF<*v)V+nZ>Eng5X-K6vCa-YhR> zvqANh7PB`^^fV72s zn3+}@y|<@Fgn-Y($B~`#V8v)J%a#lg4}1!ZGVKf3T7eKL%$hTRV2G{m^(;K}7)@P; zBW?;FCW7W}k+cLN1MCJ}VV`$5A<^Nh??zE$T3zp&tUVL>qJVg6^0wobm~MeCKfu%k z6yQ%Ddx zE^FZPiIT9M6)(E)6$G3&^J^Oq|0{ThncdS%o4)22wSWGhBKX9Q7yfsL;NQb#_7ilL zmTnkPSdF4XkAtgBE{Q{H&*m#VR0mk3Y7 z%Nk*K2P{fZ{(7=%Fz9Jc5NyC-73`};jj6X~W>_2bBU#Ju0Oifi=^Q zGzU7Qhdu4`a8FPKbzZ5ySiGn#BDf5uzWOu?4cs6fw`HU4A%|5?*u@SQII#AdZ|fG; zdeeiUxo&qlS|5yepV(e~L4UgX%gqnJ|6xhUt&WF%>w7K*-Hx>!pK5zJC3`vno*mzH zvSVB0<-A1qEcnDy!Y|al4?S9}j(^O0w&~yReH6QubEanf`cGEeA@Amtc>Q%ZY<)wx zN$}p55mzraVSrGmJ+NiX3c&Wtw|n%iejU9%E_wv7U5oeT2`1t_V*_oq?H?Jc%YD5! zY9R9PIN|TUobxg#(wfiy#ZYn;5>1>K;G%QyA*DdNjx8GQ@92dHI;HC!KV}y_yTAMf zzOp9bzRf!v$jbyT`Gd?TYKbLPq^f;wt=>&m3oliR*4fNM*Sv7Hxx23VmH@kE#$GXI zQ9d(2FubC3?8|o}XIzILh`h_8AYUlV0y;&6vRt5}C>UdkiIrqq_Swk#fZQ*|gEGo; zb9D_>%|l29lf>?B93cIED6;qJk_oHlf9~?uA63VI;YO<@(I+?)9sp*0oxN+1rLTmxIO<$(4Ps~VHWm1B?{qx+2xGNPxTN8N?8k>s- zs=E00e#zP_|Ak89THQd)ydV=;H^Fo%}3cZ+rzv zRHXFUp#m~_^kKBB)I@!2C|wvbZ~VHM3SYW%M8DO$Sg-8q&1x0eUhU}nYbk!CiaXqX zwj@CrbTx4g{^qLu8&5tR;xVU(l+8oyhbOM5Eywqef(BjP@tG#79$$@cWrec;;rWp5 zJ>ZqjBl?y8Y5q1%7WPphEhsS;9f)tYm)I`Ei=&oH+L8fL$C5t#`iA(eKrZI~-GnX& zG|+R=j3Cf7fxi`a%Q|@A$~=A^ZT)_Dm2TWe&kjh-UWIqmP84 zT&sm#B%v9nz?->Tj&}XyA5neyu>1D=%X?=Z|M+qLj}O@&zWDI|_x!iR%_3mTLb}`% zuDN2FU2%j{Z+|*+!Rtefi{e&JVrZt8I4-Rbr4Gp?)LC~En`ir9nrwvo!?Osm#}&zeo6X@6Gr5l*ds^@QxLoc-$zO-!E?d6;Zu)PAmcY_~-S)Ov`*$5uY?w)5FLrHQ z@@oY5t)y{FLTV$Pid)?sZ5~4Z1P0UZeztT`nYVj!UwAAvgbZ%x zI{05d+BlD<@5qAgsD}KzgAXAwnG}-hB7<0tf4^!T#_Am)fznfY6a2iIi`go+=y@Mf z*Bo1sS0Def0`rj3NGc1760`2>uj3+$520fc=K^{;N?~K4=;w#+l9Jtb^t;#ZmH7u?iNS$I-z2pPw+T7 zl7)-x}uHs@QlA>C<&~D=JV973h zwN~9+0vs#fa76_jIS3C~}BuN58w4m8AT zpOh)Pkd6)|Dsv)>26QEK&o|YVd9dSsZ%Dgi8;VWbp;P%?iGDb=UR?_F4VFo;i`1`I zB_C}$fRaSHSChF(;y2}rqdhX6xX51?UtNixva>kLAp&>@HY-ToT~8wf=~5gy75LD2 z7NJ$gf20?sv$xZEBT5$-6yycrH)v&AtmuKa<%zrJzsl`+5!0Q;-uBDm+}}Dc4BtO| zzID&O-p)n$zB;hv0x@CY#WXKH`l0?<|KMCnp&0K0fL<^V+s5WpoUR-Q;^XBu+)knG!5Kg0w94HQUU zCs*g;MsvLhbsjF_eB#MEseo__RZq?KnF?uO+pg#%1J~n+Z5fTLp)VR*rUKAS@g*UJ zf7y=;wP`>O0QJF-z)YVF8y(3)mnA1(Svq!gAyKeXWE63MYb=X7?#GHRe|8-Vr-7 z(*zBhxF{M%HQ|9E5ecK2_yT&1=Pclrvlw^|Y$O``-E0PuP6$J(3p$Z@JVe}<)*O9Duj-wJ4I7@N`vrl(FAqvx+D@F#w$tDVgX`rIwx?| zjnD3NhzPv_WVAK+Z=Jay@^%ac(vwvbG&x z)%}1z-4$uk^Dq$sTq`j>)pHWr)4L1J)4@^GI%`B+FUI6~FUCAe`v95WyNfLB`oUzn zi-!kv&R8zw5ea_{S%U4KvDZ@&^DF3_3odQ&(N&zEUvJZI6mX(8@Rn zi#F5pN+`*v8`J^peYOaP=^W@c8-*wADd%e|Kfyx{#I^C8UzQ1dic(NVe}ke;98rW1 z9J~()4Sd2*wVtil`49sYX+mp-n#>UBu%Dnt#LLN(&|-@j#)Cp=#=;Vvp@ckO^%L0+ z3o41}87xOsBvShfinDM;js!tjuo?6zDzp#nh&#m~AV*~bA>e%2ePs+c4(=}m3GiS={+eR6Oa?;WV&JK4nh3&VI%zXnEtFo7j+lW9VY^bU$lhtatnq+0 z+XU{fZ58>>c59iC7aUe*0+CS6^Kr{jxCkDKXM*#^yxisIz0=~*wjh2?nKQg2% zd0!IBU+~3j+4+BmLmmZ{z?!#P#{VuuS{O)_(V7WrCm=%sm{cL}NV6@~a2bth8b;v4 zuqRf*Qr^=5J_s`fRNSX>seSEep!oY838y~^Z{F1CS~_sT@8n-0d-py5FtGP94*~VU z6IsUwS3`I4alR>VcTSoAUFZ;O4c0G3>kk9965^?@KLC9{6U*(}<{%vX+9F1K63x|S zqNjhPy9;So@O|ko%q|Qw2k-5`e;4`gJDR)H)HbQ4Z7L*@7GDhHT1Z|??k&ol?^lyF z*vVL!3rK1X2g)4cH>pr^ecAwppSBeDqAIvtvIa$W+bViBY!k6DVNnFIt}=E_0? z91}qs%ail~=z?0Y2=fW`z8{O>Y3P>({3WfIiLno!Tpw-s^7ASnlNyvjBPnk7@k|sfq z(IOU6tAZSRE_v_H<@M!z@HQhh{ElF44pDHUU$YX~e$Mmva=8v;OQ5Y#+x$xI_3yaF z(2O(6*8A6ph#KrfHC0xgtF=%AatafpW}3l@QpA)PlUP-D40YW6X>JUjQi&VQpGK%LDw&Gu zqXq~W3Kmh#$hTLBzE&#gaPV1%{yC*E+9H#QZme0-|8h+uA_rYs zy_>hzm)5%2B>5tG+y#7)C~D-RPrTpK5a{}Q(mzimTaboKofn2a25G0H1RtSp7F>6FYzGEUt*2= zGkz)1AYQC!PDzYqxQVh8e(+*sF)I>|6C`UjJxM0HeXwR#GJ8d!&7>q591CRLZ%0rw5&};v5=Q z?Sl}@U*ps*Irj2_Z$UhYraceFBE4lnen(&ht*gQBb`@QuKG11zQady9>}{kON6vtj zZwjuWgvFni36`fk%8jCiFeEsw10ukJBY=o4(tJd#{t!02Ra>Mg$D5>MDbQH}u>TgI zyrdC+#yBv`br^(U^Rz1X*u7pHV?HxQPOf2NxB1CppApf zWILd9@r&=u61I4oXK7yuzO^xgf(5OA)8ciWcd!3Wckfd2o%cL&tk3?Pl9TY`uuS zMDKfB$YtWMfqmr)TyPsY#Bo7vCK%X8Qt2u*`k7|IEdXSR90QrG`!iSu^rfHKLaQcL z1!O-GL+;iAw|b9zOiX4F;e`}RL9v`F+_itfM}~?m-l_$B#b4hy{5>3U!)uu>hqyD7dZ^6|BA9C#*UZZBjx`w903n3=tAgQ)yQD3^a5IxJ0t9%<=p6FwRe0g)HUkK3<7%Ut!PC00QqH z*qJh*(&QZ-asyOnkN9f?007)YfZ7L$k%c#sJ}LDnWh5yUYfO+&^i;3L{~QL9@|De7k<5{5SR9Z6lU>hHRZNOhztr8%m z72;#|K~No}N`|Iuxn2*T)<$p80cIgtZQr-PdTQG>#-6=)NZT#fTWU4vV+%odD^cXG z`Z%=m1l8VklPtCf)naV{){#M9C?@FI!%QB*?Ct`Vl-KLz#qEvfb+=TO2F{bqr2f+j+Kl2lYr4`q(Bb_bO6o z{FnczL#1zc{2s;k_y0Mi;P@?$WRdCiPli@4c7m3o8yA&Xv7Cz$y{uzgFRvNQD1o(T zwv!Vkggng>@Q^>N7Mvl`Fq}(pM>6dQRA8FW-lq>rXLp?%ubfn3@F~Z9-;uSi5TDwF zK%e;>%tn_$yXyuUGo!E}j^7an}J9W@Z@kr(d2b>pOiqg1lk9UOS3@?H@`(ivc=&rJeLE})Q^Z=yg!J!4xvycr;4rKs^P_RM#cH+l zcuHjHOg(&rY~Cfv^%o93nU%DdU3;DFwrI(fX*M0srQq2FCJSPDBA-EEv1cl43DXYS zCcJ>)%SODgklL4w#?zdO?Lhn{3)N@4@ryC~eK0=1BW^Xj3dyp41a+>3g1G1$S{Z%n zj8A>>US6SO5noC6t>)SFOgb==uPX}GW4bZ@`e~6LLk_YKX1mKomUgyY1GJzhFsxbx zdWGlVeO-6y4l*`M(;p!{CI|9MisUNIql&9~(da@REU->ZbI!BEEl&)DAs1^WfP^V8 z_6|74J?m}5erO#-C#^H16vx}cWak`Pu0}9%`DeapQCzZVDL9AI$+MB=aw^S>`gC9>Z$pEd+l=rz%CkwWy-|xmpU?lmDhvcU&Ftk)hGjKa&K0 zh`+|t|E@#sH*rXdw_E@U?ox)&1j;0N075I`X`Tctj;a~gF z`k*jX4fe*M`U~O39@`N@aB(PNqDQz;mD6jReN+Wr<>@Ah^!%Wx9@<~RhzG>osrmB5(P;4)1j3*`7(>9_5kD{pvbD6)eER~Db zv;!vMM~eQ5$~H>|hJfHZ1@6U)y5lukdDc$|M1ko-RNTM2)aO59=ifOP3Pd0l)n8+M znq2u)c@X==T1S9a1RcT5PS}v31?Fvdz>1Yb_Ora-=8>c2NNZX7Ak@2rosELHHJ-8` zpejdp`XP^R1wbj|6H*sI==CZaAL;D7-`qo-?p2O3x2q3_*d8_m9Trmxmr{A*)o$T6 z4v|#PMS065JJ5l!oLsl+z$I3>?zRj`yM$HF5RmzWQo!B{FTW2{0^SOKc-f?^>d>KE zFN@?3w2Q0!yE_N(Xvz)H<L z)J`>gASK^4Ea!JAM#ig1y}2{Yqo(~eUph?S7YTUbxedD5c9Z4A>M3q@evo}|jt1GH zh%)t-xe_nr?L%EqP?xvJI?cT+Z&-xqOq0fuC`E9Eie-n2G|%{y1rG>gp9ep+eyM?( ztCtdR2kzH@WT?CJ%@)4D;`_V*tV4@~G5;e&>OM2?T4FzSx1vCme?jeEtjoJ&K?x+h zl%>9{t}12~i0)O#qs1k=Sk=?`40ZNkjq8DMwR(@bIsc^!$XBxq#Ce1q^(F-F(9>ON zt(;OE^F+M@T_6twKo5ntR2z+ug53{EdNEAhmE|Ga7u?*&KfMxG78tmoAnn9i&o)1A=agz*!*%(0GreGH^U7F6BBF{y<=v zY3mI$F+r6Ja3&z7zNSbGog(-T`Qb%ij7Wg=BGEU(ecAEyKh9D--hP-Lm_wtEhC2Y0x?N^wU2dafh9>qfWEUB zt2yAv%(b*uG=6!*F{~?-GB{QmfG~|5lVU7Aq7|-d#OIk;<88aH^Tr5ued3YfXzp!Y zvxBcEvea{nCEK(P&nS7~P`t%QFAN$o!wsNjB8oE6^SZ6aB9{a;I#+hP`lX+Wi5r6f zjW~gVgtl}Vm|^rsl0F^7$|7ljbPp)S)FDAXZ?S%xiB5h+<%m=A?mT2{+D}|f^_g9} zq-JaM>5zi}=I_{z#4LpFlM4i6+d;e;^}7+~sXQb@;m8(+_okw(AyH6qt8>GuZ_9Gt zF}-N633@WtYNI*b9x(AVA=E$YMb(&E(CKWPZ`6>jU}F752KsnekV9-mN75@mg-aLd zT{AYS+cXSxR!gR=$AV! zYr-oOPn)>1R}Hc{`pZJ(XzS+K4%My((>qU8)p7^1rt69{{^{i}pWa;c@}$L`(~V+{ z3ryZED;849^IsO2=!TIOPSJI$;5!A5Vfo96QUs5vqRHfRVqqdIpB8?2p#BK`jlyxe z3Aocuy=e_4YD4bACQ>N_Zz{p#SE^pKu18yZ=5e-U5P=^phGDDgn~>x8Y0jDR6Q5SG zoXx7qwr-gV-R=*7q5|o`avVpB_ma|@NTOa;j`}8=a6uKgenk50%_m(C%R*taG(Oy~ z%*KiYx1hrqSb?G-OJJK_NUkqKM)U?(DHGqf9a%iPX6G!c(2kugu+UA&+o-@AfvGBOZy;MdH1%ZpmOnB3%8~_BydcTQ zl-?!vMJ|P~GXb4-Aihfy6PToA;F-zIQ*np6>l5S}d8Ds36h;KzUD9vu}qUa5}b!el` z_)d5{z90TTEA_)LA#EhMY#y8?J*^$HL9CP#2g-x{|V-IHJng}`@m3$uGCGsTeJ_nE`b z@Y2U7>>BdcEu5F)+~lD9Z$Ay(x3fBEcgmgm>WF@icreC1^_+d(jlcx}wW<^EM(N?k zE30oXB(}EHPG0hP{?UP4H+u>)E%g>GFLX)`)VzeRb*%M%zq$9$xiZFL9_3-;rUR#I zLKquN%y53N>n6W8`bLHA{qn&5>XR>59ax#V;xgaQp=z+&VM+5DC$#_(?$Rpq+0;yW z`odd30gHd7v=iAz`E~;Z-L8-(=avHj=@lm6U9I~hOp&L-m*IE2M7D7cyD<~^w_wd| zzm)|c*_9V{2Ti!v#W0*tY0w!HTUSV$35B01i7!_3st}GI(cTJN-Hh!-n9j}F*7{G= zD&bE{V_cj7#6s(~J=J5;JG<3GlPgX2?>!-w*Nq2=V5V@by;sKbYmDwadp&meg=Bl* z=7r!c5vv~rA2h3JkNw)tHdcCiG@MH!B=p0q8M(@&p(eF}uHLp)8O)MWH&H@;*#Jf>lUy3#dNeRfxz!0x z(%YK=cGZHGGmIM7`xQqN$0EGCW>+}6_7)B8YVRxX2kLbUhiplMXmW%#t^Lj!8rz=c zXToBgolG@hiyJ?2fWi>WSonF{TG(Mz%7xPkRc(6p7CfzUSN2U**Hv^0Eoo5l2^RDc z(fSq=5(m&5Pi&D+Dic=&+DOVGxS7pYVa2o`qE+-}NXX(|4D|O&?1tBOK~3iBQ?)Tl z0286lqXYgcdD|ypXhK6SUxo!ahtJgfd>99S%i8T^#Q>Z`M)Bh_Zl|I}d+KqUkCm}M9t!6U-jc0i%0XW09TdOO{{ zi_G%S)g0V6wg=O`s}Z(TjdUYdOF9ba)5(we$W&2(uACXa_VG$6*(vZRVB2JmrcWLu zY7C5~E`v(T@8q-NtG zSNq++Mx+Kgf{&b00RncO+kjNrNT~8>g>@f&=w(X%df=~RZyrOAfD*?&+n4RoAYWq= zxAj(%35_<{-v6-rK0)KCD8mN$1Ostj_PbGU|IRm!@h5T=0Qa07GPa)NK-cruGI{sK z8;`a}9lvp|{3&+*IN2jjYOXk>j6%A$-wG2noh~5XT6Ubd;H}rIU5Vso{D959LpYW= zFSSb^^}Ayem5uZPC_<8n6LAwp%OZ-)i8-G68Lwedm=zMQTfN zLn|xg^;kH^t+ANCo3*ZI@R|&zx_7D)VS#tr$nL4KXV^1QE49tl&8pnS&2nU4@ER(F zPKfY66~I`4rIG?yimxL@%!SCk`xvv?DUds=nsD7%y z$gD41`Lr?6xy;|&dC%2y>N9V)qjqL$MpmYW5Vz|69EPZDMoxL zm|OvH9*voOTrRh?^!JYC3qbfi(&b~54Vb-s+W-FrjlH>(V|4Q+>m2cwOGJJNp@@0a z4NO&CEcx7^Bwir0obm- z7w6lYu8OVG5HEcNk|_E4)lwIk-}bB7bXbhV$1lk@+KX?#bGuTXN?PA=L!H5VAm%{G z>@HjWCwbOko1ZPb*%v?lI3?N|#j1v7HZ~j~E;q}pmA@%r=HOM7_MrnZQQ(c5q!;Lj zov8yNNoLI5IjHQm2o3mM!kC5#LiAUh*N1~75%N*Eb3Kz2rk3}E8i zl`vk6^}o-~h|quU;T%>lmLUU#XJp6#CeB?6BSQwr&d87fOq{zCMurTKosl5}m^gPO zj0_ndJ0n8|Fmdim7#T7^c1DH_VB*}BFfwF-?2HTC5#LiAUh*N K1~75%O8h_chGakh literal 0 HcmV?d00001 -- 2.49.1 From 8b8c1c93932a791f94f0257ddf079447ae7bb57d Mon Sep 17 00:00:00 2001 From: WorldTeacher Date: Thu, 18 Sep 2025 07:14:40 +0200 Subject: [PATCH 12/40] chore: add logging, minor changes --- src/logic/swb.py | 50 ++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 40 insertions(+), 10 deletions(-) diff --git a/src/logic/swb.py b/src/logic/swb.py index 826c101..ad88ff4 100644 --- a/src/logic/swb.py +++ b/src/logic/swb.py @@ -1,11 +1,27 @@ +import sys import xml.etree.ElementTree as ET from dataclasses import dataclass, field +from datetime import datetime from typing import Dict, Iterable, List, Optional, Tuple +import loguru import requests +from src import LOG_DIR from src.logic.dataclass import BookData +log = loguru.logger +log.remove() +log.add(sys.stdout, level="INFO") +log.add(f"{LOG_DIR}/application.log", rotation="1 MB", retention="10 days") + +log.add( + f"{LOG_DIR}/{datetime.now().strftime('%Y-%m-%d')}.log", + rotation="1 day", + retention="1 month", +) + + # ----------------------- # Dataclasses # ----------------------- @@ -393,15 +409,19 @@ def book_from_marc(rec: MarcRecord) -> BookData: ) isbn = subfield_values(rec, "020", "a") + lang = subfield_values(rec, "041", "a") + return BookData( ppn=ppn, title=title, signature=signature, - edition=first_subfield_value(rec, "250", "a"), + edition=first_subfield_value(rec, "250", "a") or "", year=year, - pages=first_subfield_value(rec, "300", "a"), - publisher=first_subfield_value(rec, "264", "b"), + pages=first_subfield_value(rec, "300", "a") or "", + publisher=first_subfield_value(rec, "264", "b") or "", isbn=isbn, + language=lang, + link="", ) @@ -418,7 +438,7 @@ class SWB: url = self.url.format(query) - print("Fetching from SWB:", url) + log.debug(url) headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3", "Accept": "application/xml", @@ -427,7 +447,7 @@ class SWB: response = requests.get(url, headers=headers) if response.status_code != 200: raise Exception(f"Error fetching data from SWB: {response.status_code}") - # print(response.text) + # #print(response.text) data = response.content # extract top-level response @@ -437,12 +457,22 @@ class SWB: def getBooks(self, query_args: Iterable[str]) -> List[BookData]: records: List[Record] = self.get(query_args) books: List[BookData] = [] - title = query_args[1].split("=")[1] - # print(len(records), "records found") + # extract title from query_args if present + title = None + for arg in query_args: + if arg.startswith("pica.tit="): + title = arg.split("=")[1] + break for rec in records: book = book_from_marc(rec.recordData) books.append(book) - books = [ - b for b in books if b.title and b.title.lower().startswith(title.lower()) - ] + if title: + books = [ + b + for b in books + if b.title and b.title.lower().startswith(title.lower()) + ] return books + + def getLinkForBook(self, book: BookData) -> str: + results = self.getBooks() -- 2.49.1 From 4f28cfe55c9e79d5e6ecf668c181aa8c3653b451 Mon Sep 17 00:00:00 2001 From: WorldTeacher Date: Thu, 18 Sep 2025 07:15:15 +0200 Subject: [PATCH 13/40] add sender name to mail config dataclass --- config/config.py | 1 + 1 file changed, 1 insertion(+) diff --git a/config/config.py b/config/config.py index 0eb5564..6d5bce8 100644 --- a/config/config.py +++ b/config/config.py @@ -50,6 +50,7 @@ class Mail: smtp_server: str port: int sender: str + sender_name: str password: str use_user_name: bool printer_mail: str -- 2.49.1 From f4e75831d5dbe9d242d311028528916ce811974f Mon Sep 17 00:00:00 2001 From: WorldTeacher Date: Thu, 18 Sep 2025 07:15:34 +0200 Subject: [PATCH 14/40] rework email templates --- ...Auflösung des Semesterapparates {AppNr}.eml | 52 ++++++------------- ...um Semesterapparat {AppNr} - {AppName}.eml | 50 ++++++------------ ...s Semesterapparates {AppNr} - {Appname}.eml | 10 ++++ ...ür Semesterapparat {AppNr} - {AppName}.eml | 36 ++++++------- 4 files changed, 56 insertions(+), 92 deletions(-) create mode 100644 mail_vorlagen/Information zur Auflösung des Semesterapparates {AppNr} - {Appname}.eml diff --git a/mail_vorlagen/Information bezüglich der Auflösung des Semesterapparates {AppNr}.eml b/mail_vorlagen/Information bezüglich der Auflösung des Semesterapparates {AppNr}.eml index 96e504c..7a343b9 100644 --- a/mail_vorlagen/Information bezüglich der Auflösung des Semesterapparates {AppNr}.eml +++ b/mail_vorlagen/Information bezüglich der Auflösung des Semesterapparates {AppNr}.eml @@ -1,37 +1,17 @@ -Message-ID: -Date: Mon, 17 Jul 2023 12:59:04 +0200 -MIME-Version: 1.0 -User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:102.0) Gecko/20100101 - Thunderbird/102.13.0 -Content-Language: de-DE -From: {user_name} <{user_mail}> -Subject: =?UTF-8?Q?Information_bez=c3=bcglich_der_Aufl=c3=b6sung_des_Semeste?= - =?UTF-8?Q?rapparates_=7bAppNr=7d?= -X-Mozilla-Draft-Info: internal/draft; vcard=0; receipt=0; DSN=0; uuencode=0; - attachmentreminder=0; deliveryformat=0 -X-Identity-Key: id1 -Fcc: imap://aky547@imap.ph-freiburg.de/INBOX/Sent -Content-Type: text/html; charset=UTF-8 -Content-Transfer-Encoding: 8bit - - - - - {greeting} -
-

auf die E-Mail bezüglich der Auflösung oder Verlängerung der Semesterapparate haben wir von Ihnen keine Rückmeldung erhalten. Deshalb gehen wir davon aus, dass der Apparat aufgelöst werden kann.

-

Die Medien, die in den Apparaten aufgestellt waren, werden nun wieder regulär ausleihbar und sind dann an ihren Standorten bei den Fächern zu finden.

-

-

Falls Sie den Apparat erneut, oder einen neuen Apparat anlegen wollen, können Sie mir das ausgefüllte Formular zur Einrichtung des Apparates (https://www.ph-freiburg.de/bibliothek/lernen/semesterapparate/info-lehrende-sem.html) zukommen lassen.

-

Im Falle einer Verlängerung des Apparates reicht eine Antwort auf diese Mail.
-

-

Bei Fragen können Sie sich jederzeit an mich wenden.
-

-


-

-
-- 
-{signature}
-
- - \ No newline at end of file +Subject: Information bezüglich der Auflösung des Semesterapparates {AppNr} + + +{greeting} + +auf die E-Mail bezüglich der Auflösung oder Verlängerung der Semesterapparate haben wir von Ihnen keine Rückmeldung erhalten. Deshalb gehen wir davon aus, dass der Apparat aufgelöst werden kann. +Die Medien, die in den Apparaten aufgestellt waren, werden nun wieder regulär ausleihbar und sind dann an ihren Standorten bei den Fächern zu finden. + +Falls Sie den Apparat erneut, oder einen neuen Apparat anlegen wollen, +können Sie mir das ausgefüllte Formular zur Einrichtung des Apparates +https://www.ph-freiburg.de/bibliothek/lernen/semesterapparate/info-lehrende-sem.html +zukommen lassen. Im Falle einer Verlängerung des Apparates reicht eine Antwort auf diese Mail. + +Bei Fragen können Sie sich jederzeit an mich wenden. + +{signature} \ No newline at end of file diff --git a/mail_vorlagen/Information zum Semesterapparat {AppNr} - {AppName}.eml b/mail_vorlagen/Information zum Semesterapparat {AppNr} - {AppName}.eml index 0ffbc1a..756a6b8 100644 --- a/mail_vorlagen/Information zum Semesterapparat {AppNr} - {AppName}.eml +++ b/mail_vorlagen/Information zum Semesterapparat {AppNr} - {AppName}.eml @@ -1,36 +1,16 @@ -Message-ID: -Date: Tue, 12 Sep 2023 13:01:35 +0200 -MIME-Version: 1.0 -User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:102.0) Gecko/20100101 - Thunderbird/102.15.0 -From: Alexander Kirchner -Subject: Information zum Semesterapparat {AppNr} - {Appname} -Content-Language: de-DE -X-Mozilla-Draft-Info: internal/draft; vcard=0; receipt=0; DSN=0; uuencode=0; - attachmentreminder=0; deliveryformat=0 -X-Identity-Key: id1 -Fcc: imap://aky547@imap.ph-freiburg.de/INBOX/Sent -Content-Type: text/html; charset=UTF-8 -Content-Transfer-Encoding: 8bit - - - - - {greeting} -
-

Ihr Semesterapparat {Appname} wurde angelegt.

-

Unter folgendem Link können Sie die Apparate einsehen:

-

https://bsz.ibs-bw.de/aDISWeb/app?service=direct/0/Home/$DirectLink&sp=SOPAC42&sp=SWI00000002&noRedir

-

Ihr Apparat ist unter {AppSubject} > {Profname} > {AppNr} {Appname}.
-

-


-

-

Noch nicht vorhandene Medien wurden vorgemerkt und werden nach Rückkehr in die Bibliothek eingearbeitet.

-

Bei Fragen können Sie sich per Mail bei mir melden.
-

-
-- 
-{signature}
-
- - +Subject: Information zum Semesterapparat {AppNr} - {AppName} + + +{greeting} + +Ihr Semesterapparat {Appname} wurde angelegt. +Unter folgendem Link können Sie die Apparate einsehen: +https://bsz.ibs-bw.de/aDISWeb/app?service=direct/0/Home/$DirectLink&sp=SOPAC42&sp=SWI00000002&noRedir + +Ihr Apparat ist unter {AppSubject} > {Profname} > {AppNr} {Appname} + +Noch nicht vorhandene Medien wurden vorgemerkt und werden nach Rückkehr in die Bibliothek eingearbeitet. +Bei Fragen können Sie sich per Mail bei mir melden. + +{signature} \ No newline at end of file diff --git a/mail_vorlagen/Information zur Auflösung des Semesterapparates {AppNr} - {Appname}.eml b/mail_vorlagen/Information zur Auflösung des Semesterapparates {AppNr} - {Appname}.eml new file mode 100644 index 0000000..836fd9b --- /dev/null +++ b/mail_vorlagen/Information zur Auflösung des Semesterapparates {AppNr} - {Appname}.eml @@ -0,0 +1,10 @@ + +Subject: Information zur Auflösung des Semesterapparates {AppNr} - {Appname} + + +{greeting} + +Ihr Semesterapparat "{Appname} ({AppNr})" wurde wie besprochen aufgelöst. +Die Medien sind von nun an wieder in den Regalen zu finden. + +{signature} \ No newline at end of file diff --git a/mail_vorlagen/Neuauflagen für Semesterapparat {AppNr} - {AppName}.eml b/mail_vorlagen/Neuauflagen für Semesterapparat {AppNr} - {AppName}.eml index 5f0b3be..0598de7 100644 --- a/mail_vorlagen/Neuauflagen für Semesterapparat {AppNr} - {AppName}.eml +++ b/mail_vorlagen/Neuauflagen für Semesterapparat {AppNr} - {AppName}.eml @@ -1,21 +1,15 @@ -Subject: Vorschläge für Neuauflagen - {Appname} -MIME-Version: 1.0 -Content-Type: text/html; charset="UTF-8" -Content-Transfer-Encoding: 8bit - - - - - - -

{greeting}

-


-

für Ihren Semesterapparat {AppNr} - {Appname} wurden folgende Neuauflagen gefunden:

-


-

{newEditions}

-


-

Sollen wir die alte(n) Auflage(n) aus dem Apparat durch diese austauschen?

-


-

-- 

-

{signature}

-

\ No newline at end of file + +Subject: Neuauflagen für Semesterapparat {AppNr} - {AppName} + + +{greeting} + +Für Ihren Semesterapparat {AppNr} - {Appname} wurden folgende Neuauflagen gefunden: + +{newEditions} + +Sollen wir die alte(n) Auflage(n) aus dem Apparat durch diese austauschen? +Nicht vorhandene Exemplare werden an die Erwerbungsabteilung weitergegeben +und nach Erhalt der Medien in den Apparat eingearbeitet. + +{signature} \ No newline at end of file -- 2.49.1 From 65c86a65cd2b9294a487b38f67b1b1392147569a Mon Sep 17 00:00:00 2001 From: WorldTeacher Date: Mon, 22 Sep 2025 09:36:39 +0200 Subject: [PATCH 15/40] mail: add new mail to request new editions of books --- ...ngen für Semesterapparat {AppNr} - {AppName}.eml | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 mail_vorlagen/Bitte um Bestellung von Neuerwerbungen für Semesterapparat {AppNr} - {AppName}.eml diff --git a/mail_vorlagen/Bitte um Bestellung von Neuerwerbungen für Semesterapparat {AppNr} - {AppName}.eml b/mail_vorlagen/Bitte um Bestellung von Neuerwerbungen für Semesterapparat {AppNr} - {AppName}.eml new file mode 100644 index 0000000..c69299c --- /dev/null +++ b/mail_vorlagen/Bitte um Bestellung von Neuerwerbungen für Semesterapparat {AppNr} - {AppName}.eml @@ -0,0 +1,13 @@ + +Subject: Bitte um Bestellung von Neuerwerbungen für Semesterapparat {AppNr} - {AppName} + + +Hallo zusammen, + +für den Semesterapparat {AppNr} - {Appname} wurden folgende Neuauflagen gefunden: + +{newEditionsOrdered} + +Wäre es möglich, diese, oder neuere Auflagen (wenn vorhanden), zu bestellen? + +{signature} \ No newline at end of file -- 2.49.1 From 7079b4d47f930b9e068086fafda81b1a08841eb7 Mon Sep 17 00:00:00 2001 From: WorldTeacher Date: Mon, 22 Sep 2025 09:37:25 +0200 Subject: [PATCH 16/40] add insert, request, ordered functions for new edition books --- src/backend/database.py | 91 +++++++++++++++++++++++++++++++++-------- 1 file changed, 74 insertions(+), 17 deletions(-) diff --git a/src/backend/database.py b/src/backend/database.py index 32b60c6..ab47368 100644 --- a/src/backend/database.py +++ b/src/backend/database.py @@ -22,6 +22,7 @@ from src.backend.db import ( CREATE_TABLE_FILES, CREATE_TABLE_MEDIA, CREATE_TABLE_MESSAGES, + CREATE_TABLE_NEWEDITIONS, CREATE_TABLE_PROF, CREATE_TABLE_SUBJECTS, CREATE_TABLE_USER, @@ -87,6 +88,7 @@ class Database: "elsa", "elsa_files", "elsa_media", + "neweditions", ] for table in required_tables: @@ -116,6 +118,8 @@ class Database: query = CREATE_ELSA_FILES_TABLE case "elsa_media": query = CREATE_ELSA_MEDIA_TABLE + case "neweditions": + query = CREATE_TABLE_NEWEDITIONS case _: log.error(f"Table {table_name} is not a valid table name") self.query_db(query) @@ -233,7 +237,7 @@ class Database: def query_db( self, query: str, - args: Tuple[Any, Any] = (), # type:ignore + args: Tuple[Any] = (), # type:ignore one: bool = False, # type:ignore ) -> Union[Tuple[Any, Any], List[Tuple[Any, Any]]]: """ @@ -430,9 +434,8 @@ class Database: app_id = row[2] prof_id = row[3] - sig_val = getattr(bookdata, "signature", None) or "" - title_val = getattr(bookdata, "title", None) or "" - + sig_val = bookdata.signature + title_val = bookdata.title if mode == 1: if sig_re.search(sig_val): results.append((bookdata, app_id, prof_id)) @@ -471,7 +474,7 @@ class Database: """ result = self.query_db( "SELECT id FROM media WHERE bookdata=? AND app_id=? AND prof_id=?", - (dump_pickle(bookdata), app_id, prof_id), + (bookdata.to_dict, app_id, prof_id), one=True, ) return result[0] @@ -535,17 +538,23 @@ class Database: ret_result.append(data) return ret_result - def getBooksByProfId(self, prof_id: int, deleted: int = 0): + def getBooksByProfId( + self, prof_id: int, deleted: int = 0 + ) -> list[dict[str, Union[int, BookData]]]: """ Get the Books based on the professor id - Args: - prof_id (str): The ID of the professor - deleted (int, optional): The state of the book. Set to 1 to include deleted ones. Defaults to 0. + Parameters + ---------- + prof_id : int + The ID of the professor + deleted : int, optional + If set to 1, it will include deleted books, by default 0 - Returns: - - list[dict[int, BookData, int]]: A list of dictionaries containing the id, the metadata of the book and the availability of the book + Returns + ------- + list[dict[str, Union[int, BookData]]] + A list of dictionaries containing the id, the metadata of the book and the availability of the book """ qdata = self.query_db( f"SELECT id,bookdata,available FROM media WHERE prof_id={prof_id} AND (deleted={deleted if deleted == 0 else '1 OR deleted=0'})" @@ -1170,7 +1179,7 @@ class Database: (semester, apparat_nr, apparat.name), ) # delete all books associated with the app_id - print(apparat_nr, app_id) + # print(apparat_nr, app_id) self.query_db("UPDATE media SET deleted=1 WHERE app_id=?", (app_id,)) def isEternal(self, id): @@ -1243,11 +1252,11 @@ class Database: else False ) - def checkApparatExistsById(self, app_id: Union[str, int]) -> bool: - """a check to see if the apparat is already present in the database, based on the id + def checkApparatExistsByNr(self, app_nr: Union[str, int]) -> bool: + """a check to see if the apparat is already present in the database, based on the nr. This query will exclude deleted apparats Args: - app_id (Union[str, int]): the id of the apparat + app_nr (Union[str, int]): the id of the apparat Returns: bool: True if the apparat is present, False if not @@ -1255,7 +1264,9 @@ class Database: return ( True if self.query_db( - "SELECT appnr FROM semesterapparat WHERE appnr=?", (app_id,), one=True + "SELECT id FROM semesterapparat WHERE appnr=? and deletion_status=0", + (app_nr,), + one=True, ) else False ) @@ -1844,3 +1855,49 @@ class Database: result = cursor.fetchone() connection.close() return result + + def getBookIdByPPN(self, ppn: str) -> int: + query = f"SELECT id FROM media WHERE bookdata LIKE '%{ppn}%'" + data = self.query_db(query) + if data: + return data[0][0] + else: + return None + + def getNewEditionsByApparat(self, apparat_id: int) -> list[BookData]: + """Get all new editions for a specific apparat + + Args: + apparat_id (int): the id of the apparat + + Returns: + list[tuple]: A list of tuples containing the new editions data + """ + query = "SELECT * FROM neweditions WHERE for_apparat=?" + results = self.query_db(query, (apparat_id,)) + res = [] + for result in results: + old_edition_edition = self.query_db( + "SELECT bookdata FROM media WHERE id=?", (result[2],), one=True + ) + res.append(BookData().from_string(result[1])) + return res + + def setOrdered(self, newBook_id: int): + query = "UPDATE neweditions SET ordered=1 WHERE id=?" + self.query_db(query, (newBook_id,)) + + def getNewEditionId(self, newBook: BookData): + query = "SELECT id FROM neweditions WHERE new_bookdata=?" + params = (newBook.to_dict,) + data = self.query_db(query, params, one=True) + if data: + return data[0] + else: + return None + + def insertNewEdition(self, newBook: BookData, oldBookId: int, for_apparat: int): + query = "INSERT INTO neweditions (new_bookdata, old_edition_id, for_apparat) VALUES (?,?,?)" + params = (newBook.to_dict, oldBookId, for_apparat) + + self.query_db(query, params) -- 2.49.1 From c4be1d8bfab6f72d42e7ec35e99c026328f9b01f Mon Sep 17 00:00:00 2001 From: WorldTeacher Date: Mon, 22 Sep 2025 09:42:15 +0200 Subject: [PATCH 17/40] files: reorganize imports, remove print lines --- src/ui/dialogs/Ui_login.py | 11 ++++--- src/ui/dialogs/Ui_mail_preview.py | 48 ++++++++++++++++++++---------- src/ui/dialogs/Ui_parsed_titles.py | 4 +-- src/ui/dialogs/Ui_settings.py | 2 +- src/ui/dialogs/about.py | 8 +++-- src/ui/dialogs/app_ext.py | 4 ++- src/ui/dialogs/bookdata.py | 2 +- src/ui/dialogs/docuprint.py | 21 ++++++------- src/ui/dialogs/elsa_add_entry.py | 12 ++++---- src/ui/dialogs/elsa_gen_confirm.py | 3 +- src/ui/dialogs/login.py | 2 +- src/ui/dialogs/medienadder.py | 5 ++-- src/ui/dialogs/parsed_titles.py | 14 +++++---- src/ui/dialogs/popup_confirm.py | 2 +- src/ui/dialogs/settings.py | 19 ++++++------ src/ui/widgets/MessageCalendar.py | 2 -- src/ui/widgets/admin_edit_prof.py | 18 ++++++----- src/ui/widgets/admin_edit_user.py | 9 +++--- src/ui/widgets/calendar_entry.py | 10 ++++--- src/ui/widgets/collapse.py | 17 +---------- src/ui/widgets/filepicker.py | 4 +-- src/ui/widgets/graph.py | 15 +++++----- src/utils/icon.py | 5 ++-- 23 files changed, 127 insertions(+), 110 deletions(-) diff --git a/src/ui/dialogs/Ui_login.py b/src/ui/dialogs/Ui_login.py index 7e807ac..7844c6b 100644 --- a/src/ui/dialogs/Ui_login.py +++ b/src/ui/dialogs/Ui_login.py @@ -10,8 +10,9 @@ import hashlib from PySide6 import QtCore, QtWidgets -from src.backend.database import Database from src.backend.admin_console import AdminCommands +from src.backend.database import Database + class Ui_Dialog(object): def setupUi(self, Dialog): @@ -64,13 +65,11 @@ class Ui_Dialog(object): def login(self): username = self.lineEdit.text() password = self.lineEdit_2.text() - print(type(username), password) + # print(type(username), password) # Assuming 'Database' is a class to interact with your database - db = Database() + db = Database() - hashed_password = hashlib.sha256( - password.encode() - ).hexdigest() + hashed_password = hashlib.sha256(password.encode()).hexdigest() if len(db.getUsers()) == 0: AdminCommands().create_admin() self.lresult = 1 # Indicate successful login diff --git a/src/ui/dialogs/Ui_mail_preview.py b/src/ui/dialogs/Ui_mail_preview.py index 32ed57d..1980790 100644 --- a/src/ui/dialogs/Ui_mail_preview.py +++ b/src/ui/dialogs/Ui_mail_preview.py @@ -6,17 +6,18 @@ # run again. Do not edit this file unless you know what you are doing. -from PySide6 import QtCore, QtGui, QtWidgets -import subprocess -import tempfile import os import re +import subprocess +import tempfile + from omegaconf import OmegaConf +from PySide6 import QtCore, QtWidgets config = OmegaConf.load("config.yaml") -class Ui_eMailPreview(object): +class Ui_eMailPreview(object): def setupUi( self, eMailPreview, @@ -31,7 +32,10 @@ class Ui_eMailPreview(object): self.buttonBox = QtWidgets.QDialogButtonBox(eMailPreview) self.buttonBox.setGeometry(QtCore.QRect(310, 630, 341, 32)) self.buttonBox.setOrientation(QtCore.Qt.Orientation.Horizontal) - self.buttonBox.setStandardButtons(QtWidgets.QDialogButtonBox.StandardButton.Cancel|QtWidgets.QDialogButtonBox.StandardButton.Ok) + self.buttonBox.setStandardButtons( + QtWidgets.QDialogButtonBox.StandardButton.Cancel + | QtWidgets.QDialogButtonBox.StandardButton.Ok + ) self.buttonBox.setObjectName("buttonBox") self.gridLayoutWidget = QtWidgets.QWidget(eMailPreview) self.gridLayoutWidget.setGeometry(QtCore.QRect(10, 10, 661, 621)) @@ -46,7 +50,11 @@ class Ui_eMailPreview(object): self.prof_name.setObjectName("prof_name") self.gridLayout.addWidget(self.prof_name, 2, 2, 1, 1) self.label_3 = QtWidgets.QLabel(self.gridLayoutWidget) - self.label_3.setAlignment(QtCore.Qt.AlignmentFlag.AlignLeading|QtCore.Qt.AlignmentFlag.AlignLeft|QtCore.Qt.AlignmentFlag.AlignTop) + self.label_3.setAlignment( + QtCore.Qt.AlignmentFlag.AlignLeading + | QtCore.Qt.AlignmentFlag.AlignLeft + | QtCore.Qt.AlignmentFlag.AlignTop + ) self.label_3.setObjectName("label_3") self.gridLayout.addWidget(self.label_3, 5, 0, 1, 1) self.mail_name = QtWidgets.QLineEdit(self.gridLayoutWidget) @@ -81,7 +89,12 @@ class Ui_eMailPreview(object): self.gender_non = QtWidgets.QRadioButton(self.gridLayoutWidget) self.gender_non.setObjectName("gender_non") self.horizontalLayout_3.addWidget(self.gender_non) - spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum) + spacerItem = QtWidgets.QSpacerItem( + 40, + 20, + QtWidgets.QSizePolicy.Policy.Expanding, + QtWidgets.QSizePolicy.Policy.Minimum, + ) self.horizontalLayout_3.addItem(spacerItem) self.gridLayout.addLayout(self.horizontalLayout_3, 4, 2, 1, 1) self.label_6 = QtWidgets.QLabel(self.gridLayoutWidget) @@ -89,8 +102,8 @@ class Ui_eMailPreview(object): self.gridLayout.addWidget(self.label_6, 4, 0, 1, 1) self.retranslateUi(eMailPreview) - self.buttonBox.accepted.connect(eMailPreview.accept) # type: ignore - self.buttonBox.rejected.connect(eMailPreview.reject) # type: ignore + self.buttonBox.accepted.connect(eMailPreview.accept) # type: ignore + self.buttonBox.rejected.connect(eMailPreview.reject) # type: ignore QtCore.QMetaObject.connectSlotsByName(eMailPreview) self._appid = app_id self._appname = app_name @@ -127,7 +140,6 @@ class Ui_eMailPreview(object): return "Sehr geehrte Frau" elif self.gender_non.isChecked(): return "Guten Tag" - def set_mail(self): email_template = self.comboBox.currentText() @@ -145,14 +157,19 @@ class Ui_eMailPreview(object): mail_html = mail_template.split("")[1] mail_html = "" + mail_html mail_html = mail_html.format( - Profname=self.prof_name.text().split(" ")[1], Appname=self._appname, AppNr=self._appid, AppSubject = self._subject,greeting = self.get_greeting() + Profname=self.prof_name.text().split(" ")[1], + Appname=self._appname, + AppNr=self._appid, + AppSubject=self._subject, + greeting=self.get_greeting(), ) self.mail_body.setHtml(mail_html) + def load_mail_templates(self): mail_templates = os.listdir("mail_vorlagen") mail_templates = [f for f in mail_templates if f.endswith(".eml")] - print(mail_templates) + # print(mail_templates) self.comboBox.addItems(mail_templates) def save_mail(self): @@ -168,16 +185,17 @@ class Ui_eMailPreview(object): ) as f: f.write(mail) self.mail_path = f.name - print(self.mail_path) + # print(self.mail_path) # open the file using thunderbird subprocess.Popen([f"{self.mail_path}"]) # delete the file # os.remove(self.mail_path) + def launch(): app = QtWidgets.QApplication([]) eMailPreview = QtWidgets.QDialog() ui = Ui_eMailPreview() - ui.setupUi(eMailPreview, "1","Test","Biologie","Kirchner, Alexander") + ui.setupUi(eMailPreview, "1", "Test", "Biologie", "Kirchner, Alexander") eMailPreview.show() - app.exec() \ No newline at end of file + app.exec() diff --git a/src/ui/dialogs/Ui_parsed_titles.py b/src/ui/dialogs/Ui_parsed_titles.py index b05ad32..b42eb2a 100644 --- a/src/ui/dialogs/Ui_parsed_titles.py +++ b/src/ui/dialogs/Ui_parsed_titles.py @@ -110,7 +110,7 @@ class Ui_Form(object): self.progressBar.setValue(value) def thread_quit(self): - print("Terminating thread") + # print("Terminating thread") self.thread.terminate() self.thread.quit() self.thread.deleteLater() @@ -144,7 +144,7 @@ class Ui_Form(object): def determine_progress(self, signal): # check length of listWidget length = self.listWidget.count() - print(f"Length of listWidget: {length}") + # print(f"Length of listWidget: {length}") if length == 0: logger.log_info("AutoAdder finished") self.buttonBox.accepted.emit() diff --git a/src/ui/dialogs/Ui_settings.py b/src/ui/dialogs/Ui_settings.py index 36c39f2..4009cf6 100644 --- a/src/ui/dialogs/Ui_settings.py +++ b/src/ui/dialogs/Ui_settings.py @@ -169,7 +169,7 @@ class Ui_Dialog(object): name = application.application file_type = application.extensions display_name = application.name - print(name, file_type, display_name) # + # print(name, file_type, display_name) # # create new item item = QtWidgets.QTreeWidgetItem(self.treeWidget) item.setText(0, display_name) diff --git a/src/ui/dialogs/about.py b/src/ui/dialogs/about.py index 972495e..496aadf 100644 --- a/src/ui/dialogs/about.py +++ b/src/ui/dialogs/about.py @@ -1,7 +1,9 @@ -from .dialog_sources.Ui_about import Ui_about -from PySide6 import QtWidgets import PySide6 -from src import Icon, __version__, __author__ +from PySide6 import QtWidgets + +from src import Icon, __author__, __version__ + +from .dialog_sources.about_ui import Ui_about class About(QtWidgets.QDialog, Ui_about): diff --git a/src/ui/dialogs/app_ext.py b/src/ui/dialogs/app_ext.py index 8db924f..5bf77e7 100644 --- a/src/ui/dialogs/app_ext.py +++ b/src/ui/dialogs/app_ext.py @@ -1,7 +1,9 @@ from PySide6 import QtWidgets -from .dialog_sources.Ui_apparat_extend import Ui_Dialog + from src import Icon +from .dialog_sources.apparat_extend_ui import Ui_Dialog + class ApparatExtendDialog(QtWidgets.QDialog, Ui_Dialog): def __init__( diff --git a/src/ui/dialogs/bookdata.py b/src/ui/dialogs/bookdata.py index 9561604..553a59f 100644 --- a/src/ui/dialogs/bookdata.py +++ b/src/ui/dialogs/bookdata.py @@ -2,7 +2,7 @@ from PySide6 import QtWidgets from src.logic.dataclass import BookData -from .dialog_sources.Ui_edit_bookdata import Ui_Dialog +from .dialog_sources.edit_bookdata_ui import Ui_Dialog class BookDataUI(QtWidgets.QDialog, Ui_Dialog): diff --git a/src/ui/dialogs/docuprint.py b/src/ui/dialogs/docuprint.py index 3f87c1c..98b399d 100644 --- a/src/ui/dialogs/docuprint.py +++ b/src/ui/dialogs/docuprint.py @@ -1,10 +1,11 @@ -from .dialog_sources.documentprint_ui import Ui_Dialog -from PySide6 import QtWidgets, QtCore -from src import Icon - -from src.utils.richtext import SemapSchilder, SemesterDocument -from src.backend import Semester, Database from natsort import natsorted +from PySide6 import QtWidgets + +from src import Icon +from src.backend import Database, Semester +from src.utils.richtext import SemapSchilder, SemesterDocument + +from .dialog_sources.documentprint_ui import Ui_Dialog class DocumentPrintDialog(QtWidgets.QDialog, Ui_Dialog): @@ -23,25 +24,25 @@ class DocumentPrintDialog(QtWidgets.QDialog, Ui_Dialog): # Ensure the signal is connected only once try: self.pushButton_2.clicked.disconnect() - except TypeError: + except (TypeError, RuntimeWarning): pass # Signal was not connected before self.pushButton_2.clicked.connect(self.on_pushButton_2_clicked) try: self.pushButton.clicked.disconnect() - except TypeError: + except (TypeError, RuntimeWarning): pass self.pushButton.clicked.connect(self.on_pushButton_clicked) try: self.btn_load_current_apparats.clicked.disconnect() - except TypeError: + except (TypeError, RuntimeWarning): pass self.btn_load_current_apparats.clicked.connect(self.load_current_clicked) try: self.manualCheck.clicked.disconnect() - except TypeError: + except (TypeError, RuntimeWarning): pass self.manualCheck.clicked.connect(self.manual_request) diff --git a/src/ui/dialogs/elsa_add_entry.py b/src/ui/dialogs/elsa_add_entry.py index c15a0ab..db2c0cc 100644 --- a/src/ui/dialogs/elsa_add_entry.py +++ b/src/ui/dialogs/elsa_add_entry.py @@ -1,9 +1,11 @@ -from .dialog_sources.Ui_elsa_add_table_entry import Ui_Dialog -from src.logic.webrequest import WebRequest, BibTextTransformer -from src import Icon from PySide6 import QtWidgets -from src.transformers.transformers import DictToTable + +from src import Icon +from src.logic.webrequest import BibTextTransformer, WebRequest from src.logic.zotero import ZoteroController +from src.transformers.transformers import DictToTable + +from .dialog_sources.elsa_add_table_entry_ui import Ui_Dialog zot = ZoteroController() dtt = DictToTable() @@ -174,7 +176,7 @@ class ElsaAddEntry(QtWidgets.QDialog, Ui_Dialog): self.stackedWidget.setCurrentIndex(3) def search(self, pages=None): - print("searching") + # #print("searching") param = self.searchIdent.text() web = WebRequest() web.get_ppn(param) diff --git a/src/ui/dialogs/elsa_gen_confirm.py b/src/ui/dialogs/elsa_gen_confirm.py index 8b15842..e63cc7a 100644 --- a/src/ui/dialogs/elsa_gen_confirm.py +++ b/src/ui/dialogs/elsa_gen_confirm.py @@ -1,6 +1,7 @@ -from .dialog_sources.Ui_elsa_generator_confirm import Ui_Dialog from PySide6 import QtWidgets +from .dialog_sources.elsa_generator_confirm_ui import Ui_Dialog + class ElsaGenConfirm(QtWidgets.QDialog, Ui_Dialog): def __init__(self, parent=None, data=None): diff --git a/src/ui/dialogs/login.py b/src/ui/dialogs/login.py index cc8fa34..649cbaf 100644 --- a/src/ui/dialogs/login.py +++ b/src/ui/dialogs/login.py @@ -70,7 +70,7 @@ class LoginDialog(Ui_Dialog): def login(self): username = self.lineEdit.text() password = self.lineEdit_2.text() - # print(type(username), password) + # #print(type(username), password) # Assuming 'Database' is a class to interact with your database hashed_password = hashlib.sha256(password.encode()).hexdigest() diff --git a/src/ui/dialogs/medienadder.py b/src/ui/dialogs/medienadder.py index ecdeac6..e8b5da5 100644 --- a/src/ui/dialogs/medienadder.py +++ b/src/ui/dialogs/medienadder.py @@ -1,8 +1,9 @@ from PySide6 import QtCore, QtGui, QtWidgets -from .dialog_sources.medianadder_ui import Ui_Dialog from src import Icon +from .dialog_sources.medianadder_ui import Ui_Dialog + class MedienAdder(QtWidgets.QDialog, Ui_Dialog): def __init__(self, parent=None): @@ -78,4 +79,4 @@ def launch_gui(): dialog = MedienAdder() dialog.show() app.exec() - # print(dialog.mode, dialog.data, dialog.result()) + # #print(dialog.mode, dialog.data, dialog.result()) diff --git a/src/ui/dialogs/parsed_titles.py b/src/ui/dialogs/parsed_titles.py index d5ba73b..dca3cdd 100644 --- a/src/ui/dialogs/parsed_titles.py +++ b/src/ui/dialogs/parsed_titles.py @@ -1,17 +1,19 @@ +import sys + +import loguru from PySide6 import QtWidgets +from src import LOG_DIR from src.backend import AutoAdder - from .dialog_sources.parsed_titles_ui import Ui_Form -import loguru -import sys -from src import LOG_DIR + log = loguru.logger log.remove() log.add(sys.stdout, level="INFO") log.add(f"{LOG_DIR}/application.log", rotation="1 MB", retention="10 days") + class ParsedTitles(QtWidgets.QWidget, Ui_Form): def __init__(self, parent=None): super().__init__(parent) @@ -32,7 +34,7 @@ class ParsedTitles(QtWidgets.QWidget, Ui_Form): self.progressBar.setValue(value) def worker_quit(self): - # print("Terminating worker") + # #print("Terminating worker") self.worker.terminate() self.worker.quit() self.worker.deleteLater() @@ -66,7 +68,7 @@ class ParsedTitles(QtWidgets.QWidget, Ui_Form): def determine_progress(self, signal): # check length of listWidget length = self.listWidget.count() - # print(f"Length of listWidget: {length}") + # #print(f"Length of listWidget: {length}") if length == 0: log.info("AutoAdder finished") self.buttonBox.accepted.emit() diff --git a/src/ui/dialogs/popup_confirm.py b/src/ui/dialogs/popup_confirm.py index 8b7f19c..2d8401a 100644 --- a/src/ui/dialogs/popup_confirm.py +++ b/src/ui/dialogs/popup_confirm.py @@ -10,7 +10,7 @@ from PySide6 import QtWidgets from src import Icon -from .dialog_sources.Ui_confirm_extend import Ui_extend_confirm +from .dialog_sources.confirm_extend_ui import Ui_extend_confirm class ConfirmDialog(QtWidgets.QDialog, Ui_extend_confirm): diff --git a/src/ui/dialogs/settings.py b/src/ui/dialogs/settings.py index df918d0..c21b70d 100644 --- a/src/ui/dialogs/settings.py +++ b/src/ui/dialogs/settings.py @@ -1,10 +1,12 @@ -from PySide6 import QtCore, QtGui, QtWidgets -from src import Icon, settings -from .dialog_sources.settings_ui import Ui_Dialog as _settings -from src.ui.widgets.iconLine import IconWidget -import loguru import sys -from src import LOG_DIR + +import loguru +from PySide6 import QtCore, QtGui, QtWidgets + +from src import LOG_DIR, Icon, settings +from src.ui.widgets.iconLine import IconWidget + +from .dialog_sources.settings_ui import Ui_Dialog as _settings log = loguru.logger log.remove() @@ -12,7 +14,6 @@ log.add(sys.stdout, level="INFO") log.add(f"{LOG_DIR}/application.log", rotation="1 MB", retention="10 days") - base = """' " -empty_signature = """ +empty_signature = """""" - blocks so that + str.format(...) won't treat CSS as placeholders. The doubled braces + will automatically render back to single braces after formatting. + """ - hr { height: 1px; border-width: 0; } + def repl(m): + start, css, end = m.group(1), m.group(2), m.group(3) + css_escaped = css.replace("{", "{{").replace("}", "}}") + return f"{start}{css_escaped}{end}" - li.unchecked::marker { content: "\2610"; } + return re.sub( + r"(]*>)(.*?)()", + repl, + html, + flags=re.IGNORECASE | re.DOTALL, + ) - li.checked::marker { content: "\2612"; } - +def _split_eml_headers_body(eml_text: str) -> tuple[str, str]: + """ + Return (headers, body_html). Robustly split on first blank line. + Accepts lines that contain only spaces/tabs as the separator. + """ -


-""" + parts = re.split(r"\r?\n[ \t]*\r?\n", eml_text, maxsplit=1) + if len(parts) == 2: + return parts[0], parts[1] + # Fallback: try to split right after the Content-Transfer-Encoding line + m = re.search( + r"(?:^|\r?\n)Content-Transfer-Encoding:.*?(?:\r?\n)", + eml_text, + flags=re.I | re.S, + ) + if m: + return eml_text[: m.end()], eml_text[m.end() :] + return "", eml_text # last resort: treat entire content as body class Mail_Dialog(QtWidgets.QDialog, MailPreviewDialog): def __init__( self, - app_id, - app_name, - app_subject, - prof_name, - prof_mail, + app_id=None, + app_name=None, + app_subject=None, + prof_name=None, + prof_mail=None, accepted_books=None, + ordered_books=None, parent=None, default_mail="Information zum Semesterapparat", ): @@ -58,6 +84,7 @@ class Mail_Dialog(QtWidgets.QDialog, MailPreviewDialog): self.subject = app_subject self.profname = prof_name self.books = accepted_books if accepted_books is not None else [] + self.ordered_books = ordered_books if ordered_books is not None else [] self.mail_data = "" self.signature = self.determine_signature() self.prof_mail = prof_mail @@ -65,52 +92,29 @@ class Mail_Dialog(QtWidgets.QDialog, MailPreviewDialog): self.prof_name.setText(prof_name) self.mail_name.setText(self.prof_mail) self.load_mail_templates() - # if none of the radio buttons is checked, disable the accept button of the dialog self.setWindowIcon(Icon("mail").icon) self.btn_okay.setEnabled(False) Icon("edit_note", self.newTemplate) self.newTemplate.clicked.connect(self.open_new_template) if default_mail is not None: - # get the nearest match to the default mail for i in range(self.comboBox.count()): if default_mail in self.comboBox.itemText(i): default_mail = self.comboBox.itemText(i) break self.comboBox.setCurrentText(default_mail) + self.comboBox.currentIndexChanged.connect(self.set_mail) + # re-render when user changes greeting via radio buttons self.gender_female.clicked.connect(self.set_mail) self.gender_male.clicked.connect(self.set_mail) self.gender_non.clicked.connect(self.set_mail) + + # reflect initial state (OK disabled until a greeting is chosen) + self._update_ok_button() self.btn_okay.clicked.connect(self.createAndSendMail) - def open_new_template(self): - log.info("Opening new template dialog") - # TODO: implement new mail template dialog - dialog = MailTemplateDialog() - dialog.updateSignal.connect(self.load_mail_templates) - dialog.exec() - - pass - - def determine_signature(self): - if config.mail.signature is empty_signature or config.mail.signature == "": - return """Mit freundlichen Grüßen -Ihr Semesterapparatsteam -Mail: semesterapparate@ph-freiburg.de -Tel.: 0761/682-778 | 07617682-545""" - else: - return config.mail.signature - - def load_mail_templates(self): - # print("loading mail templates") - log.info("Loading mail templates") - mail_templates = os.listdir("mail_vorlagen") - log.info(f"Mail templates: {mail_templates}") - self.comboBox.clear() - for template in mail_templates: - self.comboBox.addItem(template) - + # add these helpers inside Mail_Dialog def get_greeting(self): prof = self.profname.split(" ")[0] if self.gender_male.isChecked(): @@ -124,45 +128,104 @@ Tel.: 0761/682-778 | 07617682-545""" name = f"{self.profname.split(' ')[1]} {self.profname.split(' ')[0]}" return f"Guten Tag {name}," + def _update_ok_button(self): + checked = ( + self.gender_male.isChecked() + or self.gender_female.isChecked() + or self.gender_non.isChecked() + ) + self.btn_okay.setEnabled(checked) + + def _on_gender_toggled(self, checked: bool): + # Only refresh when a button becomes checked + if checked: + self.set_mail() + + def open_new_template(self): + log.info("Opening new template dialog") + dialog = MailTemplateDialog() + dialog.updateSignal.connect(self.load_mail_templates) + dialog.exec() + + def determine_signature(self): + # use equality, not identity + if ( + config.mail.signature == empty_signature + or config.mail.signature.strip() == "" + ): + return """Mit freundlichen Grüßen +Ihr Semesterapparatsteam +Mail: semesterapparate@ph-freiburg.de +Tel.: 0761/682-778 | 0761/682-545""" + else: + return config.mail.signature + + def load_mail_templates(self): + log.info("Loading mail templates") + mail_templates = [ + f for f in os.listdir("mail_vorlagen") if f.lower().endswith(".eml") + ] + log.info(f"Mail templates: {mail_templates}") + self.comboBox.clear() + for template in mail_templates: + self.comboBox.addItem(template) + def set_mail(self): log.info("Setting mail") + self._update_ok_button() # keep OK enabled state in sync + email_template = self.comboBox.currentText() - if email_template == "": + if not email_template: log.debug("No mail template selected") return + with open(f"mail_vorlagen/{email_template}", "r", encoding="utf-8") as f: - mail_template = f.read() + eml_text = f.read() + + # header label for UI (unchanged) email_header = email_template.split(".eml")[0] if "{AppNr}" in email_template: - email_header = email_template.split(".eml")[0] - email_header = email_header.format(AppNr=self.appid, AppName=self.appname) + email_header = email_header.format(AppNr=self.appid, AppName=self.appname) self.mail_header.setText(email_header) - self.mail_data = mail_template.split("")[0] - mail_html = mail_template.split("")[1] - mail_html = "" + mail_html - Appname = self.appname - mail_html = mail_html.format( - Profname=self.profname.split(" ")[0], - Appname=Appname, - AppNr=self.appid, - AppSubject=self.subject, - greeting=self.get_greeting(), - signature=self.signature, - newEditions="
".join( - [ - f"{book.title} von {book.author} (ISBN: {book.isbn}, Auflage: {book.edition}, In Bibliothek: {'ja' if getattr(book, 'library_location', 1) == 1 else 'nein'})" - for book in self.books - ] - ) - if self.books - else "keine neuen Auflagen gefunden", - ) - self.mail_body.setHtml(mail_html) + headers, body_html = _split_eml_headers_body(eml_text) + body_html = _escape_braces_in_style(body_html) + + # compute greeting from the current toggle selection + greeting = self.get_greeting() + + try: + body_html = body_html.format( + Profname=self.profname.split(" ")[ + 0 + ], # last name if your template uses {Profname} + Appname=self.appname, + AppNr=self.appid, + AppSubject=self.subject, + greeting=greeting, + signature=self.signature, + newEditions="\n".join( + [ + f"- {book.title} (ISBN: {','.join(book.isbn)}, Auflage: {book.edition if book.edition else 'nicht bekannt'}, In Bibliothek: {'ja' if getattr(book, 'signature', None) is not None and 'Handbibliothek' not in str(book.library_location) else 'nein'}, Typ: {book.get_book_type()}) Aktuelle Auflage: {book.old_book.edition if book.old_book and book.old_book.edition else 'nicht bekannt'}" + for book in (self.books or []) + ] + ) + if self.books + else "keine neuen Auflagen gefunden", + newEditionsOrdered="\n".join( + [ + f" - {book.title}, ISBN: {','.join(book.isbn)}, Bibliotheksstandort : {book.library_location if book.library_location else 'N/A'}, Link: {book.link}" + for book in (self.ordered_books or []) + ] + ), + ) + except Exception as e: + log.error(f"Template formatting failed: {e}") + + self.mail_body.setPlainText(body_html) def createAndSendMail(self): log.info("Sending mail") - import smtplib from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText @@ -176,31 +239,29 @@ Tel.: 0761/682-778 | 07617682-545""" message["From"] = sender_email message["To"] = self.prof_mail message["Subject"] = self.mail_header.text() - # include a Fcc to the senders sent folder - message["cc"] = "semesterapparate@ph-freiburg.de" + message["Cc"] = "semesterapparate@ph-freiburg.de" + + mail_body = self.mail_body.toPlainText() + # strange_string = """p, li { white-space: pre-wrap; } + # hr { height: 1px; border-width: 0; } + # li.unchecked::marker { content: "\2610"; } + # li.checked::marker { content: "\2612"; } + # """ + # mail_body.replace(strange_string, "") + message.attach(MIMEText(mail_body, "Plain", "utf-8")) - mail_body = self.mail_body.toHtml() - message.attach(MIMEText(mail_body, "html")) mail = message.as_string() with smtplib.SMTP_SSL(smtp_server, port) as server: - server.connect(smtp_server, port) - # server.connect(smtp_server, port) - # server.auth(mechanism="PLAIN") + server.connect(smtp_server, port) # not needed for SMTP_SSL if config.mail.use_user_name is True: - # print(config["mail"]["user_name"]) - server.login(config.mail.user_name, password) else: server.login(sender_email, password) server.sendmail(sender_email, tolist, mail) - - # print("Mail sent") - # end active process server.quit() + pass log.info("Mail sent, closing connection to server and dialog") - # close the dialog - self.accept() @@ -225,8 +286,6 @@ def launch_gui( if __name__ == "__main__": - import sys - app = QtWidgets.QApplication(sys.argv) Dialog = QtWidgets.QDialog() ui = Mail_Dialog() diff --git a/src/ui/dialogs/progress.py b/src/ui/dialogs/progress.py index 470a94b..3d83f26 100644 --- a/src/ui/dialogs/progress.py +++ b/src/ui/dialogs/progress.py @@ -7,7 +7,7 @@ from qtqdm import Qtqdm, QtqdmProgressBar from src.logic import BookData from src.logic.lehmannsapi import LehmannsClient -from src.logic.swb import SWB +from src.logic.SRU import SWB class CheckThread(QtCore.QThread): diff --git a/src/ui/semesterapparat_ui.ui b/src/ui/semesterapparat_ui.ui index 053d3d1..76f210e 100644 --- a/src/ui/semesterapparat_ui.ui +++ b/src/ui/semesterapparat_ui.ui @@ -250,7 +250,7 @@ 0 180 - 1261 + 1412 511 @@ -275,11 +275,30 @@
+ + false + + + WIP - Broken + gel. Medien anzeigen + + + + false + + + WIP - Broken + + + Nur Titel mit Neuauflagen anzeigen + + + @@ -298,8 +317,11 @@ + + Dieser Knopf prüft alle Werke, die mit einem roten X vermerkt sind. Sollten diese inzwischen im Apparat sein, wird dies aktualisiert + - im Apparat? + Medien mit ❌ im Apparat? diff --git a/src/ui/semesterapparat_ui_ui.py b/src/ui/semesterapparat_ui_ui.py index 844c5b9..e62f60c 100644 --- a/src/ui/semesterapparat_ui_ui.py +++ b/src/ui/semesterapparat_ui_ui.py @@ -157,7 +157,7 @@ class Ui_MainWindow(object): self.gridLayoutWidget_2 = QWidget(self.createApparat) self.gridLayoutWidget_2.setObjectName(u"gridLayoutWidget_2") self.gridLayoutWidget_2.setEnabled(True) - self.gridLayoutWidget_2.setGeometry(QRect(0, 180, 1261, 511)) + self.gridLayoutWidget_2.setGeometry(QRect(0, 180, 1412, 511)) self.gridLayout_2 = QGridLayout(self.gridLayoutWidget_2) self.gridLayout_2.setObjectName(u"gridLayout_2") self.gridLayout_2.setContentsMargins(0, 0, 0, 0) @@ -169,9 +169,16 @@ class Ui_MainWindow(object): self.chkbx_show_del_media = QCheckBox(self.gridLayoutWidget_2) self.chkbx_show_del_media.setObjectName(u"chkbx_show_del_media") + self.chkbx_show_del_media.setEnabled(False) self.horizontalLayout_5.addWidget(self.chkbx_show_del_media) + self.chkbx_show_only_wit_neweditions = QCheckBox(self.gridLayoutWidget_2) + self.chkbx_show_only_wit_neweditions.setObjectName(u"chkbx_show_only_wit_neweditions") + self.chkbx_show_only_wit_neweditions.setEnabled(False) + + self.horizontalLayout_5.addWidget(self.chkbx_show_only_wit_neweditions) + self.horizontalSpacer_3 = QSpacerItem(40, 20, QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Minimum) self.horizontalLayout_5.addItem(self.horizontalSpacer_3) @@ -880,8 +887,18 @@ class Ui_MainWindow(object): ___qtablewidgetitem4.setText(QCoreApplication.translate("MainWindow", u"Dauerapparat", None)); ___qtablewidgetitem5 = self.tableWidget_apparate.horizontalHeaderItem(5) ___qtablewidgetitem5.setText(QCoreApplication.translate("MainWindow", u"KontoNr", None)); +#if QT_CONFIG(tooltip) + self.chkbx_show_del_media.setToolTip(QCoreApplication.translate("MainWindow", u"WIP - Broken", None)) +#endif // QT_CONFIG(tooltip) self.chkbx_show_del_media.setText(QCoreApplication.translate("MainWindow", u"gel. Medien anzeigen", None)) - self.btn_reserve.setText(QCoreApplication.translate("MainWindow", u"im Apparat?", None)) +#if QT_CONFIG(tooltip) + self.chkbx_show_only_wit_neweditions.setToolTip(QCoreApplication.translate("MainWindow", u"WIP - Broken", None)) +#endif // QT_CONFIG(tooltip) + self.chkbx_show_only_wit_neweditions.setText(QCoreApplication.translate("MainWindow", u"Nur Titel mit Neuauflagen anzeigen", None)) +#if QT_CONFIG(tooltip) + self.btn_reserve.setToolTip(QCoreApplication.translate("MainWindow", u"Dieser Knopf pr\u00fcft alle Werke, die mit einem roten X vermerkt sind. Sollten diese inzwischen im Apparat sein, wird dies aktualisiert", None)) +#endif // QT_CONFIG(tooltip) + self.btn_reserve.setText(QCoreApplication.translate("MainWindow", u"Medien mit \u274c im Apparat?", None)) self.label_info.setText(QCoreApplication.translate("MainWindow", u"Medien werden hinzugef\u00fcgt", None)) self.progress_label.setText(QCoreApplication.translate("MainWindow", u"Medium x/y", None)) self.label_20.setText(QCoreApplication.translate("MainWindow", u"Medien werden gepr\u00fcft", None)) diff --git a/src/ui/userInterface.py b/src/ui/userInterface.py index 47488c9..5c4ef19 100644 --- a/src/ui/userInterface.py +++ b/src/ui/userInterface.py @@ -26,7 +26,6 @@ from src.backend import ( ) from src.backend.create_file import recreateFile from src.backend.delete_temp_contents import delete_temp_contents as tempdelete -from src.backend.semester import Semester from src.logic import ( APP_NRS, Apparat, @@ -34,7 +33,9 @@ from src.logic import ( BookData, Prof, SemapDocument, + Semester, csv_to_list, + eml_to_semap, pdf_to_semap, word_to_semap, ) @@ -207,6 +208,7 @@ class Ui(QtWidgets.QMainWindow, Ui_Semesterapparat): self.progressBar.setMinimum(0) self.avail_status.hide() self.chkbx_show_del_media.hide() + self.chkbx_show_only_wit_neweditions.hide() self.automation_add_selected_books.hide() # self.btn_del_select_apparats.setEnabled(False) @@ -896,7 +898,7 @@ class Ui(QtWidgets.QMainWindow, Ui_Semesterapparat): ) # thread = QThread() - appnumber = self.active_apparat + appnumber = self.drpdwn_app_nr.currentText() # #log.debug(links) self.availChecker = AvailChecker(links, appnumber, books=books) # availcheck.moveToThread(thread) @@ -939,16 +941,14 @@ class Ui(QtWidgets.QMainWindow, Ui_Semesterapparat): self.validate_semester() def update_app_media_list(self): - deleted = 0 if not self.chkbx_show_del_media.isChecked() else 1 app_id = self.db.getId(self.app_name.text()) prof_id = self.db.getProfId(self.profdata) - books: list[dict[int, BookData, int]] = self.db.getBooks( - app_id, prof_id, deleted - ) + books: list[dict[int, BookData, int]] = self.db.getBooks(app_id, prof_id, 0) # # #log.debug(books) # take the dataclass from the tuple # booklist:list[BookData]=[book[0] for book in books] + self.tableWidget_apparat_media.clearContents() self.tableWidget_apparat_media.setRowCount(0) for book in books: book["id"] @@ -1198,6 +1198,8 @@ class Ui(QtWidgets.QMainWindow, Ui_Semesterapparat): self.db.addBookToDatabase( bookdata=book, app_id=app_id, prof_id=prof_id ) + if file_type == "eml": + data = eml_to_semap(file) self.update_app_media_list() # #log.debug(len(signatures)) @@ -1590,8 +1592,8 @@ class Ui(QtWidgets.QMainWindow, Ui_Semesterapparat): mail_data = { "prof_name": "Erwerbung", "prof_mail": "erw@ph-freiburg.de", - "app_id": app_nr, - "app_name": self.db.getApparatName(app_id, prof_id), + "app_nr": app_nr, + "app_name": self.db.getApparatName(app_nr, prof_id), } orderDialog = NewEditionDialog(app_id, mail_data) orderDialog.exec() @@ -1672,12 +1674,13 @@ WHERE m.id = ?""", newEditionChecker.exec() accepted_books = newEditionChecker.accepted_books - print(accepted_books) if accepted_books == []: return for book in accepted_books: oldBookId = self.db.getBookIdByPPN(book.old_book.ppn) - + apparats_id = self.db.getId( + self.db.getApparatNameByAppNr(book.old_book.library_location) + ) self.db.insertNewEdition(book, oldBookId, apparats_id) pass @@ -1763,11 +1766,17 @@ WHERE m.id = ?""", apparat_add_action = QtGui.QAction("Zum Apparat hinzufügen") apparat_move_action = QtGui.QAction("In Apparat verschieben") apparat_copy_action = QtGui.QAction("In Apparat kopieren") + replace_old_editions = QtGui.QAction("Neuauflagen ersetzen") apparatmenu = menu.addMenu("Apparate") generalmenu = menu.addMenu("Allgemeines") apparatmenu.addActions( # type: ignore - [apparat_add_action, apparat_copy_action, apparat_move_action] + [ + apparat_add_action, + apparat_copy_action, + apparat_move_action, + replace_old_editions, + ] ) generalmenu.addActions([edit_action, delete_action, update_data_action]) # type: ignore # disable apparat_add_action @@ -1778,8 +1787,37 @@ WHERE m.id = ?""", apparat_copy_action.triggered.connect(self.copy_to_apparat) # type: ignore apparat_move_action.triggered.connect(self.move_to_apparat) # type: ignore update_data_action.triggered.connect(self.update_data) # type: ignore + replace_old_editions.triggered.connect(self.replace_old_edition) # type: ignore menu.exec(self.tableWidget_apparat_media.mapToGlobal(position)) # type: ignore + def replace_old_edition(self): + # open dialog + dialog = QtWidgets.QDialog() + dialog.setWindowTitle("Neuauflagen:") + layout = QtWidgets.QVBoxLayout() + label = QtWidgets.QLabel("Folgende Medien haben Neuauflagen:") + layout.addWidget(label) + table = QtWidgets.QTableWidget() + table.setColumnCount(4) + table.setHorizontalHeaderLabels(["Titel", "Auflage", "Signatur", "Neues Werk"]) + table.horizontalHeader().setStretchLastSection(True) + new_editions = self.db.getBooksWithNewEditions( + self.active_apparat, + ) + for book in new_editions: + table.insertRow(0) + table.setItem(0, 0, QtWidgets.QTableWidgetItem(book[0].title)) + table.setItem(0, 1, QtWidgets.QTableWidgetItem(str(book[0].edition))) + table.setItem(0, 2, QtWidgets.QTableWidgetItem(book[0].signature)) + new_ed_data = ( + f"{book[1].title} (Auflage {book[1].edition}, {book[1].signature})" + ) + table.setItem(0, 3, QtWidgets.QTableWidgetItem(new_ed_data)) + + layout.addWidget(table) + dialog.setLayout(layout) + dialog.exec() + def update_data(self): signatures = [ self.tableWidget_apparat_media.item(row, 1).text() diff --git a/src/ui/widgets/admin_query.py b/src/ui/widgets/admin_query.py index 5c933f4..c4d6fe1 100644 --- a/src/ui/widgets/admin_query.py +++ b/src/ui/widgets/admin_query.py @@ -1,9 +1,10 @@ -from .widget_sources.admin_query_ui import Ui_Form +from PySide6 import QtCore, QtWidgets -from PySide6 import QtWidgets, QtCore from src import Icon from src.backend import Database +from .widget_sources. import Ui_Form + class AdminQueryWidget(QtWidgets.QWidget, Ui_Form): def __init__(self, parent=None): @@ -22,7 +23,7 @@ class AdminQueryWidget(QtWidgets.QWidget, Ui_Form): return data = self.db.query_db(request_text) - print(data) + # print(data) table_names = ( request_text.lower().split("select")[1].split("from")[0].split(",") ) diff --git a/src/ui/widgets/elsa_main.py b/src/ui/widgets/elsa_main.py index c56e7ec..5bb5fe0 100644 --- a/src/ui/widgets/elsa_main.py +++ b/src/ui/widgets/elsa_main.py @@ -7,8 +7,8 @@ from PySide6.QtCore import QDate from PySide6.QtGui import QRegularExpressionValidator from src import LOG_DIR, Icon -from src.backend import Database, Semester, recreateElsaFile -from src.logic import Prof, elsa_word_to_csv +from src.backend import Database, recreateElsaFile +from src.logic import Prof, Semester, elsa_word_to_csv from src.ui.dialogs import ElsaAddEntry, popus_confirm from src.ui.widgets.filepicker import FilePicker from src.ui.widgets.graph import DataQtGraph diff --git a/src/ui/widgets/graph.py b/src/ui/widgets/graph.py index 4be10ba..2960ecd 100644 --- a/src/ui/widgets/graph.py +++ b/src/ui/widgets/graph.py @@ -8,7 +8,7 @@ from PySide6.QtCharts import QCategoryAxis, QChart, QChartView, QLineSeries, QVa from PySide6.QtGui import QColor, QPainter, QPen from src import LOG_DIR -from src.backend.semester import Semester +from src.logic.semester import Semester log = loguru.logger log.remove() diff --git a/src/ui/widgets/searchPage.py b/src/ui/widgets/searchPage.py index 63992a0..2c0ad86 100644 --- a/src/ui/widgets/searchPage.py +++ b/src/ui/widgets/searchPage.py @@ -1,4 +1,5 @@ import sys +from typing import List import loguru from natsort import natsorted @@ -6,8 +7,9 @@ from PySide6 import QtCore, QtGui, QtWidgets from PySide6.QtCore import Signal from src import LOG_DIR -from src.backend import Database, Semester -from src.logic import BookData, Prof, custom_sort, sort_semesters_list +from src.backend import Database +from src.logic import BookData, Prof, Semester, custom_sort, sort_semesters_list +from src.logic.dataclass import Apparat from src.ui.dialogs import ApparatExtendDialog, Mail_Dialog, ReminderDialog from src.ui.widgets import DataQtGraph, StatusWidget from src.ui.widgets.signature_update import UpdaterThread @@ -343,8 +345,7 @@ class SearchStatisticPage(QtWidgets.QDialog, Ui_Dialog): apparats = natsorted(appnrs) apparats = [str(apparat) for apparat in apparats] self.box_appnrs.addItems(apparats) - persons = self.db.getProfs() - persons = sorted(persons, key=lambda x: x.lastname) + persons: List[Prof] = sorted(self.db.getProfs(), key=lambda x: x.lastname) self.box_person.addItems( [f"{person.lastname}, {person.firstname}" for person in persons] ) @@ -398,7 +399,12 @@ class SearchStatisticPage(QtWidgets.QDialog, Ui_Dialog): selected_apparat_rows = [] for i in range(self.tableWidget.rowCount()): if self.tableWidget.cellWidget(i, 0).isChecked(): - selected_apparats.append(self.tableWidget.item(i, 2).text()) + selected_apparats.append( + Apparat( + appnr=self.tableWidget.item(i, 2).text(), + name=self.tableWidget.item(i, 1).text(), + ) + ) selected_apparat_rows.append(i) # delete all selected apparats # # ##print(selected_apparats) diff --git a/src/ui/widgets/signature_update.py b/src/ui/widgets/signature_update.py index 135eeae..63a88db 100644 --- a/src/ui/widgets/signature_update.py +++ b/src/ui/widgets/signature_update.py @@ -8,7 +8,7 @@ from PySide6.QtMultimedia import QAudioOutput, QMediaPlayer from src import LOG_DIR from src.backend.catalogue import Catalogue from src.backend.database import Database -from src.logic.swb import SWB +from src.logic.SRU import SWB from .widget_sources.admin_update_signatures_ui import Ui_Dialog -- 2.49.1 From 7e07bdea0c27094f762c6a5498b56fa88e1aa028 Mon Sep 17 00:00:00 2001 From: WorldTeacher Date: Tue, 7 Oct 2025 14:42:40 +0200 Subject: [PATCH 29/40] commit AI suggested performance enhancements --- src/backend/threads_availchecker.py | 18 +++++++++------ src/logic/SRU.py | 34 ++++++++++++++++++++++------- src/ui/dialogs/progress.py | 7 ++++-- 3 files changed, 42 insertions(+), 17 deletions(-) diff --git a/src/backend/threads_availchecker.py b/src/backend/threads_availchecker.py index 9014ccd..f7193c6 100644 --- a/src/backend/threads_availchecker.py +++ b/src/backend/threads_availchecker.py @@ -1,5 +1,4 @@ import sys -import time # from src.transformers import RDS_AVAIL_DATA import loguru @@ -23,7 +22,11 @@ class AvailChecker(QThread): updateProgress = Signal(int, int) def __init__( - self, links: list = None, appnumber: int = None, parent=None, books=list[dict] + self, + links: list[str] | None = None, + appnumber: int | None = None, + parent=None, + books: list[dict] | None = None, ): if links is None: links = [] @@ -38,11 +41,13 @@ class AvailChecker(QThread): ) self.links = links self.appnumber = appnumber - self.books = books + self.books = books or [] log.info( f"Started worker with appnumber: {self.appnumber} and links: {self.links} and {len(self.books)} books..." ) - time.sleep(2) + # Pre-create reusable request and transformer to avoid per-item overhead + self._request = WebRequest().set_apparat(self.appnumber) + self._rds_transformer = BibTextTransformer("RDS") def run(self): self.db = Database() @@ -50,9 +55,8 @@ class AvailChecker(QThread): count = 0 for link in self.links: log.info("Processing entry: " + str(link)) - data = WebRequest().set_apparat(self.appnumber).get_ppn(link).get_data() - transformer = BibTextTransformer("RDS") - rds = transformer.get_data(data).return_data("rds_availability") + data = self._request.get_ppn(link).get_data() + rds = self._rds_transformer.get_data(data).return_data("rds_availability") book_id = None if not rds or not rds.items: diff --git a/src/logic/SRU.py b/src/logic/SRU.py index 0141217..3a5ef9e 100644 --- a/src/logic/SRU.py +++ b/src/logic/SRU.py @@ -1,3 +1,4 @@ +import re import sys import xml.etree.ElementTree as ET from dataclasses import dataclass, field @@ -7,6 +8,7 @@ from typing import Dict, Iterable, List, Optional, Tuple import loguru import requests +from requests.adapters import HTTPAdapter from src import LOG_DIR from src.logic.dataclass import BookData @@ -556,7 +558,22 @@ class Api: self.site = site self.url = url self.prefix = prefix - pass + # Reuse TCP connections across requests for better performance + self._session = requests.Session() + # Slightly larger connection pool for concurrent calls + adapter = HTTPAdapter(pool_connections=10, pool_maxsize=20) + self._session.mount("http://", adapter) + self._session.mount("https://", adapter) + + def close(self): + try: + self._session.close() + except Exception: + pass + + def __del__(self): + # Best-effort cleanup + self.close() def get(self, query_args: Iterable[str]) -> List[Record]: # if any query_arg ends with =, remove it @@ -580,18 +597,18 @@ class Api: "Accept": "application/xml", "Accept-Charset": "latin1,utf-8;q=0.7,*;q=0.3", } - response = requests.get(url, headers=headers) + # Use persistent session and set timeouts to avoid hanging + response = self._session.get(url, headers=headers, timeout=(3.05, 20)) if response.status_code != 200: raise Exception(f"Error fetching data from SWB: {response.status_code}") - data = response.content - - # extract top-level response - response = parse_search_retrieve_response(data) + # extract top-level response (decode to text for the XML parser) + response = parse_search_retrieve_response(response.text) return response.records def getBooks(self, query_args: Iterable[str]) -> List[BookData]: records: List[Record] = self.get(query_args) - print(f"{self.site} found {len(records)} records") + # Avoid printing on hot paths; rely on logger if needed + log.debug(f"{self.site} found {len(records)} records for args={query_args}") books: List[BookData] = [] # extract title from query_args if present title = None @@ -611,7 +628,8 @@ class Api: return books def getLinkForBook(self, book: BookData) -> str: - results = self.getBooks() + # Not implemented: depends on catalog front-end; return empty string for now + return "" class SWB(Api): diff --git a/src/ui/dialogs/progress.py b/src/ui/dialogs/progress.py index 3d83f26..cdeab3b 100644 --- a/src/ui/dialogs/progress.py +++ b/src/ui/dialogs/progress.py @@ -32,6 +32,7 @@ class CheckThread(QtCore.QThread): range(len(self.items)), unit_scale=True, ) + swb_client = SWB() for i in tqdm_object: book: BookData = self.items[i] author = ( @@ -43,7 +44,7 @@ class CheckThread(QtCore.QThread): # remove trailing punctuation from title title = book.title.rstrip(" .:,;!?") response: list[BookData] = [] - response = SWB().getBooks( + response = swb_client.getBooks( [ "pica.bib=20735", f"pica.tit={title.split(':')[0].strip()}", @@ -88,4 +89,6 @@ class ProgressDialog(QDialog): layout.addWidget(self.start_button) def start(self): - + # Start logic is managed externally; keep method for UI wiring + pass + -- 2.49.1 From 3cc6e793d2de1b8aa2f269217a65b5c1d5bd70a8 Mon Sep 17 00:00:00 2001 From: WorldTeacher Date: Thu, 9 Oct 2025 12:35:15 +0200 Subject: [PATCH 30/40] more AI optimizations, reworked logger --- main.py | 13 +++-- src/backend/catalogue.py | 17 +----- src/backend/database.py | 88 +++++++++++++++++------------ src/backend/thread_bookgrabber.py | 19 ++----- src/backend/thread_neweditions.py | 23 ++------ src/backend/threads_availchecker.py | 12 +--- src/logic/SRU.py | 35 +++++------- src/logic/semester.py | 27 ++++----- src/logic/webrequest.py | 9 +-- src/logic/wordparser.py | 10 +--- src/shared/logging.py | 25 ++++++++ src/ui/dialogs/mail.py | 9 +-- src/ui/dialogs/newEdition.py | 14 +---- src/ui/widgets/MessageCalendar.py | 10 +--- src/ui/widgets/elsa_main.py | 32 +++++------ src/ui/widgets/graph.py | 21 ------- src/ui/widgets/searchPage.py | 19 +------ src/ui/widgets/signature_update.py | 17 +----- src/ui/widgets/welcome_wizard.py | 10 +--- src/utils/richtext.py | 28 +++------ test.py | 40 ++++++++++--- uv.lock | 28 +-------- 22 files changed, 186 insertions(+), 320 deletions(-) create mode 100644 src/shared/logging.py diff --git a/main.py b/main.py index 4066d3d..8a2e07d 100644 --- a/main.py +++ b/main.py @@ -1,11 +1,14 @@ -from src import first_launch, settings -from src.ui.widgets.welcome_wizard import launch_wizard as startup -from PySide6 import QtWidgets import sys -from src.ui.userInterface import launch_gui as UI +from PySide6 import QtWidgets + +from src import first_launch, settings +from src.shared.logging import configure +from src.ui.userInterface import launch_gui as UI +from src.ui.widgets.welcome_wizard import launch_wizard as startup if __name__ == "__main__": + configure("INFO") app = QtWidgets.QApplication(sys.argv) if not first_launch: setup = startup() @@ -16,4 +19,4 @@ if __name__ == "__main__": else: sys.exit() else: - UI() \ No newline at end of file + UI() diff --git a/src/backend/catalogue.py b/src/backend/catalogue.py index fb4cff7..be10401 100644 --- a/src/backend/catalogue.py +++ b/src/backend/catalogue.py @@ -1,28 +1,13 @@ -import sys -from datetime import datetime - -import loguru import regex import requests from bs4 import BeautifulSoup -from src import LOG_DIR from src.logic import BookData as Book +from src.shared.logging import log 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 = loguru.logger -log.remove() -log.add(sys.stdout, level="INFO") -log.add(f"{LOG_DIR}/application.log", rotation="1 MB", retention="10 days") - -log.add( - f"{LOG_DIR}/{datetime.now().strftime('%Y-%m-%d')}.log", - rotation="1 day", - retention="1 month", -) - class Catalogue: def __init__(self, timeout=15): diff --git a/src/backend/database.py b/src/backend/database.py index 603e936..a49e083 100644 --- a/src/backend/database.py +++ b/src/backend/database.py @@ -3,7 +3,6 @@ import json import os import re import sqlite3 as sql -import sys import tempfile from dataclasses import asdict from pathlib import Path @@ -13,7 +12,7 @@ from typing import Any, List, Optional, Tuple, Union import loguru -from src import DATABASE_DIR, LOG_DIR, settings +from src import DATABASE_DIR, settings from src.backend.db import ( CREATE_ELSA_FILES_TABLE, CREATE_ELSA_MEDIA_TABLE, @@ -34,9 +33,6 @@ from src.logic.semester import Semester from src.utils.blob import create_blob log = loguru.logger -log.remove() -log.add(sys.stdout, level="INFO") -log.add(f"{LOG_DIR}/application.log", rotation="1 MB", retention="10 days") ascii_lowercase = lower + digits + punctuation @@ -186,7 +182,13 @@ class Database: Returns: sql.Connection: The active connection to the database """ - return sql.connect(self.db_path) + conn = sql.connect(self.db_path) + # Fast pragmas suitable for a desktop app DB + conn.execute("PRAGMA journal_mode=WAL;") + conn.execute("PRAGMA synchronous=NORMAL;") + conn.execute("PRAGMA temp_store=MEMORY;") + conn.execute("PRAGMA mmap_size=134217728;") # 128MB + return conn def close_connection(self, conn: sql.Connection): """ @@ -214,6 +216,25 @@ class Database: cursor.execute(CREATE_ELSA_TABLE) cursor.execute(CREATE_ELSA_FILES_TABLE) cursor.execute(CREATE_ELSA_MEDIA_TABLE) + # Helpful indices to speed up frequent lookups and joins + cursor.execute( + "CREATE INDEX IF NOT EXISTS idx_media_app_prof ON media(app_id, prof_id);" + ) + cursor.execute( + "CREATE INDEX IF NOT EXISTS idx_media_deleted ON media(deleted);" + ) + cursor.execute( + "CREATE INDEX IF NOT EXISTS idx_media_available ON media(available);" + ) + cursor.execute( + "CREATE INDEX IF NOT EXISTS idx_messages_remind_at ON messages(remind_at);" + ) + cursor.execute( + "CREATE INDEX IF NOT EXISTS idx_semesterapparat_prof ON semesterapparat(prof_id);" + ) + cursor.execute( + "CREATE INDEX IF NOT EXISTS idx_semesterapparat_appnr ON semesterapparat(appnr);" + ) conn.commit() self.close_connection(conn) @@ -227,7 +248,7 @@ class Database: """ conn = self.connect() cursor = conn.cursor() - log.debug(f"Inserting {params} into database with query {query}") + log.debug(f"Inserting into DB: {query}") cursor.execute(query, params) conn.commit() self.close_connection(conn) @@ -1650,7 +1671,7 @@ class Database: tempdir.mkdir(parents=True, exist_ok=True) file = tempfile.NamedTemporaryFile( - delete=False, dir=tempdir_path, mode="wb", suffix=f".{filetype}" + delete=False, dir=tempdir, mode="wb", suffix=f".{filetype}" ) file.write(blob) # log.debug("file created") @@ -1713,9 +1734,9 @@ class Database: telnr = profdata.telnr title = profdata.title - query = f"INSERT INTO prof (fname, lname, fullname, mail, telnr,titel) VALUES ('{fname}','{lname}','{fullname}','{mail}','{telnr}','{title}')" + query = "INSERT INTO prof (fname, lname, fullname, mail, telnr, titel) VALUES (?,?,?,?,?,?)" log.debug(query) - cursor.execute(query) + cursor.execute(query, (fname, lname, fullname, mail, telnr, title)) conn.commit() conn.close() @@ -1758,10 +1779,10 @@ class Database: fullname = profdata["profname"] else: fullname = profdata.name() - query = f"SELECT id FROM prof WHERE fullname = '{fullname}'" + query = "SELECT id FROM prof WHERE fullname = ?" log.debug(query) - cursor.execute(query) + cursor.execute(query, (fullname,)) result = cursor.fetchone() if result: return result[0] @@ -1776,10 +1797,10 @@ class Database: """ conn = self.connect() cursor = conn.cursor() - query = f"SELECT * FROM prof WHERE fullname = '{fullname}'" + query = "SELECT * FROM prof WHERE fullname = ?" log.debug(query) - result = cursor.execute(query).fetchone() + result = cursor.execute(query, (fullname,)).fetchone() if result: return Prof().from_tuple(result) else: @@ -1795,8 +1816,8 @@ class Database: int | None: The id of the prof or None if not found """ - query = f"SELECT prof_id from semesterapparat WHERE appnr = '{apprarat_id}' and deletion_status = 0" - data = self.query_db(query) + query = "SELECT prof_id from semesterapparat WHERE appnr = ? and deletion_status = 0" + data = self.query_db(query, (apprarat_id,)) if data: log.info("Prof ID: " + str(data[0][0])) return data[0][0] @@ -1807,20 +1828,13 @@ class Database: # get book data new_apparat_id = apparat new_prof_id = self.getProfIDByApparat(new_apparat_id) - query = f""" - INSERT INTO media (bookdata, app_id, prof_id, deleted, available, reservation) - SELECT - bookdata, - '{new_apparat_id}', - '{new_prof_id}', - 0, - available, - reservation - FROM media - where id = '{book_id}'""" + query = ( + "INSERT INTO media (bookdata, app_id, prof_id, deleted, available, reservation) " + "SELECT bookdata, ?, ?, 0, available, reservation FROM media WHERE id = ?" + ) connection = self.connect() cursor = connection.cursor() - cursor.execute(query) + cursor.execute(query, (new_apparat_id, new_prof_id, book_id)) connection.commit() connection.close() @@ -1832,16 +1846,18 @@ class Database: appratat (int): the ID of the new apparat """ # get book data - query = f"UPDATE media SET app_id = '{appratat}' WHERE id = '{book_id}'" + query = "UPDATE media SET app_id = ? WHERE id = ?" connection = self.connect() cursor = connection.cursor() - cursor.execute(query) + cursor.execute(query, (appratat, book_id)) connection.commit() connection.close() def getApparatNameByAppNr(self, appnr: int): - query = f"SELECT name FROM semesterapparat WHERE appnr = '{appnr}' and deletion_status = 0" - data = self.query_db(query) + query = ( + "SELECT name FROM semesterapparat WHERE appnr = ? and deletion_status = 0" + ) + data = self.query_db(query, (appnr,)) if data: return data[0][0] else: @@ -1856,8 +1872,8 @@ class Database: return result def getBookIdByPPN(self, ppn: str) -> int: - query = f"SELECT id FROM media WHERE bookdata LIKE '%{ppn}%'" - data = self.query_db(query) + query = "SELECT id FROM media WHERE bookdata LIKE ?" + data = self.query_db(query, (f"%{ppn}%",)) if data: return data[0][0] else: @@ -1876,9 +1892,7 @@ class Database: results = self.query_db(query, (apparat_id,)) res = [] for result in results: - old_edition_edition = self.query_db( - "SELECT bookdata FROM media WHERE id=?", (result[2],), one=True - ) + # keep only new edition payload; old edition can be reconstructed if needed res.append(BookData().from_string(result[1])) return res diff --git a/src/backend/thread_bookgrabber.py b/src/backend/thread_bookgrabber.py index 0594caa..7736ab4 100644 --- a/src/backend/thread_bookgrabber.py +++ b/src/backend/thread_bookgrabber.py @@ -1,20 +1,10 @@ -import sys - -import loguru from PySide6.QtCore import QThread, Signal -from src import LOG_DIR from src.backend import Database from src.logic.webrequest import BibTextTransformer, WebRequest +from src.shared.logging import log -log = loguru.logger -log.remove() -log.add(sys.stdout, level="INFO") -log.add(f"{LOG_DIR}/application.log", rotation="1 MB", retention="10 days") - - -# logger.add(sys.stderr, format="{time} {level} {message}", level="INFO") -log.add(sys.stdout, level="INFO") +# Logger configured centrally in main; this module just uses `log` class BookGrabber(QThread): @@ -37,7 +27,6 @@ class BookGrabber(QThread): self.request = WebRequest() self.db = Database() - def add_values( self, app_id: int, prof_id: int, mode: str, data, any_book=False, exact=False ): @@ -50,7 +39,9 @@ class BookGrabber(QThread): log.info(f"Working on {len(self.data)} entries") self.tstate = (self.app_nr, self.prof_id, self.mode, self.data) log.debug("State: " + str(self.tstate)) - app_nr = self.db.query_db("SELECT appnr FROM semesterapparat WHERE id = ?", (self.app_id,))[0][0] + app_nr = self.db.query_db( + "SELECT appnr FROM semesterapparat WHERE id = ?", (self.app_id,) + )[0][0] self.request.set_apparat(app_nr) # log.debug(self.tstate) diff --git a/src/backend/thread_neweditions.py b/src/backend/thread_neweditions.py index 8849d24..7de4026 100644 --- a/src/backend/thread_neweditions.py +++ b/src/backend/thread_neweditions.py @@ -1,37 +1,24 @@ import os import re -import sys from concurrent.futures import ThreadPoolExecutor -from datetime import datetime from math import ceil from queue import Empty, Queue from time import monotonic # <-- NEW from typing import List, Optional -import loguru from PySide6.QtCore import QThread, Signal -from src import LOG_DIR - # from src.logic.webrequest import BibTextTransformer, WebRequest from src.backend.catalogue import Catalogue from src.logic import BookData from src.logic.SRU import SWB +from src.shared.logging import log # use all available cores - 2, but at least 1 THREAD_COUNT = max(os.cpu_count() - 2, 1) THREAD_MIN_ITEMS = 5 -log = loguru.logger -log.remove() -log.add(sys.stdout, level="INFO") -log.add(f"{LOG_DIR}/application.log", rotation="1 MB", retention="10 days") - -log.add( - f"{LOG_DIR}/{datetime.now().strftime('%Y-%m-%d')}.log", - rotation="1 day", - retention="7 days", -) +# Logger configured centrally in main; use shared `log` swb = SWB() dnb = SWB() @@ -146,7 +133,7 @@ def find_newer_edition( if not deduped: return None - # 3) Final pick (single best) + # 3) Preserve all qualifying newer editions, but order by preference def sort_key(b: BookData): year = b.year if b.year is not None else -1 ed = b.edition_number if b.edition_number is not None else -1 @@ -158,8 +145,8 @@ def find_newer_edition( ed, ) - best = max(deduped, key=sort_key) - return [best] if best else None + deduped.sort(key=sort_key, reverse=True) + return deduped class NewEditionCheckerThread(QThread): diff --git a/src/backend/threads_availchecker.py b/src/backend/threads_availchecker.py index f7193c6..669af02 100644 --- a/src/backend/threads_availchecker.py +++ b/src/backend/threads_availchecker.py @@ -1,20 +1,10 @@ -import sys - -# from src.transformers import RDS_AVAIL_DATA -import loguru - # from icecream import ic from PySide6.QtCore import QThread from PySide6.QtCore import Signal as Signal -from src import LOG_DIR from src.backend.database import Database from src.logic.webrequest import BibTextTransformer, WebRequest - -log = loguru.logger -log.remove() -log.add(sys.stdout, level="INFO") -log.add(f"{LOG_DIR}/application.log", rotation="1 MB", retention="10 days") +from src.shared.logging import log class AvailChecker(QThread): diff --git a/src/logic/SRU.py b/src/logic/SRU.py index 3a5ef9e..d31e582 100644 --- a/src/logic/SRU.py +++ b/src/logic/SRU.py @@ -1,28 +1,17 @@ import re -import sys import xml.etree.ElementTree as ET from dataclasses import dataclass, field -from datetime import datetime from enum import Enum -from typing import Dict, Iterable, List, Optional, Tuple +from typing import Dict, Iterable, List, Optional, Tuple, Union -import loguru import requests from requests.adapters import HTTPAdapter -from src import LOG_DIR +# centralized logging used via src.shared.logging from src.logic.dataclass import BookData +from src.shared.logging import log -log = loguru.logger -log.remove() -log.add(sys.stdout, level="INFO") -log.add(f"{LOG_DIR}/application.log", rotation="1 MB", retention="10 days") - -log.add( - f"{LOG_DIR}/{datetime.now().strftime('%Y-%m-%d')}.log", - rotation="1 day", - retention="1 month", -) +log # ensure imported logger is referenced # ----------------------- @@ -186,7 +175,9 @@ def parse_echoed_request(root: ET.Element) -> Optional[EchoedSearchRequest]: ) -def parse_search_retrieve_response(xml_str: str) -> SearchRetrieveResponse: +def parse_search_retrieve_response( + xml_str: Union[str, bytes], +) -> SearchRetrieveResponse: root = ET.fromstring(xml_str) # Root is zs:searchRetrieveResponse @@ -598,12 +589,12 @@ class Api: "Accept-Charset": "latin1,utf-8;q=0.7,*;q=0.3", } # Use persistent session and set timeouts to avoid hanging - response = self._session.get(url, headers=headers, timeout=(3.05, 20)) - if response.status_code != 200: - raise Exception(f"Error fetching data from SWB: {response.status_code}") - # extract top-level response (decode to text for the XML parser) - response = parse_search_retrieve_response(response.text) - return response.records + resp = self._session.get(url, headers=headers, timeout=(3.05, 60)) + if resp.status_code != 200: + raise Exception(f"Error fetching data from SWB: {resp.status_code}") + # Parse using raw bytes (original behavior) to preserve encoding edge cases + sr = parse_search_retrieve_response(resp.content) + return sr.records def getBooks(self, query_args: Iterable[str]) -> List[BookData]: records: List[Record] = self.get(query_args) diff --git a/src/logic/semester.py b/src/logic/semester.py index 997ccfd..0a529ca 100644 --- a/src/logic/semester.py +++ b/src/logic/semester.py @@ -18,16 +18,8 @@ from __future__ import annotations import datetime import re -import sys -import loguru - -from src import LOG_DIR - -log = loguru.logger -log.remove() -log.add(sys.stdout, level="INFO") -log.add(f"{LOG_DIR}/application.log", rotation="1 MB", retention="10 days") +from src.shared.logging import log class Semester: @@ -124,21 +116,22 @@ class Semester: # ------------------------------------------------------------------ # Comparison helpers # ------------------------------------------------------------------ - def isPastSemester(self, other: "Semester") -> bool: - if self.year < other.year: + def isPastSemester(self, current: "Semester") -> bool: + log.debug(f"Comparing {self} < {current}") + if self.year < current.year: return True - if self.year == other.year: + if self.year == current.year: return ( - self.semester == "WiSe" and other.semester == "SoSe" + self.semester == "WiSe" and current.semester == "SoSe" ) # WiSe before next SoSe return False - def isFutureSemester(self, other: "Semester") -> bool: - if self.year > other.year: + def isFutureSemester(self, current: "Semester") -> bool: + if self.year > current.year: return True - if self.year == other.year: + if self.year == current.year: return ( - self.semester == "SoSe" and other.semester == "WiSe" + self.semester == "SoSe" and current.semester == "WiSe" ) # SoSe after WiSe of same year return False diff --git a/src/logic/webrequest.py b/src/logic/webrequest.py index d593bf7..cdded76 100644 --- a/src/logic/webrequest.py +++ b/src/logic/webrequest.py @@ -1,23 +1,16 @@ -import sys from typing import Any, Optional, Union -import loguru import requests from bs4 import BeautifulSoup # import sleep_and_retry decorator to retry requests from ratelimit import limits, sleep_and_retry -from src import LOG_DIR from src.logic.dataclass import BookData +from src.shared.logging import log from src.transformers import ARRAYData, BibTeXData, COinSData, RDSData, RISData from src.transformers.transformers import RDS_AVAIL_DATA, RDS_GENERIC_DATA -log = loguru.logger -log.remove() -log.add(sys.stdout, level="INFO") -log.add(f"{LOG_DIR}/application.log", rotation="1 MB", retention="10 days") - # logger.add(sys.stderr, format="{time} {level} {message}", level="INFO") diff --git a/src/logic/wordparser.py b/src/logic/wordparser.py index d6b137c..a548b48 100644 --- a/src/logic/wordparser.py +++ b/src/logic/wordparser.py @@ -1,20 +1,13 @@ -import sys import zipfile from typing import Any import fitz # PyMuPDF -import loguru import pandas as pd from bs4 import BeautifulSoup from docx import Document -from src import LOG_DIR from src.logic.dataclass import Book, SemapDocument - -log = loguru.logger -log.remove() -log.add(sys.stdout, level="INFO") -log.add(f"{LOG_DIR}/application.log", rotation="1 MB", retention="10 days") +from src.shared.logging import log def word_docx_to_csv(path: str) -> list[pd.DataFrame]: @@ -50,7 +43,6 @@ def get_fach(path: str) -> str: soup = BeautifulSoup(xml_data, "xml") # text we need is in -> w:r -> w:t paragraphs = soup.find_all("w:p") - names = [] for para in paragraphs: para_id = para.get("w14:paraId") if para_id == "12456A32": diff --git a/src/shared/logging.py b/src/shared/logging.py new file mode 100644 index 0000000..995f2f2 --- /dev/null +++ b/src/shared/logging.py @@ -0,0 +1,25 @@ +import sys + +import loguru + +from src import LOG_DIR + +log = loguru.logger +_configured = False + + +def configure(level: str = "INFO", to_stdout: bool = True, rotate_bytes: str = "1 MB"): + global _configured + if _configured: + return log + log.remove() + if to_stdout: + log.add(sys.stdout, level=level) + # application rolling log + log.add( + f"{LOG_DIR}/application.log", + rotation=rotate_bytes, + retention="10 days", + ) + _configured = True + return log diff --git a/src/ui/dialogs/mail.py b/src/ui/dialogs/mail.py index 51a7194..703d08a 100644 --- a/src/ui/dialogs/mail.py +++ b/src/ui/dialogs/mail.py @@ -3,20 +3,15 @@ import re import smtplib import sys -import loguru from PySide6 import QtWidgets -from src import LOG_DIR, Icon +from src import Icon from src import settings as config +from src.shared.logging import log from .dialog_sources.mail_preview_ui import Ui_eMailPreview as MailPreviewDialog from .mailTemplate import MailTemplateDialog -log = loguru.logger -log.remove() -log.add(sys.stdout, level="INFO") -log.add(f"{LOG_DIR}/application.log", rotation="1 MB", retention="10 days") - CSS_RESET = "" empty_signature = """""" diff --git a/src/ui/dialogs/newEdition.py b/src/ui/dialogs/newEdition.py index 44b920e..a103690 100644 --- a/src/ui/dialogs/newEdition.py +++ b/src/ui/dialogs/newEdition.py @@ -1,20 +1,11 @@ -import sys - -import loguru from PySide6 import QtCore, QtWidgets -from src import LOG_DIR from src.backend.catalogue import Catalogue from src.backend.database import Database from src.ui.dialogs.mail import Mail_Dialog from .dialog_sources.order_neweditions_ui import Ui_Dialog -log = loguru.logger -log.remove() -log.add(sys.stdout, level="INFO") -log.add(f"{LOG_DIR}/application.log", rotation="1 MB", retention="10 days") - class NewEditionDialog(QtWidgets.QDialog, Ui_Dialog): def __init__(self, app_id, mail_data): @@ -31,10 +22,7 @@ class NewEditionDialog(QtWidgets.QDialog, Ui_Dialog): def populateTable(self): for book in self.books: - signature = book.signature - # if signature is None or signature == "None" and book.ppn is not None: - # signature = self.catalogue.get_signature(f"kid:{book.ppn}") - # book.signature = signature + # signature not required here; using book.signature directly when needed link_label = QtWidgets.QLabel() link = ( book.link diff --git a/src/ui/widgets/MessageCalendar.py b/src/ui/widgets/MessageCalendar.py index 12336a1..cc03437 100644 --- a/src/ui/widgets/MessageCalendar.py +++ b/src/ui/widgets/MessageCalendar.py @@ -1,20 +1,12 @@ -import sys from typing import Any import darkdetect -import loguru from PySide6 import QtCore, QtWidgets from PySide6.QtCore import QDate from PySide6.QtGui import QColor, QPen -from src import LOG_DIR from src.backend import Database - -log = loguru.logger -log.remove() -log.add(sys.stdout, level="INFO") -log.add(f"{LOG_DIR}/application.log", rotation="1 MB", retention="10 days") - +from src.shared.logging import log color = "#ddfb00" if darkdetect.isDark() else "#2204ff" pen = QPen(QColor(color)) diff --git a/src/ui/widgets/elsa_main.py b/src/ui/widgets/elsa_main.py index 5bb5fe0..6607446 100644 --- a/src/ui/widgets/elsa_main.py +++ b/src/ui/widgets/elsa_main.py @@ -1,25 +1,19 @@ import os -import sys -import loguru from PySide6 import QtCore, QtGui, QtWidgets from PySide6.QtCore import QDate from PySide6.QtGui import QRegularExpressionValidator -from src import LOG_DIR, Icon +from src import Icon from src.backend import Database, recreateElsaFile from src.logic import Prof, Semester, elsa_word_to_csv +from src.shared.logging import log from src.ui.dialogs import ElsaAddEntry, popus_confirm from src.ui.widgets.filepicker import FilePicker from src.ui.widgets.graph import DataQtGraph from .widget_sources.elsa_maindialog_ui import Ui_Dialog -log = loguru.logger -log.remove() -log.add(sys.stdout, level="INFO") -log.add(f"{LOG_DIR}/application.log", rotation="1 MB", retention="10 days") - class ElsaDialog(QtWidgets.QDialog, Ui_Dialog): def __init__(self): @@ -399,6 +393,7 @@ class ElsaDialog(QtWidgets.QDialog, Ui_Dialog): # get the file path of the selected file based on it's row row = self.dokument_list_elsa.currentRow() file = self.dokument_list_elsa.item(row, 3).text() + file_location = file if file == "Database": filename = self.dokument_list_elsa.item(row, 0).text() filetype = self.dokument_list_elsa.item(row, 1).text() @@ -415,16 +410,17 @@ class ElsaDialog(QtWidgets.QDialog, Ui_Dialog): log.debug( f"elsa_id: {elsa_id}, prof: {self.elsa_prof.currentText()}, semester: {self.elsa_semester.text()}, date: {self.elsa_date.text()}" ) - self.db.insertElsaFile( - [ - { - "name": file.split("/")[-1], - "path": file, - "type": file.split(".")[-1], - } - ], - elsa_id, - ) + if file_location != "Database": + self.db.insertElsaFile( + [ + { + "name": file.split("/")[-1], + "path": file, + "type": file.split(".")[-1], + } + ], + elsa_id, + ) for row in data: if self.seperateEntries.isChecked(): if ";" in row["pages"]: diff --git a/src/ui/widgets/graph.py b/src/ui/widgets/graph.py index 2960ecd..ad5d1d8 100644 --- a/src/ui/widgets/graph.py +++ b/src/ui/widgets/graph.py @@ -1,20 +1,12 @@ import random -import sys from typing import Any, Union -import loguru from PySide6 import QtCore, QtGui, QtWidgets from PySide6.QtCharts import QCategoryAxis, QChart, QChartView, QLineSeries, QValueAxis from PySide6.QtGui import QColor, QPainter, QPen -from src import LOG_DIR from src.logic.semester import Semester -log = loguru.logger -log.remove() -log.add(sys.stdout, level="INFO") -log.add(f"{LOG_DIR}/application.log", rotation="1 MB", retention="10 days") - def mergedicts(d1: dict[str, Any], d2: dict[str, Any]): res: dict[str, Any] = {} @@ -182,16 +174,3 @@ class DataQtGraph(QtWidgets.QWidget): # split the data back into x and y data = {"x": list(data.keys()), "y": list(data.values())} return data - - -if __name__ == "__main__": - import sys - - app = QtWidgets.QApplication(sys.argv) - - graph_data = {"x": ["WiSe 25/26", "WiSe 24/25", "SoSe 25"], "y": [1, 2, 1]} - widget = DataGraph( - "ELSA Apparate pro Semester", graph_data, True, "Anzahl der Apparate" - ) - widget.show() - sys.exit(app.exec()) diff --git a/src/ui/widgets/searchPage.py b/src/ui/widgets/searchPage.py index 2c0ad86..c8cb0f1 100644 --- a/src/ui/widgets/searchPage.py +++ b/src/ui/widgets/searchPage.py @@ -1,26 +1,19 @@ -import sys from typing import List -import loguru from natsort import natsorted from PySide6 import QtCore, QtGui, QtWidgets from PySide6.QtCore import Signal -from src import LOG_DIR from src.backend import Database from src.logic import BookData, Prof, Semester, custom_sort, sort_semesters_list from src.logic.dataclass import Apparat +from src.shared.logging import log from src.ui.dialogs import ApparatExtendDialog, Mail_Dialog, ReminderDialog from src.ui.widgets import DataQtGraph, StatusWidget from src.ui.widgets.signature_update import UpdaterThread from .widget_sources.search_statistic_page_ui import Ui_Dialog -log = loguru.logger -log.remove() -log.add(sys.stdout, level="INFO") -log.add(f"{LOG_DIR}/application.log", rotation="1 MB", retention="10 days") - class MyComboBox(QtWidgets.QComboBox): def __init__(self, parent=None): @@ -477,15 +470,7 @@ class SearchStatisticPage(QtWidgets.QDialog, Ui_Dialog): data = [] for entry in result: - if "deletable" in params.keys(): - sem = Semester().from_string( - entry[8] if entry[8] is not None else entry[5] - ) - log.info(f"Semester: {sem}") - if sem.isPastSemester(Semester()): - data.append(entry) - else: - data.append(entry) + data.append(entry) self.tableWidget.setRowCount(len(data)) if len(data) > 0: self.btn_del_select_apparats.setEnabled(True) diff --git a/src/ui/widgets/signature_update.py b/src/ui/widgets/signature_update.py index 63a88db..056540b 100644 --- a/src/ui/widgets/signature_update.py +++ b/src/ui/widgets/signature_update.py @@ -1,28 +1,13 @@ -import sys -from datetime import datetime - -import loguru from PySide6 import QtCore, QtWidgets from PySide6.QtMultimedia import QAudioOutput, QMediaPlayer -from src import LOG_DIR from src.backend.catalogue import Catalogue from src.backend.database import Database from src.logic.SRU import SWB +from src.shared.logging import log from .widget_sources.admin_update_signatures_ui import Ui_Dialog -log = loguru.logger -log.remove() -log.add(sys.stdout, level="INFO") -log.add(f"{LOG_DIR}/application.log", rotation="1 MB", retention="10 days") - -log.add( - f"{LOG_DIR}/{datetime.now().strftime('%Y-%m-%d')}.log", - rotation="1 day", - retention="1 month", -) - class UpdaterThread(QtCore.QThread): progress = QtCore.Signal(int) diff --git a/src/ui/widgets/welcome_wizard.py b/src/ui/widgets/welcome_wizard.py index a8f47f4..85418e5 100644 --- a/src/ui/widgets/welcome_wizard.py +++ b/src/ui/widgets/welcome_wizard.py @@ -1,23 +1,17 @@ -import sys from pathlib import Path from typing import Any -import loguru from appdirs import AppDirs from PySide6 import QtCore, QtWidgets -from src import LOG_DIR, settings +from src import settings from src.backend import Database +from src.shared.logging import log from .widget_sources.welcome_wizard_ui import Ui_Wizard appdirs = AppDirs("SemesterApparatsManager", "SAM") -log = loguru.logger -log.remove() -log.add(sys.stdout, level="INFO") -log.add(f"{LOG_DIR}/application.log", rotation="1 MB", retention="10 days") - class WelcomeWizard(QtWidgets.QWizard, Ui_Wizard): def __init__(self, parent=None): diff --git a/src/utils/richtext.py b/src/utils/richtext.py index ff2b005..9b26489 100644 --- a/src/utils/richtext.py +++ b/src/utils/richtext.py @@ -1,27 +1,17 @@ +import os from datetime import datetime +from os.path import basename + from docx import Document from docx.enum.text import WD_PARAGRAPH_ALIGNMENT -from docx.shared import Pt, RGBColor, Cm from docx.oxml import OxmlElement from docx.oxml.ns import qn -import os -from os.path import basename -from loguru import logger as log -import sys -from src import settings +from docx.shared import Cm, Pt, RGBColor +from src import settings +from src.shared.logging import log logger = log -logger.remove() -logger.add("logs/application.log", rotation="1 week", retention="1 month", enqueue=True) -log.add( - f"logs/{datetime.now().strftime('%Y-%m-%d')}.log", - rotation="1 day", - compression="zip", -) - -# logger.add(sys.stderr, format="{time} {level} {message}", level="INFO") -logger.add(sys.stdout) font = "Cascadia Mono" @@ -29,8 +19,8 @@ font = "Cascadia Mono" def print_document(file: str): # send document to printer as attachment of email import smtplib - from email.mime.multipart import MIMEMultipart from email.mime.application import MIMEApplication + from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText smtp = settings.mail.smtp_server @@ -108,7 +98,7 @@ class SemesterDocument: self.filename = filename if full: log.info("Full document generation") - self.cleanup + self.cleanup() log.info("Cleanup done") self.make_document() log.info("Document created") @@ -378,4 +368,4 @@ if __name__ == "__main__": "Karoß (Gymnastik - Sich Bewegen mit und ohne Handgeräte)", "Sahrai (Kindheit und Gesellschaft)", ] - doc = SemapSchilder(entries) \ No newline at end of file + doc = SemapSchilder(entries) diff --git a/test.py b/test.py index 45ccd54..5a5da36 100644 --- a/test.py +++ b/test.py @@ -1,11 +1,33 @@ -olddata = ( - None, - "Christian", - "Berger", - "alexander.kirchner@ph-freiburg.de", - "764", - "Berger Christian", -) +from src.logic.semester import Semester +sem1 = Semester.from_string("WiSe 23/24") +print(sem1.value) +sem2 = Semester.from_string("SoSe 24") +print(sem2.value) +sem3 = Semester() +print(sem3.value) -print(olddata[1], olddata[2], olddata[3], olddata[4], olddata[5]) +print("Comparing Sem1 with sem2") +assert sem1.isPastSemester(sem2) is True +assert sem1.isFutureSemester(sem2) is False +assert sem1.isMatch(sem2) is False +print("Comparing Sem2 with sem1") +assert sem2.isPastSemester(sem1) is False +assert sem2.isFutureSemester(sem1) is True +assert sem2.isMatch(sem1) is False +print("Comparing Sem1 with sem1") +assert sem1.isPastSemester(sem1) is False +assert sem1.isFutureSemester(sem1) is False +assert sem1.isMatch(sem1) is True +print("Comparing Sem2 with sem2") +assert sem2.isPastSemester(sem2) is False +assert sem2.isFutureSemester(sem2) is False +assert sem2.isMatch(sem2) is True +print("Comparing Sem3 with sem3") +assert sem3.isPastSemester(sem3) is False +assert sem3.isFutureSemester(sem3) is False +assert sem3.isMatch(sem3) is True +print("Comparing Sem1 with sem3") +assert sem1.isPastSemester(sem3) is True +assert sem1.isFutureSemester(sem3) is False +assert sem1.isMatch(sem3) is False diff --git a/uv.lock b/uv.lock index 3d4e89f..9ca2491 100644 --- a/uv.lock +++ b/uv.lock @@ -165,15 +165,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5", size = 161216 }, ] -[[package]] -name = "chardet" -version = "5.2.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f3/0d/f7b6ab21ec75897ed80c17d79b15951a719226b9fababf1e40ea74d69079/chardet-5.2.0.tar.gz", hash = "sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7", size = 2069618 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/38/6f/f5fbc992a329ee4e0f288c1fe0e2ad9485ed064cac731ed2fe47dcc38cbf/chardet-5.2.0-py3-none-any.whl", hash = "sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970", size = 199385 }, -] - [[package]] name = "charset-normalizer" version = "3.4.3" @@ -1159,19 +1150,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b5/75/0bced57e6ba014adeeaa504205c4a92d5211b6c5daa20c0a80b06de6d0f4/pyzotero-1.6.11-py3-none-any.whl", hash = "sha256:949cdff92fd688fe70f609c928f09ab25a7d2aa05f35c575725d5bd0f395d3b4", size = 26368 }, ] -[[package]] -name = "qtqdm" -version = "0.2.0" -source = { registry = "https://git.theprivateserver.de/api/packages/WorldTeacher/pypi/simple/" } -dependencies = [ - { name = "pyside6" }, - { name = "tqdm" }, -] -sdist = { url = "https://git.theprivateserver.de/api/packages/WorldTeacher/pypi/files/qtqdm/0.2.0/qtqdm-0.2.0.tar.gz", hash = "sha256:86f9b3764d0ebe32edba050de5aa4fb29e287c025d5197ad17e8e8da02155a88" } -wheels = [ - { url = "https://git.theprivateserver.de/api/packages/WorldTeacher/pypi/files/qtqdm/0.2.0/qtqdm-0.2.0-py3-none-any.whl", hash = "sha256:9a76e4086b09edb698861de0b28663e12ddda34ddb039be607bfd27a3aa07a0f" }, -] - [[package]] name = "questionary" version = "2.1.0" @@ -1240,7 +1218,7 @@ dependencies = [ { name = "appdirs" }, { name = "beautifulsoup4" }, { name = "bump-my-version" }, - { name = "chardet" }, + { name = "charset-normalizer" }, { name = "comtypes" }, { name = "darkdetect" }, { name = "docx2pdf" }, @@ -1258,7 +1236,6 @@ dependencies = [ { name = "pyside6" }, { name = "python-docx" }, { name = "pyzotero" }, - { name = "qtqdm" }, { name = "ratelimit" }, { name = "requests" }, ] @@ -1278,7 +1255,7 @@ requires-dist = [ { name = "appdirs", specifier = ">=1.4.4" }, { name = "beautifulsoup4", specifier = ">=4.13.5" }, { name = "bump-my-version", specifier = ">=0.29.0" }, - { name = "chardet", specifier = ">=5.2.0" }, + { name = "charset-normalizer", specifier = ">=3.4.3" }, { name = "comtypes", specifier = ">=1.4.9" }, { name = "darkdetect", specifier = ">=0.8.0" }, { name = "docx2pdf", specifier = ">=0.1.8" }, @@ -1296,7 +1273,6 @@ requires-dist = [ { name = "pyside6", specifier = ">=6.9.1" }, { name = "python-docx", specifier = ">=1.1.2" }, { name = "pyzotero", specifier = ">=1.6.4" }, - { name = "qtqdm" }, { name = "ratelimit", specifier = ">=2.2.1" }, { name = "requests", specifier = ">=2.32.3" }, ] -- 2.49.1 From 560d8285b5581b2fb6af90a49f2e5ef1efdab3b8 Mon Sep 17 00:00:00 2001 From: WorldTeacher Date: Fri, 10 Oct 2025 09:10:06 +0200 Subject: [PATCH 31/40] Feat: add delete edition dialog with fuzzy search --- icons/trash.svg | 1 + src/backend/database.py | 28 +- src/ui/dialogs/__init__.py | 2 + src/ui/dialogs/deletedialog.py | 129 +++++ src/ui/dialogs/dialog_sources/deletedialog.ui | 138 +++++ .../dialogs/dialog_sources/deletedialog_ui.py | 122 ++++ src/ui/semesterapparat_ui.ui | 542 +++++++++--------- src/ui/semesterapparat_ui_ui.py | 443 +++++++------- src/ui/userInterface.py | 44 +- 9 files changed, 936 insertions(+), 513 deletions(-) create mode 100644 icons/trash.svg create mode 100644 src/ui/dialogs/deletedialog.py create mode 100644 src/ui/dialogs/dialog_sources/deletedialog.ui create mode 100644 src/ui/dialogs/dialog_sources/deletedialog_ui.py diff --git a/icons/trash.svg b/icons/trash.svg new file mode 100644 index 0000000..07f045b --- /dev/null +++ b/icons/trash.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/backend/database.py b/src/backend/database.py index a49e083..99a945a 100644 --- a/src/backend/database.py +++ b/src/backend/database.py @@ -544,7 +544,15 @@ class Database: ret_result.append(data) return ret_result - def getAllBooks(self): + def getAllBooks(self) -> list[dict[str, Union[int, BookData]]]: + """ + Get all books in the database that are not set as deleted + + Returns + ------- + list[dict[str, Union[int, BookData]]] + A list of dictionaries containing the id and the metadata of the book + """ # return all books in the database qdata = self.query_db("SELECT id,bookdata FROM media WHERE deleted=0") ret_result: list[dict[str, Any]] = [] @@ -558,6 +566,14 @@ class Database: ret_result.append(data) return ret_result + def getApparatNrByBookId(self, book_id): + appNr = self.query_db( + "SELECT appnr FROM semesterapparat WHERE id IN (SELECT app_id FROM media WHERE id=?)", + (book_id,), + one=True, + ) + return appNr[0] if appNr else None + def getBooksByProfId( self, prof_id: int, deleted: int = 0 ) -> list[dict[str, Union[int, BookData]]]: @@ -611,6 +627,16 @@ class Database: """ self.query_db("UPDATE media SET deleted=1 WHERE id=?", (book_id,)) + def deleteBooks(self, ids: list[int]): + """ + Delete multiple books from the database + + Args: + ids (list[int]): A list of book ids to be deleted + """ + query = f"UPDATE media SET deleted=1 WHERE id IN ({','.join(['?'] * len(ids))})" + self.query_db(query, tuple(ids)) + # File Interactions def getBlob(self, filename: str, app_id: Union[str, int]) -> bytes: """ diff --git a/src/ui/dialogs/__init__.py b/src/ui/dialogs/__init__.py index f08323f..a4c3242 100644 --- a/src/ui/dialogs/__init__.py +++ b/src/ui/dialogs/__init__.py @@ -14,10 +14,12 @@ __all__ = [ "DocumentPrintDialog", "NewEditionDialog", "Settings", + "DeleteDialog", ] from .about import About from .app_ext import ApparatExtendDialog from .bookdata import BookDataUI +from .deletedialog import DeleteDialog from .docuprint import DocumentPrintDialog from .elsa_add_entry import ElsaAddEntry from .elsa_gen_confirm import ElsaGenConfirm diff --git a/src/ui/dialogs/deletedialog.py b/src/ui/dialogs/deletedialog.py new file mode 100644 index 0000000..03d2e43 --- /dev/null +++ b/src/ui/dialogs/deletedialog.py @@ -0,0 +1,129 @@ +from typing import Any + +from PySide6 import QtCore, QtWidgets + +from src import Icon +from src.backend.database import Database + +from .dialog_sources.deletedialog_ui import Ui_Dialog + + +class DeleteDialog(QtWidgets.QDialog, Ui_Dialog): + def __init__(self): + super().__init__() + self.setupUi(self) + self.setWindowTitle("Medien löschen") + self.setWindowIcon(Icon("trash").icon) + self.reset_btn.clicked.connect(self.reset_selection) + self.cancel_btn.clicked.connect(self.close) + self.delete_btn.clicked.connect(self.delete_selected) + + self.db = Database() + self.books = self.setBooks() + self.lineEdit.textChanged.connect(self.populate_books) + self.populate_books() + + def delete_selected(self): + to_delete = [] + for row in range(self.tableWidget.rowCount()): + checkbox = self.tableWidget.cellWidget(row, 0) + if checkbox is not None and checkbox.isChecked(): + book_id_item = self.tableWidget.item(row, 6) + if book_id_item is not None: + book_id = int(book_id_item.text()) + to_delete.append(book_id) + if to_delete: + self.db.deleteBooks(to_delete) + self.accept() + + def reset_selection(self): + for row in range(self.tableWidget.rowCount()): + checkbox = self.tableWidget.cellWidget(row, 0) + if checkbox is not None: + checkbox.setChecked(False) + + def setBooks(self) -> list[dict[str, Any]]: + result: list[dict[str, Any]] = [] + books = self.db.getAllBooks() + for book in books: + title = book["bookdata"].title + signature = book["bookdata"].signature + edition = book["bookdata"].edition + appnr = self.db.getApparatNrByBookId(book["id"]) + result.append( + { + "id": book["id"], + "appnr": appnr, + "title": title, + "signature": signature, + "edition": edition, + } + ) + return result + + def populate_books(self): + searchterm = self.lineEdit.text().lower() + self.tableWidget.setRowCount(0) + for book in self.books: + checkbox = QtWidgets.QCheckBox() + app_nr = book["appnr"] + title = book["title"] + signature = book["signature"] + edition = book["edition"] if book["edition"] else "" + + if searchterm in title.lower() or searchterm in signature.lower(): + self.tableWidget.insertRow(self.tableWidget.rowCount()) + self.tableWidget.setCellWidget( + self.tableWidget.rowCount() - 1, 0, checkbox + ) + self.tableWidget.setItem( + self.tableWidget.rowCount() - 1, + 1, + QtWidgets.QTableWidgetItem(str(app_nr)), + ) + self.tableWidget.setItem( + self.tableWidget.rowCount() - 1, + 2, + QtWidgets.QTableWidgetItem(signature), + ) + self.tableWidget.setItem( + self.tableWidget.rowCount() - 1, + 3, + QtWidgets.QTableWidgetItem(title), + ) + self.tableWidget.setItem( + self.tableWidget.rowCount() - 1, + 4, + QtWidgets.QTableWidgetItem(edition), + ) + self.tableWidget.setItem( + self.tableWidget.rowCount() - 1, + 5, + QtWidgets.QTableWidgetItem(""), + ) + self.tableWidget.setItem( + self.tableWidget.rowCount() - 1, + 6, + QtWidgets.QTableWidgetItem(str(book["id"])), + ) + else: + continue + # set column signature to be 10px wider than the longest entry + self.tableWidget.setColumnWidth(1, 100) + self.tableWidget.setColumnWidth(2, 150) + self.tableWidget.setColumnWidth(3, 150) + self.tableWidget.setColumnWidth(4, 100) + + self.tableWidget.setColumnWidth(0, 50) + # horizontal header 0 should be centered + self.tableWidget.horizontalHeader().setDefaultAlignment( + QtCore.Qt.AlignmentFlag.AlignCenter + ) + + +def launch(): + app = QtWidgets.QApplication.instance() + if app is None: + app = QtWidgets.QApplication([]) + dialog = DeleteDialog() + dialog.exec() diff --git a/src/ui/dialogs/dialog_sources/deletedialog.ui b/src/ui/dialogs/dialog_sources/deletedialog.ui new file mode 100644 index 0000000..8670475 --- /dev/null +++ b/src/ui/dialogs/dialog_sources/deletedialog.ui @@ -0,0 +1,138 @@ + + + Dialog + + + + 0 + 0 + 1001 + 649 + + + + Dialog + + + + + + + + Medium suchen + + + + + + + Titel/Signatursuche + + + + + + + + + true + + + true + + + + + + + + + Apparat + + + + + Signatur + + + + + Titel + + + + + Auflage + + + + + ISBN + + + + + ID + + + + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 20 + 20 + + + + + + + + Zurücksetzen + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Löschen + + + + + + + Abbrechen + + + + + + + + + + diff --git a/src/ui/dialogs/dialog_sources/deletedialog_ui.py b/src/ui/dialogs/dialog_sources/deletedialog_ui.py new file mode 100644 index 0000000..878a448 --- /dev/null +++ b/src/ui/dialogs/dialog_sources/deletedialog_ui.py @@ -0,0 +1,122 @@ +# -*- coding: utf-8 -*- + +################################################################################ +## Form generated from reading UI file 'deletedialog.ui' +## +## Created by: Qt User Interface Compiler version 6.9.2 +## +## WARNING! All changes made in this file will be lost when recompiling UI file! +################################################################################ + +from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale, + QMetaObject, QObject, QPoint, QRect, + QSize, QTime, QUrl, Qt) +from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor, + QFont, QFontDatabase, QGradient, QIcon, + QImage, QKeySequence, QLinearGradient, QPainter, + QPalette, QPixmap, QRadialGradient, QTransform) +from PySide6.QtWidgets import (QApplication, QDialog, QHBoxLayout, QHeaderView, + QLabel, QLineEdit, QPushButton, QSizePolicy, + QSpacerItem, QTableWidget, QTableWidgetItem, QVBoxLayout, + QWidget) + +class Ui_Dialog(object): + def setupUi(self, Dialog): + if not Dialog.objectName(): + Dialog.setObjectName(u"Dialog") + Dialog.resize(1001, 649) + self.verticalLayout = QVBoxLayout(Dialog) + self.verticalLayout.setObjectName(u"verticalLayout") + self.horizontalLayout = QHBoxLayout() + self.horizontalLayout.setObjectName(u"horizontalLayout") + self.label = QLabel(Dialog) + self.label.setObjectName(u"label") + + self.horizontalLayout.addWidget(self.label) + + self.lineEdit = QLineEdit(Dialog) + self.lineEdit.setObjectName(u"lineEdit") + + self.horizontalLayout.addWidget(self.lineEdit) + + + self.verticalLayout.addLayout(self.horizontalLayout) + + self.tableWidget = QTableWidget(Dialog) + if (self.tableWidget.columnCount() < 7): + self.tableWidget.setColumnCount(7) + __qtablewidgetitem = QTableWidgetItem() + self.tableWidget.setHorizontalHeaderItem(0, __qtablewidgetitem) + __qtablewidgetitem1 = QTableWidgetItem() + self.tableWidget.setHorizontalHeaderItem(1, __qtablewidgetitem1) + __qtablewidgetitem2 = QTableWidgetItem() + self.tableWidget.setHorizontalHeaderItem(2, __qtablewidgetitem2) + __qtablewidgetitem3 = QTableWidgetItem() + self.tableWidget.setHorizontalHeaderItem(3, __qtablewidgetitem3) + __qtablewidgetitem4 = QTableWidgetItem() + self.tableWidget.setHorizontalHeaderItem(4, __qtablewidgetitem4) + __qtablewidgetitem5 = QTableWidgetItem() + self.tableWidget.setHorizontalHeaderItem(5, __qtablewidgetitem5) + __qtablewidgetitem6 = QTableWidgetItem() + self.tableWidget.setHorizontalHeaderItem(6, __qtablewidgetitem6) + self.tableWidget.setObjectName(u"tableWidget") + self.tableWidget.setAlternatingRowColors(True) + self.tableWidget.horizontalHeader().setStretchLastSection(True) + + self.verticalLayout.addWidget(self.tableWidget) + + self.horizontalLayout_2 = QHBoxLayout() + self.horizontalLayout_2.setObjectName(u"horizontalLayout_2") + self.horizontalSpacer_2 = QSpacerItem(20, 20, QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Minimum) + + self.horizontalLayout_2.addItem(self.horizontalSpacer_2) + + self.reset_btn = QPushButton(Dialog) + self.reset_btn.setObjectName(u"reset_btn") + + self.horizontalLayout_2.addWidget(self.reset_btn) + + self.horizontalSpacer = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum) + + self.horizontalLayout_2.addItem(self.horizontalSpacer) + + self.delete_btn = QPushButton(Dialog) + self.delete_btn.setObjectName(u"delete_btn") + + self.horizontalLayout_2.addWidget(self.delete_btn) + + self.cancel_btn = QPushButton(Dialog) + self.cancel_btn.setObjectName(u"cancel_btn") + + self.horizontalLayout_2.addWidget(self.cancel_btn) + + + self.verticalLayout.addLayout(self.horizontalLayout_2) + + + self.retranslateUi(Dialog) + + QMetaObject.connectSlotsByName(Dialog) + # setupUi + + def retranslateUi(self, Dialog): + Dialog.setWindowTitle(QCoreApplication.translate("Dialog", u"Dialog", None)) + self.label.setText(QCoreApplication.translate("Dialog", u"Medium suchen", None)) + self.lineEdit.setPlaceholderText(QCoreApplication.translate("Dialog", u"Titel/Signatursuche", None)) + ___qtablewidgetitem = self.tableWidget.horizontalHeaderItem(1) + ___qtablewidgetitem.setText(QCoreApplication.translate("Dialog", u"Apparat", None)); + ___qtablewidgetitem1 = self.tableWidget.horizontalHeaderItem(2) + ___qtablewidgetitem1.setText(QCoreApplication.translate("Dialog", u"Signatur", None)); + ___qtablewidgetitem2 = self.tableWidget.horizontalHeaderItem(3) + ___qtablewidgetitem2.setText(QCoreApplication.translate("Dialog", u"Titel", None)); + ___qtablewidgetitem3 = self.tableWidget.horizontalHeaderItem(4) + ___qtablewidgetitem3.setText(QCoreApplication.translate("Dialog", u"Auflage", None)); + ___qtablewidgetitem4 = self.tableWidget.horizontalHeaderItem(5) + ___qtablewidgetitem4.setText(QCoreApplication.translate("Dialog", u"ISBN", None)); + ___qtablewidgetitem5 = self.tableWidget.horizontalHeaderItem(6) + ___qtablewidgetitem5.setText(QCoreApplication.translate("Dialog", u"ID", None)); + self.reset_btn.setText(QCoreApplication.translate("Dialog", u"Zur\u00fccksetzen", None)) + self.delete_btn.setText(QCoreApplication.translate("Dialog", u"L\u00f6schen", None)) + self.cancel_btn.setText(QCoreApplication.translate("Dialog", u"Abbrechen", None)) + # retranslateUi + diff --git a/src/ui/semesterapparat_ui.ui b/src/ui/semesterapparat_ui.ui index 76f210e..615f845 100644 --- a/src/ui/semesterapparat_ui.ui +++ b/src/ui/semesterapparat_ui.ui @@ -251,169 +251,24 @@ 0 180 1412 - 511 + 531 - - + + - - - Qt::Horizontal - - - QSizePolicy::Fixed - - - - 20 - 20 - - - - - - - - false - - - WIP - Broken + + + Qt::NoFocus - gel. Medien anzeigen + Medien hinzufügen - - - false - - - WIP - Broken - - - Nur Titel mit Neuauflagen anzeigen - - - - - - - Qt::Horizontal - - - QSizePolicy::Fixed - - - - 40 - 20 - - - - - - - - Dieser Knopf prüft alle Werke, die mit einem roten X vermerkt sind. Sollten diese inzwischen im Apparat sein, wird dies aktualisiert - - - Medien mit ❌ im Apparat? - - - - - - - - - Medien werden hinzugefügt - - - - - - - Qt::Vertical - - - - - - - Medium x/y - - - - - - - - - Qt::Horizontal - - - QSizePolicy::Fixed - - - - 40 - 20 - - - - - - - - - - Medien werden geprüft - - - - - - - 24 - - - - - - - - - - - - - - - - Qt::Vertical - - - - - - - TextLabel - - - - - - - Ausgewählte als verfügbar markieren - - - - - + Qt::Horizontal @@ -427,87 +282,6 @@ - - - - - 0 - 0 - - - - - 1259 - 0 - - - - Qt::NoFocus - - - Qt::CustomContextMenu - - - QAbstractScrollArea::AdjustToContents - - - QAbstractItemView::NoEditTriggers - - - true - - - QAbstractItemView::SelectRows - - - true - - - true - - - - Buchtitel - - - Es kann sein, dass der Buchtitel leer ist, dies kommt vor, wenn der Titel nicht passend formatiert ist - - - - - Signatur - - - - - Auflage - - - - - Autor - - - - - im Apparat? - - - Diese Angabe ist nicht zuverlässig. Ist das ❌ vorhanden, kann das Medium im Apparat sein, aber aufgrund eines Bugs nicht gefunden worden - - - - - Vorgemerkt - - - - - Link - - - - @@ -1101,23 +875,7 @@ - - - - - 9 - false - - - - Qt::ImhPreferNumbers - - - - - - - + @@ -1133,7 +891,7 @@ - + @@ -1146,6 +904,22 @@ + + + + + 9 + false + + + + Qt::ImhPreferNumbers + + + + + + @@ -1587,24 +1361,248 @@ Einige Angaben müssen ggf angepasst werden + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 20 + 20 + + + + + + + + false + + + WIP - Broken + + + Nur Titel mit Neuauflagen anzeigen + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 40 + 20 + + + + + + + + Dieser Knopf prüft alle Werke, die mit einem roten X vermerkt sind. Sollten diese inzwischen im Apparat sein, wird dies aktualisiert + + + Medien mit ❌ im Apparat? + + + + + + + + + Medien werden hinzugefügt + + + + + + + Qt::Vertical + + + + + + + Medium x/y + + + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 40 + 20 + + + + + + + + + + Medien werden geprüft + + + + + + + 24 + + + + + + + + + + + + + + + + Qt::Vertical + + + + + + + TextLabel + + + + + + + Ausgewählte als verfügbar markieren + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + 0 + 0 + + + + + 1259 + 0 + + + + Qt::NoFocus + + + Qt::CustomContextMenu + + + QAbstractScrollArea::AdjustToContents + + + QAbstractItemView::NoEditTriggers + + + true + + + QAbstractItemView::SelectRows + + + true + + + true + + + + Buchtitel + + + Es kann sein, dass der Buchtitel leer ist, dies kommt vor, wenn der Titel nicht passend formatiert ist + + + + + Signatur + + + + + Auflage + + + + + Autor + + + + + im Apparat? + + + Diese Angabe ist nicht zuverlässig. Ist das ❌ vorhanden, kann das Medium im Apparat sein, aber aufgrund eines Bugs nicht gefunden worden + + + + + Vorgemerkt + + + + + Link + + + + - - - - 3 - 695 - 121 - 20 - - - - Qt::NoFocus - - - Medien hinzufügen - - @@ -1966,6 +1964,7 @@ Einige Angaben müssen ggf angepasst werden Bearbeiten + @@ -2020,6 +2019,11 @@ Einige Angaben müssen ggf angepasst werden F1 + + + Medien löschen + + drpdwn_app_nr @@ -2038,10 +2042,8 @@ Einige Angaben müssen ggf angepasst werden check_send_mail btn_apparat_save btn_apparat_apply - chkbx_show_del_media btn_reserve select_action_box - prof_id_adis apparat_id_adis automation_add_selected_books saveandcreate diff --git a/src/ui/semesterapparat_ui_ui.py b/src/ui/semesterapparat_ui_ui.py index e62f60c..be2d9e9 100644 --- a/src/ui/semesterapparat_ui_ui.py +++ b/src/ui/semesterapparat_ui_ui.py @@ -54,6 +54,8 @@ class Ui_MainWindow(object): self.actionAbout.setMenuRole(QAction.AboutRole) self.actionDokumentation = QAction(MainWindow) self.actionDokumentation.setObjectName(u"actionDokumentation") + self.actionMedien_loeschen = QAction(MainWindow) + self.actionMedien_loeschen.setObjectName(u"actionMedien_loeschen") self.centralwidget = QWidget(MainWindow) self.centralwidget.setObjectName(u"centralwidget") sizePolicy.setHeightForWidth(self.centralwidget.sizePolicy().hasHeightForWidth()) @@ -157,142 +159,24 @@ class Ui_MainWindow(object): self.gridLayoutWidget_2 = QWidget(self.createApparat) self.gridLayoutWidget_2.setObjectName(u"gridLayoutWidget_2") self.gridLayoutWidget_2.setEnabled(True) - self.gridLayoutWidget_2.setGeometry(QRect(0, 180, 1412, 511)) + self.gridLayoutWidget_2.setGeometry(QRect(0, 180, 1412, 531)) self.gridLayout_2 = QGridLayout(self.gridLayoutWidget_2) self.gridLayout_2.setObjectName(u"gridLayout_2") self.gridLayout_2.setContentsMargins(0, 0, 0, 0) - self.horizontalLayout_5 = QHBoxLayout() - self.horizontalLayout_5.setObjectName(u"horizontalLayout_5") - self.horizontalSpacer = QSpacerItem(20, 20, QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Minimum) + self.horizontalLayout_3 = QHBoxLayout() + self.horizontalLayout_3.setObjectName(u"horizontalLayout_3") + self.add_medium = QPushButton(self.gridLayoutWidget_2) + self.add_medium.setObjectName(u"add_medium") + self.add_medium.setFocusPolicy(Qt.NoFocus) - self.horizontalLayout_5.addItem(self.horizontalSpacer) + self.horizontalLayout_3.addWidget(self.add_medium) - self.chkbx_show_del_media = QCheckBox(self.gridLayoutWidget_2) - self.chkbx_show_del_media.setObjectName(u"chkbx_show_del_media") - self.chkbx_show_del_media.setEnabled(False) + self.horizontalSpacer_5 = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum) - self.horizontalLayout_5.addWidget(self.chkbx_show_del_media) - - self.chkbx_show_only_wit_neweditions = QCheckBox(self.gridLayoutWidget_2) - self.chkbx_show_only_wit_neweditions.setObjectName(u"chkbx_show_only_wit_neweditions") - self.chkbx_show_only_wit_neweditions.setEnabled(False) - - self.horizontalLayout_5.addWidget(self.chkbx_show_only_wit_neweditions) - - self.horizontalSpacer_3 = QSpacerItem(40, 20, QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Minimum) - - self.horizontalLayout_5.addItem(self.horizontalSpacer_3) - - self.btn_reserve = QPushButton(self.gridLayoutWidget_2) - self.btn_reserve.setObjectName(u"btn_reserve") - - self.horizontalLayout_5.addWidget(self.btn_reserve) - - self.add_layout = QHBoxLayout() - self.add_layout.setObjectName(u"add_layout") - self.label_info = QLabel(self.gridLayoutWidget_2) - self.label_info.setObjectName(u"label_info") - - self.add_layout.addWidget(self.label_info) - - self.line_2 = QFrame(self.gridLayoutWidget_2) - self.line_2.setObjectName(u"line_2") - self.line_2.setFrameShape(QFrame.Shape.VLine) - self.line_2.setFrameShadow(QFrame.Shadow.Sunken) - - self.add_layout.addWidget(self.line_2) - - self.progress_label = QLabel(self.gridLayoutWidget_2) - self.progress_label.setObjectName(u"progress_label") - - self.add_layout.addWidget(self.progress_label) + self.horizontalLayout_3.addItem(self.horizontalSpacer_5) - self.horizontalLayout_5.addLayout(self.add_layout) - - self.horizontalSpacer_4 = QSpacerItem(40, 20, QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Minimum) - - self.horizontalLayout_5.addItem(self.horizontalSpacer_4) - - self.avail_layout = QHBoxLayout() - self.avail_layout.setObjectName(u"avail_layout") - self.label_20 = QLabel(self.gridLayoutWidget_2) - self.label_20.setObjectName(u"label_20") - - self.avail_layout.addWidget(self.label_20) - - self.progressBar = QProgressBar(self.gridLayoutWidget_2) - self.progressBar.setObjectName(u"progressBar") - self.progressBar.setValue(24) - - self.avail_layout.addWidget(self.progressBar) - - self.label_eta = QLabel(self.gridLayoutWidget_2) - self.label_eta.setObjectName(u"label_eta") - - self.avail_layout.addWidget(self.label_eta) - - - self.horizontalLayout_5.addLayout(self.avail_layout) - - self.line_3 = QFrame(self.gridLayoutWidget_2) - self.line_3.setObjectName(u"line_3") - self.line_3.setFrameShape(QFrame.Shape.VLine) - self.line_3.setFrameShadow(QFrame.Shadow.Sunken) - - self.horizontalLayout_5.addWidget(self.line_3) - - self.avail_status = QLabel(self.gridLayoutWidget_2) - self.avail_status.setObjectName(u"avail_status") - - self.horizontalLayout_5.addWidget(self.avail_status) - - self.automation_add_selected_books = QPushButton(self.gridLayoutWidget_2) - self.automation_add_selected_books.setObjectName(u"automation_add_selected_books") - - self.horizontalLayout_5.addWidget(self.automation_add_selected_books) - - self.horizontalSpacer_2 = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum) - - self.horizontalLayout_5.addItem(self.horizontalSpacer_2) - - - self.gridLayout_2.addLayout(self.horizontalLayout_5, 4, 0, 1, 1) - - self.tableWidget_apparat_media = QTableWidget(self.gridLayoutWidget_2) - if (self.tableWidget_apparat_media.columnCount() < 7): - self.tableWidget_apparat_media.setColumnCount(7) - __qtablewidgetitem6 = QTableWidgetItem() - self.tableWidget_apparat_media.setHorizontalHeaderItem(0, __qtablewidgetitem6) - __qtablewidgetitem7 = QTableWidgetItem() - self.tableWidget_apparat_media.setHorizontalHeaderItem(1, __qtablewidgetitem7) - __qtablewidgetitem8 = QTableWidgetItem() - self.tableWidget_apparat_media.setHorizontalHeaderItem(2, __qtablewidgetitem8) - __qtablewidgetitem9 = QTableWidgetItem() - self.tableWidget_apparat_media.setHorizontalHeaderItem(3, __qtablewidgetitem9) - __qtablewidgetitem10 = QTableWidgetItem() - self.tableWidget_apparat_media.setHorizontalHeaderItem(4, __qtablewidgetitem10) - __qtablewidgetitem11 = QTableWidgetItem() - self.tableWidget_apparat_media.setHorizontalHeaderItem(5, __qtablewidgetitem11) - __qtablewidgetitem12 = QTableWidgetItem() - self.tableWidget_apparat_media.setHorizontalHeaderItem(6, __qtablewidgetitem12) - self.tableWidget_apparat_media.setObjectName(u"tableWidget_apparat_media") - sizePolicy2 = QSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Expanding) - sizePolicy2.setHorizontalStretch(0) - sizePolicy2.setVerticalStretch(0) - sizePolicy2.setHeightForWidth(self.tableWidget_apparat_media.sizePolicy().hasHeightForWidth()) - self.tableWidget_apparat_media.setSizePolicy(sizePolicy2) - self.tableWidget_apparat_media.setMinimumSize(QSize(1259, 0)) - self.tableWidget_apparat_media.setFocusPolicy(Qt.NoFocus) - self.tableWidget_apparat_media.setContextMenuPolicy(Qt.CustomContextMenu) - self.tableWidget_apparat_media.setSizeAdjustPolicy(QAbstractScrollArea.AdjustToContents) - self.tableWidget_apparat_media.setEditTriggers(QAbstractItemView.NoEditTriggers) - self.tableWidget_apparat_media.setAlternatingRowColors(True) - self.tableWidget_apparat_media.setSelectionBehavior(QAbstractItemView.SelectRows) - self.tableWidget_apparat_media.setSortingEnabled(True) - self.tableWidget_apparat_media.horizontalHeader().setCascadingSectionResizes(True) - - self.gridLayout_2.addWidget(self.tableWidget_apparat_media, 9, 0, 1, 1) + self.gridLayout_2.addLayout(self.horizontalLayout_3, 11, 0, 1, 1) self.label = QLabel(self.gridLayoutWidget_2) self.label.setObjectName(u"label") @@ -306,11 +190,11 @@ class Ui_MainWindow(object): self.app_group_box = QGroupBox(self.gridLayoutWidget_2) self.app_group_box.setObjectName(u"app_group_box") self.app_group_box.setEnabled(True) - sizePolicy3 = QSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Fixed) - sizePolicy3.setHorizontalStretch(0) - sizePolicy3.setVerticalStretch(0) - sizePolicy3.setHeightForWidth(self.app_group_box.sizePolicy().hasHeightForWidth()) - self.app_group_box.setSizePolicy(sizePolicy3) + sizePolicy2 = QSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Fixed) + sizePolicy2.setHorizontalStretch(0) + sizePolicy2.setVerticalStretch(0) + sizePolicy2.setHeightForWidth(self.app_group_box.sizePolicy().hasHeightForWidth()) + self.app_group_box.setSizePolicy(sizePolicy2) self.app_group_box.setMinimumSize(QSize(0, 210)) font1 = QFont() font1.setPointSize(12) @@ -325,17 +209,17 @@ class Ui_MainWindow(object): font2 = QFont() font2.setFamilies([u"Arial"]) font2.setPointSize(8) - __qtablewidgetitem13 = QTableWidgetItem() - __qtablewidgetitem13.setFont(font2); - self.document_list.setHorizontalHeaderItem(0, __qtablewidgetitem13) - __qtablewidgetitem14 = QTableWidgetItem() - __qtablewidgetitem14.setFont(font2); - self.document_list.setHorizontalHeaderItem(1, __qtablewidgetitem14) - __qtablewidgetitem15 = QTableWidgetItem() - __qtablewidgetitem15.setFont(font2); - self.document_list.setHorizontalHeaderItem(2, __qtablewidgetitem15) - __qtablewidgetitem16 = QTableWidgetItem() - self.document_list.setHorizontalHeaderItem(3, __qtablewidgetitem16) + __qtablewidgetitem6 = QTableWidgetItem() + __qtablewidgetitem6.setFont(font2); + self.document_list.setHorizontalHeaderItem(0, __qtablewidgetitem6) + __qtablewidgetitem7 = QTableWidgetItem() + __qtablewidgetitem7.setFont(font2); + self.document_list.setHorizontalHeaderItem(1, __qtablewidgetitem7) + __qtablewidgetitem8 = QTableWidgetItem() + __qtablewidgetitem8.setFont(font2); + self.document_list.setHorizontalHeaderItem(2, __qtablewidgetitem8) + __qtablewidgetitem9 = QTableWidgetItem() + self.document_list.setHorizontalHeaderItem(3, __qtablewidgetitem9) self.document_list.setObjectName(u"document_list") self.document_list.setGeometry(QRect(780, 20, 321, 181)) font3 = QFont() @@ -473,25 +357,25 @@ class Ui_MainWindow(object): self.formLayout_3.setWidget(0, QFormLayout.ItemRole.LabelRole, self.label_12) - self.prof_id_adis = QLineEdit(self.formLayoutWidget_2) - self.prof_id_adis.setObjectName(u"prof_id_adis") - self.prof_id_adis.setFont(font4) - self.prof_id_adis.setInputMethodHints(Qt.ImhPreferNumbers) - - self.formLayout_3.setWidget(0, QFormLayout.ItemRole.FieldRole, self.prof_id_adis) - self.label_13 = QLabel(self.formLayoutWidget_2) self.label_13.setObjectName(u"label_13") self.label_13.setFont(font4) - self.formLayout_3.setWidget(1, QFormLayout.ItemRole.LabelRole, self.label_13) + self.formLayout_3.setWidget(2, QFormLayout.ItemRole.LabelRole, self.label_13) self.apparat_id_adis = QLineEdit(self.formLayoutWidget_2) self.apparat_id_adis.setObjectName(u"apparat_id_adis") self.apparat_id_adis.setFont(font4) self.apparat_id_adis.setInputMethodHints(Qt.ImhPreferNumbers) - self.formLayout_3.setWidget(1, QFormLayout.ItemRole.FieldRole, self.apparat_id_adis) + self.formLayout_3.setWidget(2, QFormLayout.ItemRole.FieldRole, self.apparat_id_adis) + + self.prof_id_adis = QLineEdit(self.formLayoutWidget_2) + self.prof_id_adis.setObjectName(u"prof_id_adis") + self.prof_id_adis.setFont(font4) + self.prof_id_adis.setInputMethodHints(Qt.ImhPreferNumbers) + + self.formLayout_3.setWidget(1, QFormLayout.ItemRole.FieldRole, self.prof_id_adis) self.sem_year = QLineEdit(self.app_group_box) self.sem_year.setObjectName(u"sem_year") @@ -578,35 +462,35 @@ class Ui_MainWindow(object): self.verticalLayout_8.setContentsMargins(0, 0, 0, 0) self.btn_add_document = QPushButton(self.verticalLayoutWidget_3) self.btn_add_document.setObjectName(u"btn_add_document") - sizePolicy4 = QSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.MinimumExpanding) - sizePolicy4.setHorizontalStretch(0) - sizePolicy4.setVerticalStretch(0) - sizePolicy4.setHeightForWidth(self.btn_add_document.sizePolicy().hasHeightForWidth()) - self.btn_add_document.setSizePolicy(sizePolicy4) + sizePolicy3 = QSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.MinimumExpanding) + sizePolicy3.setHorizontalStretch(0) + sizePolicy3.setVerticalStretch(0) + sizePolicy3.setHeightForWidth(self.btn_add_document.sizePolicy().hasHeightForWidth()) + self.btn_add_document.setSizePolicy(sizePolicy3) self.btn_add_document.setFont(font4) self.verticalLayout_8.addWidget(self.btn_add_document) self.btn_open_document = QPushButton(self.verticalLayoutWidget_3) self.btn_open_document.setObjectName(u"btn_open_document") - sizePolicy4.setHeightForWidth(self.btn_open_document.sizePolicy().hasHeightForWidth()) - self.btn_open_document.setSizePolicy(sizePolicy4) + sizePolicy3.setHeightForWidth(self.btn_open_document.sizePolicy().hasHeightForWidth()) + self.btn_open_document.setSizePolicy(sizePolicy3) self.btn_open_document.setFont(font4) self.verticalLayout_8.addWidget(self.btn_open_document) self.check_file = QPushButton(self.verticalLayoutWidget_3) self.check_file.setObjectName(u"check_file") - sizePolicy4.setHeightForWidth(self.check_file.sizePolicy().hasHeightForWidth()) - self.check_file.setSizePolicy(sizePolicy4) + sizePolicy3.setHeightForWidth(self.check_file.sizePolicy().hasHeightForWidth()) + self.check_file.setSizePolicy(sizePolicy3) self.check_file.setFont(font4) self.verticalLayout_8.addWidget(self.check_file) self.btn_extract_data_from_document = QPushButton(self.verticalLayoutWidget_3) self.btn_extract_data_from_document.setObjectName(u"btn_extract_data_from_document") - sizePolicy4.setHeightForWidth(self.btn_extract_data_from_document.sizePolicy().hasHeightForWidth()) - self.btn_extract_data_from_document.setSizePolicy(sizePolicy4) + sizePolicy3.setHeightForWidth(self.btn_extract_data_from_document.sizePolicy().hasHeightForWidth()) + self.btn_extract_data_from_document.setSizePolicy(sizePolicy3) self.btn_extract_data_from_document.setFont(font4) self.verticalLayout_8.addWidget(self.btn_extract_data_from_document) @@ -618,10 +502,133 @@ class Ui_MainWindow(object): self.gridLayout_2.addWidget(self.app_group_box, 1, 0, 1, 1) - self.add_medium = QPushButton(self.createApparat) - self.add_medium.setObjectName(u"add_medium") - self.add_medium.setGeometry(QRect(3, 695, 121, 20)) - self.add_medium.setFocusPolicy(Qt.NoFocus) + self.horizontalLayout_5 = QHBoxLayout() + self.horizontalLayout_5.setObjectName(u"horizontalLayout_5") + self.horizontalSpacer = QSpacerItem(20, 20, QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Minimum) + + self.horizontalLayout_5.addItem(self.horizontalSpacer) + + self.chkbx_show_only_wit_neweditions = QCheckBox(self.gridLayoutWidget_2) + self.chkbx_show_only_wit_neweditions.setObjectName(u"chkbx_show_only_wit_neweditions") + self.chkbx_show_only_wit_neweditions.setEnabled(False) + + self.horizontalLayout_5.addWidget(self.chkbx_show_only_wit_neweditions) + + self.horizontalSpacer_3 = QSpacerItem(40, 20, QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Minimum) + + self.horizontalLayout_5.addItem(self.horizontalSpacer_3) + + self.btn_reserve = QPushButton(self.gridLayoutWidget_2) + self.btn_reserve.setObjectName(u"btn_reserve") + + self.horizontalLayout_5.addWidget(self.btn_reserve) + + self.add_layout = QHBoxLayout() + self.add_layout.setObjectName(u"add_layout") + self.label_info = QLabel(self.gridLayoutWidget_2) + self.label_info.setObjectName(u"label_info") + + self.add_layout.addWidget(self.label_info) + + self.line_2 = QFrame(self.gridLayoutWidget_2) + self.line_2.setObjectName(u"line_2") + self.line_2.setFrameShape(QFrame.Shape.VLine) + self.line_2.setFrameShadow(QFrame.Shadow.Sunken) + + self.add_layout.addWidget(self.line_2) + + self.progress_label = QLabel(self.gridLayoutWidget_2) + self.progress_label.setObjectName(u"progress_label") + + self.add_layout.addWidget(self.progress_label) + + + self.horizontalLayout_5.addLayout(self.add_layout) + + self.horizontalSpacer_4 = QSpacerItem(40, 20, QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Minimum) + + self.horizontalLayout_5.addItem(self.horizontalSpacer_4) + + self.avail_layout = QHBoxLayout() + self.avail_layout.setObjectName(u"avail_layout") + self.label_20 = QLabel(self.gridLayoutWidget_2) + self.label_20.setObjectName(u"label_20") + + self.avail_layout.addWidget(self.label_20) + + self.progressBar = QProgressBar(self.gridLayoutWidget_2) + self.progressBar.setObjectName(u"progressBar") + self.progressBar.setValue(24) + + self.avail_layout.addWidget(self.progressBar) + + self.label_eta = QLabel(self.gridLayoutWidget_2) + self.label_eta.setObjectName(u"label_eta") + + self.avail_layout.addWidget(self.label_eta) + + + self.horizontalLayout_5.addLayout(self.avail_layout) + + self.line_3 = QFrame(self.gridLayoutWidget_2) + self.line_3.setObjectName(u"line_3") + self.line_3.setFrameShape(QFrame.Shape.VLine) + self.line_3.setFrameShadow(QFrame.Shadow.Sunken) + + self.horizontalLayout_5.addWidget(self.line_3) + + self.avail_status = QLabel(self.gridLayoutWidget_2) + self.avail_status.setObjectName(u"avail_status") + + self.horizontalLayout_5.addWidget(self.avail_status) + + self.automation_add_selected_books = QPushButton(self.gridLayoutWidget_2) + self.automation_add_selected_books.setObjectName(u"automation_add_selected_books") + + self.horizontalLayout_5.addWidget(self.automation_add_selected_books) + + self.horizontalSpacer_2 = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum) + + self.horizontalLayout_5.addItem(self.horizontalSpacer_2) + + + self.gridLayout_2.addLayout(self.horizontalLayout_5, 4, 0, 1, 1) + + self.tableWidget_apparat_media = QTableWidget(self.gridLayoutWidget_2) + if (self.tableWidget_apparat_media.columnCount() < 7): + self.tableWidget_apparat_media.setColumnCount(7) + __qtablewidgetitem10 = QTableWidgetItem() + self.tableWidget_apparat_media.setHorizontalHeaderItem(0, __qtablewidgetitem10) + __qtablewidgetitem11 = QTableWidgetItem() + self.tableWidget_apparat_media.setHorizontalHeaderItem(1, __qtablewidgetitem11) + __qtablewidgetitem12 = QTableWidgetItem() + self.tableWidget_apparat_media.setHorizontalHeaderItem(2, __qtablewidgetitem12) + __qtablewidgetitem13 = QTableWidgetItem() + self.tableWidget_apparat_media.setHorizontalHeaderItem(3, __qtablewidgetitem13) + __qtablewidgetitem14 = QTableWidgetItem() + self.tableWidget_apparat_media.setHorizontalHeaderItem(4, __qtablewidgetitem14) + __qtablewidgetitem15 = QTableWidgetItem() + self.tableWidget_apparat_media.setHorizontalHeaderItem(5, __qtablewidgetitem15) + __qtablewidgetitem16 = QTableWidgetItem() + self.tableWidget_apparat_media.setHorizontalHeaderItem(6, __qtablewidgetitem16) + self.tableWidget_apparat_media.setObjectName(u"tableWidget_apparat_media") + sizePolicy4 = QSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Expanding) + sizePolicy4.setHorizontalStretch(0) + sizePolicy4.setVerticalStretch(0) + sizePolicy4.setHeightForWidth(self.tableWidget_apparat_media.sizePolicy().hasHeightForWidth()) + self.tableWidget_apparat_media.setSizePolicy(sizePolicy4) + self.tableWidget_apparat_media.setMinimumSize(QSize(1259, 0)) + self.tableWidget_apparat_media.setFocusPolicy(Qt.NoFocus) + self.tableWidget_apparat_media.setContextMenuPolicy(Qt.CustomContextMenu) + self.tableWidget_apparat_media.setSizeAdjustPolicy(QAbstractScrollArea.AdjustToContents) + self.tableWidget_apparat_media.setEditTriggers(QAbstractItemView.NoEditTriggers) + self.tableWidget_apparat_media.setAlternatingRowColors(True) + self.tableWidget_apparat_media.setSelectionBehavior(QAbstractItemView.SelectRows) + self.tableWidget_apparat_media.setSortingEnabled(True) + self.tableWidget_apparat_media.horizontalHeader().setCascadingSectionResizes(True) + + self.gridLayout_2.addWidget(self.tableWidget_apparat_media, 9, 0, 1, 1) + self.tabWidget.addTab(self.createApparat, "") self.search_statistics = QWidget() self.search_statistics.setObjectName(u"search_statistics") @@ -827,11 +834,9 @@ class Ui_MainWindow(object): QWidget.setTabOrder(self.check_file, self.check_send_mail) QWidget.setTabOrder(self.check_send_mail, self.btn_apparat_save) QWidget.setTabOrder(self.btn_apparat_save, self.btn_apparat_apply) - QWidget.setTabOrder(self.btn_apparat_apply, self.chkbx_show_del_media) - QWidget.setTabOrder(self.chkbx_show_del_media, self.btn_reserve) + QWidget.setTabOrder(self.btn_apparat_apply, self.btn_reserve) QWidget.setTabOrder(self.btn_reserve, self.select_action_box) - QWidget.setTabOrder(self.select_action_box, self.prof_id_adis) - QWidget.setTabOrder(self.prof_id_adis, self.apparat_id_adis) + QWidget.setTabOrder(self.select_action_box, self.apparat_id_adis) QWidget.setTabOrder(self.apparat_id_adis, self.automation_add_selected_books) QWidget.setTabOrder(self.automation_add_selected_books, self.saveandcreate) @@ -840,6 +845,7 @@ class Ui_MainWindow(object): self.menubar.addAction(self.menuHelp.menuAction()) self.menuDatei.addAction(self.actionBeenden) self.menuEinstellungen.addAction(self.actionEinstellungen) + self.menuEinstellungen.addAction(self.actionMedien_loeschen) self.menuHelp.addAction(self.actionAbout) self.menuHelp.addAction(self.actionDokumentation) @@ -869,6 +875,7 @@ class Ui_MainWindow(object): #if QT_CONFIG(shortcut) self.actionDokumentation.setShortcut(QCoreApplication.translate("MainWindow", u"F1", None)) #endif // QT_CONFIG(shortcut) + self.actionMedien_loeschen.setText(QCoreApplication.translate("MainWindow", u"Medien l\u00f6schen", None)) #if QT_CONFIG(tooltip) self.create_document.setToolTip(QCoreApplication.translate("MainWindow", u"Erstellt die \u00dcbersicht, welche am Regal ausgeh\u00e4ngt werden kann", None)) #endif // QT_CONFIG(tooltip) @@ -887,54 +894,17 @@ class Ui_MainWindow(object): ___qtablewidgetitem4.setText(QCoreApplication.translate("MainWindow", u"Dauerapparat", None)); ___qtablewidgetitem5 = self.tableWidget_apparate.horizontalHeaderItem(5) ___qtablewidgetitem5.setText(QCoreApplication.translate("MainWindow", u"KontoNr", None)); -#if QT_CONFIG(tooltip) - self.chkbx_show_del_media.setToolTip(QCoreApplication.translate("MainWindow", u"WIP - Broken", None)) -#endif // QT_CONFIG(tooltip) - self.chkbx_show_del_media.setText(QCoreApplication.translate("MainWindow", u"gel. Medien anzeigen", None)) -#if QT_CONFIG(tooltip) - self.chkbx_show_only_wit_neweditions.setToolTip(QCoreApplication.translate("MainWindow", u"WIP - Broken", None)) -#endif // QT_CONFIG(tooltip) - self.chkbx_show_only_wit_neweditions.setText(QCoreApplication.translate("MainWindow", u"Nur Titel mit Neuauflagen anzeigen", None)) -#if QT_CONFIG(tooltip) - self.btn_reserve.setToolTip(QCoreApplication.translate("MainWindow", u"Dieser Knopf pr\u00fcft alle Werke, die mit einem roten X vermerkt sind. Sollten diese inzwischen im Apparat sein, wird dies aktualisiert", None)) -#endif // QT_CONFIG(tooltip) - self.btn_reserve.setText(QCoreApplication.translate("MainWindow", u"Medien mit \u274c im Apparat?", None)) - self.label_info.setText(QCoreApplication.translate("MainWindow", u"Medien werden hinzugef\u00fcgt", None)) - self.progress_label.setText(QCoreApplication.translate("MainWindow", u"Medium x/y", None)) - self.label_20.setText(QCoreApplication.translate("MainWindow", u"Medien werden gepr\u00fcft", None)) - self.label_eta.setText("") - self.avail_status.setText(QCoreApplication.translate("MainWindow", u"TextLabel", None)) - self.automation_add_selected_books.setText(QCoreApplication.translate("MainWindow", u"Ausgew\u00e4hlte als verf\u00fcgbar markieren", None)) - ___qtablewidgetitem6 = self.tableWidget_apparat_media.horizontalHeaderItem(0) - ___qtablewidgetitem6.setText(QCoreApplication.translate("MainWindow", u"Buchtitel", None)); -#if QT_CONFIG(tooltip) - ___qtablewidgetitem6.setToolTip(QCoreApplication.translate("MainWindow", u"Es kann sein, dass der Buchtitel leer ist, dies kommt vor, wenn der Titel nicht passend formatiert ist", None)); -#endif // QT_CONFIG(tooltip) - ___qtablewidgetitem7 = self.tableWidget_apparat_media.horizontalHeaderItem(1) - ___qtablewidgetitem7.setText(QCoreApplication.translate("MainWindow", u"Signatur", None)); - ___qtablewidgetitem8 = self.tableWidget_apparat_media.horizontalHeaderItem(2) - ___qtablewidgetitem8.setText(QCoreApplication.translate("MainWindow", u"Auflage", None)); - ___qtablewidgetitem9 = self.tableWidget_apparat_media.horizontalHeaderItem(3) - ___qtablewidgetitem9.setText(QCoreApplication.translate("MainWindow", u"Autor", None)); - ___qtablewidgetitem10 = self.tableWidget_apparat_media.horizontalHeaderItem(4) - ___qtablewidgetitem10.setText(QCoreApplication.translate("MainWindow", u"im Apparat?", None)); -#if QT_CONFIG(tooltip) - ___qtablewidgetitem10.setToolTip(QCoreApplication.translate("MainWindow", u"Diese Angabe ist nicht zuverl\u00e4ssig. Ist das \u274c vorhanden, kann das Medium im Apparat sein, aber aufgrund eines Bugs nicht gefunden worden", None)); -#endif // QT_CONFIG(tooltip) - ___qtablewidgetitem11 = self.tableWidget_apparat_media.horizontalHeaderItem(5) - ___qtablewidgetitem11.setText(QCoreApplication.translate("MainWindow", u"Vorgemerkt", None)); - ___qtablewidgetitem12 = self.tableWidget_apparat_media.horizontalHeaderItem(6) - ___qtablewidgetitem12.setText(QCoreApplication.translate("MainWindow", u"Link", None)); + self.add_medium.setText(QCoreApplication.translate("MainWindow", u"Medien hinzuf\u00fcgen", None)) self.label.setText(QCoreApplication.translate("MainWindow", u" Medienliste", None)) self.app_group_box.setTitle(QCoreApplication.translate("MainWindow", u"SemesterApparatsdetails", None)) - ___qtablewidgetitem13 = self.document_list.horizontalHeaderItem(0) - ___qtablewidgetitem13.setText(QCoreApplication.translate("MainWindow", u"Dokumentname", None)); - ___qtablewidgetitem14 = self.document_list.horizontalHeaderItem(1) - ___qtablewidgetitem14.setText(QCoreApplication.translate("MainWindow", u"Dateityp", None)); - ___qtablewidgetitem15 = self.document_list.horizontalHeaderItem(2) - ___qtablewidgetitem15.setText(QCoreApplication.translate("MainWindow", u"Neu?", None)); - ___qtablewidgetitem16 = self.document_list.horizontalHeaderItem(3) - ___qtablewidgetitem16.setText(QCoreApplication.translate("MainWindow", u"path", None)); + ___qtablewidgetitem6 = self.document_list.horizontalHeaderItem(0) + ___qtablewidgetitem6.setText(QCoreApplication.translate("MainWindow", u"Dokumentname", None)); + ___qtablewidgetitem7 = self.document_list.horizontalHeaderItem(1) + ___qtablewidgetitem7.setText(QCoreApplication.translate("MainWindow", u"Dateityp", None)); + ___qtablewidgetitem8 = self.document_list.horizontalHeaderItem(2) + ___qtablewidgetitem8.setText(QCoreApplication.translate("MainWindow", u"Neu?", None)); + ___qtablewidgetitem9 = self.document_list.horizontalHeaderItem(3) + ___qtablewidgetitem9.setText(QCoreApplication.translate("MainWindow", u"path", None)); self.appname_mand.setText(QCoreApplication.translate("MainWindow", u"*", None)) self.profname_mand.setText(QCoreApplication.translate("MainWindow", u"*", None)) self.fach_mand.setText(QCoreApplication.translate("MainWindow", u"*", None)) @@ -959,8 +929,8 @@ class Ui_MainWindow(object): self.label_10.setText(QCoreApplication.translate("MainWindow", u"Fach", None)) self.prof_mail.setPlaceholderText("") self.label_12.setText(QCoreApplication.translate("MainWindow", u"Prof-ID-aDIS", None)) - self.prof_id_adis.setText("") self.label_13.setText(QCoreApplication.translate("MainWindow", u"Apparat-ID-aDIS", None)) + self.prof_id_adis.setText("") self.sem_year.setPlaceholderText(QCoreApplication.translate("MainWindow", u"2023", None)) self.check_send_mail.setText(QCoreApplication.translate("MainWindow", u"Mail senden", None)) self.sem_winter.setText(QCoreApplication.translate("MainWindow", u"Winter", None)) @@ -997,7 +967,40 @@ class Ui_MainWindow(object): #endif // QT_CONFIG(tooltip) self.btn_extract_data_from_document.setText(QCoreApplication.translate("MainWindow", u"Daten aus Dokument\n" "\u00fcbernehmen", None)) - self.add_medium.setText(QCoreApplication.translate("MainWindow", u"Medien hinzuf\u00fcgen", None)) +#if QT_CONFIG(tooltip) + self.chkbx_show_only_wit_neweditions.setToolTip(QCoreApplication.translate("MainWindow", u"WIP - Broken", None)) +#endif // QT_CONFIG(tooltip) + self.chkbx_show_only_wit_neweditions.setText(QCoreApplication.translate("MainWindow", u"Nur Titel mit Neuauflagen anzeigen", None)) +#if QT_CONFIG(tooltip) + self.btn_reserve.setToolTip(QCoreApplication.translate("MainWindow", u"Dieser Knopf pr\u00fcft alle Werke, die mit einem roten X vermerkt sind. Sollten diese inzwischen im Apparat sein, wird dies aktualisiert", None)) +#endif // QT_CONFIG(tooltip) + self.btn_reserve.setText(QCoreApplication.translate("MainWindow", u"Medien mit \u274c im Apparat?", None)) + self.label_info.setText(QCoreApplication.translate("MainWindow", u"Medien werden hinzugef\u00fcgt", None)) + self.progress_label.setText(QCoreApplication.translate("MainWindow", u"Medium x/y", None)) + self.label_20.setText(QCoreApplication.translate("MainWindow", u"Medien werden gepr\u00fcft", None)) + self.label_eta.setText("") + self.avail_status.setText(QCoreApplication.translate("MainWindow", u"TextLabel", None)) + self.automation_add_selected_books.setText(QCoreApplication.translate("MainWindow", u"Ausgew\u00e4hlte als verf\u00fcgbar markieren", None)) + ___qtablewidgetitem10 = self.tableWidget_apparat_media.horizontalHeaderItem(0) + ___qtablewidgetitem10.setText(QCoreApplication.translate("MainWindow", u"Buchtitel", None)); +#if QT_CONFIG(tooltip) + ___qtablewidgetitem10.setToolTip(QCoreApplication.translate("MainWindow", u"Es kann sein, dass der Buchtitel leer ist, dies kommt vor, wenn der Titel nicht passend formatiert ist", None)); +#endif // QT_CONFIG(tooltip) + ___qtablewidgetitem11 = self.tableWidget_apparat_media.horizontalHeaderItem(1) + ___qtablewidgetitem11.setText(QCoreApplication.translate("MainWindow", u"Signatur", None)); + ___qtablewidgetitem12 = self.tableWidget_apparat_media.horizontalHeaderItem(2) + ___qtablewidgetitem12.setText(QCoreApplication.translate("MainWindow", u"Auflage", None)); + ___qtablewidgetitem13 = self.tableWidget_apparat_media.horizontalHeaderItem(3) + ___qtablewidgetitem13.setText(QCoreApplication.translate("MainWindow", u"Autor", None)); + ___qtablewidgetitem14 = self.tableWidget_apparat_media.horizontalHeaderItem(4) + ___qtablewidgetitem14.setText(QCoreApplication.translate("MainWindow", u"im Apparat?", None)); +#if QT_CONFIG(tooltip) + ___qtablewidgetitem14.setToolTip(QCoreApplication.translate("MainWindow", u"Diese Angabe ist nicht zuverl\u00e4ssig. Ist das \u274c vorhanden, kann das Medium im Apparat sein, aber aufgrund eines Bugs nicht gefunden worden", None)); +#endif // QT_CONFIG(tooltip) + ___qtablewidgetitem15 = self.tableWidget_apparat_media.horizontalHeaderItem(5) + ___qtablewidgetitem15.setText(QCoreApplication.translate("MainWindow", u"Vorgemerkt", None)); + ___qtablewidgetitem16 = self.tableWidget_apparat_media.horizontalHeaderItem(6) + ___qtablewidgetitem16.setText(QCoreApplication.translate("MainWindow", u"Link", None)); self.tabWidget.setTabText(self.tabWidget.indexOf(self.createApparat), QCoreApplication.translate("MainWindow", u"Anlegen", None)) self.tabWidget.setTabText(self.tabWidget.indexOf(self.search_statistics), QCoreApplication.translate("MainWindow", u"Suchen / Statistik", None)) self.tabWidget.setTabText(self.tabWidget.indexOf(self.elsatab), QCoreApplication.translate("MainWindow", u"ELSA", None)) diff --git a/src/ui/userInterface.py b/src/ui/userInterface.py index 5c4ef19..a5c0b8f 100644 --- a/src/ui/userInterface.py +++ b/src/ui/userInterface.py @@ -5,18 +5,16 @@ import sys import tempfile import time import webbrowser -from datetime import datetime from pathlib import Path from typing import Any, List, Optional, Tuple, Union -import loguru from natsort import natsorted from PySide6 import QtCore, QtGui, QtWidgets from PySide6.QtCore import QThread from PySide6.QtGui import QRegularExpressionValidator from PySide6.QtMultimedia import QAudioOutput, QMediaPlayer -from src import LOG_DIR, Icon +from src import Icon from src.backend import ( AvailChecker, BookGrabber, @@ -39,11 +37,13 @@ from src.logic import ( pdf_to_semap, word_to_semap, ) +from src.shared.logging import log from src.ui import Ui_Semesterapparat from src.ui.dialogs import ( About, ApparatExtendDialog, BookDataUI, + DeleteDialog, DocumentPrintDialog, LoginDialog, Mail_Dialog, @@ -69,16 +69,6 @@ from src.ui.widgets import ( UserCreate, ) -log = loguru.logger -log.remove() -log.add(sys.stdout, level="INFO") -log.add(f"{LOG_DIR}/application.log", rotation="3 MB", retention="10 days") - -log.add( - f"{LOG_DIR}/{datetime.now().strftime('%Y-%m-%d')}.log", - rotation="1 day", - retention="1 month", -) log.success("UI started") valid_input = (0, 0, 0, 0, 0, 0) @@ -92,7 +82,9 @@ class Ui(QtWidgets.QMainWindow, Ui_Semesterapparat): self.setupUi(MainWindow) # type:ignore self.MainWindow = MainWindow # type:ignore # set the window title - MainWindow.setWindowTitle("Semesterapparatsmanagement") # type:ignore + MainWindow.setWindowTitle( + f"Semesterapparatsmanagement Semester: {Semester().value}" + ) # type:ignore MainWindow.setWindowIcon(Icon("logo").icon) # type:ignore self.db = Database() @@ -135,6 +127,8 @@ class Ui(QtWidgets.QMainWindow, Ui_Semesterapparat): Icon("offAction", self.actionBeenden) self.actionBeenden.triggered.connect(self.quit) # type:ignore self.actionAbout.triggered.connect(self.open_about) # type:ignore + self.actionMedien_loeschen.triggered.connect(self.open_delete_dialog) + self.actionMedien_loeschen.setIcon(Icon("trash").icon) # set validators self.sem_sommer.clicked.connect(lambda: self.toggleButton(self.sem_winter)) # type:ignore @@ -191,11 +185,11 @@ class Ui(QtWidgets.QMainWindow, Ui_Semesterapparat): self.app_fach.currentTextChanged.connect(self.validate_app_fach) # type:ignore self.sem_year.textChanged.connect(self.validate_semester) # type:ignore self.check_eternal_app.stateChanged.connect(self.validate_semester) # type:ignore - self.chkbx_show_del_media.stateChanged.connect(self.update_app_media_list) # type:ignore + # self.chkbx_show_del_media.stateChanged.connect(self.update_app_media_list) # type:ignore self.progress_label.setText("Bitte warten...") # Set visibility/enabled state of certain entries - self.chkbx_show_del_media.setEnabled(False) + # self.chkbx_show_del_media.setEnabled(False) self.label_info.hide() self.app_group_box.setEnabled(False) self.line_2.hide() @@ -207,7 +201,7 @@ class Ui(QtWidgets.QMainWindow, Ui_Semesterapparat): self.progressBar.hide() self.progressBar.setMinimum(0) self.avail_status.hide() - self.chkbx_show_del_media.hide() + # self.chkbx_show_del_media.hide() self.chkbx_show_only_wit_neweditions.hide() self.automation_add_selected_books.hide() # self.btn_del_select_apparats.setEnabled(False) @@ -288,6 +282,12 @@ class Ui(QtWidgets.QMainWindow, Ui_Semesterapparat): self.document_list.installEventFilter(self) self.document_list.viewport().installEventFilter(self) + def open_delete_dialog(self): + # this will open a dialog to select and delete multiple entries. by default, all books will be loaded, and a search bar can be used to fuzzy search the books and select books. on a button press, the selected books will be deleted from the apparat(s) they are part in + # raise NotImplementedError("This feature is not yet implemented.") + dialog = DeleteDialog() + dialog.exec() + def eventFilter(self, obj, event): # Only handle events for document_list and its viewport if obj in (self.document_list, self.document_list.viewport()): @@ -564,7 +564,7 @@ class Ui(QtWidgets.QMainWindow, Ui_Semesterapparat): self.update_app_media_list() self.cancel_active_selection.click() self.check_send_mail.show() - self.chkbx_show_del_media.show() + # self.chkbx_show_del_media.show() self.cancel_active_selection.setEnabled(False) self.add_medium.setEnabled(False) # update apparat table @@ -731,12 +731,12 @@ class Ui(QtWidgets.QMainWindow, Ui_Semesterapparat): self.sem_winter.setEnabled(False) self.sem_year.setEnabled(False) self.document_list.setRowCount(0) - self.chkbx_show_del_media.setEnabled(True) + # self.chkbx_show_del_media.setEnabled(True) appdata = self.db.getApparatData(appnr, appname) self.populate_frame(appdata) self.btn_apparat_save.hide() self.btn_reserve.show() - self.chkbx_show_del_media.show() + # self.chkbx_show_del_media.show() self.drpdwn_app_nr.setDisabled(True) self.update_app_media_list() @@ -773,7 +773,7 @@ class Ui(QtWidgets.QMainWindow, Ui_Semesterapparat): self.sem_year.setEnabled(True) self.sem_sommer.setEnabled(True) self.sem_winter.setEnabled(True) - self.chkbx_show_del_media.setEnabled(True) + # self.chkbx_show_del_media.setEnabled(True) self.drpdwn_app_nr.setEnabled(True) self.app_fach.setEnabled(True) self.check_send_mail.show() @@ -919,7 +919,7 @@ class Ui(QtWidgets.QMainWindow, Ui_Semesterapparat): self.document_list.setRowCount(0) self.app_group_box.setEnabled(False) self.app_fach.setCurrentText("") - self.chkbx_show_del_media.hide() + # self.chkbx_show_del_media.hide() self.check_send_mail.hide() self.btn_reserve.hide() self.check_eternal_app.setEnabled(False) -- 2.49.1 From 0406fe4f6f04c4171517ed87406e5986c883170f Mon Sep 17 00:00:00 2001 From: WorldTeacher Date: Tue, 21 Oct 2025 09:09:54 +0200 Subject: [PATCH 32/40] Refactor and enhance type hints across multiple modules - Updated the `from_tuple` method in `Prof` class to specify return type. - Added type hints for various methods in `LehmannsClient`, `OpenAI`, `WebRequest`, and `ZoteroController` classes to improve code clarity and type safety. - Modified `pdf_to_csv` function to return a string instead of a DataFrame. - Enhanced error handling and type hints in `wordparser` and `xmlparser` modules. - Removed unused UI file `Ui_medianadder.ts`. - Improved the layout and structure of the `semesterapparat_ui` to enhance user experience. - Updated file picker to support `.doc` files in addition to `.docx`. - Added unique item handling in `Ui` class to prevent duplicates in apparat list. - General code cleanup and consistency improvements across various files. --- src/__init__.py | 4 +- src/backend/catalogue.py | 8 +- src/backend/database.py | 4 +- src/logic/SRU.py | 10 +- src/logic/c_sort.py | 51 ++--- src/logic/constants.py | 360 +++++++++++++++---------------- src/logic/dataclass.py | 3 +- src/logic/lehmannsapi.py | 26 +-- src/logic/openai.py | 37 ++-- src/logic/pdfparser.py | 3 +- src/logic/semester.py | 14 ++ src/logic/settings.py | 2 +- src/logic/webrequest.py | 14 +- src/logic/wordparser.py | 16 +- src/logic/xmlparser.py | 4 +- src/logic/zotero.py | 43 ++-- src/ui/dialogs/Ui_medianadder.ts | 4 - src/ui/dialogs/docuprint.py | 6 +- src/ui/semesterapparat_ui.ui | 122 +++++------ src/ui/semesterapparat_ui_ui.py | 27 ++- src/ui/userInterface.py | 32 ++- src/ui/widgets/filepicker.py | 2 +- src/utils/blob.py | 2 +- src/utils/documentation.py | 13 +- src/utils/pickles.py | 4 +- src/utils/richtext.py | 22 +- 26 files changed, 437 insertions(+), 396 deletions(-) delete mode 100644 src/ui/dialogs/Ui_medianadder.ts diff --git a/src/__init__.py b/src/__init__.py index 1062a81..93fbfbc 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -3,6 +3,8 @@ __author__ = "Alexander Kirchner" __all__ = ["__version__", "__author__", "Icon", "settings"] import os +from pathlib import Path +from typing import Union from appdirs import AppDirs @@ -18,7 +20,7 @@ if not os.path.exists(CONFIG_DIR): # type: ignore settings = Config(f"{CONFIG_DIR}/config.yaml") -DATABASE_DIR = ( # type: ignore +DATABASE_DIR: Union[Path, str] = ( # type: ignore app.user_config_dir if settings.database.path is None else settings.database.path # type: ignore ) if not os.path.exists(DATABASE_DIR): # type: ignore diff --git a/src/backend/catalogue.py b/src/backend/catalogue.py index be10401..c9f1a82 100644 --- a/src/backend/catalogue.py +++ b/src/backend/catalogue.py @@ -1,3 +1,5 @@ +from typing import List + import regex import requests from bs4 import BeautifulSoup @@ -33,13 +35,13 @@ class Catalogue: response = requests.get(link, timeout=self.timeout) return response.text - def get_book_links(self, searchterm: str): + def get_book_links(self, searchterm: str) -> List[str]: response = self.search_book(searchterm) soup = BeautifulSoup(response, "html.parser") links = soup.find_all("a", class_="title getFull") - res = [] + res: List[str] = [] for link in links: - res.append(BASE + link["href"]) + res.append(BASE + link["href"]) # type: ignore return res def get_book(self, searchterm: str): diff --git a/src/backend/database.py b/src/backend/database.py index 99a945a..a31651f 100644 --- a/src/backend/database.py +++ b/src/backend/database.py @@ -144,7 +144,7 @@ class Database: self.create_tables() self.insertSubjects() - def getElsaMediaID(self, work_author, signature, pages): + def getElsaMediaID(self, work_author: str, signature: str, pages: str): query = ( "SELECT id FROM elsa_media WHERE work_author=? AND signature=? AND pages=?" ) @@ -160,7 +160,7 @@ class Database: query = "SELECT type FROM elsa_media WHERE id=?" return self.query_db(query, (id,), one=True)[0] - def get_db_contents(self) -> Union[List[Tuple], None]: + def get_db_contents(self) -> Union[List[Tuple[Any]], None]: """ Get the contents of the diff --git a/src/logic/SRU.py b/src/logic/SRU.py index d31e582..a838e40 100644 --- a/src/logic/SRU.py +++ b/src/logic/SRU.py @@ -86,7 +86,7 @@ def _text(elem: Optional[ET.Element]) -> str: return (elem.text or "") if elem is not None else "" -def _req_text(parent: ET.Element, path: str) -> str: +def _req_text(parent: ET.Element, path: str) -> Optional[str]: el = parent.find(path, NS) if el is None or el.text is None: return None @@ -98,7 +98,7 @@ def parse_marc_record(record_el: ET.Element) -> MarcRecord: record_el is the element (default ns MARC in your sample) """ # leader - leader_text = _req_text(record_el, "marc:leader") + leader_text = _req_text(record_el, "marc:leader") or "" # controlfields controlfields: List[ControlField] = [] @@ -124,8 +124,8 @@ def parse_marc_record(record_el: ET.Element) -> MarcRecord: def parse_record(zs_record_el: ET.Element) -> Record: - recordSchema = _req_text(zs_record_el, "zs:recordSchema") - recordPacking = _req_text(zs_record_el, "zs:recordPacking") + recordSchema = _req_text(zs_record_el, "zs:recordSchema") or "" + recordPacking = _req_text(zs_record_el, "zs:recordPacking") or "" # recordData contains a MARC with default MARC namespace in your sample recordData_el = zs_record_el.find("zs:recordData", NS) @@ -140,7 +140,7 @@ def parse_record(zs_record_el: ET.Element) -> Record: marc_record = parse_marc_record(marc_record_el) - recordPosition = int(_req_text(zs_record_el, "zs:recordPosition")) + recordPosition = int(_req_text(zs_record_el, "zs:recordPosition") or "0") return Record( recordSchema=recordSchema, recordPacking=recordPacking, diff --git a/src/logic/c_sort.py b/src/logic/c_sort.py index ae0285d..aafbc80 100644 --- a/src/logic/c_sort.py +++ b/src/logic/c_sort.py @@ -1,36 +1,4 @@ -def parse_semester(semester: str): - """ - Parses the semester string into a sortable format. - Returns a tuple of (year, type), where type is 0 for SoSe and 1 for WiSe. - """ - if semester.startswith("SoSe"): - return int(semester.split()[1]), 0 - elif semester.startswith("WiSe"): - year_part = semester.split()[1] - start_year, _ = map(int, year_part.split("/")) - return start_year, 1 - else: - raise ValueError(f"Invalid semester format: {semester}") - - -def custom_sort(entries): - """ - Sorts the list of tuples based on the custom schema. - - :param entries: List of tuples in the format (str, int, int). - :return: Sorted list of tuples. - """ - return sorted( - entries, - key=lambda entry: ( - parse_semester(entry[0]), # Sort by semester parsed as (year, type) - entry[1], # Then by the second element of the tuple - entry[2], # Finally by the third element of the tuple - ), - ) - - -def parse_semester(semester: str): +def parse_semester(semester: str) -> tuple[int, int]: """ Parses the semester string into a sortable format. Returns a tuple of (year, type), where type is 0 for SoSe and 1 for WiSe. @@ -48,6 +16,23 @@ def parse_semester(semester: str): raise ValueError(f"Invalid semester format: {semester}") +def custom_sort(entries) -> list: + """ + Sorts the list of tuples based on the custom schema. + + :param entries: List of tuples in the format (str, int, int). + :return: Sorted list of tuples. + """ + return sorted( + entries, + key=lambda entry: ( + parse_semester(entry[0]), # Sort by semester parsed as (year, type) + entry[1], # Then by the second element of the tuple + entry[2], # Finally by the third element of the tuple + ), + ) + + def sort_semesters_list(semesters: list) -> list: """ Sorts a list of semester strings based on year and type. diff --git a/src/logic/constants.py b/src/logic/constants.py index d910e26..94f0916 100644 --- a/src/logic/constants.py +++ b/src/logic/constants.py @@ -30,184 +30,184 @@ PROF_TITLES = [ ] SEMAP_MEDIA_ACCOUNTS = { - "1": "1008000055", - "2": "1008000188", - "3": "1008000211", - "4": "1008000344", - "5": "1008000477", - "6": "1008000500", - "7": "1008000633", - "8": "1008000766", - "9": "1008000899", - "10": "1008000922", - "11": "1008001044", - "12": "1008001177", - "13": "1008001200", - "14": "1008001333", - "15": "1008001466", - "16": "1008001599", - "17": "1008001622", - "18": "1008001755", - "19": "1008001888", - "20": "1008001911", - "21": "1008002033", - "22": "1008002166", - "23": "1008002299", - "24": "1008002322", - "25": "1008002455", - "26": "1008002588", - "27": "1008002611", - "28": "1008002744", - "29": "1008002877", - "30": "1008002900", - "31": "1008003022", - "32": "1008003155", - "33": "1008003288", - "34": "1008003311", - "35": "1008003444", - "36": "1008003577", - "37": "1008003600", - "38": "1008003733", - "39": "1008003866", - "40": "1008003999", - "41": "1008004011", - "42": "1008004144", - "43": "1008004277", - "44": "1008004300", - "45": "1008004433", - "46": "1008004566", - "47": "1008004699", - "48": "1008004722", - "49": "1008004855", - "50": "1008004988", - "51": "1008005000", - "52": "1008005133", - "53": "1008005266", - "54": "1008005399", - "55": "1008005422", - "56": "1008005555", - "57": "1008005688", - "58": "1008005711", - "59": "1008005844", - "60": "1008005977", - "61": "1008006099", - "62": "1008006122", - "63": "1008006255", - "64": "1008006388", - "65": "1008006411", - "66": "1008006544", - "67": "1008006677", - "68": "1008006700", - "69": "1008006833", - "70": "1008006966", - "71": "1008007088", - "72": "1008007111", - "73": "1008007244", - "74": "1008007377", - "75": "1008007400", - "76": "1008007533", - "77": "1008007666", - "78": "1008007799", - "79": "1008007822", - "80": "1008007955", - "81": "1008008077", - "82": "1008008100", - "83": "1008008233", - "84": "1008008366", - "85": "1008008499", - "86": "1008008522", - "87": "1008008655", - "88": "1008008788", - "89": "1008008811", - "90": "1008008944", - "91": "1008009066", - "92": "1008009199", - "93": "1008009222", - "94": "1008009355", - "95": "1008009488", - "96": "1008009511", - "97": "1008009644", - "98": "1008009777", - "99": "1008009800", - "100": "1008009933", - "101": "1008010022", - "102": "1008010155", - "103": "1008010288", - "104": "1008010311", - "105": "1008010444", - "106": "1008010577", - "107": "1008010600", - "108": "1008010733", - "109": "1008010866", - "110": "1008010999", - "111": "1008011011", - "112": "1008011144", - "113": "1008011277", - "114": "1008011300", - "115": "1008011433", - "116": "1008011566", - "117": "1008011699", - "118": "1008011722", - "119": "1008011855", - "120": "1008011988", - "121": "1008012000", - "122": "1008012133", - "123": "1008012266", - "124": "1008012399", - "125": "1008012422", - "126": "1008012555", - "127": "1008012688", - "128": "1008012711", - "129": "1008012844", - "130": "1008012977", - "131": "1008013099", - "132": "1008013122", - "133": "1008013255", - "134": "1008013388", - "135": "1008013411", - "136": "1008013544", - "137": "1008013677", - "138": "1008013700", - "139": "1008013833", - "140": "1008013966", - "141": "1008014088", - "142": "1008014111", - "143": "1008014244", - "144": "1008014377", - "145": "1008014400", - "146": "1008014533", - "147": "1008014666", - "148": "1008014799", - "149": "1008014822", - "150": "1008014955", - "151": "1008015077", - "152": "1008015100", - "153": "1008015233", - "154": "1008015366", - "155": "1008015499", - "156": "1008015522", - "157": "1008015655", - "158": "1008015788", - "159": "1008015811", - "160": "1008015944", - "161": "1008016066", - "162": "1008016199", - "163": "1008016222", - "164": "1008016355", - "165": "1008016488", - "166": "1008016511", - "167": "1008016644", - "168": "1008016777", - "169": "1008016800", - "170": "1008016933", - "171": "1008017055", - "172": "1008017188", - "173": "1008017211", - "174": "1008017344", - "175": "1008017477", - "176": "1008017500", - "177": "1008017633", - "178": "1008017766", - "179": "1008017899", - "180": "1008017922", + 1: "1008000055", + 2: "1008000188", + 3: "1008000211", + 4: "1008000344", + 5: "1008000477", + 6: "1008000500", + 7: "1008000633", + 8: "1008000766", + 9: "1008000899", + 10: "1008000922", + 11: "1008001044", + 12: "1008001177", + 13: "1008001200", + 14: "1008001333", + 15: "1008001466", + 16: "1008001599", + 17: "1008001622", + 18: "1008001755", + 19: "1008001888", + 20: "1008001911", + 21: "1008002033", + 22: "1008002166", + 23: "1008002299", + 24: "1008002322", + 25: "1008002455", + 26: "1008002588", + 27: "1008002611", + 28: "1008002744", + 29: "1008002877", + 30: "1008002900", + 31: "1008003022", + 32: "1008003155", + 33: "1008003288", + 34: "1008003311", + 35: "1008003444", + 36: "1008003577", + 37: "1008003600", + 38: "1008003733", + 39: "1008003866", + 40: "1008003999", + 41: "1008004011", + 42: "1008004144", + 43: "1008004277", + 44: "1008004300", + 45: "1008004433", + 46: "1008004566", + 47: "1008004699", + 48: "1008004722", + 49: "1008004855", + 50: "1008004988", + 51: "1008005000", + 52: "1008005133", + 53: "1008005266", + 54: "1008005399", + 55: "1008005422", + 56: "1008005555", + 57: "1008005688", + 58: "1008005711", + 59: "1008005844", + 60: "1008005977", + 61: "1008006099", + 62: "1008006122", + 63: "1008006255", + 64: "1008006388", + 65: "1008006411", + 66: "1008006544", + 67: "1008006677", + 68: "1008006700", + 69: "1008006833", + 70: "1008006966", + 71: "1008007088", + 72: "1008007111", + 73: "1008007244", + 74: "1008007377", + 75: "1008007400", + 76: "1008007533", + 77: "1008007666", + 78: "1008007799", + 79: "1008007822", + 80: "1008007955", + 81: "1008008077", + 82: "1008008100", + 83: "1008008233", + 84: "1008008366", + 85: "1008008499", + 86: "1008008522", + 87: "1008008655", + 88: "1008008788", + 89: "1008008811", + 90: "1008008944", + 91: "1008009066", + 92: "1008009199", + 93: "1008009222", + 94: "1008009355", + 95: "1008009488", + 96: "1008009511", + 97: "1008009644", + 98: "1008009777", + 99: "1008009800", + 100: "1008009933", + 101: "1008010022", + 102: "1008010155", + 103: "1008010288", + 104: "1008010311", + 105: "1008010444", + 106: "1008010577", + 107: "1008010600", + 108: "1008010733", + 109: "1008010866", + 110: "1008010999", + 111: "1008011011", + 112: "1008011144", + 113: "1008011277", + 114: "1008011300", + 115: "1008011433", + 116: "1008011566", + 117: "1008011699", + 118: "1008011722", + 119: "1008011855", + 120: "1008011988", + 121: "1008012000", + 122: "1008012133", + 123: "1008012266", + 124: "1008012399", + 125: "1008012422", + 126: "1008012555", + 127: "1008012688", + 128: "1008012711", + 129: "1008012844", + 130: "1008012977", + 131: "1008013099", + 132: "1008013122", + 133: "1008013255", + 134: "1008013388", + 135: "1008013411", + 136: "1008013544", + 137: "1008013677", + 138: "1008013700", + 139: "1008013833", + 140: "1008013966", + 141: "1008014088", + 142: "1008014111", + 143: "1008014244", + 144: "1008014377", + 145: "1008014400", + 146: "1008014533", + 147: "1008014666", + 148: "1008014799", + 149: "1008014822", + 150: "1008014955", + 151: "1008015077", + 152: "1008015100", + 153: "1008015233", + 154: "1008015366", + 155: "1008015499", + 156: "1008015522", + 157: "1008015655", + 158: "1008015788", + 159: "1008015811", + 160: "1008015944", + 161: "1008016066", + 162: "1008016199", + 163: "1008016222", + 164: "1008016355", + 165: "1008016488", + 166: "1008016511", + 167: "1008016644", + 168: "1008016777", + 169: "1008016800", + 170: "1008016933", + 171: "1008017055", + 172: "1008017188", + 173: "1008017211", + 174: "1008017344", + 175: "1008017477", + 176: "1008017500", + 177: "1008017633", + 178: "1008017766", + 179: "1008017899", + 180: "1008017922", } diff --git a/src/logic/dataclass.py b/src/logic/dataclass.py index 01a95c2..ffe22f0 100644 --- a/src/logic/dataclass.py +++ b/src/logic/dataclass.py @@ -37,7 +37,7 @@ class Prof: self._title = value # add function that sets the data from a tuple - def from_tuple(self, data: tuple[Union[str, int], ...]): + def from_tuple(self, data: tuple[Union[str, int], ...]) -> "Prof": setattr(self, "id", data[0]) setattr(self, "_title", data[1]) setattr(self, "firstname", data[2]) @@ -222,6 +222,7 @@ class Subjects(Enum): for i in cls: if i.name == name: return i.id - 1 + return None @dataclass diff --git a/src/logic/lehmannsapi.py b/src/logic/lehmannsapi.py index 4be1495..e17d164 100644 --- a/src/logic/lehmannsapi.py +++ b/src/logic/lehmannsapi.py @@ -134,10 +134,10 @@ class LehmannsClient: enriched.append(r) continue - soup = BeautifulSoup(html, "html.parser") + soup = BeautifulSoup(html, "html.parser") # type: ignore # Pages - pages_node = soup.select_one( + pages_node = soup.select_one( # type: ignore "span.book-meta.meta-seiten[itemprop='numberOfPages'], " "span.book-meta.meta-seiten[itemprop='numberofpages'], " ".meta-seiten [itemprop='numberOfPages'], " @@ -151,7 +151,7 @@ class LehmannsClient: r.pages = f"{m.group(0)} Seiten" # Availability via li.availability-3 - avail_li = soup.select_one("li.availability-3") + avail_li = soup.select_one("li.availability-3") # type: ignore if avail_li: avail_text = " ".join( avail_li.get_text(" ", strip=True).split() @@ -200,12 +200,12 @@ class LehmannsClient: if not a: continue url = urljoin(BASE, a["href"].strip()) - base_title = (block.select_one(".title [itemprop='name']") or a).get_text( + base_title = (block.select_one(".title [itemprop='name']") or a).get_text( # type: ignore strip=True ) # Alternative headline => extend title - alt_tag = block.select_one(".description[itemprop='alternativeHeadline']") + alt_tag = block.select_one(".description[itemprop='alternativeHeadline']") # type: ignore alternative_headline = alt_tag.get_text(strip=True) if alt_tag else None title = ( f"{base_title} : {alternative_headline}" @@ -216,7 +216,7 @@ class LehmannsClient: # Authors from .author authors: list[str] = [] - author_div = block.select_one("div.author") + author_div = block.select_one("div.author") # type: ignore if author_div: t = author_div.get_text(" ", strip=True) t = re.sub(r"^\s*von\s+", "", t, flags=re.I) @@ -228,7 +228,7 @@ class LehmannsClient: # Media + format media_type = None book_format = None - type_text = block.select_one(".type") + type_text = block.select_one(".type") # type: ignore if type_text: t = type_text.get_text(" ", strip=True) m = re.search(r"\b(Buch|eBook|Hörbuch)\b", t) @@ -240,7 +240,7 @@ class LehmannsClient: # Year year = None - y = block.select_one("[itemprop='copyrightYear']") + y = block.select_one("[itemprop='copyrightYear']") # type: ignore if y: try: year = int(y.get_text(strip=True)) @@ -249,7 +249,7 @@ class LehmannsClient: # Edition edition = None - ed = block.select_one("[itemprop='bookEdition']") + ed = block.select_one("[itemprop='bookEdition']") # type: ignore if ed: m = re.search(r"\d+", ed.get_text(strip=True)) if m: @@ -257,15 +257,15 @@ class LehmannsClient: # Publisher publisher = None - pub = block.select_one( + pub = block.select_one( # type: ignore ".publisherprop [itemprop='name']" - ) or block.select_one(".publisher [itemprop='name']") + ) or block.select_one(".publisher [itemprop='name']") # type: ignore if pub: publisher = pub.get_text(strip=True) # ISBN-13 isbn13 = None - isbn_tag = block.select_one(".isbn [itemprop='isbn'], [itemprop='isbn']") + isbn_tag = block.select_one(".isbn [itemprop='isbn'], [itemprop='isbn']") # type: ignore if isbn_tag: digits = re.sub(r"[^0-9Xx]", "", isbn_tag.get_text(strip=True)) m = re.search(r"(97[89]\d{10})", digits) @@ -288,7 +288,7 @@ class LehmannsClient: # Image (best-effort) image = None - left_img = block.find_previous("img") + left_img = block.find_previous("img") # type: ignore if left_img and left_img.get("src"): image = urljoin(BASE, left_img["src"]) diff --git a/src/logic/openai.py b/src/logic/openai.py index 4fda61d..715be68 100644 --- a/src/logic/openai.py +++ b/src/logic/openai.py @@ -1,10 +1,12 @@ -from openai import OpenAI -from src import settings import json +from typing import Any + +from openai import OpenAI + +from src import settings - -def init_client(): +def init_client() -> OpenAI: """Initialize the OpenAI client with the API key and model from settings.""" global client, model, api_key if not settings.openAI.api_key: @@ -16,9 +18,11 @@ def init_client(): api_key = settings.openAI.api_key client = OpenAI(api_key=api_key) return client -def run_shortener(title:str, length:int): + + +def run_shortener(title: str, length: int) -> list[dict[str, Any]]: client = init_client() - response = client.responses.create( + response = client.responses.create( # type: ignore model=model, instructions="""you are a sentence shortener. The next message will contain the string to shorten and the length limit. You need to shorten the string to be under the length limit, while keeping as much detail as possible. The result may NOT be longer than the length limit. @@ -27,27 +31,28 @@ based on that, please reply only the shortened string. Give me 5 choices. if the ) answers = response.output_text return eval(answers) # type: ignore - #answers are strings in json format, so we need to convert them to a list of dicts + # answers are strings in json format, so we need to convert them to a list of dicts -def name_tester(name: str): +def name_tester(name: str) -> dict: client = init_client() - response = client.responses.create( - model = model, + response = client.responses.create( # type: ignore + model=model, instructions="""you are a name tester, You are given a name and will have to split the name into first name, last name, and if present the title. Return the name in a json format with the keys "title", "first_name", "last_name". If no title is present, set title to none. Do NOt return the answer in a codeblock, use a pure json string. Assume the names are in the usual german naming scheme""", - input = f'{{"name":"{name}"}}' + input=f'{{"name":"{name}"}}', ) answers = response.output_text return json.loads(answers) -def semester_converter(semester:str): + +def semester_converter(semester: str) -> str: client = init_client() - response = client.responses.create( - model = model, + response = client.responses.create( # type: ignore + model=model, instructions="""you are a semester converter. You will be given a string. Convert this into a string like this: SoSe YY or WiSe YY/YY+1. Do not return the answer in a codeblock, use a pure string.""", - input = semester + input=semester, ) answers = response.output_text - return answers \ No newline at end of file + return answers diff --git a/src/logic/pdfparser.py b/src/logic/pdfparser.py index 07c9409..de5e87a 100644 --- a/src/logic/pdfparser.py +++ b/src/logic/pdfparser.py @@ -1,10 +1,9 @@ # add depend path to system path -import pandas as pd from pdfquery import PDFQuery -def pdf_to_csv(path: str) -> pd.DataFrame: +def pdf_to_csv(path: str) -> str: """ Extracts the data from a pdf file and returns it as a pandas dataframe """ diff --git a/src/logic/semester.py b/src/logic/semester.py index 0a529ca..08e2b03 100644 --- a/src/logic/semester.py +++ b/src/logic/semester.py @@ -232,3 +232,17 @@ if __name__ == "__main__": # print("generate_missing:", [str(s) for s in chain]) # Parsing demo --------------------------------------------------------- + examples = [ + "SoSe 6", + "WiSe 6/7", + "WiSe 6", + "SoSe 23", + "WiSe 23/24", + "WiSe 24", + "WiSe 99/00", + "SoSe 00", + "WiSe 100/101", # test large year + ] + for ex in examples: + parsed = Semester.from_string(ex) + print(f"'{ex}' → {parsed} ({parsed.year=}, {parsed.semester=})") diff --git a/src/logic/settings.py b/src/logic/settings.py index 3b4754b..2cab463 100644 --- a/src/logic/settings.py +++ b/src/logic/settings.py @@ -13,7 +13,7 @@ class Settings: default_apps: bool = True custom_applications: list[dict] = field(default_factory=list) - def save_settings(self): + def save_settings(self) -> None: """Save the settings to the config file.""" with open("config.yaml", "w") as f: yaml.dump(self.__dict__, f) diff --git a/src/logic/webrequest.py b/src/logic/webrequest.py index cdded76..acd93b4 100644 --- a/src/logic/webrequest.py +++ b/src/logic/webrequest.py @@ -51,14 +51,14 @@ class WebRequest: log.info("Using any book") return self - def set_apparat(self, apparat: int): + def set_apparat(self, apparat: int) -> "WebRequest": self.apparat = apparat if int(self.apparat) < 10: self.apparat = f"0{self.apparat}" log.info(f"Set apparat to {self.apparat}") return self - def get_ppn(self, signature: str): + def get_ppn(self, signature: str) -> "WebRequest": self.signature = signature if "+" in signature: signature = signature.replace("+", "%2B") @@ -90,7 +90,7 @@ class WebRequest: @sleep_and_retry @limits(calls=RATE_LIMIT, period=RATE_PERIOD) - def search(self, link: str): + def search(self, link: str) -> Optional[str]: try: response = requests.get(link, timeout=self.timeout) return response.text @@ -98,7 +98,7 @@ class WebRequest: log.error(f"Request failed: {e}") return None - def get_data(self) -> Union[list[str], None]: + def get_data(self) -> Optional[list[str]]: links = self.get_book_links(self.ppn) log.debug(f"Links: {links}") return_data: list[str] = [] @@ -156,7 +156,7 @@ class WebRequest: return return_data - def get_data_elsa(self): + def get_data_elsa(self) -> Optional[list[str]]: links = self.get_book_links(self.ppn) for link in links: result = self.search(link) @@ -197,12 +197,12 @@ class BibTextTransformer: self.data = None # self.bookdata = BookData(**self.data) - def use_signature(self, signature: str): + def use_signature(self, signature: str) -> "BibTextTransformer": """use the exact signature to search for the book""" self.signature = signature return self - def get_data(self, data: Union[list[str]] = None) -> "BibTextTransformer": + def get_data(self, data: Optional[list[str]] = None) -> "BibTextTransformer": RIS_IDENT = "TY -" ARRAY_IDENT = "[kid]" COinS_IDENT = "ctx_ver" diff --git a/src/logic/wordparser.py b/src/logic/wordparser.py index a548b48..f3030cb 100644 --- a/src/logic/wordparser.py +++ b/src/logic/wordparser.py @@ -1,5 +1,5 @@ import zipfile -from typing import Any +from typing import Any, Optional import fitz # PyMuPDF import pandas as pd @@ -35,7 +35,7 @@ def word_docx_to_csv(path: str) -> list[pd.DataFrame]: return m_data -def get_fach(path: str) -> str: +def get_fach(path: str) -> Optional[str]: document = zipfile.ZipFile(path) xml_data = document.read("word/document.xml") document.close() @@ -49,10 +49,12 @@ def get_fach(path: str) -> str: # get the data in the w:t for run in para.find_all("w:r"): data = run.find("w:t") - return data.contents[0] + if data and data.contents: + return data.contents[0] + return None -def makeDict(): +def makeDict() -> dict[str, Optional[str]]: return { "work_author": None, "section_author": None, @@ -70,8 +72,8 @@ def makeDict(): } -def tuple_to_dict(tlist: tuple, type: str) -> dict: - ret = [] +def tuple_to_dict(tlist: tuple, type: str) -> list[dict[str, Optional[str]]]: + ret: list[dict[str, Optional[str]]] = [] for line in tlist: data = makeDict() if type == "Monografien": @@ -111,7 +113,7 @@ def tuple_to_dict(tlist: tuple, type: str) -> dict: return ret -def elsa_word_to_csv(path: str): +def elsa_word_to_csv(path: str) -> tuple[list[dict[str, Optional[str]]], str]: doc = Document(path) # # print all lines in doc doctype = [para.text for para in doc.paragraphs if para.text != ""][-1] diff --git a/src/logic/xmlparser.py b/src/logic/xmlparser.py index e16471f..a53fb76 100644 --- a/src/logic/xmlparser.py +++ b/src/logic/xmlparser.py @@ -56,8 +56,8 @@ def eml_parser(path: str) -> XMLMailSubmission: return parse_xml_submission(xml_content) -def eml_to_semap(path: str) -> SemapDocument: - submission = eml_parser(path) +def eml_to_semap(xml_mail: XMLMailSubmission) -> SemapDocument: + submission = eml_parser(xml_mail) semap_doc = SemapDocument( # prof=Prof(name=submission.name, lastname=submission.lastname, email=submission.email), apparat=Apparat(name=submission.app_name, subject=submission.subject), diff --git a/src/logic/zotero.py b/src/logic/zotero.py index 6c2de8b..e5847d1 100644 --- a/src/logic/zotero.py +++ b/src/logic/zotero.py @@ -1,4 +1,5 @@ from dataclasses import dataclass +from typing import Optional from pyzotero import zotero @@ -12,11 +13,11 @@ class Creator: lastName: str = None creatorType: str = "author" - def from_dict(self, data: dict): + def from_dict(self, data: dict) -> None: for key, value in data.items(): setattr(self, key, value) - def from_string(self, data: str): + def from_string(self, data: str) -> "Creator": if "," in data: self.firstName = data.split(",")[1] self.lastName = data.split(",")[0] @@ -56,7 +57,7 @@ class Book: rights: str = None extra: str = None - def to_dict(self): + def to_dict(self) -> dict: ret = {} for key, value in self.__dict__.items(): if value: @@ -95,14 +96,14 @@ class BookSection: collections = list relations = dict - def to_dict(self): + def to_dict(self) -> dict: ret = {} for key, value in self.__dict__.items(): if value: ret[key] = value return ret - def assign(self, book): + def assign(self, book) -> None: for key, value in book.__dict__.items(): if key in self.__dict__.keys(): try: @@ -142,14 +143,14 @@ class JournalArticle: collections = list relations = dict - def to_dict(self): + def to_dict(self) -> dict: ret = {} for key, value in self.__dict__.items(): if value: ret[key] = value return ret - def assign(self, book: dict): + def assign(self, book: dict) -> None: for key, value in book.__dict__.items(): if key in self.__dict__.keys(): try: @@ -164,15 +165,15 @@ class ZoteroController: def __init__(self): if self.zoterocfg.library_id is None: return - self.zot = zotero.Zotero( + self.zot = zotero.Zotero( # type: ignore self.zoterocfg.library_id, self.zoterocfg.library_type, self.zoterocfg.api_key, ) - def get_books(self): + def get_books(self) -> list: ret = [] - items = self.zot.top() + items = self.zot.top() # type: ignore for item in items: if item["data"]["itemType"] == "book": ret.append(item) @@ -180,7 +181,7 @@ class ZoteroController: # create item in zotero # item is a part of a book - def __get_data(self, isbn): + def __get_data(self, isbn) -> dict: web = WebRequest() web.get_ppn(isbn) data = web.get_data_elsa() @@ -190,7 +191,7 @@ class ZoteroController: return book # # #print(zot.item_template("bookSection")) - def createBook(self, isbn): + def createBook(self, isbn) -> Book: book = self.__get_data(isbn) bookdata = Book() @@ -209,23 +210,23 @@ class ZoteroController: bookdata.creators = authors return bookdata - def createItem(self, item): - resp = self.zot.create_items([item]) + def createItem(self, item) -> Optional[str]: + resp = self.zot.create_items([item]) # type: ignore if "successful" in resp.keys(): # #print(resp["successful"]["0"]["key"]) return resp["successful"]["0"]["key"] else: return None - def deleteItem(self, key): + def deleteItem(self, key) -> None: items = self.zot.items() for item in items: if item["key"] == key: - self.zot.delete_item(item) + self.zot.delete_item(item) # type: ignore # #print(item) break - def createHGSection(self, book: Book, data: dict): + def createHGSection(self, book: Book, data: dict) -> Optional[str]: chapter = BookSection() chapter.assign(book) chapter.pages = data["pages"] @@ -247,7 +248,7 @@ class ZoteroController: return self.createItem(chapter.to_dict()) pass - def createBookSection(self, book: Book, data: dict): + def createBookSection(self, book: Book, data: dict) -> Optional[str]: chapter = BookSection() chapter.assign(book) chapter.pages = data["pages"] @@ -258,7 +259,7 @@ class ZoteroController: return self.createItem(chapter.to_dict()) # chapter.creators - def createJournalArticle(self, journal, article): + def createJournalArticle(self, journal, article) -> Optional[str]: # #print(type(article)) journalarticle = JournalArticle() journalarticle.assign(journal) @@ -279,8 +280,8 @@ class ZoteroController: return self.createItem(journalarticle.to_dict()) - def get_citation(self, item): - title = self.zot.item( + def get_citation(self, item) -> str: + title = self.zot.item( # type: ignore item, content="bib", style="deutsche-gesellschaft-fur-psychologie", diff --git a/src/ui/dialogs/Ui_medianadder.ts b/src/ui/dialogs/Ui_medianadder.ts deleted file mode 100644 index 6401616..0000000 --- a/src/ui/dialogs/Ui_medianadder.ts +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/src/ui/dialogs/docuprint.py b/src/ui/dialogs/docuprint.py index 3ffaacd..750fc10 100644 --- a/src/ui/dialogs/docuprint.py +++ b/src/ui/dialogs/docuprint.py @@ -110,10 +110,10 @@ class DocumentPrintDialog(QtWidgets.QDialog, Ui_Dialog): def on_pushButton_clicked(self): apparats: list[tuple[int, str]] = [] apps = self.db.getAllAparats(0) - apps = natsorted(apps, key=lambda x: x[4], reverse=True) + apps = natsorted(apps, key=lambda x: x.appnr, reverse=True) for app in apps: - prof = self.db.getProfById(app[2]) - data = (app[4], f"{prof.lastname} ({app[1]})") + prof = self.db.getProfById(app.prof_id) + data = (app.appnr, f"{prof.lastname} ({app.name})") apparats.append(data) SemesterDocument( semester=self.semester.value, diff --git a/src/ui/semesterapparat_ui.ui b/src/ui/semesterapparat_ui.ui index 615f845..2138d35 100644 --- a/src/ui/semesterapparat_ui.ui +++ b/src/ui/semesterapparat_ui.ui @@ -1349,7 +1349,7 @@ Die Apparatsdetails werden aus dem Dokument gelesen und eingetragen -Einige Angaben müssen ggf angepasst werden +Die gewünschten Medien werden automatisch in die Medienliste eingetragen, evtl. unvollständig, da eBooks nicht erfasst werden könnenEinige Angaben müssen ggf angepasst werden Daten aus Dokument @@ -1618,72 +1618,72 @@ Einige Angaben müssen ggf angepasst werden Admin - + - 10 - 30 - 47 - 22 + 0 + 0 + 1251 + 711 - - Aktion: + + QFrame::StyledPanel - - - - - 60 - 30 - 181 - 22 - - - - - Nutzer anlegen - - - - - Nutzer bearbeiten - - - - - Lehrperson bearbeiten - - - - - Medien bearbeiten - - - - - - - 10 - 70 - 570 - 291 - - - - - false - - - - GroupBox - - - true - - - false + + QFrame::Raised + + + + + Aktion: + + + + + + + + Nutzer anlegen + + + + + Nutzer bearbeiten + + + + + Lehrperson bearbeiten + + + + + Medien bearbeiten + + + + + + + + + false + + + + GroupBox + + + true + + + false + + + + diff --git a/src/ui/semesterapparat_ui_ui.py b/src/ui/semesterapparat_ui_ui.py index be2d9e9..adfe069 100644 --- a/src/ui/semesterapparat_ui_ui.py +++ b/src/ui/semesterapparat_ui_ui.py @@ -638,24 +638,37 @@ class Ui_MainWindow(object): self.tabWidget.addTab(self.elsatab, "") self.admin = QWidget() self.admin.setObjectName(u"admin") - self.label_21 = QLabel(self.admin) + self.frame = QFrame(self.admin) + self.frame.setObjectName(u"frame") + self.frame.setGeometry(QRect(0, 0, 1251, 711)) + self.frame.setFrameShape(QFrame.StyledPanel) + self.frame.setFrameShadow(QFrame.Raised) + self.formLayout_2 = QFormLayout(self.frame) + self.formLayout_2.setObjectName(u"formLayout_2") + self.label_21 = QLabel(self.frame) self.label_21.setObjectName(u"label_21") - self.label_21.setGeometry(QRect(10, 30, 47, 22)) - self.select_action_box = QComboBox(self.admin) + + self.formLayout_2.setWidget(0, QFormLayout.ItemRole.LabelRole, self.label_21) + + self.select_action_box = QComboBox(self.frame) self.select_action_box.addItem("") self.select_action_box.addItem("") self.select_action_box.addItem("") self.select_action_box.addItem("") self.select_action_box.setObjectName(u"select_action_box") - self.select_action_box.setGeometry(QRect(60, 30, 181, 22)) - self.admin_action = QGroupBox(self.admin) + + self.formLayout_2.setWidget(0, QFormLayout.ItemRole.FieldRole, self.select_action_box) + + self.admin_action = QGroupBox(self.frame) self.admin_action.setObjectName(u"admin_action") - self.admin_action.setGeometry(QRect(10, 70, 570, 291)) font5 = QFont() font5.setBold(False) self.admin_action.setFont(font5) self.admin_action.setFlat(True) self.admin_action.setCheckable(False) + + self.formLayout_2.setWidget(1, QFormLayout.ItemRole.FieldRole, self.admin_action) + self.tabWidget.addTab(self.admin, "") self.gridLayout.addWidget(self.tabWidget, 0, 0, 1, 1) @@ -963,7 +976,7 @@ class Ui_MainWindow(object): " hinzuf\u00fcgen", None)) #if QT_CONFIG(tooltip) self.btn_extract_data_from_document.setToolTip(QCoreApplication.translate("MainWindow", u"Die Apparatsdetails werden aus dem Dokument gelesen und eingetragen\n" -"Einige Angaben m\u00fcssen ggf angepasst werden", None)) +"Die gew\u00fcnschten Medien werden automatisch in die Medienliste eingetragen, evtl. unvollst\u00e4ndig, da eBooks nicht erfasst werden k\u00f6nnenEinige Angaben m\u00fcssen ggf angepasst werden", None)) #endif // QT_CONFIG(tooltip) self.btn_extract_data_from_document.setText(QCoreApplication.translate("MainWindow", u"Daten aus Dokument\n" "\u00fcbernehmen", None)) diff --git a/src/ui/userInterface.py b/src/ui/userInterface.py index a5c0b8f..efac025 100644 --- a/src/ui/userInterface.py +++ b/src/ui/userInterface.py @@ -373,7 +373,7 @@ class Ui(QtWidgets.QMainWindow, Ui_Semesterapparat): self.setWidget(UpdateSignatures()) self.admin_action.setTitle("Medien bearbeiten") else: - self.hideWidget() + # self.hideWidget() self.admin_action.setTitle("") def toggleButton(self, button: QtWidgets.QCheckBox): @@ -1224,12 +1224,14 @@ class Ui(QtWidgets.QMainWindow, Ui_Semesterapparat): signatures = csv_to_list(file) # add the data to the database return signatures - if file_type == "docx": + if file_type in ("docx", "doc"): data = word_to_semap(file) log.info("Converted data from semap file") log.debug("Got the data: {}", data) return data + else: + raise ValueError("Dateityp wird nicht unterstützt") def import_data_from_document(self): global valid_input @@ -1241,6 +1243,7 @@ class Ui(QtWidgets.QMainWindow, Ui_Semesterapparat): self.prof_mail.setText(data.mail) self.prof_tel_nr.setText(str(data.phoneNumber).replace("-", "")) + self.app_name.setText(data.title) if len(data.title_suggestions) > 0: # create a dialog that has a dropdown with the suggestions, and oc and cancel button. on ok return the selected text and set it as title dialog = QtWidgets.QDialog() @@ -1271,6 +1274,7 @@ class Ui(QtWidgets.QMainWindow, Ui_Semesterapparat): self.app_name.setText(dropdown.currentText().split(" [")[0].strip()) else: self.app_name.setText("CHANGEME") + # self.app_name.setText(data.title) subjects = self.db.getSubjects() subjects = [subject[1] for subject in subjects] @@ -1287,8 +1291,10 @@ class Ui(QtWidgets.QMainWindow, Ui_Semesterapparat): if data.eternal: self.check_eternal_app.setChecked(True) self.validate_semester() + if data.books != []: + self.btn_check_file_threaded(data) - def btn_check_file_threaded(self): + def btn_check_file_threaded(self, c_document: Optional[SemapDocument] = None): for runner in self.bookGrabber: if not runner.isRunning(): runner.deleteLater() @@ -1335,7 +1341,10 @@ class Ui(QtWidgets.QMainWindow, Ui_Semesterapparat): prof_id = self.db.getProfId(self.profdata) # log.debug("Prof ID is None", prof_id) - document = self.extract_document_data() + document = None + + if c_document is None or not isinstance(c_document, SemapDocument): + document = self.extract_document_data() if document is None: log.error("Document is None") elif isinstance(document, SemapDocument): @@ -1410,7 +1419,7 @@ class Ui(QtWidgets.QMainWindow, Ui_Semesterapparat): ) prof.title = self.prof_title.text() apparat = Apparat( - appnr=self.active_apparat, + appnr=int(self.drpdwn_app_nr.currentText()), name=self.app_name.text(), created_semester=self.generateSemester(), eternal=1 if self.check_eternal_app.isChecked() else 0, @@ -1433,7 +1442,8 @@ class Ui(QtWidgets.QMainWindow, Ui_Semesterapparat): return appdata = self.db.getAllAparats() # merge self.appdata and appdata, remove duplicates - self.apparats = list(set(self.apparats + appdata)) + + self.apparats = self.__uniques(self.apparats, appdata) self.apparats = natsorted(self.apparats, key=lambda x: x[4], reverse=True) self.update_apparat_list() @@ -1452,6 +1462,16 @@ class Ui(QtWidgets.QMainWindow, Ui_Semesterapparat): self.__clear_fields() return True + def __uniques(self, list1, list2): + seen = set() + unique_list = [] + for item in list1 + list2: + identifier = (item.appnr, item.name) + if identifier not in seen: + seen.add(identifier) + unique_list.append(item) + return unique_list + def send_mail_preview(self): pass diff --git a/src/ui/widgets/filepicker.py b/src/ui/widgets/filepicker.py index e8a899b..3aaefb5 100644 --- a/src/ui/widgets/filepicker.py +++ b/src/ui/widgets/filepicker.py @@ -21,7 +21,7 @@ class FilePicker: files, _ = filepicker.getOpenFileNames( caption="Open file", dir=self.last_path, - filter="Unterstützte Dateien (*.docx *.csv *.eml );;Word (*.docx);;CSV Files (*.csv);;Mail (*.eml)", + filter="Unterstützte Dateien (*.docx *.doc *.csv *.eml );;Word (*.docx *.doc);;CSV Files (*.csv);;Mail (*.eml)", ) if files: self.last_path = files[0] diff --git a/src/utils/blob.py b/src/utils/blob.py index f2c8070..d991b67 100644 --- a/src/utils/blob.py +++ b/src/utils/blob.py @@ -1,4 +1,4 @@ -def create_blob(file: str): +def create_blob(file: str) -> bytes: """ Creates a blob from a file. """ diff --git a/src/utils/documentation.py b/src/utils/documentation.py index 88789e5..9caa9be 100644 --- a/src/utils/documentation.py +++ b/src/utils/documentation.py @@ -1,9 +1,10 @@ -import os -from pyramid.config import Configurator -from wsgiref.simple_server import WSGIRequestHandler -from src import LOG_DIR import logging +import os +from wsgiref.simple_server import WSGIRequestHandler +from pyramid.config import Configurator + +from src import LOG_DIR log_path = os.path.join(LOG_DIR, "web_documentation.log") @@ -31,7 +32,7 @@ class QuietHandler(WSGIRequestHandler): pass -def website(): +def website() -> object: config = Configurator() # Set up static file serving from the 'site/' directory @@ -40,4 +41,4 @@ def website(): ) app = config.make_wsgi_app() - return app + return app # type: ignore diff --git a/src/utils/pickles.py b/src/utils/pickles.py index e943a0d..a35c206 100644 --- a/src/utils/pickles.py +++ b/src/utils/pickles.py @@ -2,9 +2,9 @@ import pickle from typing import Any -def load_pickle(data: Any): +def load_pickle(data: Any) -> Any: return pickle.loads(data) -def dump_pickle(data: Any): +def dump_pickle(data: Any) -> bytes: return pickle.dumps(data) diff --git a/src/utils/richtext.py b/src/utils/richtext.py index 9b26489..bd163cd 100644 --- a/src/utils/richtext.py +++ b/src/utils/richtext.py @@ -16,7 +16,7 @@ logger = log font = "Cascadia Mono" -def print_document(file: str): +def print_document(file: str) -> None: # send document to printer as attachment of email import smtplib from email.mime.application import MIMEApplication @@ -98,7 +98,7 @@ class SemesterDocument: self.filename = filename if full: log.info("Full document generation") - self.cleanup() + self.cleanup log.info("Cleanup done") self.make_document() log.info("Document created") @@ -221,15 +221,15 @@ class SemesterDocument: self.create_sorted_table() - def save_document(self, name): + def save_document(self, name: str) -> None: # Save the document self.doc.save(name) - def create_pdf(self): + def create_pdf(self) -> None: # Save the document import comtypes.client - word = comtypes.client.CreateObject("Word.Application") + word = comtypes.client.CreateObject("Word.Application") # type: ignore self.save_document(self.filename + ".docx") docpath = os.path.abspath(self.filename + ".docx") doc = word.Documents.Open(docpath) @@ -240,13 +240,13 @@ class SemesterDocument: log.debug("PDF saved") @property - def cleanup(self): + def cleanup(self) -> None: if os.path.exists(f"{self.filename}.docx"): os.remove(f"{self.filename}.docx") os.remove(f"{self.filename}.pdf") @property - def send(self): + def send(self) -> None: print_document(self.filename + ".pdf") log.debug("Document sent to printer") @@ -309,11 +309,11 @@ class SemapSchilder: self.doc.save(f"{self.filename}.docx") log.debug(f"Document saved as {self.filename}.docx") - def create_pdf(self): + def create_pdf(self) -> None: # Save the document import comtypes.client - word = comtypes.client.CreateObject("Word.Application") + word = comtypes.client.CreateObject("Word.Application") # type: ignore self.save_document() docpath = os.path.abspath(f"{self.filename}.docx") doc = word.Documents.Open(docpath) @@ -323,14 +323,14 @@ class SemapSchilder: word.Quit() log.debug("PDF saved") - def cleanup(self): + def cleanup(self) -> None: if os.path.exists(f"{self.filename}.docx"): os.remove(f"{self.filename}.docx") if os.path.exists(f"{self.filename}.pdf"): os.remove(f"{self.filename}.pdf") @property - def send(self): + def send(self) -> None: print_document(self.filename + ".pdf") log.debug("Document sent to printer") -- 2.49.1 From 0764a6b06af747ca6f26aa33fb295f856b636c12 Mon Sep 17 00:00:00 2001 From: WorldTeacher Date: Tue, 21 Oct 2025 10:42:52 +0200 Subject: [PATCH 33/40] feat: add migrations to create database and add / change features down the line --- src/backend/database.py | 103 +++++++++----- src/backend/migration_runner.py | 68 +++++++++ .../migrations/V001__create_base_tables.sql | 132 ++++++++++++++++++ 3 files changed, 265 insertions(+), 38 deletions(-) create mode 100644 src/backend/migration_runner.py create mode 100644 src/backend/migrations/V001__create_base_tables.sql diff --git a/src/backend/database.py b/src/backend/database.py index a31651f..bc15c38 100644 --- a/src/backend/database.py +++ b/src/backend/database.py @@ -10,8 +10,6 @@ from string import ascii_lowercase as lower from string import digits, punctuation from typing import Any, List, Optional, Tuple, Union -import loguru - from src import DATABASE_DIR, settings from src.backend.db import ( CREATE_ELSA_FILES_TABLE, @@ -30,11 +28,9 @@ from src.errors import AppPresentError, NoResultError from src.logic import ELSA, Apparat, ApparatData, BookData, Prof from src.logic.constants import SEMAP_MEDIA_ACCOUNTS from src.logic.semester import Semester +from src.shared.logging import log from src.utils.blob import create_blob -log = loguru.logger - - ascii_lowercase = lower + digits + punctuation @@ -123,6 +119,66 @@ class Database: if not self.db_initialized: self.checkDatabaseStatus() self.db_initialized = True + # run migrations after initial creation to bring schema up-to-date + try: + if self.db_path is not None: + self.run_migrations() + except Exception as e: + log.error(f"Error while running migrations: {e}") + + # --- Migration helpers integrated into Database --- + def _ensure_migrations_table(self, conn: sql.Connection) -> None: + cursor = conn.cursor() + cursor.execute( + """ + CREATE TABLE IF NOT EXISTS schema_migrations ( + id TEXT PRIMARY KEY, + applied_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP + ) + """ + ) + conn.commit() + + def _applied_migrations(self, conn: sql.Connection) -> List[str]: + cursor = conn.cursor() + cursor.execute("SELECT id FROM schema_migrations ORDER BY id") + rows = cursor.fetchall() + return [r[0] for r in rows] + + def _apply_sql_file(self, conn: sql.Connection, path: Path) -> None: + log.info(f"Applying migration {path.name}") + sql_text = path.read_text(encoding="utf-8") + cursor = conn.cursor() + cursor.executescript(sql_text) + cursor.execute( + "INSERT OR REPLACE INTO schema_migrations (id) VALUES (?)", (path.name,) + ) + conn.commit() + + def run_migrations(self) -> None: + """Apply unapplied .sql migrations from src/backend/migrations using this Database's connection.""" + migrations_dir = Path(__file__).parent / "migrations" + if not migrations_dir.exists(): + log.debug("Migrations directory does not exist, skipping migrations") + return + + conn = self.connect() + try: + self._ensure_migrations_table(conn) + applied = set(self._applied_migrations(conn)) + + migration_files = sorted( + [p for p in migrations_dir.iterdir() if p.suffix == ".sql"] + ) + for m in migration_files: + if m.name in applied: + log.debug(f"Skipping already applied migration {m.name}") + continue + self._apply_sql_file(conn, m) + finally: + conn.close() + + # --- end migration helpers --- def overwritePath(self, new_db_path: str): log.debug("got new path, overwriting") @@ -204,39 +260,10 @@ class Database: """ Create the tables in the database """ - conn = self.connect() - cursor = conn.cursor() - cursor.execute(CREATE_TABLE_APPARAT) - cursor.execute(CREATE_TABLE_MESSAGES) - cursor.execute(CREATE_TABLE_MEDIA) - cursor.execute(CREATE_TABLE_FILES) - cursor.execute(CREATE_TABLE_PROF) - cursor.execute(CREATE_TABLE_USER) - cursor.execute(CREATE_TABLE_SUBJECTS) - cursor.execute(CREATE_ELSA_TABLE) - cursor.execute(CREATE_ELSA_FILES_TABLE) - cursor.execute(CREATE_ELSA_MEDIA_TABLE) - # Helpful indices to speed up frequent lookups and joins - cursor.execute( - "CREATE INDEX IF NOT EXISTS idx_media_app_prof ON media(app_id, prof_id);" - ) - cursor.execute( - "CREATE INDEX IF NOT EXISTS idx_media_deleted ON media(deleted);" - ) - cursor.execute( - "CREATE INDEX IF NOT EXISTS idx_media_available ON media(available);" - ) - cursor.execute( - "CREATE INDEX IF NOT EXISTS idx_messages_remind_at ON messages(remind_at);" - ) - cursor.execute( - "CREATE INDEX IF NOT EXISTS idx_semesterapparat_prof ON semesterapparat(prof_id);" - ) - cursor.execute( - "CREATE INDEX IF NOT EXISTS idx_semesterapparat_appnr ON semesterapparat(appnr);" - ) - conn.commit() - self.close_connection(conn) + # Bootstrapping of tables is handled via migrations. Run migrations instead + # of executing the hard-coded DDL here. Migrations are idempotent and + # contain the CREATE TABLE IF NOT EXISTS statements. + self.run_migrations() def insertInto(self, query: str, params: Tuple) -> None: """ diff --git a/src/backend/migration_runner.py b/src/backend/migration_runner.py new file mode 100644 index 0000000..a8393b8 --- /dev/null +++ b/src/backend/migration_runner.py @@ -0,0 +1,68 @@ +import os +import sqlite3 as sql +from pathlib import Path +from typing import List + +from src import DATABASE_DIR, settings +from src.shared.logging import log + +MIGRATIONS_DIR = Path(__file__).parent / "migrations" + + +def _ensure_migrations_table(conn: sql.Connection) -> None: + cursor = conn.cursor() + cursor.execute( + """ + CREATE TABLE IF NOT EXISTS schema_migrations ( + id TEXT PRIMARY KEY, + applied_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP + ) + """ + ) + conn.commit() + + +def _applied_migrations(conn: sql.Connection) -> List[str]: + cursor = conn.cursor() + cursor.execute("SELECT id FROM schema_migrations ORDER BY id") + rows = cursor.fetchall() + return [r[0] for r in rows] + + +def _apply_sql_file(conn: sql.Connection, path: Path) -> None: + log.info(f"Applying migration {path.name}") + sql_text = path.read_text(encoding="utf-8") + cursor = conn.cursor() + cursor.executescript(sql_text) + cursor.execute( + "INSERT OR REPLACE INTO schema_migrations (id) VALUES (?)", (path.name,) + ) + conn.commit() + + +def run_migrations(db_path: Path) -> None: + """Run all unapplied migrations from the migrations directory against the database at db_path.""" + if not MIGRATIONS_DIR.exists(): + log.debug("Migrations directory does not exist, skipping migrations") + return + + # Ensure database directory exists + db_dir = settings.database.path or Path(DATABASE_DIR) + if not db_dir.exists(): + os.makedirs(db_dir, exist_ok=True) + + conn = sql.connect(db_path) + try: + _ensure_migrations_table(conn) + applied = set(_applied_migrations(conn)) + + migration_files = sorted( + [p for p in MIGRATIONS_DIR.iterdir() if p.suffix in (".sql",)] + ) + for m in migration_files: + if m.name in applied: + log.debug(f"Skipping already applied migration {m.name}") + continue + _apply_sql_file(conn, m) + finally: + conn.close() diff --git a/src/backend/migrations/V001__create_base_tables.sql b/src/backend/migrations/V001__create_base_tables.sql new file mode 100644 index 0000000..4848add --- /dev/null +++ b/src/backend/migrations/V001__create_base_tables.sql @@ -0,0 +1,132 @@ +BEGIN TRANSACTION; + +CREATE TABLE IF NOT EXISTS semesterapparat ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + name TEXT, + prof_id INTEGER, + fach TEXT, + appnr INTEGER, + erstellsemester TEXT, + verlängert_am TEXT, + dauer BOOLEAN, + verlängerung_bis TEXT, + deletion_status INTEGER, + deleted_date TEXT, + apparat_id_adis INTEGER, + prof_id_adis INTEGER, + konto INTEGER, + FOREIGN KEY (prof_id) REFERENCES prof (id) + ); + +CREATE TABLE IF NOT EXISTS media ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + bookdata TEXT, + app_id INTEGER, + prof_id INTEGER, + deleted INTEGER DEFAULT (0), + available BOOLEAN, + reservation BOOLEAN, + FOREIGN KEY (prof_id) REFERENCES prof (id), + FOREIGN KEY (app_id) REFERENCES semesterapparat (id) + ); + +CREATE TABLE IF NOT EXISTS files ( + id INTEGER PRIMARY KEY, + filename TEXT, + fileblob BLOB, + app_id INTEGER, + filetyp TEXT, + prof_id INTEGER REFERENCES prof (id), + FOREIGN KEY (app_id) REFERENCES semesterapparat (id) + ); + +CREATE TABLE IF NOT EXISTS messages ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + created_at date NOT NULL DEFAULT CURRENT_TIMESTAMP, + message TEXT NOT NULL, + remind_at date NOT NULL DEFAULT CURRENT_TIMESTAMP, + user_id INTEGER NOT NULL, + appnr INTEGER, + FOREIGN KEY (user_id) REFERENCES user (id) + ); + +CREATE TABLE IF NOT EXISTS prof ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + titel TEXT, + fname TEXT, + lname TEXT, + fullname TEXT NOT NULL UNIQUE, + mail TEXT, + telnr TEXT + ); + +CREATE TABLE IF NOT EXISTS user ( + id integer NOT NULL PRIMARY KEY AUTOINCREMENT, + created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + username TEXT NOT NULL UNIQUE, + password TEXT NOT NULL, + salt TEXT NOT NULL, + role TEXT NOT NULL, + email TEXT UNIQUE, + name TEXT + ); + +CREATE TABLE IF NOT EXISTS subjects ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + name TEXT NOT NULL UNIQUE +); + +CREATE TABLE IF NOT EXISTS elsa ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + date TEXT NOT NULL, + semester TEXT NOT NULL, + prof_id INTEGER NOT NULL + ); + +CREATE TABLE IF NOT EXISTS elsa_files ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + filename TEXT NOT NULL, + fileblob BLOB NOT NULL, + elsa_id INTEGER NOT NULL, + filetyp TEXT NOT NULL, + FOREIGN KEY (elsa_id) REFERENCES elsa (id) + ); + +CREATE TABLE IF NOT EXISTS elsa_media ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + work_author TEXT, + section_author TEXT, + year TEXT, + edition TEXT, + work_title TEXT, + chapter_title TEXT, + location TEXT, + publisher TEXT, + signature TEXT, + issue TEXT, + pages TEXT, + isbn TEXT, + type TEXT, + elsa_id INTEGER NOT NULL, + FOREIGN KEY (elsa_id) REFERENCES elsa (id) + ); + +CREATE TABLE IF NOT EXISTS neweditions ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + new_bookdata TEXT, + old_edition_id INTEGER, + for_apparat INTEGER, + ordered BOOLEAN DEFAULT (0), + FOREIGN KEY (old_edition_id) REFERENCES media (id), + FOREIGN KEY (for_apparat) REFERENCES semesterapparat (id) +); + +-- Helpful indices to speed up frequent lookups and joins +CREATE INDEX IF NOT EXISTS idx_media_app_prof ON media(app_id, prof_id); +CREATE INDEX IF NOT EXISTS idx_media_deleted ON media(deleted); +CREATE INDEX IF NOT EXISTS idx_media_available ON media(available); +CREATE INDEX IF NOT EXISTS idx_messages_remind_at ON messages(remind_at); +CREATE INDEX IF NOT EXISTS idx_semesterapparat_prof ON semesterapparat(prof_id); +CREATE INDEX IF NOT EXISTS idx_semesterapparat_appnr ON semesterapparat(appnr); + +COMMIT; -- 2.49.1 From f63bcc8446a7c5286ff62891d75424ff652db7b8 Mon Sep 17 00:00:00 2001 From: WorldTeacher Date: Tue, 21 Oct 2025 15:26:20 +0200 Subject: [PATCH 34/40] feat: implement WebADIS authentication and add medianumber retrieval functionality --- src/backend/database.py | 15 ++ .../V002__create_table_webadis_login.sql | 10 ++ .../V003_update_webadis_add_user_area.sql | 6 + src/backend/threads_availchecker.py | 11 +- src/backend/webadis.py | 35 +++++ src/logic/dataclass.py | 1 + src/logic/webrequest.py | 21 ++- src/transformers/transformers.py | 8 +- src/ui/widgets/signature_update.py | 131 ++++++++++++++++++ .../widget_sources/admin_update_signatures.ui | 41 ++++++ .../admin_update_signatures_ui.py | 33 ++++- tests/test_migrations_runner.py | 20 +++ 12 files changed, 323 insertions(+), 9 deletions(-) create mode 100644 src/backend/migrations/V002__create_table_webadis_login.sql create mode 100644 src/backend/migrations/V003_update_webadis_add_user_area.sql create mode 100644 src/backend/webadis.py create mode 100644 tests/test_migrations_runner.py diff --git a/src/backend/database.py b/src/backend/database.py index bc15c38..1ef0477 100644 --- a/src/backend/database.py +++ b/src/backend/database.py @@ -280,6 +280,21 @@ class Database: conn.commit() self.close_connection(conn) + def getWebADISAuth(self) -> Tuple[str, str]: + """ + Get the WebADIS authentication data from the database + + Returns: + Tuple[str, str]: The username and password for WebADIS + """ + result = self.query_db( + "SELECT username, password FROM webadis_login WHERE effective_range='SAP'", + one=True, + ) + if result is None: + return ("", "") + return (result[0], result[1]) + @log.catch def query_db( self, diff --git a/src/backend/migrations/V002__create_table_webadis_login.sql b/src/backend/migrations/V002__create_table_webadis_login.sql new file mode 100644 index 0000000..5e1b3a8 --- /dev/null +++ b/src/backend/migrations/V002__create_table_webadis_login.sql @@ -0,0 +1,10 @@ +BEGIN TRANSACTION; + +CREATE TABLE IF NOT EXISTS webadis_login ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + username TEXT NOT NULL, + password TEXT NOT NULL + ); + +COMMIT; + diff --git a/src/backend/migrations/V003_update_webadis_add_user_area.sql b/src/backend/migrations/V003_update_webadis_add_user_area.sql new file mode 100644 index 0000000..1b5567b --- /dev/null +++ b/src/backend/migrations/V003_update_webadis_add_user_area.sql @@ -0,0 +1,6 @@ +BEGIN TRANSACTION; + +ALTER TABLE webadis_login +ADD COLUMN effective_range TEXT; + +COMMIT; \ No newline at end of file diff --git a/src/backend/threads_availchecker.py b/src/backend/threads_availchecker.py index 669af02..451df6e 100644 --- a/src/backend/threads_availchecker.py +++ b/src/backend/threads_availchecker.py @@ -3,7 +3,8 @@ from PySide6.QtCore import QThread from PySide6.QtCore import Signal as Signal from src.backend.database import Database -from src.logic.webrequest import BibTextTransformer, WebRequest +from src.backend.webadis import get_book_medianr +from src.logic.webrequest import BibTextTransformer, TransformerType, WebRequest from src.shared.logging import log @@ -37,7 +38,7 @@ class AvailChecker(QThread): ) # Pre-create reusable request and transformer to avoid per-item overhead self._request = WebRequest().set_apparat(self.appnumber) - self._rds_transformer = BibTextTransformer("RDS") + self._rds_transformer = BibTextTransformer(TransformerType.RDS) def run(self): self.db = Database() @@ -65,6 +66,12 @@ class AvailChecker(QThread): break log.info(f"State of {link}: " + str(state)) # #print("Updating availability of " + str(book_id) + " to " + str(state)) + # use get_book_medianr to update the medianr of the book in the database + auth = self.db.getWebADISAuth + medianr = get_book_medianr(rds.items[0].callnumber, self.appnumber, auth) + book_data = book["bookdata"] + book_data.medianr = medianr + self.db.updateBookdata(book["id"], book_data) self.db.setAvailability(book_id, state) count += 1 self.updateProgress.emit(count, len(self.links)) diff --git a/src/backend/webadis.py b/src/backend/webadis.py new file mode 100644 index 0000000..af79650 --- /dev/null +++ b/src/backend/webadis.py @@ -0,0 +1,35 @@ +from playwright.sync_api import sync_playwright + + +def get_book_medianr(signature: str, semesterapparat_nr: int, auth: tuple) -> str: + with sync_playwright() as playwright: + browser = playwright.chromium.launch(headless=True) + context = browser.new_context() + page = context.new_page() + page.goto( + "https://bsz.ibs-bw.de:22998/aDISWeb/app?service=direct/0/Home/$DirectLink&sp=SDAP42" + ) + page.get_by_role("textbox", name="Benutzer").fill(auth[0]) + page.get_by_role("textbox", name="Benutzer").press("Tab") + page.get_by_role("textbox", name="Kennwort").fill(auth[1]) + page.get_by_role("textbox", name="Kennwort").press("Enter") + page.get_by_role("button", name="Katalog").click() + page.get_by_role("textbox", name="Signatur").click() + page.get_by_role("textbox", name="Signatur").fill(signature) + page.get_by_role("textbox", name="Signatur").press("Enter") + book_list = page.locator("iframe").content_frame.get_by_role( + "cell", name="Bibliothek der Pädagogischen" + ) + # this will always find one result, we need to split the resulting text based on the entries that start with "* " + book_entries = book_list.inner_text().split("\n") + books = [] + for entry in book_entries: + if entry.startswith("* "): + books.append(entry) + for book in books: + if f"Semesterapparat: {semesterapparat_nr}" in book: + return book.split("* ")[1].split(":")[0] + + # --------------------- + context.close() + browser.close() diff --git a/src/logic/dataclass.py b/src/logic/dataclass.py index ffe22f0..a7d4688 100644 --- a/src/logic/dataclass.py +++ b/src/logic/dataclass.py @@ -80,6 +80,7 @@ class BookData: old_book: Any | None = None media_type: str | None = None # in_library: bool | None = None # whether the book is in the library or not + medianr: int | None = None # Media number in the library system def __post_init__(self): self.library_location = ( diff --git a/src/logic/webrequest.py b/src/logic/webrequest.py index acd93b4..893e994 100644 --- a/src/logic/webrequest.py +++ b/src/logic/webrequest.py @@ -1,3 +1,4 @@ +from enum import Enum from typing import Any, Optional, Union import requests @@ -33,6 +34,14 @@ RATE_LIMIT = 20 RATE_PERIOD = 30 +class TransformerType(Enum): + ARRAY = "ARRAY" + COinS = "COinS" + BibTeX = "BibTeX" + RIS = "RIS" + RDS = "RDS" + + class WebRequest: def __init__(self) -> None: """Request data from the web, and format it depending on the mode.""" @@ -185,10 +194,16 @@ class BibTextTransformer: ValueError: Raised if mode is not in valid_modes """ - valid_modes = ["ARRAY", "COinS", "BibTeX", "RIS", "RDS"] + valid_modes = [ + TransformerType.ARRAY, + TransformerType.COinS, + TransformerType.BibTeX, + TransformerType.RIS, + TransformerType.RDS, + ] - def __init__(self, mode: str = "ARRAY") -> None: - self.mode = mode + def __init__(self, mode: TransformerType = TransformerType.ARRAY) -> None: + self.mode = mode.value self.field = None self.signature = None if mode not in self.valid_modes: diff --git a/src/transformers/transformers.py b/src/transformers/transformers.py index a7ef88a..ade70b7 100644 --- a/src/transformers/transformers.py +++ b/src/transformers/transformers.py @@ -2,14 +2,15 @@ from __future__ import annotations import json import re +import sys from dataclasses import dataclass from dataclasses import field as dataclass_field from typing import Any, List +import loguru + from src import LOG_DIR from src.logic.dataclass import BookData -import loguru -import sys log = loguru.logger log.remove() @@ -36,6 +37,7 @@ class Item: department: str | None = dataclass_field(default_factory=str) locationhref: str | None = dataclass_field(default_factory=str) location: str | None = dataclass_field(default_factory=str) + ktrl_nr: str | None = dataclass_field(default_factory=str) def from_dict(self, data: dict): """Import data from dict""" @@ -382,6 +384,8 @@ class RDSData: def transform(self, data: str): # rds_availability = RDS_AVAIL_DATA() # rds_data = RDS_GENERIC_DATA() + print(data) + def __get_raw_data(data: str) -> list: # create base data to be turned into pydantic classes data = data.split("RDS ----------------------------------")[1] diff --git a/src/ui/widgets/signature_update.py b/src/ui/widgets/signature_update.py index 056540b..b0b49fe 100644 --- a/src/ui/widgets/signature_update.py +++ b/src/ui/widgets/signature_update.py @@ -1,14 +1,133 @@ +import os +import time +from concurrent.futures import ThreadPoolExecutor +from queue import Empty, Queue + from PySide6 import QtCore, QtWidgets from PySide6.QtMultimedia import QAudioOutput, QMediaPlayer from src.backend.catalogue import Catalogue from src.backend.database import Database +from src.backend.webadis import get_book_medianr from src.logic.SRU import SWB from src.shared.logging import log from .widget_sources.admin_update_signatures_ui import Ui_Dialog +class MedianrThread(QtCore.QThread): + progress = QtCore.Signal(int) + currtot = QtCore.Signal(int, int) + + def __init__(self, books=None, thread_count=6): + super().__init__() + self.books = books or [] + self.total = 0 + # Database instances are not always thread-safe across threads; create one per worker. + # Keep a shared auth that is safe to read. + db_main = Database() + self.auth = db_main.getWebADISAuth() + if self.auth == ("", ""): + raise ValueError("WebADIS authentication not set in database.") + self.thread_count = max(1, thread_count) + self._stop_requested = False + + def run(self): + # Use ThreadPoolExecutor to parallelize I/O-bound tasks. + total_books = len(self.books) + if total_books == 0: + log.debug("MedianrThread: no books to process") + return + + chunk_size = (total_books + self.thread_count - 1) // self.thread_count + chunks = [ + self.books[i : i + chunk_size] for i in range(0, total_books, chunk_size) + ] + + # queue for worker results and progress + q = Queue() + + def medianrworker(chunk: list[dict]): + # Each worker creates its own Database instance for thread-safety + db = Database() + for book in chunk: + if self._stop_requested: + break + try: + booknr = get_book_medianr( + book["bookdata"].signature, + db.getApparatNrByBookId(book["id"]), + self.auth, + ) + log.debug( + f"Book ID {book['id']} - Signature {book['bookdata'].signature} - Medianr {booknr}" + ) + book_data = book["bookdata"] + book_data.medianr = booknr + db.updateBookdata(book["id"], book_data) + q.put(("progress", 1)) + except Exception as e: + log.error(f"Medianr worker error for book {book}: {e}") + q.put(("progress", 1)) + time.sleep(10) + q.put(("done", None)) + + processed = 0 + finished_workers = 0 + + with ThreadPoolExecutor(max_workers=len(chunks)) as ex: + futures = [ex.submit(medianrworker, ch) for ch in chunks] + + log.info( + f"Launched {len(futures)} worker thread(s) for {total_books} entries: {[len(ch) for ch in chunks]} entries per thread." + ) + + # aggregate progress + while finished_workers < len(chunks): + try: + kind, payload = q.get(timeout=0.1) + except Empty: + continue + + if kind == "progress": + processed += int(payload) + self.progress.emit(processed) + # emit currtot with processed and current chunk total as best-effort + self.currtot.emit(processed, total_books) + elif kind == "done": + finished_workers += 1 + + # ensure final progress reached total_books + self.progress.emit(total_books) + self.currtot.emit(total_books, total_books) + + def stop(self): + """Request the thread to stop early.""" + self._stop_requested = True + + def process_chunk(self, books_chunk): + # kept for backward compatibility but not used by run(); still usable externally + db = Database() + for index, book in enumerate(books_chunk): + try: + booknr = get_book_medianr( + book["bookdata"].signature, + db.getApparatNrByBookId(book["id"]), + self.auth, + ) + log.debug( + f"Book ID {book['id']} - Signature {book['bookdata'].signature} - Medianr {booknr}" + ) + book_data = book["bookdata"] + book_data.medianr = booknr + db.updateBookdata(book["id"], book_data) + except Exception as e: + log.error(f"Medianr process_chunk error for book {book}: {e}") + self.progress.emit(index + 1) + self.currtot.emit(self.total + 1, len(books_chunk)) + self.total += 1 + + class UpdaterThread(QtCore.QThread): progress = QtCore.Signal(int) currtot = QtCore.Signal(int, int) @@ -98,6 +217,8 @@ class UpdateSignatures(QtWidgets.QDialog, Ui_Dialog): self.catalogue = Catalogue() self.player = QMediaPlayer() self.audio_output = QAudioOutput() + self.spin_thread_count.setMaximum(os.cpu_count()) + self.btn_add_medianr.clicked.connect(self.add_medianr) def play_sound(self, sound_file: str): self.player.setAudioOutput(self.audio_output) @@ -114,6 +235,16 @@ class UpdateSignatures(QtWidgets.QDialog, Ui_Dialog): self.updater.finished.connect(self.updater.deleteLater) self.updater.start() + def add_medianr(self): + books = self.db.getAllBooks() + books = [book for book in books if book["bookdata"].medianr is None] + total_books = len(books) + self.progressBar.setMaximum(total_books) + self.medianr_thread = MedianrThread(books, self.spin_thread_count.value()) + self.medianr_thread.progress.connect(self.update_progress) + self.medianr_thread.finished.connect(self.medianr_thread.deleteLater) + self.medianr_thread.start() + def add_missing(self): books = self.db.getAllBooks() total_books = len(books) diff --git a/src/ui/widgets/widget_sources/admin_update_signatures.ui b/src/ui/widgets/widget_sources/admin_update_signatures.ui index 15afca4..8541410 100644 --- a/src/ui/widgets/widget_sources/admin_update_signatures.ui +++ b/src/ui/widgets/widget_sources/admin_update_signatures.ui @@ -30,6 +30,47 @@ + + + + + + Anzahl Parraleler Aktionen + + + + + + + Mediennummern ergänzen + + + + + + + 1 + + + 6 + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + diff --git a/src/ui/widgets/widget_sources/admin_update_signatures_ui.py b/src/ui/widgets/widget_sources/admin_update_signatures_ui.py index 25101c6..6ce7841 100644 --- a/src/ui/widgets/widget_sources/admin_update_signatures_ui.py +++ b/src/ui/widgets/widget_sources/admin_update_signatures_ui.py @@ -15,8 +15,9 @@ from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor, QFont, QFontDatabase, QGradient, QIcon, QImage, QKeySequence, QLinearGradient, QPainter, QPalette, QPixmap, QRadialGradient, QTransform) -from PySide6.QtWidgets import (QApplication, QDialog, QFormLayout, QProgressBar, - QPushButton, QSizePolicy, QVBoxLayout, QWidget) +from PySide6.QtWidgets import (QApplication, QDialog, QFormLayout, QLabel, + QProgressBar, QPushButton, QSizePolicy, QSpacerItem, + QSpinBox, QVBoxLayout, QWidget) class Ui_Dialog(object): def setupUi(self, Dialog): @@ -37,6 +38,32 @@ class Ui_Dialog(object): self.formLayout.setWidget(0, QFormLayout.ItemRole.FieldRole, self.btn_add_missing_data) + self.formLayout_2 = QFormLayout() + self.formLayout_2.setObjectName(u"formLayout_2") + self.label = QLabel(Dialog) + self.label.setObjectName(u"label") + + self.formLayout_2.setWidget(0, QFormLayout.ItemRole.LabelRole, self.label) + + self.btn_add_medianr = QPushButton(Dialog) + self.btn_add_medianr.setObjectName(u"btn_add_medianr") + + self.formLayout_2.setWidget(1, QFormLayout.ItemRole.FieldRole, self.btn_add_medianr) + + self.spin_thread_count = QSpinBox(Dialog) + self.spin_thread_count.setObjectName(u"spin_thread_count") + self.spin_thread_count.setMinimum(1) + self.spin_thread_count.setValue(6) + + self.formLayout_2.setWidget(0, QFormLayout.ItemRole.FieldRole, self.spin_thread_count) + + + self.formLayout.setLayout(1, QFormLayout.ItemRole.FieldRole, self.formLayout_2) + + self.verticalSpacer = QSpacerItem(20, 40, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding) + + self.formLayout.setItem(2, QFormLayout.ItemRole.FieldRole, self.verticalSpacer) + self.verticalLayout.addLayout(self.formLayout) @@ -56,5 +83,7 @@ class Ui_Dialog(object): Dialog.setWindowTitle(QCoreApplication.translate("Dialog", u"Dialog", None)) self.btn_update_signatures.setText(QCoreApplication.translate("Dialog", u"Signaturen aktualisieren", None)) self.btn_add_missing_data.setText(QCoreApplication.translate("Dialog", u"Fehlende Daten hinzuf\u00fcgen", None)) + self.label.setText(QCoreApplication.translate("Dialog", u"Anzahl Parraleler Aktionen", None)) + self.btn_add_medianr.setText(QCoreApplication.translate("Dialog", u"Mediennummern erg\u00e4nzen", None)) # retranslateUi diff --git a/tests/test_migrations_runner.py b/tests/test_migrations_runner.py new file mode 100644 index 0000000..c86561c --- /dev/null +++ b/tests/test_migrations_runner.py @@ -0,0 +1,20 @@ +import sqlite3 as sql +from pathlib import Path + +from src.backend.database import Database + +p = Path("devtests_test_migrations.db") +if p.exists(): + p.unlink() + +print("Creating Database at", p) +db = Database(db_path=p) + +conn = sql.connect(p) +c = conn.cursor() +c.execute("SELECT name FROM sqlite_master WHERE type='table'") +print("Tables:", sorted([r[0] for r in c.fetchall()])) + +c.execute("SELECT id, applied_at FROM schema_migrations") +print("Migrations applied:", c.fetchall()) +conn.close() -- 2.49.1 From ab62212201de6a503857f30cd7820975057632a7 Mon Sep 17 00:00:00 2001 From: WorldTeacher Date: Tue, 28 Oct 2025 10:46:30 +0100 Subject: [PATCH 35/40] chore: elsa: fix tab orders --- .../dialog_sources/elsa_add_table_entry.ui | 65 +- .../dialog_sources/elsa_add_table_entry_ui.py | 868 ++++++++++++------ src/ui/dialogs/elsa_add_entry.py | 3 +- 3 files changed, 638 insertions(+), 298 deletions(-) diff --git a/src/ui/dialogs/dialog_sources/elsa_add_table_entry.ui b/src/ui/dialogs/dialog_sources/elsa_add_table_entry.ui index 1f44d99..882c722 100644 --- a/src/ui/dialogs/dialog_sources/elsa_add_table_entry.ui +++ b/src/ui/dialogs/dialog_sources/elsa_add_table_entry.ui @@ -7,7 +7,7 @@ 0 0 529 - 482 + 484 @@ -208,6 +208,9 @@ + + Qt::NoFocus + @@ -352,6 +355,9 @@ Nachname, Vorname + + Qt::NoFocus + @@ -471,6 +477,9 @@ Nachname, Vorname + + Qt::NoFocus + @@ -508,6 +517,9 @@ Nachname, Vorname + + Qt::NoFocus + true @@ -522,6 +534,9 @@ Nachname, Vorname + + Qt::NoFocus + true @@ -536,6 +551,9 @@ Nachname, Vorname + + Qt::NoFocus + true @@ -703,6 +721,51 @@ Nachname, Vorname + + btn_mono + btn_hg + btn_zs + searchIdent + btn_search + book_author + book_year + book_edition + book_title + book_place + book_publisher + book_signature + book_pages + book_isbn + hg_author + hg_year + hg_edition + hg_chaptertitle + hg_editor + hg_title + hg_place + hg_publisher + hg_pages + hg_signature + hg_isbn + zs_author + zs_year + zs_issue + zs_chapter_title + zs_title + zs_place + zs_publisher + zs_pages + zs_signature + zs_isbn + make_quote + file_desc_edit + filename_edit + ilias_filename + copy_filename + copy_ilias_filename + copy_qoute + retryButton + diff --git a/src/ui/dialogs/dialog_sources/elsa_add_table_entry_ui.py b/src/ui/dialogs/dialog_sources/elsa_add_table_entry_ui.py index 3a77b77..a66c087 100644 --- a/src/ui/dialogs/dialog_sources/elsa_add_table_entry_ui.py +++ b/src/ui/dialogs/dialog_sources/elsa_add_table_entry_ui.py @@ -1,411 +1,687 @@ -# Form implementation generated from reading ui file 'c:\Users\aky547\GitHub\SemesterapparatsManager\src\ui\dialogs\dialog_sources\elsa_add_table_entry.ui' -# -# Created by: PySide6 UI code generator 6.8.0 -# -# 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. +# -*- coding: utf-8 -*- +################################################################################ +## Form generated from reading UI file 'elsa_add_table_entry.ui' +## +## Created by: Qt User Interface Compiler version 6.9.2 +## +## WARNING! All changes made in this file will be lost when recompiling UI file! +################################################################################ -from PySide6 import QtCore, QtGui, QtWidgets - +from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale, + QMetaObject, QObject, QPoint, QRect, + QSize, QTime, QUrl, Qt) +from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor, + QFont, QFontDatabase, QGradient, QIcon, + QImage, QKeySequence, QLinearGradient, QPainter, + QPalette, QPixmap, QRadialGradient, QTransform) +from PySide6.QtWidgets import (QAbstractButton, QApplication, QDialog, QDialogButtonBox, + QGridLayout, QGroupBox, QHBoxLayout, QLabel, + QLineEdit, QPushButton, QRadioButton, QSizePolicy, + QSpacerItem, QStackedWidget, QTextEdit, QToolButton, + QVBoxLayout, QWidget) class Ui_Dialog(object): def setupUi(self, Dialog): - Dialog.setObjectName("Dialog") - Dialog.resize(529, 482) - self.verticalLayout = QtWidgets.QVBoxLayout(Dialog) - self.verticalLayout.setObjectName("verticalLayout") - self.groupBox = QtWidgets.QGroupBox(parent=Dialog) + if not Dialog.objectName(): + Dialog.setObjectName(u"Dialog") + Dialog.resize(529, 484) + self.verticalLayout = QVBoxLayout(Dialog) + self.verticalLayout.setObjectName(u"verticalLayout") + self.groupBox = QGroupBox(Dialog) + self.groupBox.setObjectName(u"groupBox") self.groupBox.setFlat(True) self.groupBox.setCheckable(False) - self.groupBox.setObjectName("groupBox") - self.gridLayout_4 = QtWidgets.QGridLayout(self.groupBox) - self.gridLayout_4.setObjectName("gridLayout_4") - spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum) - self.gridLayout_4.addItem(spacerItem, 0, 3, 1, 1) - self.btn_mono = QtWidgets.QRadioButton(parent=self.groupBox) + self.gridLayout_4 = QGridLayout(self.groupBox) + self.gridLayout_4.setObjectName(u"gridLayout_4") + self.horizontalSpacer = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum) + + self.gridLayout_4.addItem(self.horizontalSpacer, 0, 3, 1, 1) + + self.btn_mono = QRadioButton(self.groupBox) + self.btn_mono.setObjectName(u"btn_mono") self.btn_mono.setChecked(False) - self.btn_mono.setObjectName("btn_mono") + self.gridLayout_4.addWidget(self.btn_mono, 0, 0, 1, 1) - self.btn_zs = QtWidgets.QRadioButton(parent=self.groupBox) - self.btn_zs.setObjectName("btn_zs") + + self.btn_zs = QRadioButton(self.groupBox) + self.btn_zs.setObjectName(u"btn_zs") + self.gridLayout_4.addWidget(self.btn_zs, 0, 2, 1, 1) - self.btn_hg = QtWidgets.QRadioButton(parent=self.groupBox) - self.btn_hg.setObjectName("btn_hg") + + self.btn_hg = QRadioButton(self.groupBox) + self.btn_hg.setObjectName(u"btn_hg") + self.gridLayout_4.addWidget(self.btn_hg, 0, 1, 1, 1) + + self.verticalLayout.addWidget(self.groupBox) - self.horizontalLayout_2 = QtWidgets.QHBoxLayout() - self.horizontalLayout_2.setObjectName("horizontalLayout_2") - self.label_2 = QtWidgets.QLabel(parent=Dialog) - self.label_2.setObjectName("label_2") + + self.horizontalLayout_2 = QHBoxLayout() + self.horizontalLayout_2.setObjectName(u"horizontalLayout_2") + self.label_2 = QLabel(Dialog) + self.label_2.setObjectName(u"label_2") + self.horizontalLayout_2.addWidget(self.label_2) - self.searchIdent = QtWidgets.QLineEdit(parent=Dialog) - self.searchIdent.setObjectName("searchIdent") + + self.searchIdent = QLineEdit(Dialog) + self.searchIdent.setObjectName(u"searchIdent") + self.horizontalLayout_2.addWidget(self.searchIdent) - self.btn_search = QtWidgets.QPushButton(parent=Dialog) - self.btn_search.setObjectName("btn_search") + + self.btn_search = QPushButton(Dialog) + self.btn_search.setObjectName(u"btn_search") + self.horizontalLayout_2.addWidget(self.btn_search) - spacerItem1 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum) - self.horizontalLayout_2.addItem(spacerItem1) - self.make_quote = QtWidgets.QPushButton(parent=Dialog) - self.make_quote.setObjectName("make_quote") + + self.horizontalSpacer_2 = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum) + + self.horizontalLayout_2.addItem(self.horizontalSpacer_2) + + self.make_quote = QPushButton(Dialog) + self.make_quote.setObjectName(u"make_quote") + self.horizontalLayout_2.addWidget(self.make_quote) + + self.verticalLayout.addLayout(self.horizontalLayout_2) - self.stackedWidget = QtWidgets.QStackedWidget(parent=Dialog) - self.stackedWidget.setObjectName("stackedWidget") - self.mono = QtWidgets.QWidget() - self.mono.setObjectName("mono") - self.gridLayout_2 = QtWidgets.QGridLayout(self.mono) - self.gridLayout_2.setObjectName("gridLayout_2") - self.label = QtWidgets.QLabel(parent=self.mono) - self.label.setObjectName("label") + + self.stackedWidget = QStackedWidget(Dialog) + self.stackedWidget.setObjectName(u"stackedWidget") + self.mono = QWidget() + self.mono.setObjectName(u"mono") + self.gridLayout_2 = QGridLayout(self.mono) + self.gridLayout_2.setObjectName(u"gridLayout_2") + self.label = QLabel(self.mono) + self.label.setObjectName(u"label") + self.gridLayout_2.addWidget(self.label, 0, 0, 1, 1) - self.book_author = QtWidgets.QLineEdit(parent=self.mono) - self.book_author.setObjectName("book_author") + + self.book_author = QLineEdit(self.mono) + self.book_author.setObjectName(u"book_author") + self.gridLayout_2.addWidget(self.book_author, 0, 1, 1, 1) - self.label_3 = QtWidgets.QLabel(parent=self.mono) - self.label_3.setObjectName("label_3") + + self.label_3 = QLabel(self.mono) + self.label_3.setObjectName(u"label_3") + self.gridLayout_2.addWidget(self.label_3, 1, 0, 1, 1) - self.book_year = QtWidgets.QLineEdit(parent=self.mono) - self.book_year.setObjectName("book_year") + + self.book_year = QLineEdit(self.mono) + self.book_year.setObjectName(u"book_year") + self.gridLayout_2.addWidget(self.book_year, 1, 1, 1, 1) - self.label_4 = QtWidgets.QLabel(parent=self.mono) - self.label_4.setObjectName("label_4") + + self.label_4 = QLabel(self.mono) + self.label_4.setObjectName(u"label_4") + self.gridLayout_2.addWidget(self.label_4, 2, 0, 1, 1) - self.book_edition = QtWidgets.QLineEdit(parent=self.mono) - self.book_edition.setObjectName("book_edition") + + self.book_edition = QLineEdit(self.mono) + self.book_edition.setObjectName(u"book_edition") + self.gridLayout_2.addWidget(self.book_edition, 2, 1, 1, 1) - self.label_5 = QtWidgets.QLabel(parent=self.mono) - self.label_5.setObjectName("label_5") + + self.label_5 = QLabel(self.mono) + self.label_5.setObjectName(u"label_5") + self.gridLayout_2.addWidget(self.label_5, 3, 0, 1, 1) - self.book_title = QtWidgets.QLineEdit(parent=self.mono) - self.book_title.setObjectName("book_title") + + self.book_title = QLineEdit(self.mono) + self.book_title.setObjectName(u"book_title") + self.gridLayout_2.addWidget(self.book_title, 3, 1, 1, 1) - self.label_6 = QtWidgets.QLabel(parent=self.mono) - self.label_6.setObjectName("label_6") + + self.label_6 = QLabel(self.mono) + self.label_6.setObjectName(u"label_6") + self.gridLayout_2.addWidget(self.label_6, 4, 0, 1, 1) - self.book_place = QtWidgets.QLineEdit(parent=self.mono) - self.book_place.setObjectName("book_place") + + self.book_place = QLineEdit(self.mono) + self.book_place.setObjectName(u"book_place") + self.gridLayout_2.addWidget(self.book_place, 4, 1, 1, 1) - self.label_7 = QtWidgets.QLabel(parent=self.mono) - self.label_7.setObjectName("label_7") + + self.label_7 = QLabel(self.mono) + self.label_7.setObjectName(u"label_7") + self.gridLayout_2.addWidget(self.label_7, 5, 0, 1, 1) - self.book_publisher = QtWidgets.QLineEdit(parent=self.mono) - self.book_publisher.setObjectName("book_publisher") + + self.book_publisher = QLineEdit(self.mono) + self.book_publisher.setObjectName(u"book_publisher") + self.gridLayout_2.addWidget(self.book_publisher, 5, 1, 1, 1) - self.label_8 = QtWidgets.QLabel(parent=self.mono) - self.label_8.setObjectName("label_8") + + self.label_8 = QLabel(self.mono) + self.label_8.setObjectName(u"label_8") + self.gridLayout_2.addWidget(self.label_8, 6, 0, 1, 1) - self.book_signature = QtWidgets.QLineEdit(parent=self.mono) - self.book_signature.setObjectName("book_signature") + + self.book_signature = QLineEdit(self.mono) + self.book_signature.setObjectName(u"book_signature") + self.gridLayout_2.addWidget(self.book_signature, 6, 1, 1, 1) - self.label_9 = QtWidgets.QLabel(parent=self.mono) - self.label_9.setObjectName("label_9") + + self.label_9 = QLabel(self.mono) + self.label_9.setObjectName(u"label_9") + self.gridLayout_2.addWidget(self.label_9, 7, 0, 1, 1) - self.book_pages = QtWidgets.QLineEdit(parent=self.mono) - self.book_pages.setObjectName("book_pages") + + self.book_pages = QLineEdit(self.mono) + self.book_pages.setObjectName(u"book_pages") + self.gridLayout_2.addWidget(self.book_pages, 7, 1, 1, 1) - self.page_warn_2 = QtWidgets.QToolButton(parent=self.mono) - self.page_warn_2.setText("") + + self.page_warn_2 = QToolButton(self.mono) + self.page_warn_2.setObjectName(u"page_warn_2") + self.page_warn_2.setFocusPolicy(Qt.NoFocus) self.page_warn_2.setAutoRaise(True) - self.page_warn_2.setObjectName("page_warn_2") + self.gridLayout_2.addWidget(self.page_warn_2, 7, 2, 1, 1) - self.label_29 = QtWidgets.QLabel(parent=self.mono) - self.label_29.setObjectName("label_29") + + self.label_29 = QLabel(self.mono) + self.label_29.setObjectName(u"label_29") + self.gridLayout_2.addWidget(self.label_29, 8, 0, 1, 1) - self.book_isbn = QtWidgets.QLineEdit(parent=self.mono) - self.book_isbn.setObjectName("book_isbn") + + self.book_isbn = QLineEdit(self.mono) + self.book_isbn.setObjectName(u"book_isbn") + self.gridLayout_2.addWidget(self.book_isbn, 8, 1, 1, 1) + self.stackedWidget.addWidget(self.mono) - self.hg = QtWidgets.QWidget() - self.hg.setObjectName("hg") - self.gridLayout_3 = QtWidgets.QGridLayout(self.hg) - self.gridLayout_3.setObjectName("gridLayout_3") - self.hg_editor = QtWidgets.QLineEdit(parent=self.hg) - self.hg_editor.setObjectName("hg_editor") + self.hg = QWidget() + self.hg.setObjectName(u"hg") + self.gridLayout_3 = QGridLayout(self.hg) + self.gridLayout_3.setObjectName(u"gridLayout_3") + self.hg_editor = QLineEdit(self.hg) + self.hg_editor.setObjectName(u"hg_editor") + self.gridLayout_3.addWidget(self.hg_editor, 4, 1, 1, 1) - self.label_26 = QtWidgets.QLabel(parent=self.hg) - self.label_26.setObjectName("label_26") + + self.label_26 = QLabel(self.hg) + self.label_26.setObjectName(u"label_26") + self.gridLayout_3.addWidget(self.label_26, 7, 0, 1, 1) - self.hg_edition = QtWidgets.QLineEdit(parent=self.hg) - self.hg_edition.setObjectName("hg_edition") + + self.hg_edition = QLineEdit(self.hg) + self.hg_edition.setObjectName(u"hg_edition") + self.gridLayout_3.addWidget(self.hg_edition, 2, 1, 1, 1) - self.label_20 = QtWidgets.QLabel(parent=self.hg) - self.label_20.setObjectName("label_20") + + self.label_20 = QLabel(self.hg) + self.label_20.setObjectName(u"label_20") + self.gridLayout_3.addWidget(self.label_20, 1, 0, 1, 1) - self.label_24 = QtWidgets.QLabel(parent=self.hg) - self.label_24.setObjectName("label_24") + + self.label_24 = QLabel(self.hg) + self.label_24.setObjectName(u"label_24") + self.gridLayout_3.addWidget(self.label_24, 3, 0, 1, 1) - self.label_27 = QtWidgets.QLabel(parent=self.hg) - self.label_27.setObjectName("label_27") + + self.label_27 = QLabel(self.hg) + self.label_27.setObjectName(u"label_27") + self.gridLayout_3.addWidget(self.label_27, 8, 0, 1, 1) - self.label_28 = QtWidgets.QLabel(parent=self.hg) - self.label_28.setObjectName("label_28") + + self.label_28 = QLabel(self.hg) + self.label_28.setObjectName(u"label_28") + self.gridLayout_3.addWidget(self.label_28, 9, 0, 1, 1) - self.label_23 = QtWidgets.QLabel(parent=self.hg) - self.label_23.setObjectName("label_23") + + self.label_23 = QLabel(self.hg) + self.label_23.setObjectName(u"label_23") + self.gridLayout_3.addWidget(self.label_23, 5, 0, 1, 1) - self.label_21 = QtWidgets.QLabel(parent=self.hg) - self.label_21.setObjectName("label_21") + + self.label_21 = QLabel(self.hg) + self.label_21.setObjectName(u"label_21") + self.gridLayout_3.addWidget(self.label_21, 2, 0, 1, 1) - self.hg_pages = QtWidgets.QLineEdit(parent=self.hg) - self.hg_pages.setObjectName("hg_pages") + + self.hg_pages = QLineEdit(self.hg) + self.hg_pages.setObjectName(u"hg_pages") + self.gridLayout_3.addWidget(self.hg_pages, 8, 1, 1, 1) - self.label_19 = QtWidgets.QLabel(parent=self.hg) - self.label_19.setObjectName("label_19") + + self.label_19 = QLabel(self.hg) + self.label_19.setObjectName(u"label_19") + self.gridLayout_3.addWidget(self.label_19, 0, 0, 1, 1) - self.hg_signature = QtWidgets.QLineEdit(parent=self.hg) - self.hg_signature.setObjectName("hg_signature") + + self.hg_signature = QLineEdit(self.hg) + self.hg_signature.setObjectName(u"hg_signature") + self.gridLayout_3.addWidget(self.hg_signature, 9, 1, 1, 1) - self.label_30 = QtWidgets.QLabel(parent=self.hg) - self.label_30.setObjectName("label_30") + + self.label_30 = QLabel(self.hg) + self.label_30.setObjectName(u"label_30") + self.gridLayout_3.addWidget(self.label_30, 10, 0, 1, 1) - self.label_25 = QtWidgets.QLabel(parent=self.hg) - self.label_25.setObjectName("label_25") + + self.label_25 = QLabel(self.hg) + self.label_25.setObjectName(u"label_25") + self.gridLayout_3.addWidget(self.label_25, 6, 0, 1, 1) - self.hg_year = QtWidgets.QLineEdit(parent=self.hg) - self.hg_year.setObjectName("hg_year") + + self.hg_year = QLineEdit(self.hg) + self.hg_year.setObjectName(u"hg_year") + self.gridLayout_3.addWidget(self.hg_year, 1, 1, 1, 1) - self.label_22 = QtWidgets.QLabel(parent=self.hg) - self.label_22.setObjectName("label_22") + + self.label_22 = QLabel(self.hg) + self.label_22.setObjectName(u"label_22") + self.gridLayout_3.addWidget(self.label_22, 4, 0, 1, 1) - self.hg_title = QtWidgets.QLineEdit(parent=self.hg) - self.hg_title.setObjectName("hg_title") + + self.hg_title = QLineEdit(self.hg) + self.hg_title.setObjectName(u"hg_title") + self.gridLayout_3.addWidget(self.hg_title, 5, 1, 1, 1) - self.hg_chaptertitle = QtWidgets.QLineEdit(parent=self.hg) - self.hg_chaptertitle.setObjectName("hg_chaptertitle") + + self.hg_chaptertitle = QLineEdit(self.hg) + self.hg_chaptertitle.setObjectName(u"hg_chaptertitle") + self.gridLayout_3.addWidget(self.hg_chaptertitle, 3, 1, 1, 1) - self.hg_author = QtWidgets.QLineEdit(parent=self.hg) - self.hg_author.setObjectName("hg_author") + + self.hg_author = QLineEdit(self.hg) + self.hg_author.setObjectName(u"hg_author") + self.gridLayout_3.addWidget(self.hg_author, 0, 1, 1, 1) - self.hg_isbn = QtWidgets.QLineEdit(parent=self.hg) - self.hg_isbn.setObjectName("hg_isbn") + + self.hg_isbn = QLineEdit(self.hg) + self.hg_isbn.setObjectName(u"hg_isbn") + self.gridLayout_3.addWidget(self.hg_isbn, 10, 1, 1, 1) - self.hg_publisher = QtWidgets.QLineEdit(parent=self.hg) - self.hg_publisher.setObjectName("hg_publisher") + + self.hg_publisher = QLineEdit(self.hg) + self.hg_publisher.setObjectName(u"hg_publisher") + self.gridLayout_3.addWidget(self.hg_publisher, 7, 1, 1, 1) - self.hg_place = QtWidgets.QLineEdit(parent=self.hg) - self.hg_place.setObjectName("hg_place") + + self.hg_place = QLineEdit(self.hg) + self.hg_place.setObjectName(u"hg_place") + self.gridLayout_3.addWidget(self.hg_place, 6, 1, 1, 1) - self.page_warn_3 = QtWidgets.QToolButton(parent=self.hg) - self.page_warn_3.setText("") + + self.page_warn_3 = QToolButton(self.hg) + self.page_warn_3.setObjectName(u"page_warn_3") + self.page_warn_3.setFocusPolicy(Qt.NoFocus) self.page_warn_3.setAutoRaise(True) - self.page_warn_3.setObjectName("page_warn_3") + self.gridLayout_3.addWidget(self.page_warn_3, 8, 2, 1, 1) + self.stackedWidget.addWidget(self.hg) - self.zs = QtWidgets.QWidget() - self.zs.setObjectName("zs") - self.gridLayout = QtWidgets.QGridLayout(self.zs) - self.gridLayout.setObjectName("gridLayout") - self.label_10 = QtWidgets.QLabel(parent=self.zs) - self.label_10.setObjectName("label_10") + self.zs = QWidget() + self.zs.setObjectName(u"zs") + self.gridLayout = QGridLayout(self.zs) + self.gridLayout.setObjectName(u"gridLayout") + self.label_10 = QLabel(self.zs) + self.label_10.setObjectName(u"label_10") + self.gridLayout.addWidget(self.label_10, 0, 0, 1, 1) - self.zs_publisher = QtWidgets.QLineEdit(parent=self.zs) - self.zs_publisher.setObjectName("zs_publisher") + + self.zs_publisher = QLineEdit(self.zs) + self.zs_publisher.setObjectName(u"zs_publisher") + self.gridLayout.addWidget(self.zs_publisher, 6, 1, 1, 1) - self.zs_place = QtWidgets.QLineEdit(parent=self.zs) - self.zs_place.setObjectName("zs_place") + + self.zs_place = QLineEdit(self.zs) + self.zs_place.setObjectName(u"zs_place") + self.gridLayout.addWidget(self.zs_place, 5, 1, 1, 1) - self.label_14 = QtWidgets.QLabel(parent=self.zs) - self.label_14.setObjectName("label_14") + + self.label_14 = QLabel(self.zs) + self.label_14.setObjectName(u"label_14") + self.gridLayout.addWidget(self.label_14, 4, 0, 1, 1) - self.label_11 = QtWidgets.QLabel(parent=self.zs) - self.label_11.setObjectName("label_11") + + self.label_11 = QLabel(self.zs) + self.label_11.setObjectName(u"label_11") + self.gridLayout.addWidget(self.label_11, 1, 0, 1, 1) - self.zs_year = QtWidgets.QLineEdit(parent=self.zs) - self.zs_year.setObjectName("zs_year") + + self.zs_year = QLineEdit(self.zs) + self.zs_year.setObjectName(u"zs_year") + self.gridLayout.addWidget(self.zs_year, 1, 1, 1, 1) - self.label_17 = QtWidgets.QLabel(parent=self.zs) - self.label_17.setObjectName("label_17") + + self.label_17 = QLabel(self.zs) + self.label_17.setObjectName(u"label_17") + self.gridLayout.addWidget(self.label_17, 7, 0, 1, 1) - self.label_16 = QtWidgets.QLabel(parent=self.zs) - self.label_16.setObjectName("label_16") + + self.label_16 = QLabel(self.zs) + self.label_16.setObjectName(u"label_16") + self.gridLayout.addWidget(self.label_16, 6, 0, 1, 1) - self.zs_issue = QtWidgets.QLineEdit(parent=self.zs) - self.zs_issue.setObjectName("zs_issue") + + self.zs_issue = QLineEdit(self.zs) + self.zs_issue.setObjectName(u"zs_issue") + self.gridLayout.addWidget(self.zs_issue, 2, 1, 1, 1) - self.zs_chapter_title = QtWidgets.QLineEdit(parent=self.zs) - self.zs_chapter_title.setObjectName("zs_chapter_title") + + self.zs_chapter_title = QLineEdit(self.zs) + self.zs_chapter_title.setObjectName(u"zs_chapter_title") + self.gridLayout.addWidget(self.zs_chapter_title, 3, 1, 1, 1) - self.zs_isbn = QtWidgets.QLineEdit(parent=self.zs) - self.zs_isbn.setObjectName("zs_isbn") + + self.zs_isbn = QLineEdit(self.zs) + self.zs_isbn.setObjectName(u"zs_isbn") + self.gridLayout.addWidget(self.zs_isbn, 9, 1, 1, 1) - self.label_12 = QtWidgets.QLabel(parent=self.zs) - self.label_12.setObjectName("label_12") + + self.label_12 = QLabel(self.zs) + self.label_12.setObjectName(u"label_12") + self.gridLayout.addWidget(self.label_12, 2, 0, 1, 1) - self.label_31 = QtWidgets.QLabel(parent=self.zs) - self.label_31.setObjectName("label_31") + + self.label_31 = QLabel(self.zs) + self.label_31.setObjectName(u"label_31") + self.gridLayout.addWidget(self.label_31, 9, 0, 1, 1) - self.label_15 = QtWidgets.QLabel(parent=self.zs) - self.label_15.setObjectName("label_15") + + self.label_15 = QLabel(self.zs) + self.label_15.setObjectName(u"label_15") + self.gridLayout.addWidget(self.label_15, 5, 0, 1, 1) - self.zs_signature = QtWidgets.QLineEdit(parent=self.zs) - self.zs_signature.setObjectName("zs_signature") + + self.zs_signature = QLineEdit(self.zs) + self.zs_signature.setObjectName(u"zs_signature") + self.gridLayout.addWidget(self.zs_signature, 8, 1, 1, 1) - self.zs_pages = QtWidgets.QLineEdit(parent=self.zs) - self.zs_pages.setObjectName("zs_pages") + + self.zs_pages = QLineEdit(self.zs) + self.zs_pages.setObjectName(u"zs_pages") + self.gridLayout.addWidget(self.zs_pages, 7, 1, 1, 1) - self.label_13 = QtWidgets.QLabel(parent=self.zs) - self.label_13.setObjectName("label_13") + + self.label_13 = QLabel(self.zs) + self.label_13.setObjectName(u"label_13") + self.gridLayout.addWidget(self.label_13, 3, 0, 1, 1) - self.label_18 = QtWidgets.QLabel(parent=self.zs) - self.label_18.setObjectName("label_18") + + self.label_18 = QLabel(self.zs) + self.label_18.setObjectName(u"label_18") + self.gridLayout.addWidget(self.label_18, 8, 0, 1, 1) - self.zs_author = QtWidgets.QLineEdit(parent=self.zs) - self.zs_author.setObjectName("zs_author") + + self.zs_author = QLineEdit(self.zs) + self.zs_author.setObjectName(u"zs_author") + self.gridLayout.addWidget(self.zs_author, 0, 1, 1, 1) - self.zs_title = QtWidgets.QLineEdit(parent=self.zs) - self.zs_title.setObjectName("zs_title") + + self.zs_title = QLineEdit(self.zs) + self.zs_title.setObjectName(u"zs_title") + self.gridLayout.addWidget(self.zs_title, 4, 1, 1, 1) - self.page_warn = QtWidgets.QToolButton(parent=self.zs) - self.page_warn.setText("") + + self.page_warn = QToolButton(self.zs) + self.page_warn.setObjectName(u"page_warn") + self.page_warn.setFocusPolicy(Qt.NoFocus) self.page_warn.setAutoRaise(True) - self.page_warn.setObjectName("page_warn") + self.gridLayout.addWidget(self.page_warn, 7, 2, 1, 1) + self.stackedWidget.addWidget(self.zs) - self.page = QtWidgets.QWidget() - self.page.setLayoutDirection(QtCore.Qt.LayoutDirection.LeftToRight) - self.page.setObjectName("page") - self.gridLayout_5 = QtWidgets.QGridLayout(self.page) - self.gridLayout_5.setObjectName("gridLayout_5") - self.label_32 = QtWidgets.QLabel(parent=self.page) - self.label_32.setObjectName("label_32") + self.page = QWidget() + self.page.setObjectName(u"page") + self.page.setLayoutDirection(Qt.LeftToRight) + self.gridLayout_5 = QGridLayout(self.page) + self.gridLayout_5.setObjectName(u"gridLayout_5") + self.label_32 = QLabel(self.page) + self.label_32.setObjectName(u"label_32") + self.gridLayout_5.addWidget(self.label_32, 0, 0, 1, 1) - spacerItem2 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Expanding) - self.gridLayout_5.addItem(spacerItem2, 7, 0, 1, 1) - self.file_desc_edit = QtWidgets.QTextEdit(parent=self.page) + + self.verticalSpacer = QSpacerItem(20, 40, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding) + + self.gridLayout_5.addItem(self.verticalSpacer, 7, 0, 1, 1) + + self.file_desc_edit = QTextEdit(self.page) + self.file_desc_edit.setObjectName(u"file_desc_edit") + self.file_desc_edit.setFocusPolicy(Qt.NoFocus) self.file_desc_edit.setReadOnly(True) - self.file_desc_edit.setObjectName("file_desc_edit") + self.gridLayout_5.addWidget(self.file_desc_edit, 6, 0, 1, 1) - self.label_34 = QtWidgets.QLabel(parent=self.page) - self.label_34.setObjectName("label_34") + + self.label_34 = QLabel(self.page) + self.label_34.setObjectName(u"label_34") + self.gridLayout_5.addWidget(self.label_34, 3, 0, 1, 1) - self.filename_edit = QtWidgets.QTextEdit(parent=self.page) + + self.filename_edit = QTextEdit(self.page) + self.filename_edit.setObjectName(u"filename_edit") + self.filename_edit.setFocusPolicy(Qt.NoFocus) self.filename_edit.setReadOnly(True) - self.filename_edit.setObjectName("filename_edit") + self.gridLayout_5.addWidget(self.filename_edit, 1, 0, 1, 1) - self.label_33 = QtWidgets.QLabel(parent=self.page) - self.label_33.setObjectName("label_33") + + self.label_33 = QLabel(self.page) + self.label_33.setObjectName(u"label_33") + self.gridLayout_5.addWidget(self.label_33, 5, 0, 1, 1) - self.ilias_filename = QtWidgets.QTextEdit(parent=self.page) + + self.ilias_filename = QTextEdit(self.page) + self.ilias_filename.setObjectName(u"ilias_filename") + self.ilias_filename.setFocusPolicy(Qt.NoFocus) self.ilias_filename.setReadOnly(True) - self.ilias_filename.setObjectName("ilias_filename") + self.gridLayout_5.addWidget(self.ilias_filename, 4, 0, 1, 1) - self.verticalLayout_2 = QtWidgets.QVBoxLayout() - self.verticalLayout_2.setObjectName("verticalLayout_2") - spacerItem3 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Expanding) - self.verticalLayout_2.addItem(spacerItem3) - self.copy_filename = QtWidgets.QToolButton(parent=self.page) - self.copy_filename.setLayoutDirection(QtCore.Qt.LayoutDirection.LeftToRight) + + self.verticalLayout_2 = QVBoxLayout() + self.verticalLayout_2.setObjectName(u"verticalLayout_2") + self.verticalSpacer_3 = QSpacerItem(20, 40, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding) + + self.verticalLayout_2.addItem(self.verticalSpacer_3) + + self.copy_filename = QToolButton(self.page) + self.copy_filename.setObjectName(u"copy_filename") + self.copy_filename.setLayoutDirection(Qt.LeftToRight) self.copy_filename.setAutoFillBackground(False) - self.copy_filename.setObjectName("copy_filename") + self.verticalLayout_2.addWidget(self.copy_filename) - self.filename_edit_label = QtWidgets.QLabel(parent=self.page) - self.filename_edit_label.setText("") - self.filename_edit_label.setObjectName("filename_edit_label") + + self.filename_edit_label = QLabel(self.page) + self.filename_edit_label.setObjectName(u"filename_edit_label") + self.verticalLayout_2.addWidget(self.filename_edit_label) - spacerItem4 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Expanding) - self.verticalLayout_2.addItem(spacerItem4) + + self.verticalSpacer_2 = QSpacerItem(20, 40, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding) + + self.verticalLayout_2.addItem(self.verticalSpacer_2) + + self.gridLayout_5.addLayout(self.verticalLayout_2, 1, 1, 1, 1) - self.verticalLayout_3 = QtWidgets.QVBoxLayout() - self.verticalLayout_3.setObjectName("verticalLayout_3") - spacerItem5 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Expanding) - self.verticalLayout_3.addItem(spacerItem5) - self.copy_ilias_filename = QtWidgets.QToolButton(parent=self.page) - self.copy_ilias_filename.setObjectName("copy_ilias_filename") + + self.verticalLayout_3 = QVBoxLayout() + self.verticalLayout_3.setObjectName(u"verticalLayout_3") + self.verticalSpacer_5 = QSpacerItem(20, 40, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding) + + self.verticalLayout_3.addItem(self.verticalSpacer_5) + + self.copy_ilias_filename = QToolButton(self.page) + self.copy_ilias_filename.setObjectName(u"copy_ilias_filename") + self.verticalLayout_3.addWidget(self.copy_ilias_filename) - self.ilias_filename_label = QtWidgets.QLabel(parent=self.page) - self.ilias_filename_label.setText("") - self.ilias_filename_label.setObjectName("ilias_filename_label") + + self.ilias_filename_label = QLabel(self.page) + self.ilias_filename_label.setObjectName(u"ilias_filename_label") + self.verticalLayout_3.addWidget(self.ilias_filename_label) - spacerItem6 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Expanding) - self.verticalLayout_3.addItem(spacerItem6) + + self.verticalSpacer_4 = QSpacerItem(20, 40, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding) + + self.verticalLayout_3.addItem(self.verticalSpacer_4) + + self.gridLayout_5.addLayout(self.verticalLayout_3, 4, 1, 1, 1) - self.verticalLayout_4 = QtWidgets.QVBoxLayout() - self.verticalLayout_4.setObjectName("verticalLayout_4") - spacerItem7 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Expanding) - self.verticalLayout_4.addItem(spacerItem7) - self.copy_qoute = QtWidgets.QToolButton(parent=self.page) - self.copy_qoute.setObjectName("copy_qoute") + + self.verticalLayout_4 = QVBoxLayout() + self.verticalLayout_4.setObjectName(u"verticalLayout_4") + self.verticalSpacer_7 = QSpacerItem(20, 40, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding) + + self.verticalLayout_4.addItem(self.verticalSpacer_7) + + self.copy_qoute = QToolButton(self.page) + self.copy_qoute.setObjectName(u"copy_qoute") + self.verticalLayout_4.addWidget(self.copy_qoute) - self.file_desc_edit_label = QtWidgets.QLabel(parent=self.page) - self.file_desc_edit_label.setText("") - self.file_desc_edit_label.setObjectName("file_desc_edit_label") + + self.file_desc_edit_label = QLabel(self.page) + self.file_desc_edit_label.setObjectName(u"file_desc_edit_label") + self.verticalLayout_4.addWidget(self.file_desc_edit_label) - spacerItem8 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Expanding) - self.verticalLayout_4.addItem(spacerItem8) + + self.verticalSpacer_6 = QSpacerItem(20, 40, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding) + + self.verticalLayout_4.addItem(self.verticalSpacer_6) + + self.gridLayout_5.addLayout(self.verticalLayout_4, 6, 1, 1, 1) + self.stackedWidget.addWidget(self.page) + self.verticalLayout.addWidget(self.stackedWidget) - self.horizontalLayout = QtWidgets.QHBoxLayout() - self.horizontalLayout.setObjectName("horizontalLayout") - self.buttonBox = QtWidgets.QDialogButtonBox(parent=Dialog) - self.buttonBox.setStandardButtons(QtWidgets.QDialogButtonBox.StandardButton.Cancel|QtWidgets.QDialogButtonBox.StandardButton.Discard|QtWidgets.QDialogButtonBox.StandardButton.Ok) - self.buttonBox.setObjectName("buttonBox") + + self.horizontalLayout = QHBoxLayout() + self.horizontalLayout.setObjectName(u"horizontalLayout") + self.buttonBox = QDialogButtonBox(Dialog) + self.buttonBox.setObjectName(u"buttonBox") + self.buttonBox.setStandardButtons(QDialogButtonBox.Cancel|QDialogButtonBox.Discard|QDialogButtonBox.Ok) + self.horizontalLayout.addWidget(self.buttonBox) - self.retryButton = QtWidgets.QPushButton(parent=Dialog) - self.retryButton.setObjectName("retryButton") + + self.retryButton = QPushButton(Dialog) + self.retryButton.setObjectName(u"retryButton") + self.horizontalLayout.addWidget(self.retryButton) + + self.verticalLayout.addLayout(self.horizontalLayout) + QWidget.setTabOrder(self.btn_mono, self.btn_hg) + QWidget.setTabOrder(self.btn_hg, self.btn_zs) + QWidget.setTabOrder(self.btn_zs, self.searchIdent) + QWidget.setTabOrder(self.searchIdent, self.btn_search) + QWidget.setTabOrder(self.btn_search, self.book_author) + QWidget.setTabOrder(self.book_author, self.book_year) + QWidget.setTabOrder(self.book_year, self.book_edition) + QWidget.setTabOrder(self.book_edition, self.book_title) + QWidget.setTabOrder(self.book_title, self.book_place) + QWidget.setTabOrder(self.book_place, self.book_publisher) + QWidget.setTabOrder(self.book_publisher, self.book_signature) + QWidget.setTabOrder(self.book_signature, self.book_pages) + QWidget.setTabOrder(self.book_pages, self.book_isbn) + QWidget.setTabOrder(self.book_isbn, self.hg_author) + QWidget.setTabOrder(self.hg_author, self.hg_year) + QWidget.setTabOrder(self.hg_year, self.hg_edition) + QWidget.setTabOrder(self.hg_edition, self.hg_chaptertitle) + QWidget.setTabOrder(self.hg_chaptertitle, self.hg_editor) + QWidget.setTabOrder(self.hg_editor, self.hg_title) + QWidget.setTabOrder(self.hg_title, self.hg_place) + QWidget.setTabOrder(self.hg_place, self.hg_publisher) + QWidget.setTabOrder(self.hg_publisher, self.hg_pages) + QWidget.setTabOrder(self.hg_pages, self.hg_signature) + QWidget.setTabOrder(self.hg_signature, self.hg_isbn) + QWidget.setTabOrder(self.hg_isbn, self.zs_author) + QWidget.setTabOrder(self.zs_author, self.zs_year) + QWidget.setTabOrder(self.zs_year, self.zs_issue) + QWidget.setTabOrder(self.zs_issue, self.zs_chapter_title) + QWidget.setTabOrder(self.zs_chapter_title, self.zs_title) + QWidget.setTabOrder(self.zs_title, self.zs_place) + QWidget.setTabOrder(self.zs_place, self.zs_publisher) + QWidget.setTabOrder(self.zs_publisher, self.zs_pages) + QWidget.setTabOrder(self.zs_pages, self.zs_signature) + QWidget.setTabOrder(self.zs_signature, self.zs_isbn) + QWidget.setTabOrder(self.zs_isbn, self.make_quote) + QWidget.setTabOrder(self.make_quote, self.file_desc_edit) + QWidget.setTabOrder(self.file_desc_edit, self.filename_edit) + QWidget.setTabOrder(self.filename_edit, self.ilias_filename) + QWidget.setTabOrder(self.ilias_filename, self.copy_filename) + QWidget.setTabOrder(self.copy_filename, self.copy_ilias_filename) + QWidget.setTabOrder(self.copy_ilias_filename, self.copy_qoute) + QWidget.setTabOrder(self.copy_qoute, self.retryButton) + self.retranslateUi(Dialog) + self.stackedWidget.setCurrentIndex(3) - QtCore.QMetaObject.connectSlotsByName(Dialog) + + + QMetaObject.connectSlotsByName(Dialog) + # setupUi def retranslateUi(self, Dialog): - _translate = QtCore.QCoreApplication.translate - Dialog.setWindowTitle(_translate("Dialog", "Dialog")) - self.groupBox.setTitle(_translate("Dialog", "Medientyp?")) - self.btn_mono.setText(_translate("Dialog", "Monografie")) - self.btn_zs.setText(_translate("Dialog", "Zeitschrift")) - self.btn_hg.setText(_translate("Dialog", "Herausgeberwerk")) - self.label_2.setText(_translate("Dialog", "Identifikator")) - self.btn_search.setText(_translate("Dialog", "Suchen")) - self.make_quote.setToolTip(_translate("Dialog", "Zuerst die Seitenzahl anpassen")) - self.make_quote.setText(_translate("Dialog", "Zitat erstellen")) - self.label.setText(_translate("Dialog", "Autor(en)\n" -" Nachname, Vorname")) - self.book_author.setToolTip(_translate("Dialog", "Bei mehreren Autoren mit ; trennen")) - self.label_3.setText(_translate("Dialog", "Jahr")) - self.label_4.setText(_translate("Dialog", "Auflage")) - self.label_5.setText(_translate("Dialog", "Titel")) - self.label_6.setText(_translate("Dialog", "Ort")) - self.label_7.setText(_translate("Dialog", "Verlag")) - self.label_8.setText(_translate("Dialog", "Signatur")) - self.label_9.setText(_translate("Dialog", "Seiten")) - self.book_pages.setPlaceholderText(_translate("Dialog", "Seitenanzahl des Mediums, zum zitieren ändern!")) - self.label_29.setText(_translate("Dialog", "ISBN")) - self.hg_editor.setToolTip(_translate("Dialog", "Bei mehreren Autoren mit ; trennen")) - self.label_26.setText(_translate("Dialog", "Verlag")) - self.label_20.setText(_translate("Dialog", "Jahr")) - self.label_24.setText(_translate("Dialog", "Beitragstitel")) - self.label_27.setText(_translate("Dialog", "Seiten")) - self.label_28.setText(_translate("Dialog", "Signatur")) - self.label_23.setText(_translate("Dialog", "Titel des Werkes")) - self.label_21.setText(_translate("Dialog", "Auflage")) - self.label_19.setText(_translate("Dialog", "Autor(en)\n" -"Nachname, Vorname")) - self.label_30.setText(_translate("Dialog", "ISBN")) - self.label_25.setText(_translate("Dialog", "Ort")) - self.label_22.setText(_translate("Dialog", "Herausgebername(n)\n" -"Nachname, Vorname")) - self.hg_author.setToolTip(_translate("Dialog", "Bei mehreren Autoren mit ; trennen")) - self.label_10.setText(_translate("Dialog", "Autor(en)\n" -"Nachname, Vorname")) - self.label_14.setText(_translate("Dialog", "Name der Zeitschrift")) - self.label_11.setText(_translate("Dialog", "Jahr")) - self.label_17.setText(_translate("Dialog", "Seiten")) - self.label_16.setText(_translate("Dialog", "Verlag")) - self.label_12.setText(_translate("Dialog", "Heft")) - self.label_31.setText(_translate("Dialog", "ISSN")) - self.label_15.setText(_translate("Dialog", "Ort")) - self.label_13.setText(_translate("Dialog", "Artikeltitel")) - self.label_18.setText(_translate("Dialog", "Signatur")) - self.zs_author.setToolTip(_translate("Dialog", "Bei mehreren Autoren mit ; trennen")) - self.label_32.setText(_translate("Dialog", "Dateiname")) - self.label_34.setText(_translate("Dialog", "ILIAS Name")) - self.label_33.setText(_translate("Dialog", "ILIAS Dateibeschreibung")) - self.copy_filename.setText(_translate("Dialog", "Kopieren")) - self.copy_ilias_filename.setText(_translate("Dialog", "Kopieren")) - self.copy_qoute.setText(_translate("Dialog", "Kopieren")) - self.retryButton.setText(_translate("Dialog", "Wiederholen")) + Dialog.setWindowTitle(QCoreApplication.translate("Dialog", u"Dialog", None)) + self.groupBox.setTitle(QCoreApplication.translate("Dialog", u"Medientyp?", None)) + self.btn_mono.setText(QCoreApplication.translate("Dialog", u"Monografie", None)) + self.btn_zs.setText(QCoreApplication.translate("Dialog", u"Zeitschrift", None)) + self.btn_hg.setText(QCoreApplication.translate("Dialog", u"Herausgeberwerk", None)) + self.label_2.setText(QCoreApplication.translate("Dialog", u"Identifikator", None)) + self.btn_search.setText(QCoreApplication.translate("Dialog", u"Suchen", None)) +#if QT_CONFIG(tooltip) + self.make_quote.setToolTip(QCoreApplication.translate("Dialog", u"Zuerst die Seitenzahl anpassen", None)) +#endif // QT_CONFIG(tooltip) + self.make_quote.setText(QCoreApplication.translate("Dialog", u"Zitat erstellen", None)) + self.label.setText(QCoreApplication.translate("Dialog", u"Autor(en)\n" +" Nachname, Vorname", None)) +#if QT_CONFIG(tooltip) + self.book_author.setToolTip(QCoreApplication.translate("Dialog", u"Bei mehreren Autoren mit ; trennen", None)) +#endif // QT_CONFIG(tooltip) + self.label_3.setText(QCoreApplication.translate("Dialog", u"Jahr", None)) + self.label_4.setText(QCoreApplication.translate("Dialog", u"Auflage", None)) + self.label_5.setText(QCoreApplication.translate("Dialog", u"Titel", None)) + self.label_6.setText(QCoreApplication.translate("Dialog", u"Ort", None)) + self.label_7.setText(QCoreApplication.translate("Dialog", u"Verlag", None)) + self.label_8.setText(QCoreApplication.translate("Dialog", u"Signatur", None)) + self.label_9.setText(QCoreApplication.translate("Dialog", u"Seiten", None)) + self.book_pages.setPlaceholderText(QCoreApplication.translate("Dialog", u"Seitenanzahl des Mediums, zum zitieren \u00e4ndern!", None)) + self.page_warn_2.setText("") + self.label_29.setText(QCoreApplication.translate("Dialog", u"ISBN", None)) +#if QT_CONFIG(tooltip) + self.hg_editor.setToolTip(QCoreApplication.translate("Dialog", u"Bei mehreren Autoren mit ; trennen", None)) +#endif // QT_CONFIG(tooltip) + self.label_26.setText(QCoreApplication.translate("Dialog", u"Verlag", None)) + self.label_20.setText(QCoreApplication.translate("Dialog", u"Jahr", None)) + self.label_24.setText(QCoreApplication.translate("Dialog", u"Beitragstitel", None)) + self.label_27.setText(QCoreApplication.translate("Dialog", u"Seiten", None)) + self.label_28.setText(QCoreApplication.translate("Dialog", u"Signatur", None)) + self.label_23.setText(QCoreApplication.translate("Dialog", u"Titel des Werkes", None)) + self.label_21.setText(QCoreApplication.translate("Dialog", u"Auflage", None)) + self.label_19.setText(QCoreApplication.translate("Dialog", u"Autor(en)\n" +"Nachname, Vorname", None)) + self.label_30.setText(QCoreApplication.translate("Dialog", u"ISBN", None)) + self.label_25.setText(QCoreApplication.translate("Dialog", u"Ort", None)) + self.label_22.setText(QCoreApplication.translate("Dialog", u"Herausgebername(n)\n" +"Nachname, Vorname", None)) +#if QT_CONFIG(tooltip) + self.hg_author.setToolTip(QCoreApplication.translate("Dialog", u"Bei mehreren Autoren mit ; trennen", None)) +#endif // QT_CONFIG(tooltip) + self.page_warn_3.setText("") + self.label_10.setText(QCoreApplication.translate("Dialog", u"Autor(en)\n" +"Nachname, Vorname", None)) + self.label_14.setText(QCoreApplication.translate("Dialog", u"Name der Zeitschrift", None)) + self.label_11.setText(QCoreApplication.translate("Dialog", u"Jahr", None)) + self.label_17.setText(QCoreApplication.translate("Dialog", u"Seiten", None)) + self.label_16.setText(QCoreApplication.translate("Dialog", u"Verlag", None)) + self.label_12.setText(QCoreApplication.translate("Dialog", u"Heft", None)) + self.label_31.setText(QCoreApplication.translate("Dialog", u"ISSN", None)) + self.label_15.setText(QCoreApplication.translate("Dialog", u"Ort", None)) + self.label_13.setText(QCoreApplication.translate("Dialog", u"Artikeltitel", None)) + self.label_18.setText(QCoreApplication.translate("Dialog", u"Signatur", None)) +#if QT_CONFIG(tooltip) + self.zs_author.setToolTip(QCoreApplication.translate("Dialog", u"Bei mehreren Autoren mit ; trennen", None)) +#endif // QT_CONFIG(tooltip) + self.page_warn.setText("") + self.label_32.setText(QCoreApplication.translate("Dialog", u"Dateiname", None)) + self.label_34.setText(QCoreApplication.translate("Dialog", u"ILIAS Name", None)) + self.label_33.setText(QCoreApplication.translate("Dialog", u"ILIAS Dateibeschreibung", None)) + self.copy_filename.setText(QCoreApplication.translate("Dialog", u"Kopieren", None)) + self.filename_edit_label.setText("") + self.copy_ilias_filename.setText(QCoreApplication.translate("Dialog", u"Kopieren", None)) + self.ilias_filename_label.setText("") + self.copy_qoute.setText(QCoreApplication.translate("Dialog", u"Kopieren", None)) + self.file_desc_edit_label.setText("") + self.retryButton.setText(QCoreApplication.translate("Dialog", u"Wiederholen", None)) + # retranslateUi + diff --git a/src/ui/dialogs/elsa_add_entry.py b/src/ui/dialogs/elsa_add_entry.py index db2c0cc..b8879ac 100644 --- a/src/ui/dialogs/elsa_add_entry.py +++ b/src/ui/dialogs/elsa_add_entry.py @@ -3,6 +3,7 @@ from PySide6 import QtWidgets from src import Icon from src.logic.webrequest import BibTextTransformer, WebRequest from src.logic.zotero import ZoteroController +from src.shared.logging import log from src.transformers.transformers import DictToTable from .dialog_sources.elsa_add_table_entry_ui import Ui_Dialog @@ -183,7 +184,7 @@ class ElsaAddEntry(QtWidgets.QDialog, Ui_Dialog): data = web.get_data_elsa() # if isinstance(data, list): # data = data[0] - bib = BibTextTransformer("ARRAY") + bib = BibTextTransformer() bib.get_data(data) data = bib.return_data() self.setdata(data, pages) -- 2.49.1 From a4460ec17b5bb457887b59fe76c4b2a499f21555 Mon Sep 17 00:00:00 2001 From: WorldTeacher Date: Tue, 28 Oct 2025 10:46:45 +0100 Subject: [PATCH 36/40] chore: add log to file --- src/logic/zotero.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/logic/zotero.py b/src/logic/zotero.py index e5847d1..c508799 100644 --- a/src/logic/zotero.py +++ b/src/logic/zotero.py @@ -5,6 +5,7 @@ from pyzotero import zotero from src import settings from src.logic.webrequest import BibTextTransformer, WebRequest +from src.shared.logging import log @dataclass @@ -185,7 +186,7 @@ class ZoteroController: web = WebRequest() web.get_ppn(isbn) data = web.get_data_elsa() - bib = BibTextTransformer("ARRAY") + bib = BibTextTransformer() bib.get_data(data) book = bib.return_data() return book @@ -207,13 +208,14 @@ class ZoteroController: authors = [ Creator().from_string(author).__dict__ for author in book.author.split(";") ] + authors = [author for author in authors if author["lastName"] is not None] bookdata.creators = authors return bookdata def createItem(self, item) -> Optional[str]: resp = self.zot.create_items([item]) # type: ignore if "successful" in resp.keys(): - # #print(resp["successful"]["0"]["key"]) + log.debug(resp) return resp["successful"]["0"]["key"] else: return None @@ -227,6 +229,7 @@ class ZoteroController: break def createHGSection(self, book: Book, data: dict) -> Optional[str]: + log.debug(book) chapter = BookSection() chapter.assign(book) chapter.pages = data["pages"] @@ -244,7 +247,7 @@ class ZoteroController: ] chapter.creators += authors - # #print(chapter.to_dict()) + log.debug(chapter.to_dict()) return self.createItem(chapter.to_dict()) pass -- 2.49.1 From ee62c65ae7ce64759d8eb83fe5a211b03eea3d3e Mon Sep 17 00:00:00 2001 From: WorldTeacher Date: Wed, 29 Oct 2025 09:31:40 +0100 Subject: [PATCH 37/40] chore: restructured project, updated readme --- MIGRATION_GUIDE.md | 216 ++ README.md | 451 +++- pyproject.toml | 1 + src/admin/__init__.py | 5 + src/admin/commands.py | 104 + src/background/__init__.py | 16 + src/background/autoadder.py | 59 + src/background/availability_checker.py | 83 + src/background/book_grabber.py | 199 ++ src/background/documentation_server.py | 23 + src/background/new_editions.py | 345 +++ src/core/__init__.py | 30 + src/core/constants.py | 213 ++ src/core/models.py | 410 ++++ src/core/semester.py | 248 ++ src/database/__init__.py | 5 + src/database/connection.py | 2008 +++++++++++++++++ .../migrations/V001__create_base_tables.sql | 132 ++ .../V002__create_table_webadis_login.sql | 10 + .../V003_update_webadis_add_user_area.sql | 6 + src/database/schemas.py | 112 + src/documents/__init__.py | 2 + src/documents/generators.py | 371 +++ src/parsers/__init__.py | 13 + src/parsers/csv_parser.py | 23 + src/parsers/pdf_parser.py | 23 + src/parsers/transformers/__init__.py | 8 + src/parsers/transformers/schemas.py | 122 + src/parsers/transformers/transformers.py | 522 +++++ src/parsers/word_parser.py | 373 +++ src/parsers/xml_parser.py | 67 + src/services/__init__.py | 16 + src/services/catalogue.py | 292 +++ src/services/lehmanns.py | 312 +++ src/services/openai.py | 58 + src/services/sru.py | 631 ++++++ src/services/webadis.py | 35 + src/services/webrequest.py | 314 +++ src/services/zotero.py | 340 +++ src/shared/__init__.py | 6 + src/shared/config.py | 66 + src/ui/dialogs/Ui_edit_bookdata.py | 2 +- src/ui/dialogs/Ui_fileparser.py | 2 +- src/ui/dialogs/Ui_login.py | 4 +- src/ui/dialogs/bookdata.py | 2 +- src/ui/dialogs/deletedialog.py | 2 +- src/ui/dialogs/docuprint.py | 4 +- src/ui/dialogs/elsa_add_entry.py | 4 +- src/ui/dialogs/fileparser.py | 2 +- src/ui/dialogs/login.py | 4 +- src/ui/dialogs/newEdition.py | 4 +- src/ui/dialogs/parsed_titles.py | 2 +- src/ui/dialogs/progress.py | 6 +- src/ui/userInterface.py | 15 +- src/ui/widgets/MessageCalendar.py | 2 +- src/ui/widgets/admin_create_user.py | 3 +- src/ui/widgets/admin_edit_prof.py | 4 +- src/ui/widgets/admin_edit_user.py | 3 +- src/ui/widgets/admin_query.py | 2 +- src/ui/widgets/calendar_entry.py | 2 +- src/ui/widgets/elsa_main.py | 6 +- src/ui/widgets/graph.py | 4 +- src/ui/widgets/new_edition_check.py | 4 +- src/ui/widgets/searchPage.py | 7 +- src/ui/widgets/signature_update.py | 8 +- src/ui/widgets/welcome_wizard.py | 6 +- src/utils/files.py | 100 + test.py | 44 +- tests/test_migrations_runner.py | 2 +- uv.lock | 98 + 70 files changed, 8518 insertions(+), 100 deletions(-) create mode 100644 MIGRATION_GUIDE.md create mode 100644 src/admin/__init__.py create mode 100644 src/admin/commands.py create mode 100644 src/background/__init__.py create mode 100644 src/background/autoadder.py create mode 100644 src/background/availability_checker.py create mode 100644 src/background/book_grabber.py create mode 100644 src/background/documentation_server.py create mode 100644 src/background/new_editions.py create mode 100644 src/core/__init__.py create mode 100644 src/core/constants.py create mode 100644 src/core/models.py create mode 100644 src/core/semester.py create mode 100644 src/database/__init__.py create mode 100644 src/database/connection.py create mode 100644 src/database/migrations/V001__create_base_tables.sql create mode 100644 src/database/migrations/V002__create_table_webadis_login.sql create mode 100644 src/database/migrations/V003_update_webadis_add_user_area.sql create mode 100644 src/database/schemas.py create mode 100644 src/documents/__init__.py create mode 100644 src/documents/generators.py create mode 100644 src/parsers/__init__.py create mode 100644 src/parsers/csv_parser.py create mode 100644 src/parsers/pdf_parser.py create mode 100644 src/parsers/transformers/__init__.py create mode 100644 src/parsers/transformers/schemas.py create mode 100644 src/parsers/transformers/transformers.py create mode 100644 src/parsers/word_parser.py create mode 100644 src/parsers/xml_parser.py create mode 100644 src/services/__init__.py create mode 100644 src/services/catalogue.py create mode 100644 src/services/lehmanns.py create mode 100644 src/services/openai.py create mode 100644 src/services/sru.py create mode 100644 src/services/webadis.py create mode 100644 src/services/webrequest.py create mode 100644 src/services/zotero.py create mode 100644 src/shared/__init__.py create mode 100644 src/shared/config.py create mode 100644 src/utils/files.py diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md new file mode 100644 index 0000000..565239c --- /dev/null +++ b/MIGRATION_GUIDE.md @@ -0,0 +1,216 @@ +# Migration Guide: New File Structure + +## Overview + +The codebase has been reorganized to improve clarity, maintainability, and separation of concerns. This guide shows how to update your imports. + +## New Structure Summary + +``` +src/ +├── core/ # Domain models & constants (formerly in logic/) +├── database/ # Data persistence (formerly in backend/) +├── services/ # External APIs (from backend/ and logic/) +├── parsers/ # File parsing (formerly in logic/) +├── documents/ # Document generation (formerly in utils/) +├── background/ # Threading tasks (formerly in backend/) +├── admin/ # Admin commands (formerly in backend/) +├── shared/ # Cross-cutting concerns (logging, config) +├── utils/ # Pure utilities +├── ui/ # UI components (unchanged) +└── errors/ # Custom exceptions (unchanged) +``` + +## Import Changes + +### Core Domain Models + +**OLD:** +```python +from src.logic import BookData, Prof, Semester, Apparat +from src.logic.dataclass import BookData, Prof +from src.logic.semester import Semester +from src.logic.constants import APP_NRS, SEMAP_MEDIA_ACCOUNTS +``` + +**NEW:** +```python +from src.core.models import BookData, Prof, Semester, Apparat, ApparatData +from src.core import BookData, Prof, Semester # Can use shorthand +from src.core.semester import Semester +from src.core.constants import APP_NRS, SEMAP_MEDIA_ACCOUNTS +``` + +### Database + +**OLD:** +```python +from src.backend import Database +from src.backend.database import Database +from src.backend.db import CREATE_TABLE_MEDIA +``` + +**NEW:** +```python +from src.database import Database +from src.database.connection import Database # If you need specific module +from src.database.schemas import CREATE_TABLE_MEDIA +``` + +### External Services & APIs + +**OLD:** +```python +from src.backend.catalogue import Catalogue +from src.backend.webadis import get_book_medianr +from src.logic.SRU import SWB +from src.logic.lehmannsapi import LehmannsClient +from src.logic.zotero import ZoteroController +from src.logic.webrequest import BibTextTransformer, WebRequest +``` + +**NEW:** +```python +from src.services import Catalogue, SWB, LehmannsClient, ZoteroController +from src.services.catalogue import Catalogue +from src.services.webadis import get_book_medianr +from src.services.sru import SWB +from src.services.lehmanns import LehmannsClient +from src.services.zotero import ZoteroController +from src.services.webrequest import BibTextTransformer, WebRequest +``` + +### Parsers + +**OLD:** +```python +from src.logic import csv_to_list, word_to_semap +from src.logic.csvparser import csv_to_list +from src.logic.wordparser import word_to_semap +from src.logic.pdfparser import pdf_to_text +from src.logic.xmlparser import xml_to_dict +``` + +**NEW:** +```python +from src.parsers import csv_to_list, word_to_semap # Lazy loading +from src.parsers.csv_parser import csv_to_list +from src.parsers.word_parser import word_to_semap +from src.parsers.pdf_parser import pdf_to_text +from src.parsers.xml_parser import xml_to_dict +``` + +### Document Generation + +**OLD:** +```python +from src.utils.richtext import create_document, create_pdf +``` + +**NEW:** +```python +from src.documents import create_document, create_pdf +from src.documents.generators import create_document, create_pdf +``` + +### Background Tasks + +**OLD:** +```python +from src.backend import AutoAdder, AvailChecker, BookGrabber +from src.backend.threads_autoadder import AutoAdder +from src.backend.threads_availchecker import AvailChecker +from src.backend.thread_bookgrabber import BookGrabber +from src.backend.thread_neweditions import NewEditionCheckerThread +``` + +**NEW:** +```python +from src.background import AutoAdder, AvailChecker, BookGrabber, NewEditionCheckerThread +from src.background.autoadder import AutoAdder +from src.background.availability_checker import AvailChecker +from src.background.book_grabber import BookGrabber +from src.background.new_editions import NewEditionCheckerThread +``` + +### Admin Commands + +**OLD:** +```python +from src.backend import AdminCommands +from src.backend.admin_console import AdminCommands +``` + +**NEW:** +```python +from src.admin import AdminCommands +from src.admin.commands import AdminCommands +``` + +### Configuration & Logging + +**OLD:** +```python +from src.backend.settings import Settings +from src.logic.settings import Settings +from src.shared.logging import log # This stays the same +``` + +**NEW:** +```python +from src.shared import Settings, load_config, log +from src.shared.config import Settings, load_config +from src.shared.logging import log +``` + +## File Renames + +| Old Path | New Path | +|----------|----------| +| `logic/dataclass.py` | `core/models.py` | +| `logic/SRU.py` | `services/sru.py` | +| `logic/lehmannsapi.py` | `services/lehmanns.py` | +| `backend/database.py` | `database/connection.py` | +| `backend/db.py` | `database/schemas.py` | +| `backend/threads_autoadder.py` | `background/autoadder.py` | +| `backend/threads_availchecker.py` | `background/availability_checker.py` | +| `backend/thread_bookgrabber.py` | `background/book_grabber.py` | +| `backend/thread_neweditions.py` | `background/new_editions.py` | +| `backend/admin_console.py` | `admin/commands.py` | +| `utils/richtext.py` | `documents/generators.py` | +| `logic/csvparser.py` | `parsers/csv_parser.py` | +| `logic/pdfparser.py` | `parsers/pdf_parser.py` | +| `logic/wordparser.py` | `parsers/word_parser.py` | +| `logic/xmlparser.py` | `parsers/xml_parser.py` | + +## Quick Migration Checklist + +1. ✅ Update all `from src.backend import Database` → `from src.database import Database` +2. ✅ Update all `from src.logic import BookData` → `from src.core.models import BookData` +3. ✅ Update all `from src.backend.catalogue` → `from src.services.catalogue` +4. ✅ Update all `from src.logic.SRU` → `from src.services.sru` +5. ✅ Update all `from src.backend.admin_console` → `from src.admin` +6. ✅ Update threading imports from `src.backend.thread*` → `src.background.*` + +## Benefits + +- **Clearer architecture**: Each folder has a specific, well-defined purpose +- **Better dependency flow**: core → database/services → background → ui +- **Reduced duplication**: Merged 3 duplicate files (pickles.py, settings.py) +- **Easier navigation**: Intuitive folder names ("services" vs "logic") +- **Scalability**: Clear extension points for new features + +## Backwards Compatibility + +The old `backend/` and `logic/` folders still exist with original files. They will be removed in a future cleanup phase after thorough testing. + +## Questions? + +If you encounter import errors: +1. Check this guide for the new import path +2. Search for the class/function name in the new structure +3. Most moves follow the pattern: external APIs → `services/`, data models → `core/`, threads → `background/` + +## Status + +✅ **Migration Complete** - Application successfully starts and runs with new structure! diff --git a/README.md b/README.md index fc6e6f7..fca23a1 100644 --- a/README.md +++ b/README.md @@ -1,27 +1,438 @@ # SemesterapparatsManager -SemesterapparatsManager is a graphical tool for managing semester apparatuses in the University of Education Freiburg. It allows the users to manage the semester apparatuses in a user-friendly way. It's functions include management of physical and digital semester apparatuses, as well as creating the citations for the digital files of the digital semester apparatuses. For that it uses Zotero, an open source reference management software. The semester apparatuses are stored in a SQLite database, which is created and managed by the SemesterapparatsManager. The SemesterapparatsManager is written in Python and uses the PyQt6 library for the graphical user interface +[![Python](https://img.shields.io/badge/Python-3.10+-blue.svg)](https://www.python.org/downloads/) +[![PySide6](https://img.shields.io/badge/PySide6-Qt6-green.svg)](https://doc.qt.io/qtforpython/) +[![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) +A comprehensive desktop application for managing semester course reserve collections (Semesterapparate) at the University of Education Freiburg. This tool streamlines the workflow of creating, managing, and maintaining both physical and digital course reserves, with integrated citation management powered by Zotero. -## Features -- Manage physical semester apparatuses - - Add semester apparatuses - - Edit semester apparatuses - - Delete semester apparatuses - - Extend semester apparatuses - - Notify professors about semester apparatuses creation or deletion - - Add messages to all semester apparatuses, or an individual semester apparatus -- Manage digital semester apparatuses - - Use text parsing to extract information from the submitted form and create the scans - - if a book is used multiple parts of a book are used, it can be split into the parts - - Create the matching citations for the files -- Statistics and Search - - Search semester apparatuses by various criteria - - Show statistics about the semester apparatuses creation and deletion -- Edit user data +## 📋 Table of Contents +- [Overview](#overview) +- [Features](#features) +- [Architecture](#architecture) +- [Installation](#installation) +- [Usage](#usage) +- [Development](#development) +- [Documentation](#documentation) +- [Contributing](#contributing) +- [License](#license) -## Images +## 🎯 Overview -![Main Window](docs/images/mainUI.png) -![Statistics](docs/images/statistics.png) \ No newline at end of file +SemesterapparatsManager is a Python-based graphical application designed to simplify the complex workflow of academic course reserve management. It provides librarians and staff with tools to: + +- **Manage Physical Reserves**: Track books, media, and materials reserved for courses +- **Handle Digital Collections**: Process, scan, and catalog digital course materials +- **Automate Citations**: Generate proper bibliographic citations using Zotero integration +- **Communicate**: Send automated emails to professors about reserve status +- **Analyze**: View statistics and search through historical data +- **Integrate**: Connect with library catalogs (SWB, DNB) and vendor APIs (Lehmanns) + +### Key Technologies + +- **Framework**: PySide6 (Qt6) for cross-platform GUI +- **Database**: SQLite with migration support +- **APIs**: Integration with SWB, DNB, Zotero, OpenAI, and catalog services +- **Document Processing**: Word, PDF, CSV, and XML parsing +- **Bibliography**: Zotero-based citation management + +## ✨ Features + +### Course Reserve Management + +- **Create & Edit**: Add new semester apparatus with book and media entries +- **Extend Duration**: Extend existing reserves for additional semesters +- **Smart Search**: Find reserves by semester, professor, subject, or signature +- **Availability Checking**: Automated checks against library catalog +- **New Edition Detection**: Background thread to find newer editions of books + +### Digital Collection Features + +- **Document Parsing**: Extract information from submitted Word/PDF forms +- **Smart Splitting**: Automatically split multi-part book sections +- **Citation Generation**: Create proper citations for all digital files +- **ELSA Integration**: Manage electronic semester apparatus (ELSA) workflows +- **File Management**: Track and recreate files from database + +### Communication & Notifications + +- **Email Templates**: Pre-configured templates for common scenarios +- **Professor Notifications**: Automated emails for creation, extension, or dissolution +- **Message System**: Attach messages to specific reserves or broadcast to all + +### Data & Analytics + +- **Statistics Dashboard**: Visualize creation and deletion trends +- **Advanced Search**: Multi-criteria search across all reserves +- **Export**: Generate reports and documentation +- **Calendar View**: Timeline of reserve activities + +### Administration + +- **User Management**: Create, edit, and delete system users +- **Professor Database**: Maintain professor contact information +- **Settings Configuration**: Customize database paths, temp directories, API keys +- **Backup & Migration**: Database migration support for schema updates + +## 🏗️ Architecture + +### Project Structure + +``` +SemesterapparatsManager/ +├── src/ +│ ├── core/ # Domain models & constants +│ │ ├── models.py # BookData, Prof, Apparat, Semester, etc. +│ │ ├── constants.py # Application constants +│ │ └── semester.py # Semester handling logic +│ ├── database/ # Data persistence layer +│ │ ├── connection.py # Database class & operations +│ │ ├── schemas.py # SQL schema definitions +│ │ └── migrations/ # SQL migration files +│ ├── services/ # External API integrations +│ │ ├── catalogue.py # RDS catalog scraping +│ │ ├── sru.py # SWB/DNB library API client +│ │ ├── lehmanns.py # Lehmanns bookstore API +│ │ ├── zotero.py # Zotero integration +│ │ ├── webadis.py # WebADIS automation +│ │ └── openai.py # OpenAI API integration +│ ├── parsers/ # Document & file parsing +│ │ ├── csv_parser.py # CSV parsing +│ │ ├── word_parser.py # Word document parsing +│ │ ├── pdf_parser.py # PDF text extraction +│ │ ├── xml_parser.py # XML parsing +│ │ └── transformers/ # Bibliography format conversion +│ ├── documents/ # Document generation +│ │ └── generators.py # Word/PDF document creation +│ ├── background/ # Background tasks & threading +│ │ ├── autoadder.py # Automatic book addition +│ │ ├── availability_checker.py # Catalog availability +│ │ ├── book_grabber.py # Catalog metadata retrieval +│ │ └── new_editions.py # New edition detection +│ ├── ui/ # User interface components +│ │ ├── userInterface.py # Main application window +│ │ ├── dialogs/ # Dialog windows +│ │ └── widgets/ # Reusable UI widgets +│ ├── admin/ # Administrative functions +│ │ └── commands.py # Admin CLI commands +│ ├── utils/ # Utility functions +│ │ ├── files.py # File operations +│ │ ├── sorting.py # Custom sorting logic +│ │ └── blob.py # Binary data handling +│ ├── shared/ # Cross-cutting concerns +│ │ ├── logging.py # Centralized logging +│ │ └── config.py # Configuration management +│ └── errors/ # Custom exceptions +│ └── database.py # Database-specific errors +├── tests/ # Test suite +├── docs/ # Documentation +├── mail_vorlagen/ # Email templates +├── config.yaml # Application configuration +├── main.py # Application entry point +└── README.md +``` + +### Architecture Principles + +**Layered Architecture**: +``` +UI Layer (PySide6 Qt Widgets) + ↓ +Background Tasks (QThread workers) + ↓ +Business Logic (Core models & operations) + ↓ +Services Layer (External API integrations) + ↓ +Data Access Layer (Database & file operations) +``` + +**Key Design Patterns**: +- **Repository Pattern**: Database class abstracts data persistence +- **Service Layer**: External integrations isolated in `services/` +- **Observer Pattern**: Qt signals/slots for event-driven updates +- **Factory Pattern**: Document and citation generators +- **Strategy Pattern**: Multiple parsing strategies for different file formats + +## 🚀 Installation + +### Prerequisites + +- Python 3.10 or higher +- [uv](https://github.com/astral-sh/uv) - Fast Python package installer and resolver (recommended) + ```bash + # Install uv (Windows PowerShell) + powershell -c "irm https://astral.sh/uv/install.ps1 | iex" + + # Or using pip + pip install uv + ``` + +### Setup Steps (Using uv - Recommended) + +1. **Clone the repository**: + ```bash + git clone https://github.com/yourusername/SemesterapparatsManager.git + cd SemesterapparatsManager + ``` + +2. **Create virtual environment and install dependencies**: + ```bash + # uv automatically creates venv and installs dependencies + uv sync + ``` + +3. **Configure application**: + - First launch will present a setup wizard + - Configure database path, temp directory, and API keys + - Create admin user account + +4. **Run the application**: + ```bash + uv run python main.py + ``` + +### Alternative Setup (Using pip/venv) + +
+Click to expand traditional pip installation steps + +1. **Create virtual environment**: + ```bash + python -m venv .venv + ``` + +2. **Activate virtual environment**: + - Windows (PowerShell): + ```powershell + .venv\Scripts\Activate.ps1 + ``` + - Linux/Mac: + ```bash + source .venv/bin/activate + ``` + +3. **Install dependencies**: + ```bash + pip install -r requirements.txt + ``` + +4. **Run the application**: + ```bash + python main.py + ``` + +
+ +### Building Executable + +To build a standalone executable: + +```bash +# Using uv +uv run pyinstaller --noconfirm --onedir --windowed \ + --icon='icons/app.ico' \ + --name='SemesterapparatsManager' \ + --clean \ + --add-data='config.yaml;.' \ + --add-data='icons;icons' \ + main.py +``` + +Or use the provided build task (see `pyproject.toml`). + +## 📖 Usage + +### First Time Setup + +1. **Launch Application**: Run `python main.py` +2. **Setup Wizard**: Configure basic settings + - Database location + - Temporary files directory + - Library catalog credentials (optional) + - API keys (Zotero, OpenAI - optional) +3. **Create Admin User**: Set up your admin credentials +4. **Login**: Use your credentials to access the main interface + +### Creating a Semester Apparatus + +1. **Navigate**: Main window → "Neuer Apparat" (New Apparatus) +2. **Fill Details**: + - Semester (e.g., WiSe 2024/25) + - Professor information + - Course subject + - Apparatus number +3. **Add Books**: Click "Buch hinzufügen" (Add Book) + - Enter signature or search by title + - System fetches metadata from catalog + - Add multiple books as needed +4. **Add Media**: Click "Medium hinzufügen" (Add Media) + - DVDs, CDs, or other media types +5. **Save**: Confirm and create the apparatus +6. **Generate Email**: Optionally send notification to professor + +### Managing Digital Collections (ELSA) + +1. **Upload Form**: Submit Word/PDF form with book chapter information +### Setting Up Development Environment + +1. **Install all dependencies** (including dev dependencies): + ```bash + # Using uv (recommended) + uv sync --all-extras + + # Or using pip + pip install -r requirements-dev.txt + ``` + +2. **Enable logging**: + ```python + from src.shared.logging import configure + configure("DEBUG") # In main.py + ``` + +3. **Run tests**: + ```bash + # Using uv + uv run pytest tests/ + + # Or with activated venv + pytest tests/ + ```ministrative Tasks + +- **User Management**: Admin → Users → Create/Edit/Delete +- **Professor Database**: Admin → Professors → Manage contacts +- **System Settings**: Edit → Settings → Configure paths and APIs +- **Database Maintenance**: Admin → Database → Run migrations + +## 🛠️ Development + +### Setting Up Development Environment + +1. **Install dev dependencies**: + ```bash + pip install -r requirements-dev.txt + ``` + +2. **Enable logging**: + ```python + from src.shared.logging import configure + configure("DEBUG") # In main.py + ``` + +3. **Run tests**: + ```bash + pytest tests/ + ``` + +### Project Standards + +- **Code Style**: Follow PEP 8 +- **Type Hints**: Use type annotations where possible +- **Docstrings**: Google-style docstrings for all public functions +- **Logging**: Use centralized logger from `src.shared.logging` +- **Imports**: Use new structure (see MIGRATION_GUIDE.md) + +### Database Migrations + +To create a new migration: + +1. Create file: `src/database/migrations/V###__description.sql` +2. Use sequential numbering (V001, V002, etc.) +3. Write idempotent SQL (use `IF NOT EXISTS`) +4. Test migration on copy of production database + +Example: +```sql +-- V003__add_user_preferences.sql +CREATE TABLE IF NOT EXISTS user_preferences ( + user_id INTEGER PRIMARY KEY, + theme TEXT DEFAULT 'light', + language TEXT DEFAULT 'de', + FOREIGN KEY (user_id) REFERENCES user(id) +); +``` + +### Adding New Features + +**New Service Integration**: +1. Create module in `src/services/` +2. Implement client class with proper error handling +3. Add to `src/services/__init__.py` +4. Document API requirements + +**New Document Parser**: +1. Create module in `src/parsers/` +2. Implement parsing function returning core models +3. Add to `src/parsers/__init__.py` +4. Write unit tests + +**New UI Dialog**: +1. Design in Qt Designer (`.ui` file) +2. Convert: `pyside6-uic dialog.ui -o dialog_ui.py` +3. Create dialog class in `src/ui/dialogs/` +4. Connect signals to business logic +### Building Documentation + +```bash +# Using uv +uv run mkdocs build +uv run mkdocs serve # View at http://localhost:8000 + +# Or with activated venv +mkdocs build +mkdocs serve +```*[API Documentation](docs/)**: Detailed module documentation +- **[User Manual](docs/index.md)**: Complete user guide (MkDocs) + +### Building Documentation + +```bash +mkdocs build +mkdocs serve # View at http://localhost:8000 +``` + +## 🤝 Contributing + +Contributions are welcome! Please follow these guidelines: + +1. **Fork** the repository +2. **Create** a feature branch (`git checkout -b feature/amazing-feature`) +3. **Commit** your changes (`git commit -m 'Add amazing feature'`) +4. **Push** to the branch (`git push origin feature/amazing-feature`) +5. **Open** a Pull Request + +### Code Review Checklist + +- [ ] Code follows project style guidelines +- [ ] All tests pass +- [ ] New features have tests +- [ ] Documentation is updated +- [ ] No sensitive data in commits +- [ ] Import paths use new structure + +## 📄 License + +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. + +## 🙏 Acknowledgments + +- **University of Education Freiburg**: Project sponsor and primary user +- **Qt/PySide6**: Excellent cross-platform GUI framework +- **Zotero**: Citation management integration +- **SWB/DNB**: Library catalog services + +## 📞 Support + +For questions, issues, or feature requests: +- **Issues**: [Gitea Issues](https://git.theprivateserver.de/PHB/SemesterapparatsManager/issues) +- **Email**: alexander.kirchner@ph-freiburg.de +- **Documentation**: [Read the Docs](https://semesterapparatsmanager.readthedocs.io) + +## 🗺️ Roadmap + +TBD +--- + +**Built with ❤️ for academic libraries** \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 470172a..536773b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,6 +21,7 @@ dependencies = [ "omegaconf>=2.3.0", "openai>=1.79.0", "pandas>=2.2.3", + "pdfquery>=0.4.3", "playwright>=1.49.1", "pyramid>=2.0.2", "pyside6>=6.9.1", diff --git a/src/admin/__init__.py b/src/admin/__init__.py new file mode 100644 index 0000000..5eee246 --- /dev/null +++ b/src/admin/__init__.py @@ -0,0 +1,5 @@ +"""Administrative functions and commands.""" + +from .commands import AdminCommands + +__all__ = ["AdminCommands"] diff --git a/src/admin/commands.py b/src/admin/commands.py new file mode 100644 index 0000000..bedf8ef --- /dev/null +++ b/src/admin/commands.py @@ -0,0 +1,104 @@ +import hashlib +import random + +from src.database import Database +from src.shared.logging import log +from src import LOG_DIR + + +# change passwords for apparats, change passwords for users, list users, create and delete users etc +# create a class that has all commands. for each command, create a function that does the thing +class AdminCommands: + """Basic Admin commands for the admin console. This class is used to create, delete, and list users. It also has the ability to change passwords for users.""" + + def __init__(self, db_path=None): + """Default Constructor for the AdminCommands class.""" + if db_path is None: + self.db = Database() + else: + self.db = Database(db_path=db_path) + log.info("AdminCommands initialized with database connection.") + log.debug("location: {}", self.db.db_path) + + def create_password(self, password: str) -> tuple[str, str]: + """Create a hashed password and a salt for the password. + + Args: + password (str): the base password to be hashed. + + Returns: + tuple[str,str]: a tuple containing the hashed password and the salt used to hash the password. + """ + salt = self.create_salt() + hashed_password = self.hash_password(password) + return (hashed_password, salt) + + def create_salt(self) -> str: + """Generate a random 16 digit long salt for the password. + + Returns: + str: the randomized salt + """ + return "".join( + random.choices( + "abcdefghijklmnopqrstuvwxyzQWERTZUIOPLKJHGFDSAYXCVBNM0123456789", k=16 + ) + ) + + def create_admin(self): + """Create the admin in the database. This is only used once, when the database is created.""" + salt = self.create_salt() + hashed_password = self.hash_password("admin") + self.db.createUser("admin", salt + hashed_password, "admin", salt) + + def create_user(self, username: str, password: str, role: str = "user") -> bool: + """Create a new user in the database. + + Args: + username (str): the username of the user to be created. + password (str): the password of the user to be created. + role (str, optional): the role of the user to be created. Defaults to "user". + """ + hashed_password, salt = self.create_password(password) + status = self.db.createUser( + user=username, password=salt + hashed_password, role=role, salt=salt + ) + return status + + def hash_password(self, password: str) -> str: + """Hash a password using SHA256. + + Args: + password (str): the password to be hashed. + + Returns: + str: the hashed password. + """ + hashed = hashlib.sha256((password).encode("utf-8")).hexdigest() + return hashed + + def list_users(self) -> list[tuple]: + """List all available users in the database. + + Returns: + list[tuple]: a list of all users, containing all stored data for each user in a tuple. + """ + return self.db.getUsers() + + def delete_user(self, username: str): + """Delete a selected user from the database. + + Args: + username (str): the username of the user to be deleted. + """ + self.db.deleteUser(username) + + def change_password(self, username, password): + """change the password for a user. + + Args: + username (str): username of the user to change the password for. + password (str): the new, non-hashed password to change to. + """ + hashed_password = self.hash_password(password) + self.db.changePassword(username, hashed_password) diff --git a/src/background/__init__.py b/src/background/__init__.py new file mode 100644 index 0000000..08935ad --- /dev/null +++ b/src/background/__init__.py @@ -0,0 +1,16 @@ +"""Background tasks and threading operations.""" + +from .autoadder import AutoAdder +from .availability_checker import AvailChecker +from .book_grabber import BookGrabber, BookGrabberTest +from .new_editions import NewEditionCheckerThread +from .documentation_server import DocumentationThread + +__all__ = [ + "AutoAdder", + "AvailChecker", + "BookGrabber", + "BookGrabberTest", + "NewEditionCheckerThread", + "DocumentationThread", +] diff --git a/src/background/autoadder.py b/src/background/autoadder.py new file mode 100644 index 0000000..d5863b7 --- /dev/null +++ b/src/background/autoadder.py @@ -0,0 +1,59 @@ +import sys +import time + +import loguru + +# from icecream import ic +from PySide6.QtCore import QThread +from PySide6.QtCore import Signal as Signal + +from src import LOG_DIR +from src.database import Database + +log = loguru.logger +log.remove() +log.add(sys.stdout, level="INFO") +log.add(f"{LOG_DIR}/application.log", rotation="1 MB", retention="10 days") + + +# from src.transformers import RDS_AVAIL_DATA + + +class AutoAdder(QThread): + updateSignal = Signal(int) + + setTextSignal = Signal(int) + progress = Signal(int) + + def __init__(self, data=None, app_id=None, prof_id=None, parent=None): + super().__init__(parent) + self.data = data + self.app_id = app_id + self.prof_id = prof_id + + # #print("Launched AutoAdder") + # #print(self.data, self.app_id, self.prof_id) + + def run(self): + self.db = Database() + # show the dialog, start the thread to gather data and dynamically update progressbar and listwidget + log.info("Starting worker thread") + item = 0 + for entry in self.data: + try: + self.updateSignal.emit(item) + self.setTextSignal.emit(entry) + item += 1 + self.progress.emit(item) + time.sleep(1) + + except Exception as e: + # #print(e) + log.exception( + f"The query failed with message {e} for signature {entry}" + ) + continue + if item == len(self.data): + log.info("Worker thread finished") + # teminate thread + self.finished.emit() diff --git a/src/background/availability_checker.py b/src/background/availability_checker.py new file mode 100644 index 0000000..b749a64 --- /dev/null +++ b/src/background/availability_checker.py @@ -0,0 +1,83 @@ +# from icecream import ic +from PySide6.QtCore import QThread +from PySide6.QtCore import Signal as Signal + +from src.database import Database +from src.services.webadis import get_book_medianr +from src.services.webrequest import BibTextTransformer, TransformerType, WebRequest +from src.shared.logging import log + + +class AvailChecker(QThread): + updateSignal = Signal(str, int) + updateProgress = Signal(int, int) + + def __init__( + self, + links: list[str] | None = None, + appnumber: int | None = None, + parent=None, + books: list[dict] | None = None, + ): + if links is None: + links = [] + super().__init__(parent) + log.info("Starting worker thread") + log.info( + "Checking availability for " + + str(links) + + " with appnumber " + + str(appnumber) + + "..." + ) + self.links = links + self.appnumber = appnumber + self.books = books or [] + log.info( + f"Started worker with appnumber: {self.appnumber} and links: {self.links} and {len(self.books)} books..." + ) + # Pre-create reusable request and transformer to avoid per-item overhead + self._request = WebRequest().set_apparat(self.appnumber) + self._rds_transformer = BibTextTransformer(TransformerType.RDS) + + def run(self): + self.db = Database() + state = 0 + count = 0 + for link in self.links: + log.info("Processing entry: " + str(link)) + data = self._request.get_ppn(link).get_data() + rds = self._rds_transformer.get_data(data).return_data("rds_availability") + + book_id = None + if not rds or not rds.items: + log.warning(f"No RDS data found for link {link}") + continue + for item in rds.items: + sign = item.superlocation + loc = item.location + # # #print(item.location) + if str(self.appnumber) in sign or str(self.appnumber) in loc: + state = 1 + break + for book in self.books: + if book["bookdata"].signature == link: + book_id = book["id"] + break + log.info(f"State of {link}: " + str(state)) + # #print("Updating availability of " + str(book_id) + " to " + str(state)) + # use get_book_medianr to update the medianr of the book in the database + auth = self.db.getWebADISAuth + medianr = get_book_medianr(rds.items[0].callnumber, self.appnumber, auth) + book_data = book["bookdata"] + book_data.medianr = medianr + self.db.updateBookdata(book["id"], book_data) + self.db.setAvailability(book_id, state) + count += 1 + self.updateProgress.emit(count, len(self.links)) + self.updateSignal.emit(item.callnumber, state) + + log.info("Worker thread finished") + # teminate thread + + self.quit() diff --git a/src/background/book_grabber.py b/src/background/book_grabber.py new file mode 100644 index 0000000..a134d88 --- /dev/null +++ b/src/background/book_grabber.py @@ -0,0 +1,199 @@ +from PySide6.QtCore import QThread, Signal + +from src.database import Database +from src.services.webrequest import BibTextTransformer, WebRequest +from src.shared.logging import log + +# Logger configured centrally in main; this module just uses `log` + + +class BookGrabber(QThread): + updateSignal = Signal(int, int) + done = Signal() + + def __init__(self): + super(BookGrabber, self).__init__(parent=None) + self.is_Running = True + log.info("Starting worker thread") + self.data = [] + self.app_id = None + self.prof_id = None + self.mode = None + self.book_id = None + self.use_any = False + self.use_exact = False + self.app_nr = None + self.tstate = (self.app_id, self.prof_id, self.mode, self.data) + self.request = WebRequest() + self.db = Database() + + def add_values( + self, app_id: int, prof_id: int, mode: str, data, any_book=False, exact=False + ): + self.app_id = app_id + self.prof_id = prof_id + self.mode = mode + self.data: list[str] = data + self.use_any = any_book + self.use_exact = exact + log.info(f"Working on {len(self.data)} entries") + self.tstate = (self.app_nr, self.prof_id, self.mode, self.data) + log.debug("State: " + str(self.tstate)) + app_nr = self.db.query_db( + "SELECT appnr FROM semesterapparat WHERE id = ?", (self.app_id,) + )[0][0] + self.request.set_apparat(app_nr) + # log.debug(self.tstate) + + def run(self): + item = 0 + iterdata = self.data + # log.debug(iterdata) + + for entry in iterdata: + # log.debug(entry) + log.info("Processing entry: {}", entry) + + webdata = self.request.get_ppn(entry) + if self.use_any: + webdata = webdata.use_any_book + webdata = webdata.get_data() + + if webdata == "error": + continue + + bd = BibTextTransformer(self.mode) + log.debug(webdata) + if self.mode == "ARRAY": + if self.use_exact: + bd = bd.use_signature(entry) + bd = bd.get_data(webdata).return_data() + log.debug(bd) + if bd is None: + # bd = BookData + continue + bd.signature = entry + transformer = ( + BibTextTransformer("RDS").get_data(webdata).return_data("rds_data") + ) + + # confirm lock is acquired + self.db.addBookToDatabase(bd, self.app_id, self.prof_id) + # get latest book id + self.book_id = self.db.getLastBookId() + log.info("Added book to database") + state = 0 + for result in transformer.RDS_DATA: + # log.debug(result.RDS_LOCATION) + if str(self.app_nr) in result.RDS_LOCATION: + state = 1 + break + + log.info(f"State of {entry}: {state}") + log.debug( + "updating availability of " + str(self.book_id) + " to " + str(state) + ) + try: + self.db.setAvailability(self.book_id, state) + log.debug("Added book to database") + except Exception as e: + log.error(f"Failed to update availability: {e}") + log.debug("Failed to update availability: " + str(e)) + + # time.sleep(5) + item += 1 + self.updateSignal.emit(item, len(self.data)) + log.info("Worker thread finished") + # self.done.emit() + self.quit() + + def stop(self): + self.is_Running = False + + +class BookGrabberTest(QThread): + updateSignal = Signal(int, int) + done = Signal() + + def __init__(self, appnr: int): + super(BookGrabberTest, self).__init__(parent=None) + self.is_Running = True + log.info("Starting worker thread") + self.data = None + self.app_nr = None + self.prof_id = None + self.mode = None + self.book_id = None + self.use_any = False + self.use_exact = False + self.app_nr = appnr + self.tstate = (self.app_nr, self.prof_id, self.mode, self.data) + self.results = [] + + def add_values( + self, app_nr: int, prof_id: int, mode: str, data, any_book=False, exact=False + ): + self.app_nr = app_nr + self.prof_id = prof_id + self.mode = mode + self.data = data + self.use_any = any_book + self.use_exact = exact + log.info(f"Working on {len(self.data)} entries") + self.tstate = (self.app_nr, self.prof_id, self.mode, self.data) + log.debug("State: " + str(self.tstate)) + # log.debug(self.tstate) + + def run(self): + item = 0 + iterdata = self.data + # log.debug(iterdata) + for entry in iterdata: + # log.debug(entry) + signature = str(entry) + log.info("Processing entry: " + signature) + + webdata = WebRequest().set_apparat(self.app_nr).get_ppn(entry) + if self.use_any: + webdata = webdata.use_any_book + webdata = webdata.get_data() + + if webdata == "error": + continue + + bd = BibTextTransformer(self.mode) + if self.mode == "ARRAY": + if self.use_exact: + bd = bd.use_signature(entry) + bd = bd.get_data(webdata).return_data() + if bd is None: + # bd = BookData + continue + bd.signature = entry + transformer = ( + BibTextTransformer("RDS").get_data(webdata).return_data("rds_data") + ) + + # confirm lock is acquired + # get latest book id + log.info("Added book to database") + state = 0 + for result in transformer.RDS_DATA: + # log.debug(result.RDS_LOCATION) + if str(self.app_nr) in result.RDS_LOCATION: + state = 1 + break + + log.info(f"State of {signature}: {state}") + # log.debug("updating availability of " + str(self.book_id) + " to " + str(state)) + self.results.append(bd) + + # time.sleep(5) + item += 1 + self.updateSignal.emit(item, len(self.data)) + log.info("Worker thread finished") + # self.done.emit() + self.quit() + + def stop(self): + self.is_Running = False diff --git a/src/background/documentation_server.py b/src/background/documentation_server.py new file mode 100644 index 0000000..57cffcb --- /dev/null +++ b/src/background/documentation_server.py @@ -0,0 +1,23 @@ +from PySide6.QtCore import QThread, Slot +from src.utils.documentation import website, QuietHandler +from wsgiref.simple_server import make_server + + +class DocumentationThread(QThread): + def __init__(self): + super().__init__() + self._server = None # store server so we can shut it down + + def run(self): + # launch_documentation() + self._server = make_server( + "localhost", 8000, website(), handler_class=QuietHandler + ) + while not self.isInterruptionRequested(): + self._server.handle_request() + + @Slot() # slot you can connect to aboutToQuit + def stop(self): + self.requestInterruption() # ask the loop above to exit + if self._server: + self._server.shutdown() # unblock handle_request() \ No newline at end of file diff --git a/src/background/new_editions.py b/src/background/new_editions.py new file mode 100644 index 0000000..05245a4 --- /dev/null +++ b/src/background/new_editions.py @@ -0,0 +1,345 @@ +import os +import re +from concurrent.futures import ThreadPoolExecutor +from math import ceil +from queue import Empty, Queue +from time import monotonic # <-- NEW +from typing import List, Optional + +from PySide6.QtCore import QThread, Signal + +# from src.services.webrequest import BibTextTransformer, WebRequest +from src.services.catalogue import Catalogue +from src.core.models import BookData +from src.services.sru import SWB +from src.shared.logging import log + +# use all available cores - 2, but at least 1 +THREAD_COUNT = max(os.cpu_count() - 2, 1) +THREAD_MIN_ITEMS = 5 + +# Logger configured centrally in main; use shared `log` + +swb = SWB() +dnb = SWB() +cat = Catalogue() + +RVK_ALLOWED = r"[A-Z0-9.\-\/]" # conservative RVK character set + + +def find_newer_edition( + swb_result: BookData, dnb_result: List[BookData] +) -> Optional[List[BookData]]: + """ + New edition if: + - year > swb.year OR + - edition_number > swb.edition_number + BUT: discard any candidate with year < swb.year (if both years are known). + + Same-work check: + - Compare RVK roots of signatures (after stripping trailing '+N' and '(N)'). + - If both have signatures and RVKs differ -> skip. + + Preferences (in order): + 1) RVK matches SWB + 2) Print over Online-Ressource + 3) Has signature + 4) Newer: (year desc, edition_number desc) + """ + + def strip_copy_and_edition(s: str) -> str: + s = re.sub(r"\(\s*\d+\s*\)", "", s) # remove '(N)' + s = re.sub(r"\s*\+\s*\d+\s*$", "", s) # remove trailing '+N' + return s + + def extract_rvk_root(sig: Optional[str]) -> str: + if not sig: + return "" + t = strip_copy_and_edition(sig.upper()) + t = re.sub(r"\s+", " ", t).strip() + m = re.match(rf"^([A-Z]{{1,3}}\s*{RVK_ALLOWED}*)", t) + if not m: + cleaned = re.sub(rf"[^{RVK_ALLOWED} ]+", "", t).strip() + return cleaned.split(" ")[0] if cleaned else "" + return re.sub(r"\s+", " ", m.group(1)).strip() + + def has_sig(b: BookData) -> bool: + return bool(getattr(b, "signature", None)) + + def is_online(b: BookData) -> bool: + return (getattr(b, "media_type", None) or "").strip() == "Online-Ressource" + + def is_print(b: BookData) -> bool: + return not is_online(b) + + def rvk_matches_swb(b: BookData) -> bool: + if not has_sig(b) or not has_sig(swb_result): + return False + return extract_rvk_root(b.signature) == extract_rvk_root(swb_result.signature) + + def strictly_newer(b: BookData) -> bool: + # Hard guard: if both years are known and candidate is older, discard + if ( + b.year is not None + and swb_result.year is not None + and b.year < swb_result.year + ): + return False + + newer_by_year = ( + b.year is not None + and swb_result.year is not None + and b.year > swb_result.year + ) + newer_by_edition = ( + b.edition_number is not None + and swb_result.edition_number is not None + and b.edition_number > swb_result.edition_number + ) + # Thanks to the guard above, newer_by_edition can't pick something with a smaller year. + return newer_by_year or newer_by_edition + + swb_has_sig = has_sig(swb_result) + swb_rvk = extract_rvk_root(getattr(swb_result, "signature", None)) + + # 1) Filter: same work (by RVK if both have sigs) AND strictly newer + candidates: List[BookData] = [] + for b in dnb_result: + if has_sig(b) and swb_has_sig: + if extract_rvk_root(b.signature) != swb_rvk: + continue # different work + if strictly_newer(b): + candidates.append(b) + + if not candidates: + return None + + # 2) Dedupe by PPN → prefer (rvk-match, is-print, has-signature) + def pref_score(x: BookData) -> tuple[int, int, int]: + return ( + 1 if rvk_matches_swb(x) else 0, + 1 if is_print(x) else 0, + 1 if has_sig(x) else 0, + ) + + by_ppn: dict[Optional[str], BookData] = {} + for b in candidates: + key = getattr(b, "ppn", None) + prev = by_ppn.get(key) + if prev is None or pref_score(b) > pref_score(prev): + by_ppn[key] = b + + deduped = list(by_ppn.values()) + if not deduped: + return None + + # 3) Preserve all qualifying newer editions, but order by preference + def sort_key(b: BookData): + year = b.year if b.year is not None else -1 + ed = b.edition_number if b.edition_number is not None else -1 + return ( + 1 if rvk_matches_swb(b) else 0, + 1 if is_print(b) else 0, + 1 if has_sig(b) else 0, + year, + ed, + ) + + deduped.sort(key=sort_key, reverse=True) + return deduped + + +class NewEditionCheckerThread(QThread): + updateSignal = Signal(int, int) # (processed, total) + updateProgress = Signal(int, int) # (processed, total) + total_entries_signal = Signal(int) + resultsSignal = Signal(list) # list[tuple[BookData, list[BookData]]] + + # NEW: metrics signals + rateSignal = Signal(float) # items per second ("it/s") + etaSignal = Signal(int) # seconds remaining (-1 when unknown) + + def __init__(self, entries: Optional[list["BookData"]] = None, parent=None): + super().__init__(parent) + self.entries: list["BookData"] = entries if entries is not None else [] + self.results: list[tuple["BookData", list["BookData"]]] = [] + + def reset(self): + self.entries = [] + self.results = [] + + # ---------- internal helpers ---------- + + @staticmethod + def _split_evenly(items: list, parts: int) -> list[list]: + """Split items as evenly as possible into `parts` chunks (no empty tails).""" + if parts <= 1 or len(items) <= 1: + return [items] + n = len(items) + base = n // parts + extra = n % parts + chunks = [] + i = 0 + for k in range(parts): + size = base + (1 if k < extra else 0) + if size == 0: + continue + chunks.append(items[i : i + size]) + i += size + return chunks + + @staticmethod + def _clean_title(raw: str) -> str: + title = raw.rstrip(" .:,;!?") + title = re.sub(r"\s*\(.*\)", "", title) + return title.strip() + + @classmethod + def _process_book( + cls, book: "BookData" + ) -> tuple["BookData", list["BookData"]] | None: + """Process one book; returns (original, [found editions]) or None on failure.""" + if not book.title: + return None + response: list["BookData"] = [] + query = [ + f"pica.tit={book.title}", + f"pica.vlg={book.publisher}", + ] + + swb_result = swb.getBooks(["pica.bib=20735", f"pica.ppn={book.ppn}"])[0] + dnb_results = swb.getBooks(query) + new_editions = find_newer_edition(swb_result, dnb_results) + + if new_editions is not None: + for new_edition in new_editions: + new_edition.library_location = cat.get_location(new_edition.ppn) + try: + isbn = ( + str(new_edition.isbn[0]) + if isinstance(new_edition.isbn, list) + else str(new_edition.isbn) + ) + new_edition.link = ( + f"https://www.lehmanns.de/search/quick?mediatype_id=2&q={isbn}" + ) + except (IndexError, TypeError): + isbn = None + new_edition.in_library = cat.in_library(new_edition.ppn) + response = new_editions + + # client = SWB() + # response: list["BookData"] = [] + # # First, search by title only + # results = client.getBooks([f"pica.title={title}", f"pica.vlg={book.publisher}"]) + + # lehmanns = LehmannsClient() + # results = lehmanns.search_by_title(title) + # for result in results: + # if "(eBook)" in result.title: + # result.title = result.title.replace("(eBook)", "").strip() + # swb_results = client.getBooks( + # [ + # f"pica.tit={result.title}", + # f"pica.vlg={result.publisher.split(',')[0]}", + # ] + # ) + # for swb in swb_results: + # if swb.isbn == result.isbn: + # result.ppn = swb.ppn + # result.signature = swb.signature + # response.append(result) + # if (result.edition_number < swb.edition_number) and ( + # swb.year > result.year + # ): + # response.append(result) + if response == []: + return None + # Remove duplicates based on ppn + return (book, response) + + @classmethod + def _worker(cls, items: list["BookData"], q: Queue) -> None: + """Worker for one chunk; pushes ('result', ...), ('progress', 1), and ('done', None).""" + try: + for book in items: + try: + result = cls._process_book(book) + except Exception: + result = None + if result is not None: + q.put(("result", result)) + q.put(("progress", 1)) + finally: + q.put(("done", None)) + + # ---------- thread entry point ---------- + + def run(self): + total = len(self.entries) + self.total_entries_signal.emit(total) + + # start timer for metrics + t0 = monotonic() + + if total == 0: + log.debug("No entries to process.") + # emit metrics (zero work) + self.rateSignal.emit(0.0) + self.etaSignal.emit(0) + self.resultsSignal.emit([]) + return + + # Up to 4 workers; ~20 items per worker + num_workers = min(THREAD_COUNT, max(1, ceil(total / THREAD_MIN_ITEMS))) + chunks = self._split_evenly(self.entries, num_workers) + sizes = [len(ch) for ch in chunks] + + q: Queue = Queue() + processed = 0 + finished_workers = 0 + + with ThreadPoolExecutor(max_workers=len(chunks)) as ex: + futures = [ex.submit(self._worker, ch, q) for ch in chunks] + + log.info( + f"Launched {len(futures)} worker thread(s) for {total} entries: {sizes} entries per thread." + ) + for idx, sz in enumerate(sizes, 1): + log.debug(f"Thread {idx}: {sz} entries") + + # Aggregate progress/results + while finished_workers < len(chunks): + try: + kind, payload = q.get(timeout=0.1) + except Empty: + continue + + if kind == "progress": + processed += int(payload) + self.updateSignal.emit(processed, total) + self.updateProgress.emit(processed, total) + + # ---- NEW: compute & emit metrics ---- + elapsed = max(1e-9, monotonic() - t0) + rate = processed / elapsed # items per second + remaining = max(0, total - processed) + eta_sec = int(round(remaining / rate)) if rate > 0 else -1 + + self.rateSignal.emit(rate) + # clamp negative just in case + self.etaSignal.emit(max(0, eta_sec) if eta_sec >= 0 else -1) + # ------------------------------------- + + elif kind == "result": + self.results.append(payload) + elif kind == "done": + finished_workers += 1 + + # Final metrics on completion + elapsed_total = max(1e-9, monotonic() - t0) + final_rate = total / elapsed_total + self.rateSignal.emit(final_rate) + self.etaSignal.emit(0) + + self.resultsSignal.emit(self.results) diff --git a/src/core/__init__.py b/src/core/__init__.py new file mode 100644 index 0000000..15185c3 --- /dev/null +++ b/src/core/__init__.py @@ -0,0 +1,30 @@ +"""Core domain models and business constants.""" + +from .models import ( + Apparat, + ApparatData, + Book, + BookData, + ELSA, + MailData, + Prof, + SemapDocument, + Subjects, + XMLMailSubmission, +) +from .constants import * +from .semester import Semester + +__all__ = [ + "Apparat", + "ApparatData", + "Book", + "BookData", + "ELSA", + "MailData", + "Prof", + "SemapDocument", + "Subjects", + "XMLMailSubmission", + "Semester", +] diff --git a/src/core/constants.py b/src/core/constants.py new file mode 100644 index 0000000..94f0916 --- /dev/null +++ b/src/core/constants.py @@ -0,0 +1,213 @@ +APP_NRS = [i for i in range(1, 181)] + +PROF_TITLES = [ + "Dr. mult.", + "Dr. paed.", + "Dr. rer. pol.", + "Dr. sc. techn.", + "Drs.", + "Dr. agr.", + "Dr. habil.", + "Dr. oec.", + "Dr. med.", + "Dr. e. h.", + "Dr. oec. publ.", + "Dr. -Ing.", + "Dr. theol.", + "Dr. med. vet.", + "Dr. ing.", + "Dr. rer. nat.", + "Dr. des.", + "Dr. sc. mus.", + "Dr. h. c.", + "Dr. pharm.", + "Dr. med. dent.", + "Dr. phil. nat.", + "Dr. phil.", + "Dr. iur.", + "Dr.", + "Kein Titel", +] + +SEMAP_MEDIA_ACCOUNTS = { + 1: "1008000055", + 2: "1008000188", + 3: "1008000211", + 4: "1008000344", + 5: "1008000477", + 6: "1008000500", + 7: "1008000633", + 8: "1008000766", + 9: "1008000899", + 10: "1008000922", + 11: "1008001044", + 12: "1008001177", + 13: "1008001200", + 14: "1008001333", + 15: "1008001466", + 16: "1008001599", + 17: "1008001622", + 18: "1008001755", + 19: "1008001888", + 20: "1008001911", + 21: "1008002033", + 22: "1008002166", + 23: "1008002299", + 24: "1008002322", + 25: "1008002455", + 26: "1008002588", + 27: "1008002611", + 28: "1008002744", + 29: "1008002877", + 30: "1008002900", + 31: "1008003022", + 32: "1008003155", + 33: "1008003288", + 34: "1008003311", + 35: "1008003444", + 36: "1008003577", + 37: "1008003600", + 38: "1008003733", + 39: "1008003866", + 40: "1008003999", + 41: "1008004011", + 42: "1008004144", + 43: "1008004277", + 44: "1008004300", + 45: "1008004433", + 46: "1008004566", + 47: "1008004699", + 48: "1008004722", + 49: "1008004855", + 50: "1008004988", + 51: "1008005000", + 52: "1008005133", + 53: "1008005266", + 54: "1008005399", + 55: "1008005422", + 56: "1008005555", + 57: "1008005688", + 58: "1008005711", + 59: "1008005844", + 60: "1008005977", + 61: "1008006099", + 62: "1008006122", + 63: "1008006255", + 64: "1008006388", + 65: "1008006411", + 66: "1008006544", + 67: "1008006677", + 68: "1008006700", + 69: "1008006833", + 70: "1008006966", + 71: "1008007088", + 72: "1008007111", + 73: "1008007244", + 74: "1008007377", + 75: "1008007400", + 76: "1008007533", + 77: "1008007666", + 78: "1008007799", + 79: "1008007822", + 80: "1008007955", + 81: "1008008077", + 82: "1008008100", + 83: "1008008233", + 84: "1008008366", + 85: "1008008499", + 86: "1008008522", + 87: "1008008655", + 88: "1008008788", + 89: "1008008811", + 90: "1008008944", + 91: "1008009066", + 92: "1008009199", + 93: "1008009222", + 94: "1008009355", + 95: "1008009488", + 96: "1008009511", + 97: "1008009644", + 98: "1008009777", + 99: "1008009800", + 100: "1008009933", + 101: "1008010022", + 102: "1008010155", + 103: "1008010288", + 104: "1008010311", + 105: "1008010444", + 106: "1008010577", + 107: "1008010600", + 108: "1008010733", + 109: "1008010866", + 110: "1008010999", + 111: "1008011011", + 112: "1008011144", + 113: "1008011277", + 114: "1008011300", + 115: "1008011433", + 116: "1008011566", + 117: "1008011699", + 118: "1008011722", + 119: "1008011855", + 120: "1008011988", + 121: "1008012000", + 122: "1008012133", + 123: "1008012266", + 124: "1008012399", + 125: "1008012422", + 126: "1008012555", + 127: "1008012688", + 128: "1008012711", + 129: "1008012844", + 130: "1008012977", + 131: "1008013099", + 132: "1008013122", + 133: "1008013255", + 134: "1008013388", + 135: "1008013411", + 136: "1008013544", + 137: "1008013677", + 138: "1008013700", + 139: "1008013833", + 140: "1008013966", + 141: "1008014088", + 142: "1008014111", + 143: "1008014244", + 144: "1008014377", + 145: "1008014400", + 146: "1008014533", + 147: "1008014666", + 148: "1008014799", + 149: "1008014822", + 150: "1008014955", + 151: "1008015077", + 152: "1008015100", + 153: "1008015233", + 154: "1008015366", + 155: "1008015499", + 156: "1008015522", + 157: "1008015655", + 158: "1008015788", + 159: "1008015811", + 160: "1008015944", + 161: "1008016066", + 162: "1008016199", + 163: "1008016222", + 164: "1008016355", + 165: "1008016488", + 166: "1008016511", + 167: "1008016644", + 168: "1008016777", + 169: "1008016800", + 170: "1008016933", + 171: "1008017055", + 172: "1008017188", + 173: "1008017211", + 174: "1008017344", + 175: "1008017477", + 176: "1008017500", + 177: "1008017633", + 178: "1008017766", + 179: "1008017899", + 180: "1008017922", +} diff --git a/src/core/models.py b/src/core/models.py new file mode 100644 index 0000000..a7d4688 --- /dev/null +++ b/src/core/models.py @@ -0,0 +1,410 @@ +import json +from dataclasses import dataclass, field +from enum import Enum +from typing import Any, Optional, Union + +import regex + +from src.logic.openai import name_tester, run_shortener, semester_converter +from src.logic.semester import Semester + + +@dataclass +class Prof: + id: Optional[int] = None + _title: Optional[str] = None + firstname: Optional[str] = None + lastname: Optional[str] = None + fullname: Optional[str] = None + mail: Optional[str] = None + telnr: Optional[str] = None + + # add function that sets the data based on a dict + def from_dict(self, data: dict[str, Union[str, int]]): + for key, value in data.items(): + if hasattr(self, key): + setattr(self, key, value) + return self + + @property + def title(self) -> str: + if self._title is None or self._title == "None": + return "" + return self._title + + @title.setter + def title(self, value: str): + self._title = value + + # add function that sets the data from a tuple + def from_tuple(self, data: tuple[Union[str, int], ...]) -> "Prof": + setattr(self, "id", data[0]) + setattr(self, "_title", data[1]) + setattr(self, "firstname", data[2]) + setattr(self, "lastname", data[3]) + setattr(self, "fullname", data[4]) + setattr(self, "mail", data[5]) + setattr(self, "telnr", data[6]) + return self + + def name(self, comma: bool = False) -> Optional[str]: + if self.firstname is None and self.lastname is None: + if "," in self.fullname: + self.firstname = self.fullname.split(",")[1].strip() + self.lastname = self.fullname.split(",")[0].strip() + else: + return self.fullname + + if comma: + return f"{self.lastname}, {self.firstname}" + return f"{self.lastname} {self.firstname}" + + +@dataclass +class BookData: + ppn: str | None = None + title: str | None = None + signature: str | None = None + edition: str | None = None + link: str | None = None + isbn: Union[str, list[str], None] = field(default_factory=list) + author: str | None = None + language: Union[str, list[str], None] = field(default_factory=list) + publisher: str | None = None + place: str | None = None + year: int | None = None + pages: str | None = None + library_location: str | None = None + in_apparat: bool | None = False + adis_idn: str | None = None + old_book: Any | None = None + media_type: str | None = None # + in_library: bool | None = None # whether the book is in the library or not + medianr: int | None = None # Media number in the library system + + def __post_init__(self): + self.library_location = ( + str(self.library_location) if self.library_location else None + ) + if isinstance(self.language, list) and self.language: + self.language = [lang.strip() for lang in self.language if lang.strip()] + self.language = ",".join(self.language) + self.year = regex.sub(r"[^\d]", "", str(self.year)) if self.year else None + self.in_library = True if self.signature else False + + def from_dict(self, data: dict) -> "BookData": + for key, value in data.items(): + setattr(self, key, value) + return self + + def merge(self, other: "BookData") -> "BookData": + for key, value in other.__dict__.items(): + # merge lists, if the attribute is a list, extend it + if isinstance(value, list): + current_value = getattr(self, key) + if current_value is None: + current_value = [] + elif not isinstance(current_value, list): + current_value = [current_value] + # extend the list with the new values, but only if they are not already in the list + for v in value: + if v not in current_value: + current_value.append(v) + setattr(self, key, current_value) + if value is not None and ( + getattr(self, key) is None or getattr(self, key) == "" + ): + setattr(self, key, value) + # in language, drop all entries that are longer than 3 characters + if isinstance(self.language, list): + self.language = [lang for lang in self.language if len(lang) <= 4] + return self + + @property + def to_dict(self) -> str: + """Convert the dataclass to a dictionary.""" + data_dict = { + key: value for key, value in self.__dict__.items() if value is not None + } + # remove old_book from data_dict + if "old_book" in data_dict: + del data_dict["old_book"] + return json.dumps(data_dict, ensure_ascii=False) + + def from_dataclass(self, dataclass: Optional[Any]) -> None: + if dataclass is None: + return + for key, value in dataclass.__dict__.items(): + setattr(self, key, value) + + def get_book_type(self) -> str: + if "Online" in self.pages: + return "eBook" + else: + return "Druckausgabe" + + def from_string(self, data: str) -> "BookData": + ndata = json.loads(data) + + return BookData(**ndata) + + def from_LehmannsSearchResult(self, result: Any) -> "BookData": + self.title = result.title + self.author = "; ".join(result.authors) if result.authors else None + self.edition = str(result.edition) if result.edition else None + self.link = result.url + self.isbn = ( + result.isbn13 + if isinstance(result.isbn13, list) + else [result.isbn13] + if result.isbn13 + else [] + ) + self.pages = str(result.pages) if result.pages else None + self.publisher = result.publisher + self.year = str(result.year) if result.year else None + # self.pages = str(result.pages) if result.pages else None + return self + + @property + def edition_number(self) -> Optional[int]: + if self.edition is None: + return 0 + match = regex.search(r"(\d+)", self.edition) + if match: + return int(match.group(1)) + return 0 + + +@dataclass +class MailData: + subject: Optional[str] = None + body: Optional[str] = None + mailto: Optional[str] = None + prof: Optional[str] = None + + +class Subjects(Enum): + BIOLOGY = (1, "Biologie") + CHEMISTRY = (2, "Chemie") + GERMAN = (3, "Deutsch") + ENGLISH = (4, "Englisch") + PEDAGOGY = (5, "Erziehungswissenschaft") + FRENCH = (6, "Französisch") + GEOGRAPHY = (7, "Geographie") + HISTORY = (8, "Geschichte") + HEALTH_EDUCATION = (9, "Gesundheitspädagogik") + HTW = (10, "Haushalt / Textil") + ART = (11, "Kunst") + MATH_IT = (12, "Mathematik / Informatik") + MEDIAPEDAGOGY = (13, "Medien in der Bildung") + MUSIC = (14, "Musik") + PHILOSOPHY = (15, "Philosophie") + PHYSICS = (16, "Physik") + POLITICS = (17, "Politikwissenschaft") + PRORECTORATE = (18, "Prorektorat Lehre und Studium") + PSYCHOLOGY = (19, "Psychologie") + SOCIOLOGY = (20, "Soziologie") + SPORT = (21, "Sport") + TECHNIC = (22, "Technik") + THEOLOGY = (23, "Theologie") + ECONOMICS = (24, "Wirtschaftslehre") + + @property + def id(self) -> int: + return self.value[0] + + @property + def name(self) -> str: + return self.value[1] + + @classmethod + def get_index(cls, name: str) -> Optional[int]: + for i in cls: + if i.name == name: + return i.id - 1 + return None + + +@dataclass +class Apparat: + id: int | None = None + name: str | None = None + prof_id: int | None = None + subject: str | None = None + appnr: int | None = None + created_semester: str | None = None + extended_at: str | None = None + eternal: bool = False + extend_until: str | None = None + deleted: int | None = None + deleted_date: str | None = None + apparat_id_adis: str | None = None + prof_id_adis: str | None = None + konto: int | None = None + + def from_tuple(self, data: tuple[Any, ...]) -> "Apparat": + self.id = data[0] + self.name = data[1] + self.prof_id = data[2] + self.subject = data[3] + self.appnr = data[4] + self.created_semester = data[5] + self.extended_at = data[6] + self.eternal = data[7] + self.extend_until = data[8] + self.deleted = data[9] + self.deleted_date = data[10] + self.apparat_id_adis = data[11] + self.prof_id_adis = data[12] + self.konto = data[13] + return self + + @property + def get_semester(self) -> Optional[str]: + if self.extend_until is not None: + return self.extend_until + else: + return self.created_semester + + +@dataclass +class ELSA: + id: int | None = None + date: str | None = None + semester: str | None = None + prof_id: int | None = None + + def from_tuple(self, data: tuple[Any, ...]) -> "ELSA": + self.id = data[0] + self.date = data[1] + self.semester = data[2] + self.prof_id = data[3] + return self + + +@dataclass +class ApparatData: + prof: Prof = field(default_factory=Prof) + apparat: Apparat = field(default_factory=Apparat) + + +@dataclass +class XMLMailSubmission: + name: Optional[str] = None + lastname: Optional[str] = None + title: Optional[str] = None + telno: Optional[int] = None + email: Optional[str] = None + app_name: Optional[str] = None + subject: Optional[str] = None + semester: Optional[Semester] = None + books: Optional[list[BookData]] = None + + +@dataclass +class Book: + author: str = None + year: str = None + edition: str = None + title: str = None + location: str = None + publisher: str = None + signature: str = None + internal_notes: str = None + + @property + def has_signature(self) -> bool: + return self.signature is not None and self.signature != "" + + @property + def is_empty(self) -> bool: + return all( + [ + self.author == "", + self.year == "", + self.edition == "", + self.title == "", + self.location == "", + self.publisher == "", + self.signature == "", + self.internal_notes == "", + ] + ) + + def from_dict(self, data: dict[str, Any]): + for key, value in data.items(): + value = value.strip() + if value == "\u2002\u2002\u2002\u2002\u2002": + value = "" + + if key == "Autorenname(n):Nachname, Vorname": + self.author = value + elif key == "Jahr/Auflage": + self.year = value.split("/")[0] if "/" in value else value + self.edition = value.split("/")[1] if "/" in value else "" + elif key == "Titel": + self.title = value + elif key == "Ort und Verlag": + self.location = value.split(",")[0] if "," in value else value + self.publisher = value.split(",")[1] if "," in value else "" + elif key == "Standnummer": + self.signature = value.strip() + elif key == "Interne Vermerke": + self.internal_notes = value + + +@dataclass +class SemapDocument: + subject: str = None + phoneNumber: int = None + mail: str = None + title: str = None + title_suggestions: list[str] = None + semester: Union[str, Semester] = None + books: list[Book] = None + eternal: bool = False + personName: str = None + personTitle: str = None + title_length = 0 + title_max_length = 0 + + def __post_init__(self): + self.title_suggestions = [] + + @property + def nameSetter(self): + data = name_tester(self.personTitle) + name = f"{data['last_name']}, {data['first_name']}" + if data["title"] is not None: + title = data["title"] + self.personTitle = title + self.personName = name + self.title_length = len(self.title) + 3 + len(self.personName.split(",")[0]) + if self.title_length > 40: + name_len = len(self.personName.split(",")[0]) + self.title_max_length = 38 - name_len + suggestions = run_shortener(self.title, self.title_max_length) + for suggestion in suggestions: + self.title_suggestions.append(suggestion["shortened_string"]) + else: + self.title_suggestions = [] + pass + + @property + def renameSemester(self) -> None: + if self.semester: + if ", Dauer" in self.semester: + self.semester = self.semester.split(",")[0] + self.eternal = True + self.semester = Semester().from_string(self.semester) + else: + self.semester = Semester().from_string( + semester_converter(self.semester) + ) + + @property + def signatures(self) -> list[str]: + if self.books is not None: + return [book.signature for book in self.books if book.has_signature] + return [] diff --git a/src/core/semester.py b/src/core/semester.py new file mode 100644 index 0000000..08e2b03 --- /dev/null +++ b/src/core/semester.py @@ -0,0 +1,248 @@ +"""Semester helper class + +A small utility around the *German* academic calendar that distinguishes +between *Wintersemester* (WiSe) and *Sommersemester* (SoSe). + +Key points +---------- +* A **`Semester`** is identified by a *term* ("SoSe" or "WiSe") and the last two + digits of the calendar year in which the term *starts*. +* Formatting **never** pads the year with a leading zero – so ``6`` stays ``6``. +* ``offset(n)`` and the static ``generate_missing`` reliably walk the timeline + one semester at a time with correct year transitions: + + SoSe 6 → **WiSe 6/7** → SoSe 7 → WiSe 7/8 → … +""" + +from __future__ import annotations + +import datetime +import re + +from src.shared.logging import log + + +class Semester: + """Represents a German university semester (WiSe or SoSe).""" + + # ------------------------------------------------------------------ + # Class‑level defaults – will be *copied* to each instance and then + # potentially overwritten in ``__init__``. + # ------------------------------------------------------------------ + _year: int | None = int(str(datetime.datetime.now().year)[2:]) # 24 → 24 + _semester: str | None = None # "WiSe" or "SoSe" – set later + _month: int | None = datetime.datetime.now().month + value: str | None = None # Human‑readable label, e.g. "WiSe 23/24" + + # ------------------------------------------------------------------ + # Construction helpers + # ------------------------------------------------------------------ + def __init__( + self, + year: int | None = None, + semester: str | None = None, + month: int | None = None, + ) -> None: + if year is not None: + self._year = int(year) + if semester is not None: + if semester not in ("WiSe", "SoSe"): + raise ValueError("semester must be 'WiSe' or 'SoSe'") + self._semester = semester + if month is not None: + self._month = month + + self.__post_init__() + + def __post_init__(self) -> None: # noqa: D401 – keep original name + if self._year is None: + self._year = int(str(datetime.datetime.now().year)[2:]) + if self._month is None: + self._month = datetime.datetime.now().month + if self._semester is None: + self._generate_semester_from_month() + self._compute_value() + + # ------------------------------------------------------------------ + # Dunder helpers + # ------------------------------------------------------------------ + def __str__(self) -> str: # noqa: D401 – keep original name + return self.value or "" + + def __repr__(self) -> str: # Helpful for debugging lists + return f"Semester({self._year!r}, {self._semester!r})" + + # ------------------------------------------------------------------ + # Internal helpers + # ------------------------------------------------------------------ + def _generate_semester_from_month(self) -> None: + """Infer *WiSe* / *SoSe* from the month attribute.""" + self._semester = "WiSe" if (self._month <= 3 or self._month > 9) else "SoSe" + + def _compute_value(self) -> None: + """Human‑readable semester label – e.g. ``WiSe 23/24`` or ``SoSe 24``.""" + year = self._year + if self._semester == "WiSe": + next_year = (year + 1) % 100 # wrap 99 → 0 + self.value = f"WiSe {year}/{next_year}" + else: # SoSe + self.value = f"SoSe {year}" + + # ------------------------------------------------------------------ + # Public API + # ------------------------------------------------------------------ + def offset(self, value: int) -> "Semester": + """Return a new :class:`Semester` *value* steps away. + + The algorithm maps every semester to a monotonically increasing + *linear index* so that simple addition suffices: + + ``index = year * 2 + (0 if SoSe else 1)``. + """ + if not isinstance(value, int): + raise TypeError("value must be an int (number of semesters to jump)") + if value == 0: + return Semester(self._year, self._semester) + + current_idx = self._year * 2 + (0 if self._semester == "SoSe" else 1) + target_idx = current_idx + value + if target_idx < 0: + raise ValueError("offset would result in a negative year – not supported") + + new_year, semester_bit = divmod(target_idx, 2) + new_semester = "SoSe" if semester_bit == 0 else "WiSe" + return Semester(new_year, new_semester) + + # ------------------------------------------------------------------ + # Comparison helpers + # ------------------------------------------------------------------ + def isPastSemester(self, current: "Semester") -> bool: + log.debug(f"Comparing {self} < {current}") + if self.year < current.year: + return True + if self.year == current.year: + return ( + self.semester == "WiSe" and current.semester == "SoSe" + ) # WiSe before next SoSe + return False + + def isFutureSemester(self, current: "Semester") -> bool: + if self.year > current.year: + return True + if self.year == current.year: + return ( + self.semester == "SoSe" and current.semester == "WiSe" + ) # SoSe after WiSe of same year + return False + + def isMatch(self, other: "Semester") -> bool: + return self.year == other.year and self.semester == other.semester + + # ------------------------------------------------------------------ + # Convenience properties + # ------------------------------------------------------------------ + @property + def next(self) -> "Semester": + return self.offset(1) + + @property + def previous(self) -> "Semester": + return self.offset(-1) + + @property + def year(self) -> int: + return self._year + + @property + def semester(self) -> str: + return self._semester + + # ------------------------------------------------------------------ + # Static helpers + # ------------------------------------------------------------------ + @staticmethod + def generate_missing(start: "Semester", end: "Semester") -> list[str]: + """Return all consecutive semesters from *start* to *end* (inclusive).""" + if not isinstance(start, Semester) or not isinstance(end, Semester): + raise TypeError("start and end must be Semester instances") + if start.isFutureSemester(end) and not start.isMatch(end): + raise ValueError("'start' must not be after 'end'") + + chain: list[Semester] = [start.value] + current = start + while not current.isMatch(end): + current = current.next + chain.append(current.value) + if len(chain) > 1000: # sanity guard + raise RuntimeError("generate_missing exceeded sane iteration limit") + return chain + + # ------------------------------------------------------------------ + # Parsing helper + # ------------------------------------------------------------------ + @classmethod + def from_string(cls, s: str) -> "Semester": + """Parse a human‑readable semester label and return a :class:`Semester`. + + Accepted formats (case‑insensitive):: + + "SoSe " → SoSe of year YY + "WiSe /" → Winter term starting in YY + "WiSe " → Shorthand for the above (next year implied) + + ``YY`` may contain a leading zero ("06" → 6). + """ + if not isinstance(s, str): + raise TypeError("s must be a string") + + pattern = r"\s*(WiSe|SoSe)\s+(\d{1,2})(?:\s*/\s*(\d{1,2}))?\s*" + m = re.fullmatch(pattern, s, flags=re.IGNORECASE) + if not m: + raise ValueError( + "invalid semester string format – expected 'SoSe YY' or 'WiSe YY/YY' (spacing flexible)" + ) + + term_raw, y1_str, y2_str = m.groups() + term = term_raw.capitalize() # normalize case → "WiSe" or "SoSe" + year = int(y1_str.lstrip("0") or "0") # "06" → 6, "0" stays 0 + + if term == "SoSe": + if y2_str is not None: + raise ValueError( + "SoSe string should not contain '/' followed by a second year" + ) + return cls(year, "SoSe") + + # term == "WiSe" + if y2_str is not None: + next_year = int(y2_str.lstrip("0") or "0") + expected_next = (year + 1) % 100 + if next_year != expected_next: + raise ValueError("WiSe second year must equal first year + 1 (mod 100)") + # Accept both explicit "WiSe 6/7" and shorthand "WiSe 6" + return cls(year, "WiSe") + + +# ------------------------- quick self‑test ------------------------- +if __name__ == "__main__": + # Chain generation demo ------------------------------------------------ + s_start = Semester(6, "SoSe") # SoSe 6 + s_end = Semester(25, "WiSe") # WiSe 25/26 + chain = Semester.generate_missing(s_start, s_end) + # print("generate_missing:", [str(s) for s in chain]) + + # Parsing demo --------------------------------------------------------- + examples = [ + "SoSe 6", + "WiSe 6/7", + "WiSe 6", + "SoSe 23", + "WiSe 23/24", + "WiSe 24", + "WiSe 99/00", + "SoSe 00", + "WiSe 100/101", # test large year + ] + for ex in examples: + parsed = Semester.from_string(ex) + print(f"'{ex}' → {parsed} ({parsed.year=}, {parsed.semester=})") diff --git a/src/database/__init__.py b/src/database/__init__.py new file mode 100644 index 0000000..4e811a4 --- /dev/null +++ b/src/database/__init__.py @@ -0,0 +1,5 @@ +"""Database layer for data persistence.""" + +from .connection import Database + +__all__ = ["Database"] diff --git a/src/database/connection.py b/src/database/connection.py new file mode 100644 index 0000000..4111457 --- /dev/null +++ b/src/database/connection.py @@ -0,0 +1,2008 @@ +import datetime +import json +import os +import re +import sqlite3 as sql +import tempfile +from dataclasses import asdict +from pathlib import Path +from string import ascii_lowercase as lower +from string import digits, punctuation +from typing import Any, List, Optional, Tuple, Union + +from src import DATABASE_DIR, settings +from src.database.schemas import ( + CREATE_ELSA_FILES_TABLE, + CREATE_ELSA_MEDIA_TABLE, + CREATE_ELSA_TABLE, + CREATE_TABLE_APPARAT, + CREATE_TABLE_FILES, + CREATE_TABLE_MEDIA, + CREATE_TABLE_MESSAGES, + CREATE_TABLE_NEWEDITIONS, + CREATE_TABLE_PROF, + CREATE_TABLE_SUBJECTS, + CREATE_TABLE_USER, +) +from src.errors import AppPresentError, NoResultError +from src.core.models import ELSA, Apparat, ApparatData, BookData, Prof +from src.core.constants import SEMAP_MEDIA_ACCOUNTS +from src.core.semester import Semester +from src.shared.logging import log +from src.utils.blob import create_blob + +ascii_lowercase = lower + digits + punctuation + + +# get the line that called the function +class Database: + """ + Initialize the database and create the tables if they do not exist. + """ + + def __init__(self, db_path: Union[Path, None] = 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: + if settings.database.path is not None: + self.db_path = Path( + settings.database.path.expanduser(), settings.database.name + ) + else: + self.db_path = None + + # self.db_path = self.db_path.replace("~", str(Path.home())) + else: + self.db_path = db_path + log.debug(f"Database path: {self.db_path}") + self.db_initialized = False + self.startup_check() + + def startup_check(self): + # check existence of all tables. if any is missing, recreate the table + if not self.db_initialized: + self.initializeDatabase() + tables = self.get_db_contents() + tables = [t[1] for t in tables] if tables is not None else [] + required_tables = [ + "semesterapparat", + "messages", + "media", + "files", + "prof", + "user", + "subjects", + "elsa", + "elsa_files", + "elsa_media", + "neweditions", + ] + + for table in required_tables: + if table not in tables: + log.critical(f"Table {table} is missing, recreating...") + self.create_table(table) + + def create_table(self, table_name: str): + match table_name: + case "semesterapparat": + query = CREATE_TABLE_APPARAT + case "messages": + query = CREATE_TABLE_MESSAGES + case "media": + query = CREATE_TABLE_MEDIA + case "files": + query = CREATE_TABLE_FILES + case "prof": + query = CREATE_TABLE_PROF + case "user": + query = CREATE_TABLE_USER + case "subjects": + query = CREATE_TABLE_SUBJECTS + case "elsa": + query = CREATE_ELSA_TABLE + case "elsa_files": + query = CREATE_ELSA_FILES_TABLE + case "elsa_media": + query = CREATE_ELSA_MEDIA_TABLE + case "neweditions": + query = CREATE_TABLE_NEWEDITIONS + case _: + log.error(f"Table {table_name} is not a valid table name") + self.query_db(query) + + def initializeDatabase(self): + if not self.db_initialized: + self.checkDatabaseStatus() + self.db_initialized = True + # run migrations after initial creation to bring schema up-to-date + try: + if self.db_path is not None: + self.run_migrations() + except Exception as e: + log.error(f"Error while running migrations: {e}") + + # --- Migration helpers integrated into Database --- + def _ensure_migrations_table(self, conn: sql.Connection) -> None: + cursor = conn.cursor() + cursor.execute( + """ + CREATE TABLE IF NOT EXISTS schema_migrations ( + id TEXT PRIMARY KEY, + applied_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP + ) + """ + ) + conn.commit() + + def _applied_migrations(self, conn: sql.Connection) -> List[str]: + cursor = conn.cursor() + cursor.execute("SELECT id FROM schema_migrations ORDER BY id") + rows = cursor.fetchall() + return [r[0] for r in rows] + + def _apply_sql_file(self, conn: sql.Connection, path: Path) -> None: + log.info(f"Applying migration {path.name}") + sql_text = path.read_text(encoding="utf-8") + cursor = conn.cursor() + cursor.executescript(sql_text) + cursor.execute( + "INSERT OR REPLACE INTO schema_migrations (id) VALUES (?)", (path.name,) + ) + conn.commit() + + def run_migrations(self) -> None: + """Apply unapplied .sql migrations from src/backend/migrations using this Database's connection.""" + migrations_dir = Path(__file__).parent / "migrations" + if not migrations_dir.exists(): + log.debug("Migrations directory does not exist, skipping migrations") + return + + conn = self.connect() + try: + self._ensure_migrations_table(conn) + applied = set(self._applied_migrations(conn)) + + migration_files = sorted( + [p for p in migrations_dir.iterdir() if p.suffix == ".sql"] + ) + for m in migration_files: + if m.name in applied: + log.debug(f"Skipping already applied migration {m.name}") + continue + self._apply_sql_file(conn, m) + finally: + conn.close() + + # --- end migration helpers --- + + def overwritePath(self, new_db_path: str): + log.debug("got new path, overwriting") + self.db_path = Path(new_db_path) + + def checkDatabaseStatus(self): + path = settings.database.path + if path is None: + path = Path(DATABASE_DIR) + # path = path.replace("~", str(Path.home())) + # path = os.path.abspath(path) + if not os.path.exists(path): + # create path + # log.debug(path) + os.makedirs(path) + if self.get_db_contents() == []: + log.critical("Database does not exist, creating tables") + log.critical(f"Path: {path}") + self.create_tables() + self.insertSubjects() + + def getElsaMediaID(self, work_author: str, signature: str, pages: str): + query = ( + "SELECT id FROM elsa_media WHERE work_author=? AND signature=? AND pages=?" + ) + params = (work_author, signature, pages) + result = self.query_db(query, params, one=True) + if result is None: + return NoResultError( + f"work_author: {work_author}, signature: {signature}, pages: {pages}" + ).__str__() + return result[0] + + def getElsaMediaType(self, id): + query = "SELECT type FROM elsa_media WHERE id=?" + return self.query_db(query, (id,), one=True)[0] + + def get_db_contents(self) -> Union[List[Tuple[Any]], None]: + """ + Get the contents of the + + Returns: + Union[List[Tuple], None]: _description_ + """ + try: + with sql.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute("SELECT * FROM sqlite_master WHERE type='table'") + return cursor.fetchall() + except sql.OperationalError: + return None + + def connect(self) -> sql.Connection: + """ + Connect to the database + + Returns: + sql.Connection: The active connection to the database + """ + conn = sql.connect(self.db_path) + # Fast pragmas suitable for a desktop app DB + conn.execute("PRAGMA journal_mode=WAL;") + conn.execute("PRAGMA synchronous=NORMAL;") + conn.execute("PRAGMA temp_store=MEMORY;") + conn.execute("PRAGMA mmap_size=134217728;") # 128MB + return conn + + 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 create_tables(self): + """ + Create the tables in the database + """ + # Bootstrapping of tables is handled via migrations. Run migrations instead + # of executing the hard-coded DDL here. Migrations are idempotent and + # contain the CREATE TABLE IF NOT EXISTS statements. + self.run_migrations() + + def insertInto(self, query: str, params: Tuple) -> None: + """ + Insert sent data into the database + + Args: + query (str): The query to be executed + params (Tuple): the parameters to be inserted into the database + """ + conn = self.connect() + cursor = conn.cursor() + log.debug(f"Inserting into DB: {query}") + cursor.execute(query, params) + conn.commit() + self.close_connection(conn) + + def getWebADISAuth(self) -> Tuple[str, str]: + """ + Get the WebADIS authentication data from the database + + Returns: + Tuple[str, str]: The username and password for WebADIS + """ + result = self.query_db( + "SELECT username, password FROM webadis_login WHERE effective_range='SAP'", + one=True, + ) + if result is None: + return ("", "") + return (result[0], result[1]) + + @log.catch + def query_db( + self, + query: str, + args: Tuple[Any] = (), # type:ignore + one: bool = False, # type:ignore + ) -> Union[Tuple[Any, Any], List[Tuple[Any, Any]]]: + """ + Query the Database for the sent query. + + Args: + query (str): The query to be executed + args (Tuple, optional): The arguments for the query. Defaults to (). + one (bool, optional): Return the first result only. Defaults to False. + + Returns: + Union[Tuple | List[Tuple]]: Returns the result of the query + """ + conn = self.connect() + cursor = conn.cursor() + logs_query = query + + logs_args = args + # if "fileblob" in query: + # # set fileblob arg in logger to "too long" + # logs_query = query + # fileblob_location = query.find("fileblob") + # # remove fileblob from query + # logs_query = query[:fileblob_location] + "fileblob = too long" + + log_message = f"Querying database with query {logs_query}, args: {logs_args}" + # if "INSERT" in query: + # log_message = f"Querying database with query {query}" + if "INTO user" in query: + log_message = f"Querying database with query {query}" + # log.debug(f"DB Query: {log_message}") + log.debug(log_message) + try: + cursor.execute(query, args) + rv = cursor.fetchall() + conn.commit() + self.close_connection(conn) + except sql.OperationalError as e: + log.error(f"Error in query: {e}") + return None + return (rv[0] if rv else None) if one else rv + + # Books + def addBookToDatabase( + self, bookdata: BookData, app_id: Union[str, int], prof_id: Union[str, int] + ): + """ + Add books to the database. Both app_id and prof_id are required to add the book to the database, as the app_id and prof_id are used to select the books later on. + + Args: + bookdata (BookData): The metadata of the book to be added + app_id (str): The apparat id where the book should be added to + prof_id (str): The id of the professor where the book should be added to. + """ + log.info(f"Adding book {bookdata.signature} to database") + if app_id is None or prof_id is None: + raise ValueError("Apparate ID or Prof ID is None") + conn = self.connect() + cursor = conn.cursor() + t_query = ( + f"SELECT bookdata FROM media WHERE app_id={app_id} AND prof_id={prof_id}" + ) + log.debug(t_query) + # # log.debug(t_query) + result = cursor.execute(t_query).fetchall() + result = [BookData().from_string(i[0]) for i in result] + if bookdata in result: + # log.debug("Bookdata already in database") + # check if the book was deleted in the apparat + query = ( + "SELECT deleted FROM media WHERE app_id=? AND prof_id=? AND bookdata=?" + ) + params = (app_id, prof_id, json.dumps(asdict(bookdata), ensure_ascii=False)) + result = cursor.execute(query, params).fetchone() + if result[0] == 1: + # log.debug("Book was deleted, updating bookdata") + query = "UPDATE media SET deleted=0 WHERE app_id=? AND prof_id=? AND bookdata=?" + params = ( + app_id, + prof_id, + json.dumps(asdict(bookdata), ensure_ascii=False), + ) + cursor.execute(query, params) + conn.commit() + return + + query = ( + "INSERT INTO media (bookdata, app_id, prof_id,deleted) VALUES (?, ?, ?,?)" + ) + converted = json.dumps(asdict(bookdata), ensure_ascii=False) + params = (converted, app_id, prof_id, 0) + cursor.execute(query, params) + logMessage = f"Added book with signature {bookdata.signature} to database, data: {converted}" + log.info(logMessage) + conn.commit() + self.close_connection(conn) + + def getBookIdBasedOnSignature( + self, app_id: Union[str, int], prof_id: Union[str, int], signature: str + ) -> int: + """ + Get a book id based on the signature of the book. + + Args: + app_id (str): The apparat id the book should be associated with + prof_id (str): The professor id the book should be associated with + signature (str): The signature of the book + + Returns: + int: The id of the book + """ + result = self.query_db( + "SELECT bookdata, id FROM media WHERE app_id=? AND prof_id=?", + (app_id, prof_id), + ) + books = [(BookData().from_string(i[0]), i[1]) for i in result] + book = [i for i in books if i[0].signature == signature][0][1] + return book + + def getBookBasedOnSignature( + self, app_id: Union[str, int], prof_id: Union[str, int], signature: str + ) -> BookData: + """ + Get the book based on the signature of the book. + + Args: + app_id (str): The apparat id the book should be associated with + prof_id (str): The professor id the book should be associated with + signature (str): The signature of the book + + Returns: + BookData: The total metadata of the book wrapped in a BookData object + """ + result = self.query_db( + "SELECT bookdata FROM media WHERE app_id=? AND prof_id=?", (app_id, prof_id) + ) + books: list[BookData] = [BookData().from_string(i[0]) for i in result] + book = [i for i in books if i.signature == signature][0] + return book + + def getLastBookId(self) -> int: + """ + Get the last book id in the database + + Returns: + int: ID of the last book in the database + """ + return self.query_db("SELECT id FROM media ORDER BY id DESC", one=True)[0] + + def searchBook( + self, data: dict[str, str] + ) -> Optional[list[tuple["BookData", int, int]]]: + """ + Search a book in the database using regex against signature/title. + + Args: + data: may contain: + - "signature": regex to match against BookData.signature + - "title": regex to match against BookData.title + + Returns: + list of (BookData, app_id, prof_id) tuples, or None if invalid args + """ + + # Determine mode (kept compatible with your original logic) + mode = 0 + if len(data) == 1 and "signature" in data: + mode = 1 + elif len(data) == 1 and "title" in data: + mode = 2 + elif len(data) == 2 and "signature" in data and "title" in data: + mode = 3 + else: + return None + + def _compile(expr: str) -> re.Pattern: + try: + return re.compile(expr, re.IGNORECASE | re.UNICODE) + except re.error: + # If user provided a broken regex, treat it as a literal + return re.compile(re.escape(expr), re.IGNORECASE | re.UNICODE) + + sig_re = _compile(data["signature"]) if mode in (1, 3) else None + title_re = _compile(data["title"]) if mode in (2, 3) else None + + # Fetch candidates once + rows = self.query_db("SELECT * FROM media WHERE deleted=0") + + results: list[tuple["BookData", int, int]] = [] + for row in rows: + bookdata = BookData().from_string( + row[1] + ) # assumes row[1] is the serialized bookdata + app_id = row[2] + prof_id = row[3] + + sig_val = bookdata.signature + title_val = bookdata.title + if mode == 1: + if sig_re.search(sig_val): + results.append((bookdata, app_id, prof_id)) + elif mode == 2: + if title_re.search(title_val): + results.append((bookdata, app_id, prof_id)) + else: # mode == 3 + if sig_re.search(sig_val) and title_re.search(title_val): + results.append((bookdata, app_id, prof_id)) + + return results + + def setAvailability(self, book_id: str, available: str): + """ + Set the availability of a book in the database + + Args: + book_id (str): The id of the book + available (str): The availability of the book + """ + self.query_db("UPDATE media SET available=? WHERE id=?", (available, book_id)) + + def getBookId( + self, bookdata: BookData, app_id: Union[str, int], prof_id: Union[str, int] + ) -> int: + """ + Get the id of a book based on the metadata of the book + + Args: + bookdata (BookData): The wrapped metadata of the book + app_id (str): The apparat id the book should be associated with + prof_id (str): The professor id the book should be associated with + + Returns: + int: ID of the book + """ + result = self.query_db( + "SELECT id FROM media WHERE bookdata=? AND app_id=? AND prof_id=?", + (bookdata.to_dict, app_id, prof_id), + one=True, + ) + return result[0] + + def getBook(self, book_id: int) -> BookData: + """ + Get the book based on the id in the database + + Args: + book_id (int): The id of the book + + Returns: + BookData: The metadata of the book wrapped in a BookData object + """ + return BookData().from_string( + self.query_db( + "SELECT bookdata FROM media WHERE id=?", (book_id,), one=True + )[0] + ) + + def getBooks( + self, app_id: Union[str, int], prof_id: Union[str, int], deleted: int = 0 + ) -> list[dict[str, Union[BookData, int]]]: + """ + Get the Books based on the apparat id and the professor id + + Args: + app_id (str): The ID of the apparat + prof_id (str): The ID of the professor + deleted (int, optional): The state of the book. Set to 1 to include deleted ones. Defaults to 0. + + Returns: + + list[dict[int, BookData, int]]: A list of dictionaries containing the id, the metadata of the book and the availability of the book + """ + qdata = self.query_db( + f"SELECT id,bookdata,available FROM media WHERE (app_id={app_id} AND prof_id={prof_id}) AND (deleted={deleted if deleted == 0 else '1 OR deleted=0'})" + ) + ret_result = [] + if qdata is None: + return [] + for result_a in qdata: + data: dict[str, Any] = {"id": int, "bookdata": BookData, "available": int} + data["id"] = result_a[0] + data["bookdata"] = BookData().from_string(result_a[1]) + data["available"] = result_a[2] + ret_result.append(data) + return ret_result + + def getAllBooks(self) -> list[dict[str, Union[int, BookData]]]: + """ + Get all books in the database that are not set as deleted + + Returns + ------- + list[dict[str, Union[int, BookData]]] + A list of dictionaries containing the id and the metadata of the book + """ + # return all books in the database + qdata = self.query_db("SELECT id,bookdata FROM media WHERE deleted=0") + ret_result: list[dict[str, Any]] = [] + if qdata is None: + return [] + for result_a in qdata: + data: dict[str, Any] = {"id": int, "bookdata": BookData} + data["id"] = result_a[0] + data["bookdata"] = BookData().from_string(result_a[1]) + + ret_result.append(data) + return ret_result + + def getApparatNrByBookId(self, book_id): + appNr = self.query_db( + "SELECT appnr FROM semesterapparat WHERE id IN (SELECT app_id FROM media WHERE id=?)", + (book_id,), + one=True, + ) + return appNr[0] if appNr else None + + def getBooksByProfId( + self, prof_id: int, deleted: int = 0 + ) -> list[dict[str, Union[int, BookData]]]: + """ + Get the Books based on the professor id + + Parameters + ---------- + prof_id : int + The ID of the professor + deleted : int, optional + If set to 1, it will include deleted books, by default 0 + + Returns + ------- + list[dict[str, Union[int, BookData]]] + A list of dictionaries containing the id, the metadata of the book and the availability of the book + """ + qdata = self.query_db( + f"SELECT id,bookdata,available FROM media WHERE prof_id={prof_id} AND (deleted={deleted if deleted == 0 else '1 OR deleted=0'})" + ) + ret_result = [] + if qdata is None: + return [] + for result_a in qdata: + data: dict[str, Any] = {"id": int, "bookdata": BookData, "available": int} + data["id"] = result_a[0] + data["bookdata"] = BookData().from_string(result_a[1]) + data["available"] = result_a[2] + ret_result.append(data) + return ret_result + + def updateBookdata(self, book_id: int, bookdata: BookData): + """ + Update the bookdata in the database + + Args: + book_id (str): The id of the book + bookdata (BookData): The new metadata of the book + """ + query = "UPDATE media SET bookdata= ? WHERE id=?" + book = bookdata.to_dict + self.query_db(query, (book, book_id)) + + def deleteBook(self, book_id: int): + """ + Delete a book from the database + + Args: + book_id (str): ID of the book + """ + self.query_db("UPDATE media SET deleted=1 WHERE id=?", (book_id,)) + + def deleteBooks(self, ids: list[int]): + """ + Delete multiple books from the database + + Args: + ids (list[int]): A list of book ids to be deleted + """ + query = f"UPDATE media SET deleted=1 WHERE id IN ({','.join(['?'] * len(ids))})" + self.query_db(query, tuple(ids)) + + # File Interactions + def getBlob(self, filename: str, app_id: Union[str, int]) -> bytes: + """ + Get a blob from the database + + Args: + filename (str): The name of the file + app_id (str): ID of the apparat + + Returns: + bytes: The file stored in + """ + return self.query_db( + "SELECT fileblob FROM files WHERE filename=? AND app_id=?", + (filename, app_id), + one=True, + )[0] + + def insertFile( + self, file: list[dict], app_id: Union[str, int], prof_id: Union[str, int] + ): + """Instert a list of files into the database + + Args: + file (list[dict]): a list containing all the files to be inserted + Structured: [{"name": "filename", "path": "path", "type": "filetype"}] + app_id (int): the id of the apparat + prof_id (str): the id of the professor + """ + for f in file: + filename = f["name"] + path = f["path"] + filetyp = f["type"] + if path == "Database": + continue + blob = create_blob(path) + query = "INSERT OR IGNORE INTO files (filename, fileblob, app_id, filetyp,prof_id) VALUES (?, ?, ?, ?,?)" + self.query_db(query, (filename, blob, app_id, filetyp, prof_id)) + + def recreateFile( + self, filename: str, app_id: Union[str, int], filetype: str + ) -> str: + """Recreate a file from the database + + Args: + filename (str): the name of the file + app_id (Union[str,int]): the id of the apparat + filetype (str): the extension of the file to be created + + Returns: + str: The filename of the recreated file + """ + blob = self.getBlob(filename, app_id) + log.debug(blob) + tempdir = settings.database.temp.expanduser() + if not tempdir.exists(): + tempdir.mkdir(parents=True, exist_ok=True) + file = tempfile.NamedTemporaryFile( + delete=False, dir=tempdir, mode="wb", suffix=f".{filetype}" + ) + file.write(blob) + # log.debug("file created") + return file.name + + def getFiles(self, app_id: Union[str, int], prof_id: int) -> list[tuple]: + """Get all the files associated with the apparat and the professor + + Args: + app_id (Union[str,int]): The id of the apparat + prof_id (Union[str,int]): the id of the professor + + Returns: + list[tuple]: a list of tuples containing the filename and the filetype for the corresponding apparat and professor + """ + return self.query_db( + "SELECT filename, filetyp FROM files WHERE app_id=? AND prof_id=?", + (app_id, prof_id), + ) + + def getSemesters(self) -> list[str]: + """Return all the unique semesters in the database + + Returns: + list: a list of strings containing the semesters + """ + data = self.query_db("SELECT DISTINCT erstellsemester FROM semesterapparat") + return [i[0] for i in data] + + def insertSubjects(self): + # log.debug("Inserting subjects") + subjects = [ + "Biologie", + "Chemie", + "Deutsch", + "Englisch", + "Erziehungswissenschaft", + "Französisch", + "Geographie", + "Geschichte", + "Gesundheitspädagogik", + "Haushalt / Textil", + "Kunst", + "Mathematik / Informatik", + "Medien in der Bildung", + "Musik", + "Philosophie", + "Physik", + "Politikwissenschaft", + "Prorektorat Lehre und Studium", + "Psychologie", + "Soziologie", + "Sport", + "Technik", + "Theologie", + "Wirtschaftslehre", + ] + conn = self.connect() + cursor = conn.cursor() + for subject in subjects: + cursor.execute("INSERT INTO subjects (name) VALUES (?)", (subject,)) + conn.commit() + self.close_connection(conn) + + def getSubjects(self): + """Get all the subjects in the database + + Returns: + list[tuple]: a list of tuples containing the subjects + """ + return self.query_db("SELECT * FROM subjects") + + # Messages + def addMessage( + self, messages: list[dict[str, Any]], user: str, app_id: Union[str, int] + ): + """add a Message to the database + + Args: + messages (list[dict[str, Any]]): the messages to be added + user (str): the user who added the messages + app_id (Union[str,int]): the id of the apparat + """ + + def __getUserId(user: str): + return self.query_db( + "SELECT id FROM user WHERE username=?", (user,), one=True + )[0] + + user_id = __getUserId(user) + for message in messages: + self.query_db( + "INSERT INTO messages (message, user_id, remind_at,appnr) VALUES (?,?,?,?)", + (message["message"], user_id, message["remind_at"], app_id), + ) + + def getAllMessages(self) -> list[dict[str, str, str, str]]: + """Get all the messages in the database + + Returns: + list[dict[str, str, str, str]]: a list of dictionaries containing the message, the user who added the message, the apparat id and the id of the message + """ + + def __get_user_name(user_id: int): + return self.query_db( + "SELECT username FROM user WHERE id=?", (user_id,), one=True + )[0] + + messages = self.query_db("SELECT * FROM messages") + ret = [ + { + "message": i[2], + "user": __get_user_name(i[4]), + "appnr": i[5], + "id": i[0], + "remind_at": i[3], + } + for i in messages + ] + return ret + + def getMessages(self, date: str) -> list[dict[str, str]]: + """Get all the messages for a specific date + + Args: + date (str): a date.datetime object formatted as a string in the format "YYYY-MM-DD" + + Returns: + list[dict[str, str]]: a list of dictionaries containing the message, the user who added the message, the apparat id and the id of the message + """ + + def __get_user_name(user_id: int): + return self.query_db( + "SELECT username FROM user WHERE id=?", (user_id,), one=True + )[0] + + messages = self.query_db("SELECT * FROM messages WHERE remind_at=?", (date,)) + ret = [ + {"message": i[2], "user": __get_user_name(i[4]), "appnr": i[5], "id": i[0]} + for i in messages + ] + return ret + + def deleteMessage(self, message_id: int): + """Delete a message from the database + + Args: + message_id (str): the id of the message + """ + log.debug(f"Deleting message with id {message_id}") + self.query_db("DELETE FROM messages WHERE id=?", (message_id,)) + + # Prof data + def getProfNameById(self, prof_id: Union[str, int], add_title: bool = False) -> str: + """Get a professor name based on the id + + Args: + prof_id (Union[str,int]): The id of the professor + add_title (bool, optional): wether to add the title or no. Defaults to False. + + Returns: + str: The name of the professor + """ + prof = self.query_db( + "SELECT fullname FROM prof WHERE id=?", (prof_id,), one=True + ) + if add_title: + return f"{self.getTitleById(prof_id)}{prof[0]}" + else: + return prof[0] + + def getProfMailById(self, prof_id: Union[str, int]) -> str: + """get the mail of a professor based on the id + + Args: + prof_id (Union[str,int]): the id of the professor + + Returns: + str: the mail of the professor + """ + mail = self.query_db("SELECT mail FROM prof WHERE id=?", (prof_id,), one=True)[ + 0 + ] + return mail if mail is not None else "" + + def getTitleById(self, prof_id: Union[str, int]) -> str: + """get the title of a professor based on the id + + Args: + prof_id (Union[str,int]): the id of the professor + + Returns: + str: the title of the professor, with an added whitespace at the end, if no title is present, an empty string is returned + """ + title = self.query_db( + "SELECT titel FROM prof WHERE id=?", (prof_id,), one=True + )[0] + return f"{title} " if title is not None else "" + + def getSpecificProfData( + self, prof_id: Union[str, int], fields: List[str] + ) -> tuple[Any, ...]: + """A customisable function to get specific data of a professor based on the id + + Args: + prof_id (Union[str,int]): the id of the professor + fields (List[str]): a list of fields to be returned + + Returns: + tuple: a tuple containing the requested data + """ + query = "SELECT " + for field in fields: + query += f"{field}," + query = query[:-1] + query += " FROM prof WHERE id=?" + return self.query_db(query, (prof_id,), one=True)[0] + + def getProfById(self, prof_id: Union[str, int]) -> Prof: + """Get a professor based on the id + + Args: + prof_id (Union[str,int]): the id of the professor + + Returns: + Prof: a Prof object containing the data of the professor + """ + data = self.query_db("SELECT * FROM prof WHERE id=?", (prof_id,), one=True) + return Prof().from_tuple(data) + + def getProfData(self, profname: str): + """Get mail, telephone number and title of a professor based on the name + + Args: + profname (str): name of the professor + + Returns: + tuple: the mail, telephone number and title of the professor + """ + data = self.query_db( + "SELECT * FROM prof WHERE fullname=?", + (profname.replace(",", ""),), + one=True, + ) + person = Prof() + return person.from_tuple(data) + + def getProf(self, id) -> Prof: + """Get a professor based on the id + + Args: + id ([type]): the id of the professor + + Returns: + Prof: a Prof object containing the data of the professor + """ + data = self.query_db("SELECT * FROM prof WHERE id=?", (id,), one=True) + return Prof().from_tuple(data) + + def getProfs(self) -> list[Prof]: + """Return all the professors in the database + + Returns: + list[tuple]: a list containing all the professors in individual tuples + tuple: (id, titel, fname, lname, fullname, mail, telnr) + """ + profs = self.query_db("SELECT * FROM prof") + return [Prof().from_tuple(prof) for prof in profs] + + # Apparat + def getAllAparats(self, deleted: int = 0) -> list[Apparat]: + """Get all the apparats in the database + + Args: + deleted (int, optional): Switch the result to use . Defaults to 0. + + Returns: + list[tuple]: a list of tuples containing the apparats + """ + apparats = self.query_db( + "SELECT * FROM semesterapparat WHERE deletion_status=?", (deleted,) + ) + ret: list[Apparat] = [] + for apparat in apparats: + ret.append(Apparat().from_tuple(apparat)) + return ret + + def getApparatData(self, appnr, appname) -> ApparatData: + """Get the Apparat data based on the apparat number and the name + + Args: + appnr (str): the apparat number + appname (str): the name of the apparat + + Raises: + NoResultError: an error is raised if no result is found + + Returns: + ApparatData: the appended data of the apparat wrapped in an ApparatData object + """ + result = self.query_db( + "SELECT * FROM semesterapparat WHERE appnr=? AND name=?", + (appnr, appname), + one=True, + ) + if result is None: + raise NoResultError("No result found") + apparat = ApparatData() + apparat.apparat.id = result[0] + apparat.apparat.name = result[1] + apparat.apparat.appnr = result[4] + apparat.apparat.eternal = True if result[7] == 1 else False + apparat.prof = self.getProfData(self.getProfNameById(result[2])) + apparat.prof.fullname = self.getProfNameById(result[2]) + apparat.apparat.prof_id = result[2] + + apparat.apparat.subject = result[3] + apparat.apparat.created_semester = result[5] + apparat.apparat.extend_until = result[8] + apparat.apparat.deleted = result[9] + apparat.apparat.apparat_id_adis = result[11] + apparat.apparat.prof_id_adis = result[12] + apparat.apparat.konto = result[13] + return apparat + + def getUnavailableApparatNumbers(self) -> List[int]: + """Get a list of all the apparat numbers in the database that are currently in use + + Returns: + List[int]: the list of used apparat numbers + """ + numbers = self.query_db( + "SELECT appnr FROM semesterapparat WHERE deletion_status=0" + ) + numbers = [i[0] for i in numbers] + numbers.sort() + log.info(f"Currently used apparat numbers: {numbers}") + return numbers + + def setNewSemesterDate(self, app_id: Union[str, int], newDate, dauerapp=False): + """Set the new semester date for an apparat + + Args: + app_id (Union[str,int]): the id of the apparat + newDate (str): the new date + dauerapp (bool, optional): if the apparat was changed to dauerapparat. Defaults to False. + """ + # today as yyyy-mm-dd + today = datetime.datetime.now().strftime("%Y-%m-%d") + + if dauerapp: + self.query_db( + "UPDATE semesterapparat SET verlängerung_bis=?, dauer=?, verlängert_am=? WHERE appnr=?", + (newDate, dauerapp, today, app_id), + ) + else: + self.query_db( + "UPDATE semesterapparat SET verlängerung_bis=?, verlängert_am=? WHERE appnr=?", + (newDate, today, app_id), + ) + + def getId(self, apparat_name) -> Optional[int]: + """get the id of an apparat based on the name + + Args: + apparat_name (str): the name of the apparat e.g. "Semesterapparat 1" + + Returns: + Optional[int]: the id of the apparat, if the apparat is not found, None is returned + """ + data = self.query_db( + "SELECT id FROM semesterapparat WHERE name=?", (apparat_name,), one=True + ) + if data is None: + return None + else: + return data[0] + + def getApparatId(self, apparat_name) -> Optional[int]: + """get the id of an apparat based on the name + + Args: + apparat_name (str): the name of the apparat e.g. "Semesterapparat 1" + + Returns: + Optional[int]: the id of the apparat, if the apparat is not found, None is returned + """ + data = self.query_db( + "SELECT appnr FROM semesterapparat WHERE name=?", (apparat_name,), one=True + ) + if data is None: + return None + else: + return data[0] + + def createApparat(self, apparat: ApparatData) -> int: + """create the apparat in the database + + Args: + apparat (ApparatData): the wrapped metadata of the apparat + + Raises: + AppPresentError: an error describing that the apparats chosen id is already present in the database + + Returns: + Optional[int]: the id of the apparat + """ + log.debug(apparat) + app = apparat.apparat + prof = apparat.prof + present_prof = self.getProfByName(prof.name()) + prof_id = present_prof.id + log.debug(present_prof) + + app_id = self.getApparatId(app.name) + if app_id: + return AppPresentError(app_id) + if not prof_id: + log.debug("prof id not present, creating prof with data", prof) + prof_id = self.createProf(prof) + log.debug(prof_id) + query = f"INSERT OR IGNORE INTO semesterapparat (appnr, name, erstellsemester, dauer, prof_id, fach,deletion_status,konto) VALUES ('{app.appnr}', '{app.name}', '{app.created_semester}', '{app.eternal}', {prof_id}, '{app.subject}', '{0}', '{SEMAP_MEDIA_ACCOUNTS[app.appnr]}')" + log.debug(query) + self.query_db(query) + return None + + def getApparatsByProf(self, prof_id: Union[str, int]) -> list[Apparat]: + """Get all apparats based on the professor id + + Args: + prof_id (Union[str,int]): the id of the professor + + Returns: + list[tuple]: a list of tuples containing the apparats + """ + data = self.query_db( + "SELECT * FROM semesterapparat WHERE prof_id=?", (prof_id,) + ) + ret = [] + for i in data: + log.debug(i) + ret.append(Apparat().from_tuple(i)) + return ret + + def getApparatsBySemester(self, semester: str) -> dict[list]: + """get all apparats based on the semester + + Args: + semester (str): the selected semester + + Returns: + dict[list]: a list off all created and deleted apparats for the selected semester + """ + data = self.query_db( + "SELECT name, prof_id FROM semesterapparat WHERE erstellsemester=?", + (semester,), + ) + conn = self.connect() + cursor = conn.cursor() + c_tmp = [] + for i in data: + c_tmp.append((i[0], self.getProfNameById(i[1]))) + query = ( + f"SELECT name,prof_id FROM semesterapparat WHERE deleted_date='{semester}'" + ) + result = cursor.execute(query).fetchall() + d_tmp = [] + for i in result: + d_tmp.append((i[0], self.getProfNameById(i[1]))) + # group the apparats by prof + c_ret = {} + for i in c_tmp: + if i[1] not in c_ret.keys(): + c_ret[i[1]] = [i[0]] + else: + c_ret[i[1]].append(i[0]) + d_ret = {} + for i in d_tmp: + if i[1] not in d_ret.keys(): + d_ret[i[1]] = [i[0]] + else: + d_ret[i[1]].append(i[0]) + self.close_connection(conn) + return {"created": c_ret, "deleted": d_ret} + + def getApparatCountBySemester(self) -> tuple[list[str], list[int]]: + """get a list of all apparats created and deleted by semester + + Returns: + tuple[list[str],list[int]]: a tuple containing two lists, the first list contains the semesters, the second list contains the amount of apparats created and deleted for the corresponding semester + """ + conn = self.connect() + cursor = conn.cursor() + semesters = self.getSemesters() + created = [] + deleted = [] + for semester in semesters: + query = f"SELECT COUNT(*) FROM semesterapparat WHERE erstellsemester='{semester}'" + result = cursor.execute(query).fetchone() + created.append(result[0]) + query = f"SELECT COUNT(*) FROM semesterapparat WHERE deletion_status=1 AND deleted_date='{semester}'" + result = cursor.execute(query).fetchone() + deleted.append(result[0]) + # store data in a tuple + ret = [] + for sem in semesters: + e_tuple = ( + sem, + created[semesters.index(sem)], + deleted[semesters.index(sem)], + ) + ret.append(e_tuple) + self.close_connection(conn) + return ret + + def deleteApparat(self, apparat: Apparat, semester: str): + """Delete an apparat from the database + + Args: + apparat: (Apparat): the apparat to be deleted + semester (str): the semester the apparat should be deleted from + """ + apparat_nr = apparat.appnr + app_id = self.getId(apparat.name) + self.query_db( + "UPDATE semesterapparat SET deletion_status=1, deleted_date=? WHERE appnr=? AND name=?", + (semester, apparat_nr, apparat.name), + ) + # delete all books associated with the app_id + # print(apparat_nr, app_id) + self.query_db("UPDATE media SET deleted=1 WHERE app_id=?", (app_id,)) + + def isEternal(self, id): + """check if the apparat is eternal (dauerapparat) + + Args: + id (int): the id of the apparat to be checked + + Returns: + int: the state of the apparat + """ + return self.query_db( + "SELECT dauer FROM semesterapparat WHERE appnr=?", (id,), one=True + ) + + def getApparatName(self, app_id: Union[str, int], prof_id: Union[str, int]): + """get the name of the apparat based on the id + + Args: + app_id (Union[str,int]): the id of the apparat + prof_id (Union[str,int]): the id of the professor + + Returns: + str: the name of the apparat + """ + result = self.query_db( + "SELECT name FROM semesterapparat WHERE appnr=? AND prof_id=?", + (app_id, prof_id), + one=True, + ) + if result: + return result[0] + return None + + def updateApparat(self, apparat_data: ApparatData): + """Update an apparat in the database + + Args: + apparat_data (ApparatData): the new metadata of the apparat + """ + query = "UPDATE semesterapparat SET name = ?, fach = ?, dauer = ?, prof_id = ?, prof_id_adis = ?, apparat_id_adis = ? WHERE appnr = ?" + params = ( + apparat_data.apparat.name, + apparat_data.apparat.subject, + apparat_data.apparat.eternal, + self.getProfData(apparat_data.prof.fullname).id, + apparat_data.apparat.prof_id_adis, + apparat_data.apparat.apparat_id_adis, + apparat_data.apparat.appnr, + ) + log.debug(f"Updating apparat with query {query} and params {params}") + self.query_db(query, params) + + def checkApparatExists(self, app_name: str): + """check if the apparat is already present in the database based on the name + + Args: + apparat_name (str): the name of the apparat + + Returns: + bool: True if the apparat is present, False if not + """ + return ( + True + if self.query_db( + "SELECT appnr FROM semesterapparat WHERE name=?", + (app_name,), + one=True, + ) + else False + ) + + def checkApparatExistsByNr(self, app_nr: Union[str, int]) -> bool: + """a check to see if the apparat is already present in the database, based on the nr. This query will exclude deleted apparats + + Args: + app_nr (Union[str, int]): the id of the apparat + + Returns: + bool: True if the apparat is present, False if not + """ + return ( + True + if self.query_db( + "SELECT id FROM semesterapparat WHERE appnr=? and deletion_status=0", + (app_nr,), + one=True, + ) + else False + ) + + # Statistics + def statistic_request(self, **kwargs: Any): + """Take n amount of kwargs and return the result of the query""" + + def __query(query): + """execute the query and return the result + + Args: + query (str): the constructed query + + Returns: + list: the result of the query + """ + log.debug(f"Query: {query}") + conn = self.connect() + cursor = conn.cursor() + result = cursor.execute(query).fetchall() + for result_a in result: + orig_value = result_a + prof_name = self.getProfNameById(result_a[2]) + # replace the prof_id with the prof_name + result_a = list(result_a) + result_a[2] = prof_name + result_a = tuple(result_a) + result[result.index(orig_value)] = result_a + self.close_connection(conn) + log.debug(f"Query result: {result}") + return result + + if "deletable" in kwargs.keys(): + query = f"""SELECT * FROM semesterapparat + WHERE deletion_status=0 AND dauer=0 AND + ( + (erstellsemester!='{kwargs["deletesemester"]}' AND verlängerung_bis IS NULL) OR + (erstellsemester!='{kwargs["deletesemester"]}' AND verlängerung_bis!='{kwargs["deletesemester"]}' AND verlängerung_bis!='{Semester().next}') + )""" + return __query(query) + if "dauer" in kwargs.keys(): + kwargs["dauer"] = kwargs["dauer"].replace("Ja", "1").replace("Nein", "0") + query = "SELECT * FROM semesterapparat WHERE " + for key, value in kwargs.items() if kwargs.items() is not None else {}: + # log.debug(key, value) + query += f"{key}='{value}' AND " + # log.debug(query) + # remove deletesemester part from normal query, as this will be added to the database upon deleting the apparat + if "deletesemester" in kwargs.keys(): + query = query.replace( + f"deletesemester='{kwargs['deletesemester']}' AND ", "" + ) + if "endsemester" in kwargs.keys(): + if "erstellsemester" in kwargs.keys(): + query = query.replace(f"endsemester='{kwargs['endsemester']}' AND ", "") + query = query.replace( + f"erstellsemester='{kwargs['erstellsemester']} AND ", "xyz" + ) + else: + query = query.replace( + f"endsemester='{kwargs['endsemester']}' AND ", "xyz" + ) + # log.debug("replaced") + query = query.replace( + "xyz", + f"(erstellsemester='{kwargs['endsemester']}' OR verlängerung_bis='{kwargs['endsemester']}') AND ", + ) + # remove all x="" parts from the query where x is a key in kwargs + log.info(f"Query before: {query}") + query = query.strip() + query = query[:-4] + log.info(f"Query after: {query}") + # check if query ends with lowercase letter or a '. if not, remove last symbol and try again + while query[-1] not in ascii_lowercase and query[-1] != "'": + query = query[:-1] + query = query.strip() + + # log.debug(query) + res = __query(query) + # log.debug(res) + return res + + # Admin data + def getUser(self): + """Get a single user from the database""" + return self.query_db("SELECT * FROM user", one=True) + + def getUsers(self) -> list[tuple]: + """Return a list of tuples of all the users in the database""" + return self.query_db("SELECT * FROM user") + + def login(self, user, hashed_password): + """try to login the user. + The salt for the user will be requested from the database and then added to the hashed password. The password will then be compared to the password in the database + + Args: + user (str): username that tries to login + hashed_password (str): the password the user tries to login with + + Returns: + bool: True if the login was successful, False if not + """ + try: + salt = self.query_db( + "SELECT salt FROM user WHERE username=?", (user,), one=True + )[0] + if salt is None: + return False + except TypeError: + return False + hashed_password = salt + hashed_password + password = self.query_db( + "SELECT password FROM user WHERE username=?", (user,), one=True + )[0] + if password == hashed_password: + return True + else: + return False + + def changePassword(self, user, new_password): + """change the password of a user. + The password will be added with the salt and then committed to the database + + Args: + user (str): username + new_password (str): the hashed password + """ + salt = self.query_db( + "SELECT salt FROM user WHERE username=?", (user,), one=True + )[0] + new_password = salt + new_password + self.query_db( + "UPDATE user SET password=? WHERE username=?", (new_password, user) + ) + + def getRole(self, user: str) -> str: + """get the role of the user + + Args: + user (str): username + + Returns: + str: the name of the role + """ + return self.query_db( + "SELECT role FROM user WHERE username=?", (user,), one=True + )[0] + + def getRoles(self) -> list[tuple]: + """get all the roles in the database + + Returns: + list[str]: a list of all the roles + """ + roles = self.query_db("SELECT role FROM user") + return [i[0] for i in roles] + + def checkUsername(self, user) -> bool: + """a check to see if the username is already present in the database + + Args: + user (str): the username + + Returns: + bool: True if the username is present, False if not + """ + data = self.query_db( + "SELECT username FROM user WHERE username=?", (user,), one=True + ) + return True if data is not None else False + + def createUser(self, user, password, role, salt): + """create an user from the AdminCommands class. + + Args: + user (str): the username of the user + password (str): a hashed password + role (str): the role of the user + salt (str): a salt for the password + """ + self.query_db( + "INSERT OR IGNORE INTO user (username, password, role, salt) VALUES (?,?,?,?)", + (user, password, role, salt), + ) + # check if user was created + return ( + self.query_db( + "SELECT username FROM user WHERE username=?", (user,), one=True + ) + is not None + ) + + def deleteUser(self, user): + """delete an unser + + Args: + user (str): username of the user + """ + self.query_db("DELETE FROM user WHERE username=?", (user,)) + + def updateUser(self, username, data: dict[str, str]): + """changge the data of a user + + Args: + username (str): the username of the user + data (dict[str, str]): the data to be changed + """ + conn = self.connect() + cursor = conn.cursor() + query = "UPDATE user SET " + for key, value in data.items(): + if key == "username": + continue + query += f"{key}='{value}'," + query = query[:-1] + query += " WHERE username=?" + params = (username,) + cursor.execute(query, params) + conn.commit() + self.close_connection(conn) + + def getFacultyMember(self, name: str) -> tuple: + """get a faculty member based on the name + + Args: + name (str): the name to be searched for + + Returns: + tuple: a tuple containing the data of the faculty member + """ + return self.query_db( + "SELECT titel, fname,lname,mail,telnr,fullname FROM prof WHERE fullname=?", + (name,), + one=True, + ) + + def updateFacultyMember(self, data: dict, oldlname: str, oldfname: str): + """update the data of a faculty member + + Args: + data (dict): a dictionary containing the data to be updated + oldlname (str): the old last name of the faculty member + oldfname (str): the old first name of the faculty member + """ + placeholders = ", ".join([f"{i}=:{i} " for i in data.keys()]) + query = f"UPDATE prof SET {placeholders} WHERE lname = :oldlname AND fname = :oldfname" + data["oldlname"] = oldlname + data["oldfname"] = oldfname + self.query_db(query, data) + + def getFacultyMembers(self): + """get a list of all faculty members + + Returns: + list[tuple]: a list of tuples containing the faculty members + """ + return self.query_db("SELECT titel, fname,lname,mail,telnr,fullname FROM prof") + + def restoreApparat(self, app_id: Union[str, int], app_name: str): + """restore an apparat from the database + + Args: + app_id (Union[str, int]): the id of the apparat + """ + return self.query_db( + "UPDATE semesterapparat SET deletion_status=0, deleted_date=NULL WHERE appnr=? and name=?", + (app_id, app_name), + ) + + # ELSA + + def createElsaApparat(self, date, prof_id, semester) -> int: + """create a new apparat in the database for the ELSA system + + Args: + date (str): the name of the apparat + prof_id (int): the id of the professor + semester (str): the semester the apparat is created in + + Returns: + int: the id of the apparat + """ + self.query_db( + "INSERT OR IGNORE INTO elsa (date, prof_id, semester) VALUES (?,?,?)", + (date, prof_id, semester), + ) + # get the id of the apparat + apparat_id = self.query_db( + "SELECT id FROM elsa WHERE date=? AND prof_id=? AND semester=?", + (date, prof_id, semester), + one=True, + )[0] + return apparat_id + + def updateElsaApparat(self, date, prof_id, semester, elsa_id): + """update an ELSA apparat in the database + + Args: + date (str): the name of the apparat + prof_id (int): the id of the professor + semester (str): the semester the apparat is created in + elsa_id (int): the id of the ELSA apparat + """ + self.query_db( + "UPDATE elsa SET date=?, prof_id=?, semester=? WHERE id=?", + (date, prof_id, semester, elsa_id), + ) + + def addElsaMedia(self, data: dict, elsa_id: int): + """add a media to the ELSA system + + Args: + data (dict): a dictionary containing the data of the media, + elsa_id (int): the id of the ELSA apparat + """ + headers = [] + entries = [] + for key, value in data.items(): + headers.append(key) + entries.append(value) + headers.append("elsa_id") + entries.append(elsa_id) + query = f"INSERT INTO elsa_media ({', '.join(headers)}) VALUES ({', '.join(['?' for i in range(len(headers))])})" + self.query_db(query, entries) + + def getElsaMedia(self, elsa_id: int): + """get all the media of an ELSA apparat + + Args: + elsa_id (int): the id of the ELSA apparat + + Returns: + list[tuple]: a list of tuples containing the media + """ + media = self.query_db("SELECT * FROM elsa_media WHERE elsa_id=?", (elsa_id,)) + # convert the media to a list of dictionaries + ret = [] + table_fields = self.query_db("PRAGMA table_info(elsa_media)") + for m in media: + tmp = {} + for i in range(len(m)): + tmp[table_fields[i][1]] = m[i] + ret.append(tmp) + return ret + + def insertElsaFile(self, file: list[dict], elsa_id: int): + """Instert a list of files into the ELSA system + + Args: + file (list[dict]): a list containing all the files to be inserted + Structured: [{"name": "filename", "path": "path", "type": "filetype"}] + elsa_id (int): the id of the ELSA apparat + """ + for f in file: + filename = f["name"] + path = f["path"] + filetyp = f["type"] + if path == "Database": + continue + blob = create_blob(path) + query = "INSERT OR IGNORE INTO elsa_files (filename, fileblob, elsa_id, filetyp) VALUES (?, ?, ?, ?)" + self.query_db(query, (filename, blob, elsa_id, filetyp)) + + def recreateElsaFile(self, filename: str, filetype: str) -> str: + """Recreate a file from the ELSA system + + Args: + filename (str): the name of the file + elsa_id (int): the id of the ELSA apparat + filetype (str): the extension of the file to be created + + Returns: + str: The filename of the recreated file + """ + blob = self.query_db( + "SELECT fileblob FROM elsa_files WHERE filename=?", (filename,), one=True + )[0] + # log.debug(blob) + tempdir = settings.database.temp.expanduser() + if not tempdir.exists(): + tempdir.mkdir(parents=True, exist_ok=True) + + file = tempfile.NamedTemporaryFile( + delete=False, dir=tempdir, mode="wb", suffix=f".{filetype}" + ) + file.write(blob) + # log.debug("file created") + return file.name + + def getElsaApparats(self) -> ELSA: + """Get all the ELSA apparats in the database + + Returns: + list[tuple]: a list of tuples containing the ELSA apparats + """ + return self.query_db( + "SELECT * FROM elsa ORDER BY substr(date, 7, 4) || '-' || substr(date, 4, 2) || '-' || substr(date, 1, 2)" + ) + + def getElsaId(self, prof_id: int, semester: str, date: str) -> int: + """get the id of an ELSA apparat based on the professor, semester and date + + Args: + prof_id (int): the id of the professor + semester (str): the semester + date (str): the date of the apparat + + Returns: + int: the id of the ELSA apparat + """ + + data = self.query_db( + "SELECT id FROM elsa WHERE prof_id=? AND semester=? AND date=?", + (prof_id, semester, date), + one=True, + ) + if data is None: + return None + return data[0] + + def getElsaFiles(self, elsa_id: int): + """get all the files of an ELSA apparat + + Args: + elsa_id (int): the id of the ELSA apparat + + Returns: + list[tuple]: a list of tuples containing the files + """ + return self.query_db( + "SELECT filename, filetyp FROM elsa_files WHERE elsa_id=?", (elsa_id,) + ) + + ### + + def createProf(self, profdata: Prof): + log.debug(profdata) + conn = self.connect() + cursor = conn.cursor() + fname = profdata.firstname + lname = profdata.lastname + fullname = f"{lname} {fname}" + mail = profdata.mail + telnr = profdata.telnr + title = profdata.title + + query = "INSERT INTO prof (fname, lname, fullname, mail, telnr, titel) VALUES (?,?,?,?,?,?)" + log.debug(query) + cursor.execute(query, (fname, lname, fullname, mail, telnr, title)) + + conn.commit() + conn.close() + return self.getProfId(profdata) + + def getElsaProfId(self, profname): + query = f"SELECT id FROM elsa_prof WHERE fullname = '{profname}'" + data = self.query_db(query) + if data: + return data[0][0] + else: + return None + + def getElsaProfs(self) -> list[str]: + query = "SELECT fullname FROM elsa_prof" + data = self.query_db(query) + if data: + return [i[0] for i in data] + else: + return [] + + def getProfId(self, profdata: dict[str, Any] | Prof): + """Get the prof ID based on the profdata + + Args: + profdata (dict | Prof): either a dictionary containing the prof data or a Prof object + + Returns: + int | None: The id of the prof or None if not found + """ + conn = self.connect() + cursor = conn.cursor() + if isinstance(profdata, dict): + name = profdata["profname"] + if "," in name: + fname = name.split(", ")[1].strip() + lname = name.split(", ")[0].strip() + fullname = f"{lname} {fname}" + else: + fullname = profdata["profname"] + else: + fullname = profdata.name() + query = "SELECT id FROM prof WHERE fullname = ?" + log.debug(query) + + cursor.execute(query, (fullname,)) + result = cursor.fetchone() + if result: + return result[0] + else: + return None + + def getProfByName(self, fullname): + """Get all Data of the prof based on fullname + + Args: + fullname (str): The full name of the prof + """ + conn = self.connect() + cursor = conn.cursor() + query = "SELECT * FROM prof WHERE fullname = ?" + log.debug(query) + + result = cursor.execute(query, (fullname,)).fetchone() + if result: + return Prof().from_tuple(result) + else: + return Prof() + + def getProfIDByApparat(self, apprarat_id: int) -> Optional[int]: + """Get the prof id based on the semesterapparat id from the database + + Args: + apprarat_id (int): Number of the apparat + + Returns: + + int | None: The id of the prof or None if not found + """ + query = "SELECT prof_id from semesterapparat WHERE appnr = ? and deletion_status = 0" + data = self.query_db(query, (apprarat_id,)) + if data: + log.info("Prof ID: " + str(data[0][0])) + return data[0][0] + else: + return None + + def copyBookToApparat(self, book_id: int, apparat: int): + # get book data + new_apparat_id = apparat + new_prof_id = self.getProfIDByApparat(new_apparat_id) + query = ( + "INSERT INTO media (bookdata, app_id, prof_id, deleted, available, reservation) " + "SELECT bookdata, ?, ?, 0, available, reservation FROM media WHERE id = ?" + ) + connection = self.connect() + cursor = connection.cursor() + cursor.execute(query, (new_apparat_id, new_prof_id, book_id)) + connection.commit() + connection.close() + + def moveBookToApparat(self, book_id: int, appratat: int): + """Move the book to the new apparat + + Args: + book_id (int): the ID of the book + appratat (int): the ID of the new apparat + """ + # get book data + query = "UPDATE media SET app_id = ? WHERE id = ?" + connection = self.connect() + cursor = connection.cursor() + cursor.execute(query, (appratat, book_id)) + connection.commit() + connection.close() + + def getApparatNameByAppNr(self, appnr: int): + query = ( + "SELECT name FROM semesterapparat WHERE appnr = ? and deletion_status = 0" + ) + data = self.query_db(query, (appnr,)) + if data: + return data[0][0] + else: + return None + + def fetch_one(self, query: str, args: tuple[Any, ...] = ()) -> tuple[Any, ...]: + connection = self.connect() + cursor = connection.cursor() + cursor.execute(query, args) + result = cursor.fetchone() + connection.close() + return result + + def getBookIdByPPN(self, ppn: str) -> int: + query = "SELECT id FROM media WHERE bookdata LIKE ?" + data = self.query_db(query, (f"%{ppn}%",)) + if data: + return data[0][0] + else: + return None + + def getNewEditionsByApparat(self, apparat_id: int) -> list[BookData]: + """Get all new editions for a specific apparat + + Args: + apparat_id (int): the id of the apparat + + Returns: + list[tuple]: A list of tuples containing the new editions data + """ + query = "SELECT * FROM neweditions WHERE for_apparat=? AND ordered=0" + results = self.query_db(query, (apparat_id,)) + res = [] + for result in results: + # keep only new edition payload; old edition can be reconstructed if needed + res.append(BookData().from_string(result[1])) + return res + + def setOrdered(self, newBook_id: int): + query = "UPDATE neweditions SET ordered=1 WHERE id=?" + self.query_db(query, (newBook_id,)) + + def getBooksWithNewEditions(self, app_id) -> List[BookData]: + # select all bookdata from media, based on the old_edition_id in neweditions where for_apparat = app_id; also get the new_edition bookdata + + query = "SELECT m.bookdata, new_bookdata FROM media m JOIN neweditions n ON m.id = n.old_edition_id WHERE n.for_apparat = ?" + results = self.query_db(query, (app_id,)) + # store results in tuple old,new + res = [] + for result in results: + oldedition = BookData().from_string(result[0]) + newedition = BookData().from_string(result[1]) + res.append((oldedition, newedition)) + return res + + def getNewEditionId(self, newBook: BookData): + query = "SELECT id FROM neweditions WHERE new_bookdata LIKE ?" + args = ( + newBook.isbn[0] if newBook.isbn and len(newBook.isbn) > 0 else newBook.ppn + ) + params = (f"%{args}%",) + data = self.query_db(query, params, one=True) + if data: + return data[0] + else: + return None + + def insertNewEdition(self, newBook: BookData, oldBookId: int, for_apparat: int): + # check if new edition already in table, check based on newBook.ppn + check_query = "SELECT id FROM neweditions WHERE new_bookdata LIKE ?" + check_params = (f"%{newBook.ppn}%",) + data = self.query_db(check_query, check_params, one=True) + if data: + log.info("New edition already in table, skipping insert") + return + + query = "INSERT INTO neweditions (new_bookdata, old_edition_id, for_apparat) VALUES (?,?,?)" + params = (newBook.to_dict, oldBookId, for_apparat) + + self.query_db(query, params) diff --git a/src/database/migrations/V001__create_base_tables.sql b/src/database/migrations/V001__create_base_tables.sql new file mode 100644 index 0000000..4848add --- /dev/null +++ b/src/database/migrations/V001__create_base_tables.sql @@ -0,0 +1,132 @@ +BEGIN TRANSACTION; + +CREATE TABLE IF NOT EXISTS semesterapparat ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + name TEXT, + prof_id INTEGER, + fach TEXT, + appnr INTEGER, + erstellsemester TEXT, + verlängert_am TEXT, + dauer BOOLEAN, + verlängerung_bis TEXT, + deletion_status INTEGER, + deleted_date TEXT, + apparat_id_adis INTEGER, + prof_id_adis INTEGER, + konto INTEGER, + FOREIGN KEY (prof_id) REFERENCES prof (id) + ); + +CREATE TABLE IF NOT EXISTS media ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + bookdata TEXT, + app_id INTEGER, + prof_id INTEGER, + deleted INTEGER DEFAULT (0), + available BOOLEAN, + reservation BOOLEAN, + FOREIGN KEY (prof_id) REFERENCES prof (id), + FOREIGN KEY (app_id) REFERENCES semesterapparat (id) + ); + +CREATE TABLE IF NOT EXISTS files ( + id INTEGER PRIMARY KEY, + filename TEXT, + fileblob BLOB, + app_id INTEGER, + filetyp TEXT, + prof_id INTEGER REFERENCES prof (id), + FOREIGN KEY (app_id) REFERENCES semesterapparat (id) + ); + +CREATE TABLE IF NOT EXISTS messages ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + created_at date NOT NULL DEFAULT CURRENT_TIMESTAMP, + message TEXT NOT NULL, + remind_at date NOT NULL DEFAULT CURRENT_TIMESTAMP, + user_id INTEGER NOT NULL, + appnr INTEGER, + FOREIGN KEY (user_id) REFERENCES user (id) + ); + +CREATE TABLE IF NOT EXISTS prof ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + titel TEXT, + fname TEXT, + lname TEXT, + fullname TEXT NOT NULL UNIQUE, + mail TEXT, + telnr TEXT + ); + +CREATE TABLE IF NOT EXISTS user ( + id integer NOT NULL PRIMARY KEY AUTOINCREMENT, + created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + username TEXT NOT NULL UNIQUE, + password TEXT NOT NULL, + salt TEXT NOT NULL, + role TEXT NOT NULL, + email TEXT UNIQUE, + name TEXT + ); + +CREATE TABLE IF NOT EXISTS subjects ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + name TEXT NOT NULL UNIQUE +); + +CREATE TABLE IF NOT EXISTS elsa ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + date TEXT NOT NULL, + semester TEXT NOT NULL, + prof_id INTEGER NOT NULL + ); + +CREATE TABLE IF NOT EXISTS elsa_files ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + filename TEXT NOT NULL, + fileblob BLOB NOT NULL, + elsa_id INTEGER NOT NULL, + filetyp TEXT NOT NULL, + FOREIGN KEY (elsa_id) REFERENCES elsa (id) + ); + +CREATE TABLE IF NOT EXISTS elsa_media ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + work_author TEXT, + section_author TEXT, + year TEXT, + edition TEXT, + work_title TEXT, + chapter_title TEXT, + location TEXT, + publisher TEXT, + signature TEXT, + issue TEXT, + pages TEXT, + isbn TEXT, + type TEXT, + elsa_id INTEGER NOT NULL, + FOREIGN KEY (elsa_id) REFERENCES elsa (id) + ); + +CREATE TABLE IF NOT EXISTS neweditions ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + new_bookdata TEXT, + old_edition_id INTEGER, + for_apparat INTEGER, + ordered BOOLEAN DEFAULT (0), + FOREIGN KEY (old_edition_id) REFERENCES media (id), + FOREIGN KEY (for_apparat) REFERENCES semesterapparat (id) +); + +-- Helpful indices to speed up frequent lookups and joins +CREATE INDEX IF NOT EXISTS idx_media_app_prof ON media(app_id, prof_id); +CREATE INDEX IF NOT EXISTS idx_media_deleted ON media(deleted); +CREATE INDEX IF NOT EXISTS idx_media_available ON media(available); +CREATE INDEX IF NOT EXISTS idx_messages_remind_at ON messages(remind_at); +CREATE INDEX IF NOT EXISTS idx_semesterapparat_prof ON semesterapparat(prof_id); +CREATE INDEX IF NOT EXISTS idx_semesterapparat_appnr ON semesterapparat(appnr); + +COMMIT; diff --git a/src/database/migrations/V002__create_table_webadis_login.sql b/src/database/migrations/V002__create_table_webadis_login.sql new file mode 100644 index 0000000..5e1b3a8 --- /dev/null +++ b/src/database/migrations/V002__create_table_webadis_login.sql @@ -0,0 +1,10 @@ +BEGIN TRANSACTION; + +CREATE TABLE IF NOT EXISTS webadis_login ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + username TEXT NOT NULL, + password TEXT NOT NULL + ); + +COMMIT; + diff --git a/src/database/migrations/V003_update_webadis_add_user_area.sql b/src/database/migrations/V003_update_webadis_add_user_area.sql new file mode 100644 index 0000000..1b5567b --- /dev/null +++ b/src/database/migrations/V003_update_webadis_add_user_area.sql @@ -0,0 +1,6 @@ +BEGIN TRANSACTION; + +ALTER TABLE webadis_login +ADD COLUMN effective_range TEXT; + +COMMIT; \ No newline at end of file diff --git a/src/database/schemas.py b/src/database/schemas.py new file mode 100644 index 0000000..991cb59 --- /dev/null +++ b/src/database/schemas.py @@ -0,0 +1,112 @@ +CREATE_TABLE_APPARAT = """CREATE TABLE semesterapparat ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + name TEXT, + prof_id INTEGER, + fach TEXT, + appnr INTEGER, + erstellsemester TEXT, + verlängert_am TEXT, + dauer BOOLEAN, + verlängerung_bis TEXT, + deletion_status INTEGER, + deleted_date TEXT, + apparat_id_adis INTEGER, + prof_id_adis INTEGER, + konto INTEGER, + FOREIGN KEY (prof_id) REFERENCES prof (id) + )""" +CREATE_TABLE_MEDIA = """CREATE TABLE media ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + bookdata TEXT, + app_id INTEGER, + prof_id INTEGER, + deleted INTEGER DEFAULT (0), + available BOOLEAN, + reservation BOOLEAN, + FOREIGN KEY (prof_id) REFERENCES prof (id), + FOREIGN KEY (app_id) REFERENCES semesterapparat (id) + )""" + +CREATE_TABLE_FILES = """CREATE TABLE files ( + id INTEGER PRIMARY KEY, + filename TEXT, + fileblob BLOB, + app_id INTEGER, + filetyp TEXT, + prof_id INTEGER REFERENCES prof (id), + FOREIGN KEY (app_id) REFERENCES semesterapparat (id) + )""" +CREATE_TABLE_MESSAGES = """CREATE TABLE messages ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + created_at date NOT NULL DEFAULT CURRENT_TIMESTAMP, + message TEXT NOT NULL, + remind_at date NOT NULL DEFAULT CURRENT_TIMESTAMP, + user_id INTEGER NOT NULL, + appnr INTEGER, + FOREIGN KEY (user_id) REFERENCES user (id) + )""" +CREATE_TABLE_PROF = """CREATE TABLE prof ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + titel TEXT, + fname TEXT, + lname TEXT, + fullname TEXT NOT NULL UNIQUE, + mail TEXT, + telnr TEXT + )""" +CREATE_TABLE_USER = """CREATE TABLE user ( + id integer NOT NULL PRIMARY KEY AUTOINCREMENT, + created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + username TEXT NOT NULL UNIQUE, + password TEXT NOT NULL, + salt TEXT NOT NULL, + role TEXT NOT NULL, + email TEXT UNIQUE, + name TEXT + )""" +CREATE_TABLE_SUBJECTS = """CREATE TABLE subjects ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + name TEXT NOT NULL UNIQUE +)""" + +CREATE_ELSA_TABLE = """CREATE TABLE elsa ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + date TEXT NOT NULL, + semester TEXT NOT NULL, + prof_id INTEGER NOT NULL + )""" +CREATE_ELSA_FILES_TABLE = """CREATE TABLE elsa_files ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + filename TEXT NOT NULL, + fileblob BLOB NOT NULL, + elsa_id INTEGER NOT NULL, + filetyp TEXT NOT NULL, + FOREIGN KEY (elsa_id) REFERENCES elsa (id) + )""" +CREATE_ELSA_MEDIA_TABLE = """CREATE TABLE elsa_media ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + work_author TEXT, + section_author TEXT, + year TEXT, + edition TEXT, + work_title TEXT, + chapter_title TEXT, + location TEXT, + publisher TEXT, + signature TEXT, + issue TEXT, + pages TEXT, + isbn TEXT, + type TEXT, + elsa_id INTEGER NOT NULL, + FOREIGN KEY (elsa_id) REFERENCES elsa (id) + )""" +CREATE_TABLE_NEWEDITIONS = """CREATE TABLE neweditions ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + new_bookdata TEXT, + old_edition_id INTEGER, + for_apparat INTEGER, + ordered BOOLEAN DEFAULT (0), + FOREIGN KEY (old_edition_id) REFERENCES media (id), + FOREIGN KEY (for_apparat) REFERENCES semesterapparat (id) +)""" diff --git a/src/documents/__init__.py b/src/documents/__init__.py new file mode 100644 index 0000000..139597f --- /dev/null +++ b/src/documents/__init__.py @@ -0,0 +1,2 @@ + + diff --git a/src/documents/generators.py b/src/documents/generators.py new file mode 100644 index 0000000..bd163cd --- /dev/null +++ b/src/documents/generators.py @@ -0,0 +1,371 @@ +import os +from datetime import datetime +from os.path import basename + +from docx import Document +from docx.enum.text import WD_PARAGRAPH_ALIGNMENT +from docx.oxml import OxmlElement +from docx.oxml.ns import qn +from docx.shared import Cm, Pt, RGBColor + +from src import settings +from src.shared.logging import log + +logger = log + +font = "Cascadia Mono" + + +def print_document(file: str) -> None: + # send document to printer as attachment of email + import smtplib + from email.mime.application import MIMEApplication + from email.mime.multipart import MIMEMultipart + from email.mime.text import MIMEText + + smtp = settings.mail.smtp_server + port = settings.mail.port + sender_email = settings.mail.sender + password = settings.mail.password + receiver = settings.mail.printer_mail + message = MIMEMultipart() + message["From"] = sender_email + message["To"] = receiver + message["cc"] = settings.mail.sender + message["Subject"] = "." + mail_body = "." + message.attach(MIMEText(mail_body, "html")) + with open(file, "rb") as fil: + part = MIMEApplication(fil.read(), Name=basename(file)) + # After the file is closed + part["Content-Disposition"] = 'attachment; filename="%s"' % basename(file) + message.attach(part) + mail = message.as_string() + with smtplib.SMTP_SSL(smtp, port) as server: + server.connect(smtp, port) + server.login(settings.mail.user_name, password) + server.sendmail(sender_email, receiver, mail) + server.quit() + log.success("Mail sent") + + +class SemesterError(Exception): + """Custom exception for semester-related errors.""" + + def __init__(self, message: str): + super().__init__(message) + log.error(message) + + def __str__(self): + return f"SemesterError: {self.args[0]}" + + +class SemesterDocument: + def __init__( + self, + apparats: list[tuple[int, str]], + semester: str, + filename: str, + full: bool = False, + ): + assert isinstance(apparats, list), SemesterError( + "Apparats must be a list of tuples" + ) + assert all(isinstance(apparat, tuple) for apparat in apparats), SemesterError( + "Apparats must be a list of tuples" + ) + assert all(isinstance(apparat[0], int) for apparat in apparats), SemesterError( + "Apparat numbers must be integers" + ) + assert all(isinstance(apparat[1], str) for apparat in apparats), SemesterError( + "Apparat names must be strings" + ) + assert isinstance(semester, str), SemesterError("Semester must be a string") + assert "." not in filename and isinstance(filename, str), SemesterError( + "Filename must be a string and not contain an extension" + ) + self.doc = Document() + self.apparats = apparats + self.semester = semester + self.table_font_normal = font + self.table_font_bold = font + self.header_font = font + self.header_font_size = Pt(26) + self.sub_header_font_size = Pt(18) + self.table_font_size = Pt(10) + self.color_red = RGBColor(255, 0, 0) + self.color_blue = RGBColor(0, 0, 255) + self.filename = filename + if full: + log.info("Full document generation") + self.cleanup + log.info("Cleanup done") + self.make_document() + log.info("Document created") + self.create_pdf() + log.info("PDF created") + print_document(self.filename + ".pdf") + log.info("Document printed") + + def set_table_border(self, table): + """ + Adds a full border to the table. + + :param table: Table object to which the border will be applied. + """ + tbl = table._element + tbl_pr = tbl.xpath("w:tblPr")[0] + tbl_borders = OxmlElement("w:tblBorders") + + # Define border styles + for border_name in ["top", "left", "bottom", "right", "insideH", "insideV"]: + border = OxmlElement(f"w:{border_name}") + border.set(qn("w:val"), "single") + border.set(qn("w:sz"), "4") # Thickness of the border + border.set(qn("w:space"), "0") + border.set(qn("w:color"), "000000") # Black color + tbl_borders.append(border) + + tbl_pr.append(tbl_borders) + + def create_sorted_table(self) -> None: + # Sort the apparats list by the string in the tuple (index 1) + self.apparats.sort(key=lambda x: x[1]) + # Create a table with rows equal to the length of the apparats list + table = self.doc.add_table(rows=len(self.apparats), cols=2) + table.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER + + # Set column widths by directly modifying the cell properties + widths = [Cm(1.19), Cm(10)] + for col_idx, width in enumerate(widths): + for cell in table.columns[col_idx].cells: + cell_width_element = cell._element.xpath(".//w:tcPr")[0] + tcW = OxmlElement("w:tcW") + tcW.set(qn("w:w"), str(int(width.cm * 567))) # Convert cm to twips + tcW.set(qn("w:type"), "dxa") + cell_width_element.append(tcW) + + # Adjust row heights + for row in table.rows: + trPr = row._tr.get_or_add_trPr() # Get or add the element + trHeight = OxmlElement("w:trHeight") + trHeight.set( + qn("w:val"), str(int(Pt(15).pt * 20)) + ) # Convert points to twips + trHeight.set(qn("w:hRule"), "exact") # Use "exact" for fixed height + trPr.append(trHeight) + + # Fill the table with sorted data + for row_idx, (number, name) in enumerate(self.apparats): + row = table.rows[row_idx] + + # Set font for the first column (number) + cell_number_paragraph = row.cells[0].paragraphs[0] + cell_number_run = cell_number_paragraph.add_run(str(number)) + cell_number_run.font.name = self.table_font_bold + cell_number_run.font.size = self.table_font_size + cell_number_run.font.bold = True + cell_number_run.font.color.rgb = self.color_red + cell_number_paragraph.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER + + # Set font for the second column (name) + cell_name_paragraph = row.cells[1].paragraphs[0] + words = name.split() + if words: + # Add the first word in bold + bold_run = cell_name_paragraph.add_run(words[0]) + bold_run.font.bold = True + bold_run.font.name = self.table_font_bold + bold_run.font.size = self.table_font_size + + # Add the rest of the words normally + if len(words) > 1: + normal_run = cell_name_paragraph.add_run(" " + " ".join(words[1:])) + normal_run.font.name = self.table_font_normal + normal_run.font.size = self.table_font_size + cell_name_paragraph.alignment = WD_PARAGRAPH_ALIGNMENT.LEFT + + self.set_table_border(table) + + def make_document(self): + # Create a new Document + section = self.doc.sections[0] + section.top_margin = Cm(2.54) # Default 1 inch (can adjust as needed) + section.bottom_margin = Cm(1.5) # Set bottom margin to 1.5 cm + section.left_margin = Cm(2.54) # Default 1 inch + section.right_margin = Cm(2.54) # Default 1 inch + + # Add the current date + current_date = datetime.now().strftime("%Y-%m-%d") + date_paragraph = self.doc.add_paragraph(current_date) + date_paragraph.alignment = WD_PARAGRAPH_ALIGNMENT.LEFT + + # Add a header + semester = f"Semesterapparate {self.semester}" + header = self.doc.add_paragraph(semester) + header.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER + header_run = header.runs[0] + header_run.font.name = self.header_font + header_run.font.size = self.header_font_size + header_run.font.bold = True + header_run.font.color.rgb = self.color_blue + + sub_header = self.doc.add_paragraph("(Alphabetisch)") + sub_header.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER + sub_header_run = sub_header.runs[0] + sub_header_run.font.name = self.header_font + sub_header_run.font.size = self.sub_header_font_size + sub_header_run.font.color.rgb = self.color_red + + self.doc.add_paragraph("") + + self.create_sorted_table() + + def save_document(self, name: str) -> None: + # Save the document + self.doc.save(name) + + def create_pdf(self) -> None: + # Save the document + import comtypes.client + + word = comtypes.client.CreateObject("Word.Application") # type: ignore + self.save_document(self.filename + ".docx") + docpath = os.path.abspath(self.filename + ".docx") + doc = word.Documents.Open(docpath) + curdir = os.getcwd() + doc.SaveAs(f"{curdir}/{self.filename}.pdf", FileFormat=17) + doc.Close() + word.Quit() + log.debug("PDF saved") + + @property + def cleanup(self) -> None: + if os.path.exists(f"{self.filename}.docx"): + os.remove(f"{self.filename}.docx") + os.remove(f"{self.filename}.pdf") + + @property + def send(self) -> None: + print_document(self.filename + ".pdf") + log.debug("Document sent to printer") + + +class SemapSchilder: + def __init__(self, entries: list[str]): + self.entries = entries + self.filename = "Schilder" + self.font_size = Pt(23) + self.font_name = font + self.doc = Document() + self.define_doc_properties() + self.add_entries() + self.cleanup() + self.create_pdf() + + def define_doc_properties(self): + # set the doc to have a top margin of 1cm, left and right are 0.5cm, bottom is 0cm + section = self.doc.sections[0] + section.top_margin = Cm(1) + section.bottom_margin = Cm(0) + section.left_margin = Cm(0.5) + section.right_margin = Cm(0.5) + + # set the font to Times New Roman, size 23 bold, color black + for paragraph in self.doc.paragraphs: + for run in paragraph.runs: + run.font.name = self.font_name + run.font.size = self.font_size + run.font.bold = True + run.font.color.rgb = RGBColor(0, 0, 0) + paragraph.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER + + # if the length of the text is + + def add_entries(self): + for entry in self.entries: + paragraph = self.doc.add_paragraph(entry) + paragraph.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER + paragraph.paragraph_format.line_spacing = Pt(23) # Set fixed line spacing + paragraph.paragraph_format.space_before = Pt(2) # Remove spacing before + paragraph.paragraph_format.space_after = Pt(2) # Remove spacing after + + run = paragraph.runs[0] + run.font.name = self.font_name + run.font.size = self.font_size + run.font.bold = True + run.font.color.rgb = RGBColor(0, 0, 0) + + # Add a line to be used as a guideline for cutting + line = self.doc.add_paragraph() + line.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER + line.paragraph_format.line_spacing = Pt(23) # Match line spacing + line.paragraph_format.space_before = Pt(2) # Remove spacing before + line.paragraph_format.space_after = Pt(2) # Remove spacing after + line.add_run("--------------------------") + + def save_document(self): + # Save the document + self.doc.save(f"{self.filename}.docx") + log.debug(f"Document saved as {self.filename}.docx") + + def create_pdf(self) -> None: + # Save the document + import comtypes.client + + word = comtypes.client.CreateObject("Word.Application") # type: ignore + self.save_document() + docpath = os.path.abspath(f"{self.filename}.docx") + doc = word.Documents.Open(docpath) + curdir = os.getcwd() + doc.SaveAs(f"{curdir}/{self.filename}.pdf", FileFormat=17) + doc.Close() + word.Quit() + log.debug("PDF saved") + + def cleanup(self) -> None: + if os.path.exists(f"{self.filename}.docx"): + os.remove(f"{self.filename}.docx") + if os.path.exists(f"{self.filename}.pdf"): + os.remove(f"{self.filename}.pdf") + + @property + def send(self) -> None: + print_document(self.filename + ".pdf") + log.debug("Document sent to printer") + + +if __name__ == "__main__": + entries = [ + "Lüsebrink (Theorie und Praxis der Leichtathletik)", + "Kulovics (ISP-Betreuung)", + "Köhler (Ausgewählte Aspekte der materiellen Kultur Textil)", + "Grau (Young Adult Literature)", + "Schiebel (Bewegung II:Ausgewählte Problemfelder)", + "Schiebel (Ernährungswiss. Perspektive)", + "Park (Kommunikation und Kooperation)", + "Schiebel (Schwimmen)", + "Huppertz (Philosophieren mit Kindern)", + "Heyl (Heyl)", + "Reuter (Verschiedene Veranstaltungen)", + "Reinhold (Arithmetik und mathematisches Denken)", + "Wirtz (Forschungsmethoden)", + "Schleider (Essstörungen)", + "Schleider (Klinische Psychologie)", + "Schleider (Doktorandenkolloquium)", + "Schleider (Störungen Sozialverhaltens/Delinquenz)", + "Burth (EU Forschung im Int. Vergleich/EU Gegenstand biling. Didaktik)", + "Reinhardt (Einführung Politikdidaktik)", + "Schleider (Psychologische Interventionsmethoden)", + "Schleider (ADHS)", + "Schleider (Beratung und Teamarbeit)", + "Schleider (LRS)", + "Schleider (Gesundheitspsychologie)", + "Schleider (Elterntraining)", + "Wulff (Hochschulzertifikat DaZ)", + "Dinkelaker ( )", + "Droll (Einführung in die Sprachwissenschaft)", + "Karoß (Gymnastik - Sich Bewegen mit und ohne Handgeräte)", + "Sahrai (Kindheit und Gesellschaft)", + ] + doc = SemapSchilder(entries) diff --git a/src/parsers/__init__.py b/src/parsers/__init__.py new file mode 100644 index 0000000..bbe28e3 --- /dev/null +++ b/src/parsers/__init__.py @@ -0,0 +1,13 @@ +__all__ = [ + "csv_to_list", + "pdf_to_csv", + "word_to_semap", + "eml_parser", + "eml_to_semap", +] + + +from .csv_parser import csv_to_list +from .pdf_parser import pdf_to_csv +from .word_parser import word_to_semap +from .xml_parser import eml_parser, eml_to_semap diff --git a/src/parsers/csv_parser.py b/src/parsers/csv_parser.py new file mode 100644 index 0000000..750d8e8 --- /dev/null +++ b/src/parsers/csv_parser.py @@ -0,0 +1,23 @@ +import csv + +from charset_normalizer import detect + + +def csv_to_list(path: str) -> list[str]: + """ + Extracts the data from a csv file and returns it as a pandas dataframe + """ + encoding = detect(open(path, "rb").read())["encoding"] + with open(path, newline="", encoding=encoding) as csvfile: + # if decoder fails to map, assign "" + reader = csv.reader(csvfile, delimiter=";", quotechar="|") + ret = [] + for row in reader: + ret.append(row[0].replace('"', "")) + return ret + + +if __name__ == "__main__": + text = csv_to_list("C:/Users/aky547/Desktop/semap/71.csv") + # remove linebreaks + # #print(text) diff --git a/src/parsers/pdf_parser.py b/src/parsers/pdf_parser.py new file mode 100644 index 0000000..de5e87a --- /dev/null +++ b/src/parsers/pdf_parser.py @@ -0,0 +1,23 @@ +# add depend path to system path + +from pdfquery import PDFQuery + + +def pdf_to_csv(path: str) -> str: + """ + Extracts the data from a pdf file and returns it as a pandas dataframe + """ + file = PDFQuery(path) + file.load() + # get the text from the pdf file + text_elems = file.extract([("with_formatter", "text"), ("all_text", "*")]) + extracted_text = text_elems["all_text"] + + return extracted_text + + +if __name__ == "__main__": + text = pdf_to_csv("54_pdf.pdf") + # remove linebreaks + text = text.replace("\n", "") + # print(text) diff --git a/src/parsers/transformers/__init__.py b/src/parsers/transformers/__init__.py new file mode 100644 index 0000000..96f8070 --- /dev/null +++ b/src/parsers/transformers/__init__.py @@ -0,0 +1,8 @@ +from .transformers import ( + RDS_AVAIL_DATA, + ARRAYData, + BibTeXData, + COinSData, + RDSData, + RISData, +) diff --git a/src/parsers/transformers/schemas.py b/src/parsers/transformers/schemas.py new file mode 100644 index 0000000..cc479ea --- /dev/null +++ b/src/parsers/transformers/schemas.py @@ -0,0 +1,122 @@ +from __future__ import annotations + +from typing import Optional, Any, List +from dataclasses import dataclass +from dataclasses import field as dataclass_field +import json + + +@dataclass +class Item: + superlocation: str | None = dataclass_field(default_factory=str) + status: str | None = dataclass_field(default_factory=str) + availability: str | None = dataclass_field(default_factory=str) + notes: str | None = dataclass_field(default_factory=str) + limitation: str | None = dataclass_field(default_factory=str) + duedate: str | None = dataclass_field(default_factory=str) + id: str | None = dataclass_field(default_factory=str) + item_id: str | None = dataclass_field(default_factory=str) + ilslink: str | None = dataclass_field(default_factory=str) + number: int | None = dataclass_field(default_factory=int) + barcode: str | None = dataclass_field(default_factory=str) + reserve: str | None = dataclass_field(default_factory=str) + callnumber: str | None = dataclass_field(default_factory=str) + department: str | None = dataclass_field(default_factory=str) + locationhref: str | None = dataclass_field(default_factory=str) + location: str | None = dataclass_field(default_factory=str) + + def from_dict(self, data: dict): + """Import data from dict""" + data = data["items"] + for entry in data: + for key, value in entry.items(): + setattr(self, key, value) + return self + + +@dataclass +class RDS_AVAIL_DATA: + """Class to store RDS availability data""" + + library_sigil: str = dataclass_field(default_factory=str) + items: List[Item] = dataclass_field(default_factory=list) + + def import_from_dict(self, data: str): + """Import data from dict""" + edata = json.loads(data) + # library sigil is first key + + self.library_sigil = str(list(edata.keys())[0]) + # get data from first key + edata = edata[self.library_sigil] + for location in edata: + item = Item(superlocation=location).from_dict(edata[location]) + + self.items.append(item) + return self + + +@dataclass +class RDS_DATA: + """Class to store RDS data""" + + RDS_SIGNATURE: str = dataclass_field(default_factory=str) + RDS_STATUS: str = dataclass_field(default_factory=str) + RDS_LOCATION: str = dataclass_field(default_factory=str) + RDS_URL: Any = dataclass_field(default_factory=str) + RDS_HINT: Any = dataclass_field(default_factory=str) + RDS_COMMENT: Any = dataclass_field(default_factory=str) + RDS_HOLDING: Any = dataclass_field(default_factory=str) + RDS_HOLDING_LEAK: Any = dataclass_field(default_factory=str) + RDS_INTERN: Any = dataclass_field(default_factory=str) + RDS_PROVENIENCE: Any = dataclass_field(default_factory=str) + RDS_LOCAL_NOTATION: str = dataclass_field(default_factory=str) + RDS_LEA: Any = dataclass_field(default_factory=str) + + def import_from_dict(self, data: dict) -> RDS_DATA: + """Import data from dict""" + for key, value in data.items(): + setattr(self, key, value) + return self + + +@dataclass +class RDS_GENERIC_DATA: + LibrarySigil: str = dataclass_field(default_factory=str) + RDS_DATA: List[RDS_DATA] = dataclass_field(default_factory=list) + + def import_from_dict(self, data: str) -> RDS_GENERIC_DATA: + """Import data from dict""" + edata = json.loads(data) + # library sigil is first key + self.LibrarySigil = str(list(edata.keys())[0]) + # get data from first key + edata = edata[self.LibrarySigil] + for entry in edata: + rds_data = RDS_DATA() # Create a new RDS_DATA instance + # Populate the RDS_DATA instance from the entry + # This assumes that the entry is a dictionary that matches the structure of the RDS_DATA class + rds_data.import_from_dict(entry) + self.RDS_DATA.append(rds_data) # Add the RDS_DATA instance to the list + return self + + +@dataclass +class LoksatzData: + type: Optional[str] = None + adis_idn: Optional[str] = None + t_idn: Optional[str] = None + ktrl_nr: Optional[str] = None + adis_isil: Optional[str] = None + adis_sigel: Optional[str] = None + bib_sigel: Optional[str] = None + standort: Optional[str] = None + signatur: Optional[str] = None + ausleihcode: Optional[str] = None + sig_katalog: Optional[str] = None + erwerb_datum: Optional[str] = None + medientypcode: Optional[str] = None + bestellart: Optional[str] = None + faecherstatistik: Optional[str] = None + exemplar_stat: Optional[str] = None + so_standort: Optional[str] = None diff --git a/src/parsers/transformers/transformers.py b/src/parsers/transformers/transformers.py new file mode 100644 index 0000000..ade70b7 --- /dev/null +++ b/src/parsers/transformers/transformers.py @@ -0,0 +1,522 @@ +from __future__ import annotations + +import json +import re +import sys +from dataclasses import dataclass +from dataclasses import field as dataclass_field +from typing import Any, List + +import loguru + +from src import LOG_DIR +from src.logic.dataclass import BookData + +log = loguru.logger +log.remove() +log.add(sys.stdout, level="INFO") +log.add(f"{LOG_DIR}/application.log", rotation="1 MB", retention="10 days") + + +###Pydatnic models +@dataclass +class Item: + superlocation: str | None = dataclass_field(default_factory=str) + status: str | None = dataclass_field(default_factory=str) + availability: str | None = dataclass_field(default_factory=str) + notes: str | None = dataclass_field(default_factory=str) + limitation: str | None = dataclass_field(default_factory=str) + duedate: str | None = dataclass_field(default_factory=str) + id: str | None = dataclass_field(default_factory=str) + item_id: str | None = dataclass_field(default_factory=str) + ilslink: str | None = dataclass_field(default_factory=str) + number: int | None = dataclass_field(default_factory=int) + barcode: str | None = dataclass_field(default_factory=str) + reserve: str | None = dataclass_field(default_factory=str) + callnumber: str | None = dataclass_field(default_factory=str) + department: str | None = dataclass_field(default_factory=str) + locationhref: str | None = dataclass_field(default_factory=str) + location: str | None = dataclass_field(default_factory=str) + ktrl_nr: str | None = dataclass_field(default_factory=str) + + def from_dict(self, data: dict): + """Import data from dict""" + data = data["items"] + for entry in data: + for key, value in entry.items(): + setattr(self, key, value) + return self + + +@dataclass +class RDS_AVAIL_DATA: + """Class to store RDS availability data""" + + library_sigil: str = dataclass_field(default_factory=str) + items: List[Item] = dataclass_field(default_factory=list) + + def import_from_dict(self, data: str): + """Import data from dict""" + edata = json.loads(data) + # library sigil is first key + + self.library_sigil = str(list(edata.keys())[0]) + # get data from first key + edata = edata[self.library_sigil] + for location in edata: + item = Item(superlocation=location).from_dict(edata[location]) + + self.items.append(item) + return self + + +@dataclass +class RDS_DATA: + """Class to store RDS data""" + + RDS_SIGNATURE: str = dataclass_field(default_factory=str) + RDS_STATUS: str = dataclass_field(default_factory=str) + RDS_LOCATION: str = dataclass_field(default_factory=str) + RDS_URL: Any = dataclass_field(default_factory=str) + RDS_HINT: Any = dataclass_field(default_factory=str) + RDS_COMMENT: Any = dataclass_field(default_factory=str) + RDS_HOLDING: Any = dataclass_field(default_factory=str) + RDS_HOLDING_LEAK: Any = dataclass_field(default_factory=str) + RDS_INTERN: Any = dataclass_field(default_factory=str) + RDS_PROVENIENCE: Any = dataclass_field(default_factory=str) + RDS_LOCAL_NOTATION: str = dataclass_field(default_factory=str) + RDS_LEA: Any = dataclass_field(default_factory=str) + + def import_from_dict(self, data: dict) -> RDS_DATA: + """Import data from dict""" + for key, value in data.items(): + setattr(self, key, value) + return self + + +@dataclass +class RDS_GENERIC_DATA: + LibrarySigil: str = dataclass_field(default_factory=str) + RDS_DATA: List[RDS_DATA] = dataclass_field(default_factory=list) + + def import_from_dict(self, data: str) -> RDS_GENERIC_DATA: + """Import data from dict""" + edata = json.loads(data) + # library sigil is first key + self.LibrarySigil = str(list(edata.keys())[0]) + # get data from first key + edata = edata[self.LibrarySigil] + for entry in edata: + rds_data = RDS_DATA() # Create a new RDS_DATA instance + # Populate the RDS_DATA instance from the entry + # This assumes that the entry is a dictionary that matches the structure of the RDS_DATA class + rds_data.import_from_dict(entry) + self.RDS_DATA.append(rds_data) # Add the RDS_DATA instance to the list + return self + + +class BaseStruct: + def __init__(self, **kwargs): + for key, value in kwargs.items(): + setattr(self, key, value) + + +class ARRAYData: + def __init__(self, signature=None) -> None: + self.signature = None + pass + + def transform(self, data: str) -> BookData: + def _get_line(source: str, search: str) -> str: + try: + data = ( + source.split(search)[1] + .split("\n")[0] + .strip() + .replace("=>", "") + .strip() + ) + return data + + except Exception: + # # log.debug(f"ARRAYData.transform failed, {source}, {search}") + log.exception(f"ARRAYData.transform failed, no string {search}") + return "" + + def _get_list_entry(source: str, search: str, entry: str) -> str: + try: + source = source.replace("\t", "").replace("\r", "") + source = source.split(search)[1].split(")")[0] + return _get_line(source, entry).replace("=>", "").strip() + except: + return "" + + def _get_isbn(source: str) -> list: + try: + isbn = source.split("[isbn]")[1].split(")")[0].strip() + isbn = isbn.split("(")[1] + isbns = isbn.split("=>") + ret = [] + for _ in isbns: + # remove _ from list + isb = _.split("\n")[0].strip() + if isb == "": + continue + ret.append(isb) if isb not in ret else None + return ret + except: + isbn = [] + return isbn + + def _get_signature(data): + try: + sig_data = ( + data.split("[loksatz]")[1] + .split("[0] => ")[1] + .split("\n")[0] + .strip() + ) + signature_data = eval(sig_data) + return signature_data["signatur"] + except Exception: + return None + + def _get_author(data): + try: + array = data.split("[au_display_short]")[1].split(")\n")[0].strip() + except Exception: + return "" + entries = array.split("\n") + authors = [] + hg_present = False + verf_present = False + lines = [] + for entry in entries: + if "=>" in entry: + line = entry.split("=>")[1].strip() + if "[HerausgeberIn]" in line: + hg_present = True + if "[VerfasserIn]" in line: + verf_present = True + lines.append(line) + for line in lines: + if hg_present and verf_present: + if "[HerausgeberIn]" in line: + authors.append(line.split("[")[0].strip()) + elif verf_present: + if "[VerfasserIn]" in line: + authors.append(line.split("[")[0].strip()) + else: + pass + return ";".join(authors) + + def _get_title(data): + titledata = None + title = "" + if "[ti_long]" in data: + titledata = data.split("[ti_long]")[1].split(")\n")[0].strip() + title = titledata.split("=>")[1].strip().split("/")[0].strip() + if "[ti_long_f]" in data: + titledata = data.split("[ti_long_f]")[1].split(")\n")[0].strip() + title = titledata.split("=>")[1].strip().split("/")[0].strip() + return title + + def _get_adis_idn(data, signature): + loksatz_match = re.search( + r"\[loksatz\] => Array\s*\((.*?)\)", data, re.DOTALL + ) + if loksatz_match: + loksatz_content = loksatz_match.group(1) + + # Step 2: Extract JSON objects within the loksatz section + json_objects = re.findall(r"{.*?}", loksatz_content, re.DOTALL) + # Print each JSON object + for obj in json_objects: + data = eval(obj) + if data["signatur"] == signature: + return data["adis_idn"] + + def _get_in_apparat(data): + loksatz_match = re.search( + r"\[loksatz\] => Array\s*\((.*?)\)", data, re.DOTALL + ) + if loksatz_match: + loksatz_content = loksatz_match.group(1) + + # Step 2: Extract JSON objects within the loksatz section + json_objects = re.findall(r"{.*?}", loksatz_content, re.DOTALL) + # Print each JSON object + for obj in json_objects: + data = eval(obj) + if data["ausleihcode"] == "R" and data["standort"] == "40": + return True + else: + return False + + ppn = _get_line(data, "[kid]") + title = _get_title(data).strip() + author = _get_author(data) + edition = _get_list_entry(data, "[ausgabe]", "[0]").replace(",", "") + link = f"https://rds.ibs-bw.de/phfreiburg/link?kid={_get_line(data, '[kid]')}" + isbn = _get_isbn(data) + # [self._get_list_entry(data,"[isbn]","[0]"),self._get_list_entry(data,"[is]","[1]")], + language = _get_list_entry(data, "[la_facet]", "[0]") + publisher = _get_list_entry(data, "[pu]", "[0]") + year = _get_list_entry(data, "[py_display]", "[0]") + pages = _get_list_entry(data, "[umfang]", "[0]").split(":")[0].strip() + signature = ( + self.signature if self.signature is not None else _get_signature(data) + ) + + place = _get_list_entry(data, "[pp]", "[0]") + adis_idn = _get_adis_idn(data, signature=signature) + in_apparat = _get_in_apparat(data) + return BookData( + ppn=ppn, + title=title, + author=author, + edition=edition, + link=link, + isbn=isbn, + language=language, + publisher=publisher, + year=year, + pages=pages, + signature=signature, + place=place, + adis_idn=adis_idn, + in_apparat=in_apparat, + ) + + +class COinSData: + def __init__(self) -> None: + pass + + def transform(self, data: str) -> BookData: + def _get_line(source: str, search: str) -> str: + try: + data = source.split(f"{search}=")[1] # .split("")[0].strip() + return data.split("rft")[0].strip() if "rft" in data else data + except: + return "" + + return BookData( + ppn=_get_line(data, "rft_id").split("=")[1], + title=_get_line(data, "rft.btitle"), + author=f"{_get_line(data, 'rft.aulast')}, {_get_line(data, 'rft.aufirst')}", + edition=_get_line(data, "rft.edition"), + link=_get_line(data, "rft_id"), + isbn=_get_line(data, "rft.isbn"), + publisher=_get_line(data, "rft.pub"), + year=_get_line(data, "rft.date"), + pages=_get_line(data, "rft.tpages").split(":")[0].strip(), + ) + + +class RISData: + def __init__(self) -> None: + pass + + def transform(self, data: str) -> BookData: + def _get_line(source: str, search: str) -> str: + try: + data = source.split(f"{search} - ")[1] # .split("")[0].strip() + return data.split("\n")[0].strip() if "\n" in data else data + except: + return "" + + return BookData( + ppn=_get_line(data, "DP").split("=")[1], + title=_get_line(data, "TI"), + signature=_get_line(data, "CN"), + edition=_get_line(data, "ET").replace(",", ""), + link=_get_line(data, "DP"), + isbn=_get_line(data, "SN").split(","), + author=_get_line(data, "AU").split("[")[0].strip(), + language=_get_line(data, "LA"), + publisher=_get_line(data, "PB"), + year=_get_line(data, "PY"), + pages=_get_line(data, "SP"), + ) + + +class BibTeXData: + def __init__(self): + pass + + def transform(self, data: str) -> BookData: + def _get_line(source: str, search: str) -> str: + try: + return ( + data.split(search)[1] + .split("\n")[0] + .strip() + .split("=")[1] + .strip() + .replace("{", "") + .replace("}", "") + .replace(",", "") + .replace("[", "") + .replace("];", "") + ) + except: + return "" + + return BookData( + ppn=None, + title=_get_line(data, "title"), + signature=_get_line(data, "bestand"), + edition=_get_line(data, "edition"), + isbn=_get_line(data, "isbn"), + author=";".join(_get_line(data, "author").split(" and ")), + language=_get_line(data, "language"), + publisher=_get_line(data, "publisher"), + year=_get_line(data, "year"), + pages=_get_line(data, "pages"), + ) + + +class RDSData: + def __init__(self): + self.retlist = [] + + def transform(self, data: str): + # rds_availability = RDS_AVAIL_DATA() + # rds_data = RDS_GENERIC_DATA() + print(data) + + def __get_raw_data(data: str) -> list: + # create base data to be turned into pydantic classes + data = data.split("RDS ----------------------------------")[1] + edata = data.strip() + edata = edata.split("\n", 9)[9] + edata = edata.split("\n")[1:] + entry_1 = edata[0] + edata = edata[1:] + entry_2 = "".join(edata) + edata = [] + edata.append(entry_1) + edata.append(entry_2) + return edata + + ret_data = __get_raw_data(data) + # assign data[1] to RDS_AVAIL_DATA + # assign data[0] to RDS_DATA + self.rds_data = RDS_GENERIC_DATA().import_from_dict(ret_data[1]) + self.rds_availability = RDS_AVAIL_DATA().import_from_dict(ret_data[0]) + self.retlist.append(self.rds_availability) + self.retlist.append(self.rds_data) + return self + + def return_data(self, option=None): + if option == "rds_availability": + return self.retlist[0] + elif option == "rds_data": + return self.retlist[1] + else: + return {"rds_availability": self.retlist[0], "rds_data": self.retlist[1]} + + +class DictToTable: + def __init__(self): + self.work_author = None + self.section_author = None + self.year = None + self.edition = None + self.work_title = None + self.chapter_title = None + self.location = None + self.publisher = None + self.signature = None + self.type = None + self.pages = None + self.issue = None + self.isbn = None + + def makeResult(self): + data = { + "work_author": self.work_author, + "section_author": self.section_author, + "year": self.year, + "edition": self.edition, + "work_title": self.work_title, + "chapter_title": self.chapter_title, + "location": self.location, + "publisher": self.publisher, + "signature": self.signature, + "issue": self.issue, + "pages": self.pages, + "isbn": self.isbn, + "type": self.type, + } + data = {k: v for k, v in data.items() if v is not None} + return data + + def reset(self): + for key in self.__dict__: + setattr(self, key, None) + + def transform(self, data: dict): + mode = data["mode"] + self.reset() + if mode == "book": + return self.book_assign(data) + elif mode == "hg": + return self.hg_assign(data) + elif mode == "zs": + return self.zs_assign(data) + else: + return None + + def book_assign(self, data): + self.type = "book" + self.work_author = data["book_author"] + self.signature = data["book_signature"] + self.location = data["book_place"] + self.year = data["book_year"] + self.work_title = data["book_title"] + self.edition = data["book_edition"] + self.pages = data["book_pages"] + self.publisher = data["book_publisher"] + self.isbn = data["book_isbn"] + return self.makeResult() + + def hg_assign(self, data): + self.type = "hg" + self.section_author = data["hg_author"] + self.work_author = data["hg_editor"] + self.year = data["hg_year"] + self.work_title = data["hg_title"] + self.publisher = data["hg_publisher"] + self.location = data["hg_place"] + self.edition = data["hg_edition"] + self.chapter_title = data["hg_chaptertitle"] + self.pages = data["hg_pages"] + self.signature = data["hg_signature"] + self.isbn = data["hg_isbn"] + return self.makeResult() + + def zs_assign(self, data): + self.type = "zs" + self.section_author = data["zs_author"] + self.chapter_title = data["zs_chapter_title"] + self.location = data["zs_place"] + self.issue = data["zs_issue"] + self.pages = data["zs_pages"] + self.publisher = data["zs_publisher"] + self.isbn = data["zs_isbn"] + + self.year = data["zs_year"] + self.signature = data["zs_signature"] + self.work_title = data["zs_title"] + return self.makeResult() + + +if __name__ == "__main__": + with open("daiadata", "r") as f: + data = f.read() + + ret = RDSData().transform(data) + data = ret.return_data("rds_availability") + # log.debug(data) diff --git a/src/parsers/word_parser.py b/src/parsers/word_parser.py new file mode 100644 index 0000000..44fb2d8 --- /dev/null +++ b/src/parsers/word_parser.py @@ -0,0 +1,373 @@ +import zipfile +from typing import Any, Optional + +import fitz # PyMuPDF +import pandas as pd +from bs4 import BeautifulSoup +from docx import Document + +from src.core.models import Book, SemapDocument +from src.shared.logging import log + + +def word_docx_to_csv(path: str) -> list[pd.DataFrame]: + doc = Document(path) + tables = doc.tables + m_data = [] + for table in tables: + data = [] + for row in table.rows: + row_data: list[Any] = [] + for cell in row.cells: + text = cell.text + + text = text.replace("\n", "") + row_data.append(text) + # if text == "Ihr Fach:": + # row_data.append(get_fach(path)) + data.append(row_data) + df = pd.DataFrame(data) + df.columns = df.iloc[0] + df = df.iloc[1:] + + m_data.append(df) + + return m_data + + +def get_fach(path: str) -> Optional[str]: + document = zipfile.ZipFile(path) + xml_data = document.read("word/document.xml") + document.close() + + soup = BeautifulSoup(xml_data, "xml") + # text we need is in -> w:r -> w:t + paragraphs = soup.find_all("w:p") + for para in paragraphs: + para_id = para.get("w14:paraId") + if para_id == "12456A32": + # get the data in the w:t + for run in para.find_all("w:r"): + data = run.find("w:t") + if data and data.contents: + return data.contents[0] + return None + + +def makeDict() -> dict[str, Optional[str]]: + return { + "work_author": None, + "section_author": None, + "year": None, + "edition": None, + "work_title": None, + "chapter_title": None, + "location": None, + "publisher": None, + "signature": None, + "issue": None, + "pages": None, + "isbn": None, + "type": None, + } + + +def tuple_to_dict(tlist: tuple, type: str) -> list[dict[str, Optional[str]]]: + ret: list[dict[str, Optional[str]]] = [] + for line in tlist: + data = makeDict() + if type == "Monografien": + data["type"] = type + data["work_author"] = line[0] + data["year"] = line[1] + data["edition"] = line[2] + data["work_title"] = line[3] + data["location"] = line[4] + data["publisher"] = line[5] + data["signature"] = line[6] + data["pages"] = line[7] + elif type == "Herausgeberwerke": + data["type"] = type + data["section_author"] = line[0] + data["year"] = line[1] + data["edition"] = line[2] + data["chapter_title"] = line[3] + data["work_author"] = line[4] + data["work_title"] = line[5] + data["location"] = line[6] + data["publisher"] = line[7] + data["signature"] = line[9] + data["pages"] = line[8] + elif type == "Zeitschriftenaufsätze": + data["type"] = type + data["section_author"] = line[0] + data["year"] = line[1] + data["issue"] = line[2] + data["chapter_title"] = line[3] + data["work_title"] = line[4] + data["location"] = line[5] + data["publisher"] = line[6] + data["signature"] = line[8] + data["pages"] = line[7] + ret.append(data) + return ret + + +def elsa_word_to_csv(path: str) -> tuple[list[dict[str, Optional[str]]], str]: + doc = Document(path) + # # print all lines in doc + doctype = [para.text for para in doc.paragraphs if para.text != ""][-1] + tuples = { + "Monografien": ("", "", "", "", "", "", "", "", ""), + "Herausgeberwerke": ("", "", "", "", "", "", "", "", "", "", ""), + "Zeitschriftenaufsätze": ("", "", "", "", "", "", "", "", "", ""), + } + tables = doc.tables + + m_data: list[pd.DataFrame] = [] + for table in tables: + data: list[list[str]] = [] + for row in table.rows: + row_data: list[str] = [] + for cell in row.cells: + text = cell.text + text = text.replace("\n", "") + text = text.replace("\u2002", "") + row_data.append(text) + data.append(row_data) + df = pd.DataFrame(data) + df.columns = df.iloc[0] + df = df.iloc[1:] + m_data.append(df) + df = m_data[0] + # split df to rows + data = [ + row for row in df.itertuples(index=False, name=None) if row != tuples[doctype] + ] + # log.debug(data) + return tuple_to_dict(data, doctype), doctype + + +def word_to_semap(word_path: str, ai: bool = True) -> SemapDocument: + log.info("Parsing Word Document {}", word_path) + semap = SemapDocument() + df = word_docx_to_csv(word_path) + apparatdata = df[0] + apparatdata = apparatdata.to_dict() + keys = list(apparatdata.keys()) + # print(apparatdata, keys) + + appdata = {keys[i]: keys[i + 1] for i in range(0, len(keys) - 1, 2)} + semap.phoneNumber = appdata["Telefon:"] + semap.subject = appdata["Ihr Fach:"] + semap.mail = appdata["Mailadresse:"] + semap.personName = ",".join(appdata["Ihr Name und Titel:"].split(",")[:-1]) + semap.personTitle = ",".join(appdata["Ihr Name und Titel:"].split(",")[-1:]).strip() + apparatdata = df[1] + apparatdata = apparatdata.to_dict() + keys = list(apparatdata.keys()) + appdata = {keys[i]: keys[i + 1] for i in range(0, len(keys), 2)} + semap.title = appdata["Veranstaltung:"] + semap.semester = appdata["Semester:"] + if ai: + semap.renameSemester + semap.nameSetter + + books = df[2] + booklist = [] + for i in range(len(books)): + if books.iloc[i].isnull().all(): + continue + data = books.iloc[i].to_dict() + book = Book() + book.from_dict(data) + if book.is_empty: + continue + elif not book.has_signature: + continue + else: + booklist.append(book) + log.info("Found {} books", len(booklist)) + semap.books = booklist + return semap + + +def pdf_to_semap(pdf_path: str, ai: bool = True) -> SemapDocument: + """ + Parse a Semesterapparat PDF like the sample you provided and return a SemapDocument. + - No external programs, only PyMuPDF. + - Robust to multi-line field values (e.g., hyphenated emails) and multi-line table cells. + - Works across multiple pages; headers only need to exist on the first page. + """ + doc = fitz.open(pdf_path) + semap = SemapDocument() + + # ---------- helpers ---------- + def _join_tokens(tokens: list[str]) -> str: + """Join tokens, preserving hyphen/URL joins across line wraps.""" + parts = [] + for tok in tokens: + if parts and ( + parts[-1].endswith("-") + or parts[-1].endswith("/") + or parts[-1].endswith(":") + ): + parts[-1] = parts[-1] + tok # no space after '-', '/' or ':' + else: + parts.append(tok) + return " ".join(parts).strip() + + def _extract_row_values_multiline( + page, labels: list[str], y_window: float = 24 + ) -> dict[str, str]: + """For a row of inline labels (e.g., Name/Fach/Telefon/Mail), grab text to the right of each label.""" + rects = [] + for lab in labels: + hits = page.search_for(lab) + if hits: + rects.append((lab, hits[0])) + if not rects: + return {} + + rects.sort(key=lambda t: t[1].x0) + words = page.get_text("words") + out = {} + for i, (lab, r) in enumerate(rects): + x0 = r.x1 + 1 + x1 = rects[i + 1][1].x0 - 1 if i + 1 < len(rects) else page.rect.width - 5 + y0 = r.y0 - 3 + y1 = r.y0 + y_window + toks = [w for w in words if x0 <= w[0] <= x1 and y0 <= w[1] <= y1] + toks.sort(key=lambda w: (w[1], w[0])) # line, then x + out[lab] = _join_tokens([w[4] for w in toks]) + return out + + def _compute_columns_from_headers(page0): + """Find column headers (once) and derive column centers + header baseline.""" + headers = [ + ("Autorenname(n):", "Autorenname(n):Nachname, Vorname"), + ("Jahr/Auflage", "Jahr/Auflage"), + ("Titel", "Titel"), + ("Ort und Verlag", "Ort und Verlag"), + ("Standnummer", "Standnummer"), + ("Interne Vermerke", "Interne Vermerke"), + ] + found = [] + for label, canon in headers: + rects = [ + r for r in page0.search_for(label) if r.y0 > 200 + ] # skip top-of-form duplicates + if rects: + found.append((canon, rects[0])) + found.sort(key=lambda t: t[1].x0) + cols = [(canon, r.x0, r.x1, (r.x0 + r.x1) / 2.0) for canon, r in found] + header_y = min(r.y0 for _, r in found) if found else 0 + return cols, header_y + + def _extract_table_rows_from_page( + page, cols, header_y, y_top_margin=5, y_bottom_margin=40, y_tol=26.0 + ): + """ + Group words into logical rows (tolerant to wrapped lines), then map each word + to the nearest column by x-center and join tokens per column. + """ + words = [ + w + for w in page.get_text("words") + if w[1] > header_y + y_top_margin + and w[3] < page.rect.height - y_bottom_margin + ] + + # group into row bands by y (tolerance big enough to capture wrapped lines, but below next row gap) + rows = [] + for w in sorted(words, key=lambda w: w[1]): + y = w[1] + for row in rows: + if abs(row["y_mean"] - y) <= y_tol: + row["ys"].append(y) + row["y_mean"] = sum(row["ys"]) / len(row["ys"]) + row["words"].append(w) + break + else: + rows.append({"y_mean": y, "ys": [y], "words": [w]}) + + # map to columns + join + joined_rows = [] + for row in rows: + rowdict = {canon: "" for canon, *_ in cols} + words_by_col = {canon: [] for canon, *_ in cols} + for w in sorted(row["words"], key=lambda w: (w[1], w[0])): + xmid = (w[0] + w[2]) / 2.0 + canon = min(cols, key=lambda c: abs(xmid - c[3]))[0] + words_by_col[canon].append(w[4]) + for canon, toks in words_by_col.items(): + rowdict[canon] = _join_tokens(toks) + if any(v for v in rowdict.values()): + joined_rows.append(rowdict) + return joined_rows + + # ---------- top-of-form fields ---------- + p0 = doc[0] + row1 = _extract_row_values_multiline( + p0, + ["Ihr Name und Titel:", "Ihr Fach:", "Telefon:", "Mailadresse:"], + y_window=22, + ) + row2 = _extract_row_values_multiline( + p0, ["Veranstaltung:", "Semester:"], y_window=20 + ) + + name_title = row1.get("Ihr Name und Titel:", "") or "" + semap.subject = row1.get("Ihr Fach:", None) + semap.phoneNumber = row1.get("Telefon:", None) # keep as-is (string like "682-308") + semap.mail = row1.get("Mailadresse:", None) + semap.personName = ",".join(name_title.split(",")[:-1]) if name_title else None + semap.personTitle = ( + ",".join(name_title.split(",")[-1:]).strip() if name_title else None + ) + + semap.title = row2.get("Veranstaltung:", None) + semap.semester = row2.get("Semester:", None) + + # ---------- table extraction (all pages) ---------- + cols, header_y = _compute_columns_from_headers(p0) + all_rows: list[dict[str, Any]] = [] + for pn in range(len(doc)): + all_rows.extend(_extract_table_rows_from_page(doc[pn], cols, header_y)) + + # drop the sub-header line "Nachname, Vorname" etc. + filtered = [] + for r in all_rows: + if r.get("Autorenname(n):Nachname, Vorname", "").strip() in ( + "", + "Nachname, Vorname", + ): + # skip if it's just the sub-header line + if all(not r[c] for c in r if c != "Autorenname(n):Nachname, Vorname"): + continue + filtered.append(r) + + # build Book objects (same filters as your word parser) + booklist: list[Book] = [] + for row in filtered: + b = Book() + b.from_dict(row) + if b.is_empty: + continue + if not b.has_signature: + continue + booklist.append(b) + + semap.books = booklist + + # keep parity with your post-processing + if ai: + _ = semap.renameSemester + _ = semap.nameSetter + + return semap + + +if __name__ == "__main__": + else_df = pdf_to_semap("C:/Users/aky547/Dokumente/testsemap.pdf") + # print(else_df) diff --git a/src/parsers/xml_parser.py b/src/parsers/xml_parser.py new file mode 100644 index 0000000..90ffdb0 --- /dev/null +++ b/src/parsers/xml_parser.py @@ -0,0 +1,67 @@ +import xml.etree.ElementTree as ET + +from src.core.models import Apparat, BookData, SemapDocument, XMLMailSubmission +from src.core.semester import Semester + + +def parse_xml_submission(xml_string: str) -> XMLMailSubmission: + """ + Parse an XML string representing a mail submission and return an XMLMailSubmission object. + """ + submission = XMLMailSubmission() + root = ET.fromstring(xml_string) + static_data = root.find("static") + static_info = {child.tag: child.text for child in static_data} + books = root.find("books") + books_info = [] + for book in books: + book_details = {detail.tag: detail.text for detail in book} + book = BookData( + author=book_details.get("authorname"), + year=book_details.get("year").split("/")[0] + if "/" in book_details.get("year") + else book_details.get("year"), + edition=book_details.get("year").split("/")[1] + if "/" in book_details.get("year") + else None, + title=book_details.get("title"), + signature=book_details.get("signature"), + ) + books_info.append(book) + # Extract static data + submission.name = static_info.get("name") + submission.lastname = static_info.get("lastname") + submission.title = static_info.get("title") + submission.telno = int(static_info.get("telno")) + submission.email = static_info.get("mail") + submission.app_name = static_info.get("apparatsname") + submission.subject = static_info.get("subject") + sem_year = static_info.get("semester").split()[1] + sem_term = static_info.get("semester").split()[0] + submission.semester = Semester(semester=sem_term, year=int(sem_year)) + submission.books = books_info + # Extract book information + # book_info = [] + # for book in books: + # book_details = {detail.tag: detail.text for detail in book} + # book_info.append(book_details) + return submission + + +def eml_parser(path: str) -> XMLMailSubmission: + with open(path, "r", encoding="utf-8") as file: + xml_content = file.read().split("\n\n", 1)[1] # Skip headers + print("EML content loaded, parsing XML...") + print(xml_content) + return parse_xml_submission(xml_content) + + +def eml_to_semap(xml_mail: XMLMailSubmission) -> SemapDocument: + submission = eml_parser(xml_mail) + semap_doc = SemapDocument( + # prof=Prof(name=submission.name, lastname=submission.lastname, email=submission.email), + apparat=Apparat(name=submission.app_name, subject=submission.subject), + semester=submission.semester, + books=submission.books, + ) + return semap_doc diff --git a/src/services/__init__.py b/src/services/__init__.py new file mode 100644 index 0000000..dec0a5f --- /dev/null +++ b/src/services/__init__.py @@ -0,0 +1,16 @@ +"""External service integrations and API clients.""" + +from .catalogue import Catalogue +from .sru import SWB +from .lehmanns import LehmannsClient +from .zotero import ZoteroController +from .webrequest import BibTextTransformer, WebRequest + +__all__ = [ + "Catalogue", + "SWB", + "LehmannsClient", + "ZoteroController", + "BibTextTransformer", + "WebRequest", +] diff --git a/src/services/catalogue.py b/src/services/catalogue.py new file mode 100644 index 0000000..ff3b306 --- /dev/null +++ b/src/services/catalogue.py @@ -0,0 +1,292 @@ +from typing import List + +import regex +import requests +from bs4 import BeautifulSoup + +from src.core.models import BookData as Book +from src.shared.logging import log + +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" + + +class Catalogue: + def __init__(self, timeout=15): + self.timeout = timeout + reachable = self.check_connection() + if not reachable: + log.error("No internet connection available.") + raise ConnectionError("No internet connection available.") + + 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 + + def search(self, link: str): + response = requests.get(link, timeout=self.timeout) + return response.text + + def get_book_links(self, searchterm: str) -> List[str]: + response = self.search_book(searchterm) + soup = BeautifulSoup(response, "html.parser") + links = soup.find_all("a", class_="title getFull") + res: List[str] = [] + for link in links: + res.append(BASE + link["href"]) # type: ignore + return res + + def get_book(self, searchterm: str): + log.info(f"Searching for term: {searchterm}") + + links = self.get_book_links(searchterm) + print(links) + for elink in links: + result = self.search(elink) + # in result search for class col-xs-12 rds-dl RDS_LOCATION + # if found, return text of href + soup = BeautifulSoup(result, "html.parser") + + # Optional (unchanged): title and ppn if you need them + title_el = soup.find("div", class_="headline text") + title = title_el.get_text(strip=True) if title_el else None + + ppn_el = soup.find( + "div", class_="col-xs-12 col-md-5 col-lg-4 rds-dl-head RDS_PPN" + ) + # in ppn_el, get text of div col-xs-12 col-md-7 col-lg-8 rds-dl-panel + ppn = ( + ppn_el.find_next_sibling( + "div", class_="col-xs-12 col-md-7 col-lg-8 rds-dl-panel" + ).get_text(strip=True) + if ppn_el + else None + ) + + # get edition text at div class col-xs-12 col-md-5 col-lg-4 rds-dl-head RDS_EDITION + edition_el = soup.find( + "div", class_="col-xs-12 col-md-5 col-lg-4 rds-dl-head RDS_EDITION" + ) + edition = ( + edition_el.find_next_sibling( + "div", class_="col-xs-12 col-md-7 col-lg-8 rds-dl-panel" + ).get_text(strip=True) + if edition_el + else None + ) + + authors = soup.find_all( + "div", class_="col-xs-12 col-md-5 col-lg-4 rds-dl-head RDS_PERSON" + ) + author = None + if authors: + # get the names of the a href links in the div col-xs-12 col-md-7 col-lg-8 rds-dl-panel + author_names = [] + for author in authors: + panel = author.find_next_sibling( + "div", class_="col-xs-12 col-md-7 col-lg-8 rds-dl-panel" + ) + if panel: + links = panel.find_all("a") + for link in links: + author_names.append(link.text.strip()) + author = ( + ";".join(author_names) if len(author_names) > 1 else author_names[0] + ) + signature = None + + panel = soup.select_one("div.panel-body") + if panel: + # Collect the RDS_* blocks in order, using the 'space' divs as separators + groups = [] + cur = {} + for node in panel.select( + "div.rds-dl.RDS_SIGNATURE, div.rds-dl.RDS_STATUS, div.rds-dl.RDS_LOCATION, div.col-xs-12.space" + ): + classes = node.get("class", []) + # Separator between entries + if "space" in classes: + if cur: + groups.append(cur) + cur = {} + continue + + # Read the value from the corresponding panel cell + val_el = node.select_one(".rds-dl-panel") + val = ( + val_el.get_text(" ", strip=True) + if val_el + else node.get_text(" ", strip=True) + ) + + if "RDS_SIGNATURE" in classes: + cur["signature"] = val + elif "RDS_STATUS" in classes: + cur["status"] = val + elif "RDS_LOCATION" in classes: + cur["location"] = val + + if cur: # append the last group if not followed by a space + groups.append(cur) + + # Find the signature for the entry whose location mentions "Semesterapparat" + for g in groups: + loc = g.get("location", "").lower() + if "semesterapparat" in loc: + signature = g.get("signature") + return Book( + title=title, + ppn=ppn, + signature=signature, + library_location=loc.split("-")[-1], + link=elink, + author=author, + edition=edition, + ) + else: + return Book( + title=title, + ppn=ppn, + signature=signature, + library_location=loc.split("\n\n")[-1], + link=elink, + author=author, + edition=edition, + ) + + def get(self, ppn: str) -> Book | None: + # based on PPN, get title, people, edition, year, language, pages, isbn, + link = f"https://rds.ibs-bw.de/phfreiburg/opac/RDSIndexrecord/{ppn}" + result = self.search(link) + soup = BeautifulSoup(result, "html.parser") + + def get_ppn(self, searchterm: str) -> str | None: + links = self.get_book_links(searchterm) + ppn = None + for link in links: + result = self.search(link) + soup = BeautifulSoup(result, "html.parser") + print(link) + ppn = link.split("/")[-1] + if ppn and regex.match(r"^\d{8,10}[X\d]?$", ppn): + return ppn + return ppn + + def get_semesterapparat_number(self, searchterm: str) -> int: + links = self.get_book_links(searchterm) + for link in links: + result = self.search(link) + # in result search for class col-xs-12 rds-dl RDS_LOCATION + # if found, return text of href + soup = BeautifulSoup(result, "html.parser") + + locations = soup.find_all("div", class_="col-xs-12 rds-dl RDS_LOCATION") + for location_el in locations: + if "Semesterapparat-" in location_el.text: + match = regex.search(r"Semesterapparat-(\d+)", location_el.text) + if match: + return int(match.group(1)) + if "Handbibliothek-" in location_el.text: + return location_el.text.strip().split("\n\n")[-1].strip() + return location_el.text.strip().split("\n\n")[-1].strip() + return 0 + + def get_author(self, link: str) -> str: + links = self.get_book_links(f"kid:{link}") + author = None + for link in links: + # print(link) + result = self.search(link) + soup = BeautifulSoup(result, "html.parser") + # get all authors, return them as a string seperated by ; + authors = soup.find_all( + "div", class_="col-xs-12 col-md-5 col-lg-4 rds-dl-head RDS_PERSON" + ) + if authors: + # get the names of the a href links in the div col-xs-12 col-md-7 col-lg-8 rds-dl-panel + author_names = [] + for author in authors: + panel = author.find_next_sibling( + "div", class_="col-xs-12 col-md-7 col-lg-8 rds-dl-panel" + ) + if panel: + links = panel.find_all("a") + for link in links: + author_names.append(link.text.strip()) + author = "; ".join(author_names) + return author + + def get_signature(self, isbn: str): + links = self.get_book_links(f"{isbn}") + signature = None + for link in links: + result = self.search(link) + soup = BeautifulSoup(result, "html.parser") + panel = soup.select_one("div.panel-body") + if panel: + # Collect the RDS_* blocks in order, using the 'space' divs as separators + groups = [] + cur = {} + for node in panel.select( + "div.rds-dl.RDS_SIGNATURE, div.rds-dl.RDS_STATUS, div.rds-dl.RDS_LOCATION, div.col-xs-12.space" + ): + classes = node.get("class", []) + # Separator between entries + if "space" in classes: + if cur: + groups.append(cur) + cur = {} + continue + + # Read the value from the corresponding panel cell + val_el = node.select_one(".rds-dl-panel") + val = ( + val_el.get_text(" ", strip=True) + if val_el + else node.get_text(" ", strip=True) + ) + + if "RDS_SIGNATURE" in classes: + cur["signature"] = val + elif "RDS_STATUS" in classes: + cur["status"] = val + elif "RDS_LOCATION" in classes: + cur["location"] = val + + if cur: # append the last group if not followed by a space + groups.append(cur) + + # Find the signature for the entry whose location mentions "Semesterapparat" + for g in groups: + print(g) + loc = g.get("location", "").lower() + if "semesterapparat" in loc: + signature = g.get("signature") + return signature + else: + signature = g.get("signature") + return signature + print("No signature found") + return signature + + def in_library(self, ppn: str) -> bool: + if ppn is None: + return False + links = self.get_book_links(f"kid:{ppn}") + return len(links) > 0 + + def get_location(self, ppn: str) -> str | None: + if ppn is None: + return None + link = self.get_book(f"{ppn}") + if link is None: + return None + return link.library_location diff --git a/src/services/lehmanns.py b/src/services/lehmanns.py new file mode 100644 index 0000000..9640275 --- /dev/null +++ b/src/services/lehmanns.py @@ -0,0 +1,312 @@ +from __future__ import annotations + +import re +from dataclasses import asdict, dataclass, field +from typing import Iterable, List, Optional +from urllib.parse import quote_plus, urljoin + +import httpx +from bs4 import BeautifulSoup + +from src.core.models import BookData + +BASE = "https://www.lehmanns.de" +SEARCH_URL = "https://www.lehmanns.de/search/quick?mediatype_id=&q=" + + +@dataclass +class LehmannsSearchResult: + title: str + url: str + + # Core fields from the listing card + year: Optional[int] = None + edition: Optional[int] = None + publisher: Optional[str] = None + isbn13: Optional[str] = None + + # Extras from the listing card + description: Optional[str] = None + authors: list[str] = field(default_factory=list) + media_type: Optional[str] = None + book_format: Optional[str] = None + price_eur: Optional[float] = None + currency: str = "EUR" + image: Optional[str] = None + + # From detail page: + pages: Optional[str] = None # " Seiten" + buyable: bool = True # set in enrich_pages (detail page) + unavailable_hint: Optional[str] = ( + None # e.g. "Titel ist leider vergriffen; keine Neuauflage" + ) + + def to_dict(self) -> dict: + return asdict(self) + + +class LehmannsClient: + """Scrapes quick-search results, then enriches (and filters) via product pages.""" + + def __init__(self, timeout: float = 20.0): + self.client = httpx.Client( + headers={ + "User-Agent": ( + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 " + "(KHTML, like Gecko) Chrome/124.0 Safari/537.36" + ), + "Accept-Language": "de-DE,de;q=0.9,en;q=0.8", + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", + }, + timeout=timeout, + follow_redirects=True, + ) + + def close(self): + self.client.close() + + def __enter__(self): + return self + + def __exit__(self, *exc): + self.close() + + # ------------------- Search (listing) ------------------- + + def build_search_url(self, title: str) -> str: + # spaces -> '+' + return SEARCH_URL + quote_plus(title) + + def search_by_title( + self, + title: str, + limit: Optional[int] = None, + strict: bool = False, + only_latest: bool = True, + ) -> List[BookData]: + """ + Parse the listing page only (no availability check here). + Use enrich_pages(...) afterwards to fetch detail pages, add 'pages', + and drop unbuyable items. + """ + url = self.build_search_url(title=title) + html = self._get(url) + if not html: + return [] + results = self._parse_results(html) + self.enrich_pages(results) + + results = [BookData().from_LehmannsSearchResult(r) for r in results] + if strict: + # filter results to only those with exact title match (case-insensitive) + title_lower = title.lower() + results = [r for r in results if r.title and r.title.lower() == title_lower] + # results = [r for r in results if r.buyable] + return results + if limit is not None: + results = results[: max(0, limit)] + if only_latest and len(results) > 1: + # keep only the latest edition (highest edition number) + results.sort(key=lambda r: (r.edition_number or 0), reverse=True) + results = [results[0]] + return results + + # ------------------- Detail enrichment & filtering ------------------- + + def enrich_pages( + self, results: Iterable[LehmannsSearchResult], drop_unbuyable: bool = True + ) -> List[LehmannsSearchResult]: + """ + Fetch each result.url, extract: + - pages: from ... + - availability: from
  • ...
  • + * if it contains "Titel ist leider vergriffen", mark buyable=False + * if it also contains "keine Neuauflage", set unavailable_hint accordingly + If drop_unbuyable=True, exclude non-buyable results from the returned list. + """ + enriched: List[LehmannsSearchResult] = [] + for r in results: + try: + html = self._get(r.url) + if not html: + # Can't verify; keep as-is when not dropping, else skip + if not drop_unbuyable: + enriched.append(r) + continue + + soup = BeautifulSoup(html, "html.parser") # type: ignore + + # Pages + pages_node = soup.select_one( # type: ignore + "span.book-meta.meta-seiten[itemprop='numberOfPages'], " + "span.book-meta.meta-seiten[itemprop='numberofpages'], " + ".meta-seiten [itemprop='numberOfPages'], " + ".meta-seiten[itemprop='numberOfPages'], " + ".book-meta.meta-seiten" + ) + if pages_node: + text = pages_node.get_text(" ", strip=True) + m = re.search(r"\d+", text) + if m: + r.pages = f"{m.group(0)} Seiten" + + # Availability via li.availability-3 + avail_li = soup.select_one("li.availability-3") # type: ignore + if avail_li: + avail_text = " ".join( + avail_li.get_text(" ", strip=True).split() + ).lower() + if "titel ist leider vergriffen" in avail_text: + r.buyable = False + if "keine neuauflage" in avail_text: + r.unavailable_hint = ( + "Titel ist leider vergriffen; keine Neuauflage" + ) + else: + r.unavailable_hint = "Titel ist leider vergriffen" + + # Append or drop + if (not drop_unbuyable) or r.buyable: + enriched.append(r) + + except Exception: + # On any per-item error, keep the record if not dropping; else skip + if not drop_unbuyable: + enriched.append(r) + continue + + return enriched + + # ------------------- Internals ------------------- + + def _get(self, url: str) -> Optional[str]: + try: + r = self.client.get(url) + r.encoding = "utf-8" + if r.status_code == 200 and "text/html" in ( + r.headers.get("content-type") or "" + ): + return r.text + except httpx.HTTPError: + pass + return None + + def _parse_results(self, html: str) -> List[LehmannsSearchResult]: + soup = BeautifulSoup(html, "html.parser") + results: list[LehmannsSearchResult] = [] + + for block in soup.select("div.info-block"): + a = block.select_one(".title a[href]") + if not a: + continue + url = urljoin(BASE, a["href"].strip()) + base_title = (block.select_one(".title [itemprop='name']") or a).get_text( # type: ignore + strip=True + ) + + # Alternative headline => extend title + alt_tag = block.select_one(".description[itemprop='alternativeHeadline']") # type: ignore + alternative_headline = alt_tag.get_text(strip=True) if alt_tag else None + title = ( + f"{base_title} : {alternative_headline}" + if alternative_headline + else base_title + ) + description = alternative_headline + + # Authors from .author + authors: list[str] = [] + author_div = block.select_one("div.author") # type: ignore + if author_div: + t = author_div.get_text(" ", strip=True) + t = re.sub(r"^\s*von\s+", "", t, flags=re.I) + for part in re.split(r"\s*;\s*|\s*&\s*|\s+und\s+", t): + name = " ".join(part.split()) + if name: + authors.append(name) + + # Media + format + media_type = None + book_format = None + type_text = block.select_one(".type") # type: ignore + if type_text: + t = type_text.get_text(" ", strip=True) + m = re.search(r"\b(Buch|eBook|Hörbuch)\b", t) + if m: + media_type = m.group(1) + fm = re.search(r"\(([^)]+)\)", t) + if fm: + book_format = fm.group(1).strip().upper() + + # Year + year = None + y = block.select_one("[itemprop='copyrightYear']") # type: ignore + if y: + try: + year = int(y.get_text(strip=True)) + except ValueError: + pass + + # Edition + edition = None + ed = block.select_one("[itemprop='bookEdition']") # type: ignore + if ed: + m = re.search(r"\d+", ed.get_text(strip=True)) + if m: + edition = int(m.group()) + + # Publisher + publisher = None + pub = block.select_one( # type: ignore + ".publisherprop [itemprop='name']" + ) or block.select_one(".publisher [itemprop='name']") # type: ignore + if pub: + publisher = pub.get_text(strip=True) + + # ISBN-13 + isbn13 = None + isbn_tag = block.select_one(".isbn [itemprop='isbn'], [itemprop='isbn']") # type: ignore + if isbn_tag: + digits = re.sub(r"[^0-9Xx]", "", isbn_tag.get_text(strip=True)) + m = re.search(r"(97[89]\d{10})", digits) + if m: + isbn13 = m.group(1) + + # Price (best effort) + price_eur = None + txt = block.get_text(" ", strip=True) + mprice = re.search(r"(\d{1,3}(?:\.\d{3})*,\d{2})\s*€", txt) + if not mprice and block.parent: + sib = block.parent.get_text(" ", strip=True) + mprice = re.search(r"(\d{1,3}(?:\.\d{3})*,\d{2})\s*€", sib) + if mprice: + num = mprice.group(1).replace(".", "").replace(",", ".") + try: + price_eur = float(num) + except ValueError: + pass + + # Image (best-effort) + image = None + left_img = block.find_previous("img") # type: ignore + if left_img and left_img.get("src"): + image = urljoin(BASE, left_img["src"]) + + results.append( + LehmannsSearchResult( + title=title, + url=url, + description=description, + authors=authors, + media_type=media_type, + book_format=book_format, + year=year, + edition=edition, + publisher=publisher, + isbn13=isbn13, + price_eur=price_eur, + image=image, + ) + ) + + return results diff --git a/src/services/openai.py b/src/services/openai.py new file mode 100644 index 0000000..715be68 --- /dev/null +++ b/src/services/openai.py @@ -0,0 +1,58 @@ +import json +from typing import Any + +from openai import OpenAI + +from src import settings + + +def init_client() -> OpenAI: + """Initialize the OpenAI client with the API key and model from settings.""" + global client, model, api_key + if not settings.openAI.api_key: + raise ValueError("OpenAI API key is not set in the configuration.") + if not settings.openAI.model: + raise ValueError("OpenAI model is not set in the configuration.") + + model = settings.openAI.model + api_key = settings.openAI.api_key + client = OpenAI(api_key=api_key) + return client + + +def run_shortener(title: str, length: int) -> list[dict[str, Any]]: + client = init_client() + response = client.responses.create( # type: ignore + model=model, + instructions="""you are a sentence shortener. The next message will contain the string to shorten and the length limit. +You need to shorten the string to be under the length limit, while keeping as much detail as possible. The result may NOT be longer than the length limit. +based on that, please reply only the shortened string. Give me 5 choices. if the length is too long, discard the string and try another one.Return the data as a python list containing the result as {"shortened_string": shortened_string, "length": lengthasInt}. Do not return the answer in a codeblock, use a pure string. Before answering, check the results and if ANY is longer than the needed_length, discard all and try again""", + input=f'{{"string":"{title}", "needed_length":{length}}}', + ) + answers = response.output_text + return eval(answers) # type: ignore + # answers are strings in json format, so we need to convert them to a list of dicts + + +def name_tester(name: str) -> dict: + client = init_client() + response = client.responses.create( # type: ignore + model=model, + instructions="""you are a name tester, You are given a name and will have to split the name into first name, last name, and if present the title. Return the name in a json format with the keys "title", "first_name", "last_name". If no title is present, set title to none. Do NOt return the answer in a codeblock, use a pure json string. Assume the names are in the usual german naming scheme""", + input=f'{{"name":"{name}"}}', + ) + answers = response.output_text + + return json.loads(answers) + + +def semester_converter(semester: str) -> str: + client = init_client() + response = client.responses.create( # type: ignore + model=model, + instructions="""you are a semester converter. You will be given a string. Convert this into a string like this: SoSe YY or WiSe YY/YY+1. Do not return the answer in a codeblock, use a pure string.""", + input=semester, + ) + answers = response.output_text + + return answers diff --git a/src/services/sru.py b/src/services/sru.py new file mode 100644 index 0000000..9705995 --- /dev/null +++ b/src/services/sru.py @@ -0,0 +1,631 @@ +import re +import xml.etree.ElementTree as ET +from dataclasses import dataclass, field +from enum import Enum +from typing import Dict, Iterable, List, Optional, Tuple, Union + +import requests +from requests.adapters import HTTPAdapter + +# centralized logging used via src.shared.logging +from src.core.models import BookData +from src.shared.logging import log + +log # ensure imported logger is referenced + + +# ----------------------- +# Dataclasses +# ----------------------- + + +# --- MARC XML structures --- +@dataclass +class ControlField: + tag: str + value: str + + +@dataclass +class SubField: + code: str + value: str + + +@dataclass +class DataField: + tag: str + ind1: str = " " + ind2: str = " " + subfields: List[SubField] = field(default_factory=list) + + +@dataclass +class MarcRecord: + leader: str + controlfields: List[ControlField] = field(default_factory=list) + datafields: List[DataField] = field(default_factory=list) + + +# --- SRU record wrapper --- +@dataclass +class Record: + recordSchema: str + recordPacking: str + recordData: MarcRecord + recordPosition: int + + +@dataclass +class EchoedSearchRequest: + version: str + query: str + maximumRecords: int + recordPacking: str + recordSchema: str + + +@dataclass +class SearchRetrieveResponse: + version: str + numberOfRecords: int + records: List[Record] = field(default_factory=list) + echoedSearchRetrieveRequest: Optional[EchoedSearchRequest] = None + + +# ----------------------- +# Parser +# ----------------------- + +ZS = "http://www.loc.gov/zing/srw/" +MARC = "http://www.loc.gov/MARC21/slim" +NS = {"zs": ZS, "marc": MARC} + + +def _text(elem: Optional[ET.Element]) -> str: + return (elem.text or "") if elem is not None else "" + + +def _req_text(parent: ET.Element, path: str) -> Optional[str]: + el = parent.find(path, NS) + if el is None or el.text is None: + return None + return el.text + + +def parse_marc_record(record_el: ET.Element) -> MarcRecord: + """ + record_el is the element (default ns MARC in your sample) + """ + # leader + leader_text = _req_text(record_el, "marc:leader") or "" + + # controlfields + controlfields: List[ControlField] = [] + for cf in record_el.findall("marc:controlfield", NS): + tag = cf.get("tag", "").strip() + controlfields.append(ControlField(tag=tag, value=_text(cf))) + + # datafields + datafields: List[DataField] = [] + for df in record_el.findall("marc:datafield", NS): + tag = df.get("tag", "").strip() + ind1 = df.get("ind1") or " " + ind2 = df.get("ind2") or " " + subfields: List[SubField] = [] + for sf in df.findall("marc:subfield", NS): + code = sf.get("code", "") + subfields.append(SubField(code=code, value=_text(sf))) + datafields.append(DataField(tag=tag, ind1=ind1, ind2=ind2, subfields=subfields)) + + return MarcRecord( + leader=leader_text, controlfields=controlfields, datafields=datafields + ) + + +def parse_record(zs_record_el: ET.Element) -> Record: + recordSchema = _req_text(zs_record_el, "zs:recordSchema") or "" + recordPacking = _req_text(zs_record_el, "zs:recordPacking") or "" + + # recordData contains a MARC with default MARC namespace in your sample + recordData_el = zs_record_el.find("zs:recordData", NS) + if recordData_el is None: + raise ValueError("Missing zs:recordData") + + marc_record_el = recordData_el.find("marc:record", NS) + if marc_record_el is None: + # If the MARC record uses default ns (xmlns="...") ElementTree still needs the ns-qualified name + # We already searched with prefix; this covers both default and prefixed cases. + raise ValueError("Missing MARC21 record inside zs:recordData") + + marc_record = parse_marc_record(marc_record_el) + + recordPosition = int(_req_text(zs_record_el, "zs:recordPosition") or "0") + return Record( + recordSchema=recordSchema, + recordPacking=recordPacking, + recordData=marc_record, + recordPosition=recordPosition, + ) + + +def parse_echoed_request(root: ET.Element) -> Optional[EchoedSearchRequest]: + el = root.find("zs:echoedSearchRetrieveRequest", NS) + if el is None: + return None + + # Be permissive with missing fields + version = _text(el.find("zs:version", NS)) + query = _text(el.find("zs:query", NS)) + maximumRecords_text = _text(el.find("zs:maximumRecords", NS)) or "0" + recordPacking = _text(el.find("zs:recordPacking", NS)) + recordSchema = _text(el.find("zs:recordSchema", NS)) + + try: + maximumRecords = int(maximumRecords_text) + except ValueError: + maximumRecords = 0 + + return EchoedSearchRequest( + version=version, + query=query, + maximumRecords=maximumRecords, + recordPacking=recordPacking, + recordSchema=recordSchema, + ) + + +def parse_search_retrieve_response( + xml_str: Union[str, bytes], +) -> SearchRetrieveResponse: + root = ET.fromstring(xml_str) + + # Root is zs:searchRetrieveResponse + version = _req_text(root, "zs:version") + numberOfRecords = int(_req_text(root, "zs:numberOfRecords") or "0") + + records_parent = root.find("zs:records", NS) + records: List[Record] = [] + if records_parent is not None: + for r in records_parent.findall("zs:record", NS): + records.append(parse_record(r)) + + echoed = parse_echoed_request(root) + + return SearchRetrieveResponse( + version=version, + numberOfRecords=numberOfRecords, + records=records, + echoedSearchRetrieveRequest=echoed, + ) + + +# --- Query helpers over MarcRecord --- + + +def iter_datafields( + rec: MarcRecord, + tag: Optional[str] = None, + ind1: Optional[str] = None, + ind2: Optional[str] = None, +) -> Iterable[DataField]: + """Yield datafields, optionally filtered by tag/indicators.""" + for df in rec.datafields: + if tag is not None and df.tag != tag: + continue + if ind1 is not None and df.ind1 != ind1: + continue + if ind2 is not None and df.ind2 != ind2: + continue + yield df + + +def subfield_values( + rec: MarcRecord, + tag: str, + code: str, + *, + ind1: Optional[str] = None, + ind2: Optional[str] = None, +) -> List[str]: + """All values for subfield `code` in every `tag` field (respecting indicators).""" + out: List[str] = [] + for df in iter_datafields(rec, tag, ind1, ind2): + out.extend(sf.value for sf in df.subfields if sf.code == code) + return out + + +def first_subfield_value( + rec: MarcRecord, + tag: str, + code: str, + *, + ind1: Optional[str] = None, + ind2: Optional[str] = None, + default: Optional[str] = None, +) -> Optional[str]: + """First value for subfield `code` in `tag` (respecting indicators).""" + for df in iter_datafields(rec, tag, ind1, ind2): + for sf in df.subfields: + if sf.code == code: + return sf.value + return default + + +def find_datafields_with_subfields( + rec: MarcRecord, + tag: str, + *, + where_all: Optional[Dict[str, str]] = None, + where_any: Optional[Dict[str, str]] = None, + casefold: bool = False, + ind1: Optional[str] = None, + ind2: Optional[str] = None, +) -> List[DataField]: + """ + Return datafields of `tag` whose subfields match constraints: + - where_all: every (code -> exact value) must be present + - where_any: at least one (code -> exact value) present + Set `casefold=True` for case-insensitive comparison. + """ + where_all = where_all or {} + where_any = where_any or {} + matched: List[DataField] = [] + + for df in iter_datafields(rec, tag, ind1, ind2): + # Map code -> list of values (with optional casefold applied) + vals: Dict[str, List[str]] = {} + for sf in df.subfields: + v = sf.value.casefold() if casefold else sf.value + vals.setdefault(sf.code, []).append(v) + + ok = True + for c, v in where_all.items(): + vv = v.casefold() if casefold else v + if c not in vals or vv not in vals[c]: + ok = False + break + + if ok and where_any: + any_ok = any( + (c in vals) and ((v.casefold() if casefold else v) in vals[c]) + for c, v in where_any.items() + ) + if not any_ok: + ok = False + + if ok: + matched.append(df) + + return matched + + +def controlfield_value( + rec: MarcRecord, tag: str, default: Optional[str] = None +) -> Optional[str]: + """Get the first controlfield value by tag (e.g., '001', '005').""" + for cf in rec.controlfields: + if cf.tag == tag: + return cf.value + return default + + +def datafields_value( + data: List[DataField], code: str, default: Optional[str] = None +) -> Optional[str]: + """Get the first value for a specific subfield code in a list of datafields.""" + for df in data: + for sf in df.subfields: + if sf.code == code: + return sf.value + return default + + +def datafield_value( + df: DataField, code: str, default: Optional[str] = None +) -> Optional[str]: + """Get the first value for a specific subfield code in a datafield.""" + for sf in df.subfields: + if sf.code == code: + return sf.value + return default + + +def _smart_join_title(a: str, b: Optional[str]) -> str: + """ + Join 245 $a and $b with MARC-style punctuation. + If $b is present, join with ' : ' unless either side already supplies punctuation. + """ + a = a.strip() + if not b: + return a + b = b.strip() + if a.endswith((":", ";", "/")) or b.startswith((":", ";", "/")): + return f"{a} {b}" + return f"{a} : {b}" + + +def subfield_values_from_fields( + fields: Iterable[DataField], + code: str, +) -> List[str]: + """All subfield values with given `code` across a list of DataField.""" + return [sf.value for df in fields for sf in df.subfields if sf.code == code] + + +def first_subfield_value_from_fields( + fields: Iterable[DataField], + code: str, + default: Optional[str] = None, +) -> Optional[str]: + """First subfield value with given `code` across a list of DataField.""" + for df in fields: + for sf in df.subfields: + if sf.code == code: + return sf.value + return default + + +def subfield_value_pairs_from_fields( + fields: Iterable[DataField], + code: str, +) -> List[Tuple[DataField, str]]: + """ + Return (DataField, value) pairs for all subfields with `code`. + Useful if you need to know which field a value came from. + """ + out: List[Tuple[DataField, str]] = [] + for df in fields: + for sf in df.subfields: + if sf.code == code: + out.append((df, sf.value)) + return out + + +def book_from_marc(rec: MarcRecord) -> BookData: + # PPN from controlfield 001 + ppn = controlfield_value(rec, "001") + + # Title = 245 $a + 245 $b (if present) + t_a = first_subfield_value(rec, "245", "a") + t_b = first_subfield_value(rec, "245", "b") + title = _smart_join_title(t_a, t_b) if t_a else None + + # Signature = 924 where $9 == "Frei 129" → take that field's $g + frei_fields = find_datafields_with_subfields( + rec, "924", where_all={"9": "Frei 129"} + ) + signature = first_subfield_value_from_fields(frei_fields, "g") + + # Year = 264 $c (prefer ind2="1" publication; fallback to any 264) + year = first_subfield_value(rec, "264", "c", ind2="1") or first_subfield_value( + rec, "264", "c" + ) + isbn = subfield_values(rec, "020", "a") + mediatype = first_subfield_value(rec, "338", "a") + lang = subfield_values(rec, "041", "a") + authors = subfield_values(rec, "700", "a") + author = None + if authors: + author = "; ".join(authors) + + return BookData( + ppn=ppn, + title=title, + signature=signature, + edition=first_subfield_value(rec, "250", "a") or "", + year=year, + pages=first_subfield_value(rec, "300", "a") or "", + publisher=first_subfield_value(rec, "264", "b") or "", + isbn=isbn, + language=lang, + link="", + author=author, + media_type=mediatype, + ) + + +class SWBData(Enum): + URL = "https://sru.k10plus.de/opac-de-627!rec=1?version=1.1&operation=searchRetrieve&query={}&maximumRecords=100&recordSchema=marcxml" + ARGSCHEMA = "pica." + NAME = "SWB" + + +class DNBData(Enum): + URL = "https://services.dnb.de/sru/dnb?version=1.1&operation=searchRetrieve&query={}&maximumRecords=100&recordSchema=MARC21-xml" + ARGSCHEMA = "" + NAME = "DNB" + + +class SRUSite(Enum): + SWB = SWBData + DNB = DNBData + + +RVK_ALLOWED = r"[A-Z0-9.\-\/]" # conservative char set typically seen in RVK notations + + +def find_newer_edition( + swb_result: BookData, dnb_result: List[BookData] +) -> Optional[List[BookData]]: + """ + New edition if: + - year > swb.year OR + - edition_number > swb.edition_number + + Additional guards & preferences: + - If both have signatures and they differ, skip (not the same work). + - For duplicates (same ppn): keep the one that has a signature, and + prefer a signature that matches swb_result.signature. + - If multiple remain: keep the single 'latest' by (year desc, + edition_number desc, best-signature-match desc, has-signature desc). + """ + + def norm_sig(s: Optional[str]) -> str: + if not s: + return "" + # normalize: lowercase, collapse whitespace, keep alnum + a few separators + s = s.lower() + s = re.sub(r"\s+", " ", s).strip() + # remove obvious noise; adjust if your signature format differs + s = re.sub(r"[^a-z0-9\-_/\. ]+", "", s) + return s + + def has_sig(b: BookData) -> bool: + return bool(getattr(b, "signature", None)) + + def sig_matches_swb(b: BookData) -> bool: + if not has_sig(b) or not has_sig(swb_result): + return False + return norm_sig(b.signature) == norm_sig(swb_result.signature) + + def strictly_newer(b: BookData) -> bool: + by_year = ( + b.year is not None + and swb_result.year is not None + and b.year > swb_result.year + ) + by_edition = ( + b.edition_number is not None + and swb_result.edition_number is not None + and b.edition_number > swb_result.edition_number + ) + return by_year or by_edition + + swb_sig_norm = norm_sig(getattr(swb_result, "signature", None)) + + # 1) Filter to same-work AND newer + candidates: List[BookData] = [] + for b in dnb_result: + # Skip if both signatures exist and don't match (different work) + b_sig = getattr(b, "signature", None) + if b_sig and swb_result.signature: + if norm_sig(b_sig) != swb_sig_norm: + continue # not the same work + + # Keep only if newer by rules + if strictly_newer(b): + candidates.append(b) + + if not candidates: + return None + + # 2) Dedupe by PPN, preferring signature (and matching signature if possible) + by_ppn: dict[Optional[str], BookData] = {} + for b in candidates: + key = getattr(b, "ppn", None) + prev = by_ppn.get(key) + if prev is None: + by_ppn[key] = b + continue + + # Compute preference score for both + def ppn_pref_score(x: BookData) -> tuple[int, int]: + # (signature matches swb, has signature) + return (1 if sig_matches_swb(x) else 0, 1 if has_sig(x) else 0) + + if ppn_pref_score(b) > ppn_pref_score(prev): + by_ppn[key] = b + + deduped = list(by_ppn.values()) + if not deduped: + return None + + # 3) If multiple remain, keep only the latest one. + # Order: year desc, edition_number desc, signature-match desc, has-signature desc + def sort_key(b: BookData): + year = b.year if b.year is not None else -1 + ed = b.edition_number if b.edition_number is not None else -1 + sig_match = 1 if sig_matches_swb(b) else 0 + sig_present = 1 if has_sig(b) else 0 + return (year, ed, sig_match, sig_present) + + best = max(deduped, key=sort_key) + return [best] if best else None + + +class Api: + def __init__(self, site: str, url: str, prefix: str): + self.site = site + self.url = url + self.prefix = prefix + # Reuse TCP connections across requests for better performance + self._session = requests.Session() + # Slightly larger connection pool for concurrent calls + adapter = HTTPAdapter(pool_connections=10, pool_maxsize=20) + self._session.mount("http://", adapter) + self._session.mount("https://", adapter) + + def close(self): + try: + self._session.close() + except Exception: + pass + + def __del__(self): + # Best-effort cleanup + self.close() + + def get(self, query_args: Iterable[str]) -> List[Record]: + # if any query_arg ends with =, remove it + if self.site == "DNB": + args = [arg for arg in query_args if not arg.startswith("pica.")] + if args == []: + raise ValueError("DNB queries must include at least one search term") + query_args = args + # query_args = [f"{self.prefix}{arg}" for arg in query_args] + query = "+and+".join(query_args) + query = query.replace(" ", "%20").replace("&", "%26") + # query_args = [arg for arg in query_args if not arg.endswith("=")] + # query = "+and+".join(query_args) + # query = query.replace(" ", "%20").replace("&", "%26") + # insert the query into the url url is + url = self.url.format(query) + + log.debug(url) + headers = { + "User-Agent": f"{self.site} SRU Client, ", + "Accept": "application/xml", + "Accept-Charset": "latin1,utf-8;q=0.7,*;q=0.3", + } + # Use persistent session and set timeouts to avoid hanging + resp = self._session.get(url, headers=headers, timeout=(3.05, 60)) + if resp.status_code != 200: + raise Exception(f"Error fetching data from SWB: {resp.status_code}") + # Parse using raw bytes (original behavior) to preserve encoding edge cases + sr = parse_search_retrieve_response(resp.content) + return sr.records + + def getBooks(self, query_args: Iterable[str]) -> List[BookData]: + records: List[Record] = self.get(query_args) + # Avoid printing on hot paths; rely on logger if needed + log.debug(f"{self.site} found {len(records)} records for args={query_args}") + books: List[BookData] = [] + # extract title from query_args if present + title = None + for arg in query_args: + if arg.startswith("pica.tit="): + title = arg.split("=")[1] + break + for rec in records: + book = book_from_marc(rec.recordData) + books.append(book) + if title: + books = [ + b + for b in books + if b.title and b.title.lower().startswith(title.lower()) + ] + return books + + def getLinkForBook(self, book: BookData) -> str: + # Not implemented: depends on catalog front-end; return empty string for now + return "" + + +class SWB(Api): + def __init__(self): + self.site = SWBData.NAME.value + self.url = SWBData.URL.value + self.prefix = SWBData.ARGSCHEMA.value + super().__init__(self.site, self.url, self.prefix) diff --git a/src/services/webadis.py b/src/services/webadis.py new file mode 100644 index 0000000..af79650 --- /dev/null +++ b/src/services/webadis.py @@ -0,0 +1,35 @@ +from playwright.sync_api import sync_playwright + + +def get_book_medianr(signature: str, semesterapparat_nr: int, auth: tuple) -> str: + with sync_playwright() as playwright: + browser = playwright.chromium.launch(headless=True) + context = browser.new_context() + page = context.new_page() + page.goto( + "https://bsz.ibs-bw.de:22998/aDISWeb/app?service=direct/0/Home/$DirectLink&sp=SDAP42" + ) + page.get_by_role("textbox", name="Benutzer").fill(auth[0]) + page.get_by_role("textbox", name="Benutzer").press("Tab") + page.get_by_role("textbox", name="Kennwort").fill(auth[1]) + page.get_by_role("textbox", name="Kennwort").press("Enter") + page.get_by_role("button", name="Katalog").click() + page.get_by_role("textbox", name="Signatur").click() + page.get_by_role("textbox", name="Signatur").fill(signature) + page.get_by_role("textbox", name="Signatur").press("Enter") + book_list = page.locator("iframe").content_frame.get_by_role( + "cell", name="Bibliothek der Pädagogischen" + ) + # this will always find one result, we need to split the resulting text based on the entries that start with "* " + book_entries = book_list.inner_text().split("\n") + books = [] + for entry in book_entries: + if entry.startswith("* "): + books.append(entry) + for book in books: + if f"Semesterapparat: {semesterapparat_nr}" in book: + return book.split("* ")[1].split(":")[0] + + # --------------------- + context.close() + browser.close() diff --git a/src/services/webrequest.py b/src/services/webrequest.py new file mode 100644 index 0000000..947a8ba --- /dev/null +++ b/src/services/webrequest.py @@ -0,0 +1,314 @@ +from enum import Enum +from typing import Any, Optional, Union + +import requests +from bs4 import BeautifulSoup + +# import sleep_and_retry decorator to retry requests +from ratelimit import limits, sleep_and_retry + +from src.core.models import BookData +from src.shared.logging import log +from src.transformers import ARRAYData, BibTeXData, COinSData, RDSData, RISData +from src.transformers.transformers import RDS_AVAIL_DATA, RDS_GENERIC_DATA + +# logger.add(sys.stderr, format="{time} {level} {message}", level="INFO") + + +API_URL = "https://rds.ibs-bw.de/phfreiburg/opac/RDSIndexrecord/{}/" +PPN_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" +# +TITLE = "RDS_TITLE" +SIGNATURE = "RDS_SIGNATURE" +EDITION = "RDS_EDITION" +ISBN = "RDS_ISBN" +AUTHOR = "RDS_PERSON" + +HEADERS = { + "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 \ + (HTML, like Gecko) Chrome/44.0.2403.157 Safari/537.36", + "Accept-Language": "en-US, en;q=0.5", +} +RATE_LIMIT = 20 +RATE_PERIOD = 30 + + +class TransformerType(Enum): + ARRAY = "ARRAY" + COinS = "COinS" + BibTeX = "BibTeX" + RIS = "RIS" + RDS = "RDS" + + +class WebRequest: + def __init__(self) -> None: + """Request data from the web, and format it depending on the mode.""" + self.apparat = None + self.use_any = False # use any book that matches the search term + self.signature = None + self.ppn = None + self.data = None + self.timeout = 5 + log.info("Initialized WebRequest") + + @property + def use_any_book(self): + """use any book that matches the search term""" + self.use_any = True + log.info("Using any book") + return self + + def set_apparat(self, apparat: int) -> "WebRequest": + self.apparat = apparat + if int(self.apparat) < 10: + self.apparat = f"0{self.apparat}" + log.info(f"Set apparat to {self.apparat}") + return self + + def get_ppn(self, signature: str) -> "WebRequest": + self.signature = signature + if "+" in signature: + signature = signature.replace("+", "%2B") + if "doi.org" in signature: + signature = signature.split("/")[-1] + self.ppn = signature + return self + + @sleep_and_retry + @limits(calls=RATE_LIMIT, period=RATE_PERIOD) + def search_book(self, searchterm: str) -> str: + response = requests.get(PPN_URL.format(searchterm), timeout=self.timeout) + return response.text + + @sleep_and_retry + @limits(calls=RATE_LIMIT, period=RATE_PERIOD) + def search_ppn(self, ppn: str) -> str: + response = requests.get(API_URL.format(ppn), timeout=self.timeout) + return response.text + + def get_book_links(self, searchterm: str) -> list[str]: + response: str = self.search_book(searchterm) # type:ignore + soup = BeautifulSoup(response, "html.parser") + links = soup.find_all("a", class_="title getFull") + res: list[str] = [] + for link in links: + res.append(BASE + link["href"]) + return res + + @sleep_and_retry + @limits(calls=RATE_LIMIT, period=RATE_PERIOD) + def search(self, link: str) -> Optional[str]: + try: + response = requests.get(link, timeout=self.timeout) + return response.text + except requests.exceptions.RequestException as e: + log.error(f"Request failed: {e}") + return None + + def get_data(self) -> Optional[list[str]]: + links = self.get_book_links(self.ppn) + log.debug(f"Links: {links}") + return_data: list[str] = [] + for link in links: + result: str = self.search(link) # type:ignore + # in result search for class col-xs-12 rds-dl RDS_LOCATION + # if found, return text of href + soup = BeautifulSoup(result, "html.parser") + locations = soup.find_all("div", class_="col-xs-12 rds-dl RDS_LOCATION") + if locations: + for location in locations: + if "1. OG Semesterapparat" in location.text: + log.success("Found Semesterapparat, adding entry") + pre_tag = soup.find_all("pre") + return_data = [] + if pre_tag: + for tag in pre_tag: + data = tag.text.strip() + return_data.append(data) + return return_data + else: + log.error("No
     tag found")
    +                            return return_data
    +                    else:
    +                        item_location = location.find(
    +                            "div", class_="col-xs-12 col-md-7 col-lg-8 rds-dl-panel"
    +                        ).text.strip()
    +                        log.debug(f"Item location: {item_location}")
    +                        if self.use_any:
    +                            pre_tag = soup.find_all("pre")
    +                            if pre_tag:
    +                                for tag in pre_tag:
    +                                    data = tag.text.strip()
    +                                    return_data.append(data)
    +                                return return_data
    +                            else:
    +                                log.error("No 
     tag found")
    +                                raise ValueError("No 
     tag found")
    +                        elif f"Semesterapparat-{self.apparat}" in item_location:
    +                            pre_tag = soup.find_all("pre")
    +                            return_data = []
    +                            if pre_tag:
    +                                for tag in pre_tag:
    +                                    data = tag.text.strip()
    +                                    return_data.append(data)
    +                                return return_data
    +                            else:
    +                                log.error("No 
     tag found")
    +                                return return_data
    +                        else:
    +                            log.error(
    +                                f"Signature {self.signature} not found in {item_location}"
    +                            )
    +                            # return_data = []
    +
    +        return return_data
    +
    +    def get_data_elsa(self) -> Optional[list[str]]:
    +        links = self.get_book_links(self.ppn)
    +        for link in links:
    +            result = self.search(link)
    +            # in result search for class col-xs-12 rds-dl RDS_LOCATION
    +            # if found, return text of href
    +            soup = BeautifulSoup(result, "html.parser")
    +            locations = soup.find_all("div", class_="col-xs-12 rds-dl RDS_LOCATION")
    +            if locations:
    +                for _ in locations:
    +                    pre_tag = soup.find_all("pre")
    +                    return_data = []
    +                    if pre_tag:
    +                        for tag in pre_tag:
    +                            data = tag.text.strip()
    +                            return_data.append(data)
    +                        return return_data
    +                    else:
    +                        log.error("No 
     tag found")
    +                        return return_data
    +
    +
    +class BibTextTransformer:
    +    """Transforms data from the web into a BibText format.
    +        Valid Modes are ARRAY, COinS, BibTeX, RIS, RDS
    +    Raises:
    +        ValueError: Raised if mode is not in valid_modes
    +    """
    +
    +    valid_modes = [
    +        TransformerType.ARRAY,
    +        TransformerType.COinS,
    +        TransformerType.BibTeX,
    +        TransformerType.RIS,
    +        TransformerType.RDS,
    +    ]
    +
    +    def __init__(self, mode: TransformerType = TransformerType.ARRAY) -> None:
    +        self.mode = mode.value
    +        self.field = None
    +        self.signature = None
    +        if mode not in self.valid_modes:
    +            log.error(f"Mode {mode} not valid")
    +            raise ValueError(f"Mode {mode} not valid")
    +        self.data = None
    +        # self.bookdata = BookData(**self.data)
    +
    +    def use_signature(self, signature: str) -> "BibTextTransformer":
    +        """use the exact signature to search for the book"""
    +        self.signature = signature
    +        return self
    +
    +    def get_data(self, data: Optional[list[str]] = None) -> "BibTextTransformer":
    +        RIS_IDENT = "TY  -"
    +        ARRAY_IDENT = "[kid]"
    +        COinS_IDENT = "ctx_ver"
    +        BIBTEX_IDENT = "@book"
    +        RDS_IDENT = "RDS ---------------------------------- "
    +
    +        if data is None:
    +            self.data = None
    +            return self
    +
    +        if self.mode == "RIS":
    +            for line in data:
    +                if RIS_IDENT in line:
    +                    self.data = line
    +        elif self.mode == "ARRAY":
    +            for line in data:
    +                if ARRAY_IDENT in line:
    +                    self.data = line
    +        elif self.mode == "COinS":
    +            for line in data:
    +                if COinS_IDENT in line:
    +                    self.data = line
    +        elif self.mode == "BibTeX":
    +            for line in data:
    +                if BIBTEX_IDENT in line:
    +                    self.data = line
    +        elif self.mode == "RDS":
    +            for line in data:
    +                if RDS_IDENT in line:
    +                    self.data = line
    +        return self
    +
    +    def return_data(
    +        self, option: Any = None
    +    ) -> Union[
    +        Optional[BookData],
    +        Optional[RDS_GENERIC_DATA],
    +        Optional[RDS_AVAIL_DATA],
    +        None,
    +        dict[str, Union[RDS_AVAIL_DATA, RDS_GENERIC_DATA]],
    +    ]:
    +        """Return Data to caller.
    +
    +        Args:
    +            option (string, optional): Option for RDS as there are two filetypes. Use rds_availability or rds_data. Anything else gives a dict of both responses. Defaults to None.
    +
    +        Returns:
    +            BookData: a dataclass containing data about the book
    +        """
    +        if self.data is None:
    +            return None
    +        match self.mode:
    +            case "ARRAY":
    +                return ARRAYData(self.signature).transform(self.data)
    +            case "COinS":
    +                return COinSData().transform(self.data)
    +            case "BibTeX":
    +                return BibTeXData().transform(self.data)
    +            case "RIS":
    +                return RISData().transform(self.data)
    +            case "RDS":
    +                return RDSData().transform(self.data).return_data(option)
    +            case _:
    +                return None
    +
    +        # if self.mode == "ARRAY":
    +        #     return ARRAYData().transform(self.data)
    +        # elif self.mode == "COinS":
    +        #     return COinSData().transform(self.data)
    +        # elif self.mode == "BibTeX":
    +        #     return BibTeXData().transform(self.data)
    +        # elif self.mode == "RIS":
    +        #     return RISData().transform(self.data)
    +        # elif self.mode == "RDS":
    +        #     return RDSData().transform(self.data).return_data(option)
    +
    +
    +def cover(isbn):
    +    test_url = f"https://www.buchhandel.de/cover/{isbn}/{isbn}-cover-m.jpg"
    +    # log.debug(test_url)
    +    data = requests.get(test_url, stream=True)
    +    return data.content
    +
    +
    +def get_content(soup, css_class):
    +    return soup.find("div", class_=css_class).text.strip()
    +
    +
    +if __name__ == "__main__":
    +    # log.debug("main")
    +    link = "CU 8500 K64"
    +    data = WebRequest(71).get_ppn(link).get_data()
    +    bib = BibTextTransformer("ARRAY").get_data().return_data()
    +    log.debug(bib)
    diff --git a/src/services/zotero.py b/src/services/zotero.py
    new file mode 100644
    index 0000000..6ca2588
    --- /dev/null
    +++ b/src/services/zotero.py
    @@ -0,0 +1,340 @@
    +from dataclasses import dataclass
    +from typing import Optional
    +
    +from pyzotero import zotero
    +
    +from src import settings
    +from src.services.webrequest import BibTextTransformer, WebRequest
    +from src.shared.logging import log
    +
    +
    +@dataclass
    +class Creator:
    +    firstName: str = None
    +    lastName: str = None
    +    creatorType: str = "author"
    +
    +    def from_dict(self, data: dict) -> None:
    +        for key, value in data.items():
    +            setattr(self, key, value)
    +
    +    def from_string(self, data: str) -> "Creator":
    +        if "," in data:
    +            self.firstName = data.split(",")[1]
    +            self.lastName = data.split(",")[0]
    +
    +        return self
    +
    +    # set __dict__ object to be used in json
    +
    +
    +@dataclass
    +class Book:
    +    itemType: str = "book"
    +    creators: list[Creator] = None
    +    tags: list = None
    +    collections: list = None
    +    relations: dict = None
    +    title: str = None
    +    abstractNote: str = None
    +    series: str = None
    +    seriesNumber: str = None
    +    volume: str = None
    +    numberOfVolumes: str = None
    +    edition: str = None
    +    place: str = None
    +    publisher: str = None
    +    date: str = None
    +    numPages: str = None
    +    language: str = None
    +    ISBN: str = None
    +    shortTitle: str = None
    +    url: str = None
    +    accessDate: str = None
    +    archive: str = None
    +    archiveLocation: str = None
    +    libraryCatalog: str = None
    +    callNumber: str = None
    +    rights: str = None
    +    extra: str = None
    +
    +    def to_dict(self) -> dict:
    +        ret = {}
    +        for key, value in self.__dict__.items():
    +            if value:
    +                ret[key] = value
    +        return ret
    +
    +
    +@dataclass
    +class BookSection:
    +    itemType: str = "bookSection"
    +    title: str = None
    +    creators: list[Creator] = None
    +    abstractNote: str = None
    +    bookTitle: str = None
    +    series: str = None
    +    seriesNumber: str = None
    +    volume: str = None
    +    numberOfVolumes: str = None
    +    edition: str = None
    +    place: str = None
    +    publisher: str = None
    +    date: str = None
    +    pages: str = None
    +    language: str = None
    +    ISBN: str = None
    +    shortTitle: str = None
    +    url: str = None
    +    accessDate: str = None
    +    archive: str = None
    +    archiveLocation: str = None
    +    libraryCatalog: str = None
    +    callNumber: str = None
    +    rights: str = None
    +    extra: str = None
    +    tags = list
    +    collections = list
    +    relations = dict
    +
    +    def to_dict(self) -> dict:
    +        ret = {}
    +        for key, value in self.__dict__.items():
    +            if value:
    +                ret[key] = value
    +        return ret
    +
    +    def assign(self, book) -> None:
    +        for key, value in book.__dict__.items():
    +            if key in self.__dict__.keys():
    +                try:
    +                    setattr(self, key, value)
    +                except AttributeError:
    +                    pass
    +
    +
    +@dataclass
    +class JournalArticle:
    +    itemType = "journalArticle"
    +    title: str = None
    +    creators: list[Creator] = None
    +    abstractNote: str = None
    +    publicationTitle: str = None
    +    volume: str = None
    +    issue: str = None
    +    pages: str = None
    +    date: str = None
    +    series: str = None
    +    seriesTitle: str = None
    +    seriesText: str = None
    +    journalAbbreviation: str = None
    +    language: str = None
    +    DOI: str = None
    +    ISSN: str = None
    +    shortTitle: str = None
    +    url: str = None
    +    accessDate: str = None
    +    archive: str = None
    +    archiveLocation: str = None
    +    libraryCatalog: str = None
    +    callNumber: str = None
    +    rights: str = None
    +    extra: str = None
    +    tags = list
    +    collections = list
    +    relations = dict
    +
    +    def to_dict(self) -> dict:
    +        ret = {}
    +        for key, value in self.__dict__.items():
    +            if value:
    +                ret[key] = value
    +        return ret
    +
    +    def assign(self, book: dict) -> None:
    +        for key, value in book.__dict__.items():
    +            if key in self.__dict__.keys():
    +                try:
    +                    setattr(self, key, value)
    +                except AttributeError:
    +                    pass
    +
    +
    +class ZoteroController:
    +    zoterocfg = settings.zotero
    +
    +    def __init__(self):
    +        if self.zoterocfg.library_id is None:
    +            return
    +        self.zot = zotero.Zotero(  # type: ignore
    +            self.zoterocfg.library_id,
    +            self.zoterocfg.library_type,
    +            self.zoterocfg.api_key,
    +        )
    +
    +    def get_books(self) -> list:
    +        ret = []
    +        items = self.zot.top()  # type: ignore
    +        for item in items:
    +            if item["data"]["itemType"] == "book":
    +                ret.append(item)
    +        return ret
    +
    +    # create item in zotero
    +    # item is a part of a book
    +    def __get_data(self, isbn) -> dict:
    +        web = WebRequest()
    +        web.get_ppn(isbn)
    +        data = web.get_data_elsa()
    +        bib = BibTextTransformer()
    +        bib.get_data(data)
    +        book = bib.return_data()
    +        return book
    +
    +    # # #print(zot.item_template("bookSection"))
    +    def createBook(self, isbn) -> Book:
    +        book = self.__get_data(isbn)
    +
    +        bookdata = Book()
    +        bookdata.title = book.title.split(":")[0]
    +        bookdata.ISBN = book.isbn
    +        bookdata.language = book.language
    +        bookdata.date = book.year
    +        bookdata.publisher = book.publisher
    +        bookdata.url = book.link
    +        bookdata.edition = book.edition
    +        bookdata.place = book.place
    +        bookdata.numPages = book.pages
    +        authors = [
    +            Creator().from_string(author).__dict__ for author in book.author.split(";")
    +        ]
    +        authors = [author for author in authors if author["lastName"] is not None]
    +        bookdata.creators = authors
    +        return bookdata
    +
    +    def createItem(self, item) -> Optional[str]:
    +        resp = self.zot.create_items([item])  # type: ignore
    +        if "successful" in resp.keys():
    +            log.debug(resp)
    +            return resp["successful"]["0"]["key"]
    +        else:
    +            return None
    +
    +    def deleteItem(self, key) -> None:
    +        items = self.zot.items()
    +        for item in items:
    +            if item["key"] == key:
    +                self.zot.delete_item(item)  # type: ignore
    +                # #print(item)
    +                break
    +
    +    def createHGSection(self, book: Book, data: dict) -> Optional[str]:
    +        log.debug(book)
    +        chapter = BookSection()
    +        chapter.assign(book)
    +        chapter.pages = data["pages"]
    +        chapter.itemType = "bookSection"
    +        chapter.ISBN = ""
    +        chapter.url = ""
    +        chapter.title = data["chapter_title"]
    +        creators = chapter.creators
    +        for creator in creators:
    +            creator["creatorType"] = "editor"
    +        chapter.creators = creators
    +        authors = [
    +            Creator().from_string(author).__dict__
    +            for author in data["section_author"].split(";")
    +        ]
    +        chapter.creators += authors
    +
    +        log.debug(chapter.to_dict())
    +        return self.createItem(chapter.to_dict())
    +        pass
    +
    +    def createBookSection(self, book: Book, data: dict) -> Optional[str]:
    +        chapter = BookSection()
    +        chapter.assign(book)
    +        chapter.pages = data["pages"]
    +        chapter.itemType = "bookSection"
    +        chapter.ISBN = ""
    +        chapter.url = ""
    +        chapter.title = ""
    +        return self.createItem(chapter.to_dict())
    +        # chapter.creators
    +
    +    def createJournalArticle(self, journal, article) -> Optional[str]:
    +        # #print(type(article))
    +        journalarticle = JournalArticle()
    +        journalarticle.assign(journal)
    +        journalarticle.itemType = "journalArticle"
    +        journalarticle.creators = [
    +            Creator().from_string(author).__dict__
    +            for author in article["section_author"].split(";")
    +        ]
    +        journalarticle.date = article["year"]
    +        journalarticle.title = article["chapter_title"]
    +        journalarticle.publicationTitle = article["work_title"].split(":")[0].strip()
    +        journalarticle.pages = article["pages"]
    +        journalarticle.ISSN = article["isbn"]
    +        journalarticle.issue = article["issue"]
    +        journalarticle.url = article["isbn"]
    +
    +        # #print(journalarticle.to_dict())
    +
    +        return self.createItem(journalarticle.to_dict())
    +
    +    def get_citation(self, item) -> str:
    +        title = self.zot.item(  # type: ignore
    +            item,
    +            content="bib",
    +            style="deutsche-gesellschaft-fur-psychologie",
    +        )[0]
    +        # title = title[0]
    +        title = (
    +            title.replace("", "")
    +            .replace("", "")
    +            .replace('
    ', "") + .replace("
    ", "") + .replace("&", "&") + ) + return title + + +if __name__ == "__main__": + zot = ZoteroController() + book = zot.createBook("DV 3000 D649 (4)") + row = "Döbert, Hans & Hörner, Wolfgang & Kopp, Bortho von & Reuter, Lutz R." + zot.createBookSection() + + # book = Book() + # # # book. + # ISBN = "9783801718718" + # book = createBook(isbn=ISBN) + # chapter = BookSection() + # chapter.title = "Geistige Behinderung" + # chapter.bookTitle = book.title + # chapter.pages = "511 - 538" + # chapter.publisher = book.publisher + # authors = [ + # Creator("Jennifer M.", "Phillips").__dict__, + # Creator("Hower", "Kwon").__dict__, + # Creator("Carl", "Feinstein").__dict__, + # Creator("Inco", "Spintczok von Brisinski").__dict__, + # ] + # publishers = book.author + # if isinstance(publishers, str): + # publishers = [publishers] + # for publisher in publishers: + # # #print(publisher) + # creator = Creator().from_string(publisher) + # creator.creatorType = "editor" + # authors.append(creator.__dict__) + + # chapter.creators = authors + # chapter.publisher = book.publisher + # # #print(chapter.to_dict()) + # createBookSection(chapter.to_dict()) + # get_citation("9ZXH8DDE") + # # # #print() + # # #print(get_books()) + # # #print(zot.item_creator_types("bookSection")) diff --git a/src/shared/__init__.py b/src/shared/__init__.py new file mode 100644 index 0000000..fe09d94 --- /dev/null +++ b/src/shared/__init__.py @@ -0,0 +1,6 @@ +"""Shared utilities and cross-cutting concerns.""" + +from .logging import log +from .config import Settings, load_config + +__all__ = ["log", "Settings", "load_config"] diff --git a/src/shared/config.py b/src/shared/config.py new file mode 100644 index 0000000..029dd15 --- /dev/null +++ b/src/shared/config.py @@ -0,0 +1,66 @@ +"""Application configuration and settings.""" + +from dataclasses import dataclass, field +from pathlib import Path +from typing import Any + +import yaml + +from src.shared.logging import log + + +@dataclass +class Settings: + """Settings for the application.""" + + save_path: str + database_name: str + database_path: str + bib_id: str = "" + default_apps: bool = True + custom_applications: list[dict[str, Any]] = field(default_factory=list) + + def save_settings(self, config_path: str | Path = "config.yaml") -> None: + """Save the settings to the config file. + + Args: + config_path: Path to the configuration file + """ + try: + with open(config_path, "w") as f: + yaml.dump(self.__dict__, f) + log.info(f"Settings saved to {config_path}") + except Exception as e: + log.error(f"Failed to save settings: {e}") + raise + + @classmethod + def load_settings(cls, config_path: str | Path = "config.yaml") -> dict[str, Any]: + """Load the settings from the config file. + + Args: + config_path: Path to the configuration file + + Returns: + Dictionary containing the loaded settings + """ + try: + with open(config_path, "r") as f: + data = yaml.safe_load(f) + log.info(f"Settings loaded from {config_path}") + return data + except Exception as e: + log.error(f"Failed to load settings: {e}") + raise + + +def load_config(config_path: str | Path = "config.yaml") -> dict[str, Any]: + """Convenience function to load configuration. + + Args: + config_path: Path to the configuration file + + Returns: + Dictionary containing the loaded settings + """ + return Settings.load_settings(config_path) diff --git a/src/ui/dialogs/Ui_edit_bookdata.py b/src/ui/dialogs/Ui_edit_bookdata.py index eba44e0..f9865d6 100644 --- a/src/ui/dialogs/Ui_edit_bookdata.py +++ b/src/ui/dialogs/Ui_edit_bookdata.py @@ -8,7 +8,7 @@ from PySide6 import QtCore, QtGui, QtWidgets -from src.logic.dataclass import BookData +from src.core.models import BookData class Ui_Dialog(object): diff --git a/src/ui/dialogs/Ui_fileparser.py b/src/ui/dialogs/Ui_fileparser.py index da869db..72fbb60 100644 --- a/src/ui/dialogs/Ui_fileparser.py +++ b/src/ui/dialogs/Ui_fileparser.py @@ -8,7 +8,7 @@ from PySide6 import QtCore, QtGui, QtWidgets -from src.logic.webrequest import BibTextTransformer, WebRequest +from src.services.webrequest import BibTextTransformer, WebRequest class Ui_Dialog(object): diff --git a/src/ui/dialogs/Ui_login.py b/src/ui/dialogs/Ui_login.py index 7844c6b..b9089dd 100644 --- a/src/ui/dialogs/Ui_login.py +++ b/src/ui/dialogs/Ui_login.py @@ -10,8 +10,8 @@ import hashlib from PySide6 import QtCore, QtWidgets -from src.backend.admin_console import AdminCommands -from src.backend.database import Database +from src.admin import AdminCommands +from src.database import Database class Ui_Dialog(object): diff --git a/src/ui/dialogs/bookdata.py b/src/ui/dialogs/bookdata.py index 553a59f..0a7e34d 100644 --- a/src/ui/dialogs/bookdata.py +++ b/src/ui/dialogs/bookdata.py @@ -1,6 +1,6 @@ from PySide6 import QtWidgets -from src.logic.dataclass import BookData +from src.core.models import BookData from .dialog_sources.edit_bookdata_ui import Ui_Dialog diff --git a/src/ui/dialogs/deletedialog.py b/src/ui/dialogs/deletedialog.py index 03d2e43..0c81bdf 100644 --- a/src/ui/dialogs/deletedialog.py +++ b/src/ui/dialogs/deletedialog.py @@ -3,7 +3,7 @@ from typing import Any from PySide6 import QtCore, QtWidgets from src import Icon -from src.backend.database import Database +from src.database import Database from .dialog_sources.deletedialog_ui import Ui_Dialog diff --git a/src/ui/dialogs/docuprint.py b/src/ui/dialogs/docuprint.py index 750fc10..0339238 100644 --- a/src/ui/dialogs/docuprint.py +++ b/src/ui/dialogs/docuprint.py @@ -2,8 +2,8 @@ from natsort import natsorted from PySide6 import QtWidgets from src import Icon -from src.backend import Database -from src.logic import Semester +from src.database import Database +from src.core.models import Semester from src.utils.richtext import SemapSchilder, SemesterDocument from .dialog_sources.documentprint_ui import Ui_Dialog diff --git a/src/ui/dialogs/elsa_add_entry.py b/src/ui/dialogs/elsa_add_entry.py index b8879ac..17fc5b7 100644 --- a/src/ui/dialogs/elsa_add_entry.py +++ b/src/ui/dialogs/elsa_add_entry.py @@ -1,8 +1,8 @@ from PySide6 import QtWidgets from src import Icon -from src.logic.webrequest import BibTextTransformer, WebRequest -from src.logic.zotero import ZoteroController +from src.services.webrequest import BibTextTransformer, WebRequest +from src.services.zotero import ZoteroController from src.shared.logging import log from src.transformers.transformers import DictToTable diff --git a/src/ui/dialogs/fileparser.py b/src/ui/dialogs/fileparser.py index c0c29cd..edb736f 100644 --- a/src/ui/dialogs/fileparser.py +++ b/src/ui/dialogs/fileparser.py @@ -1,6 +1,6 @@ from PySide6 import QtWidgets -from src.logic.webrequest import BibTextTransformer, WebRequest +from src.services.webrequest import BibTextTransformer, WebRequest from .dialog_sources.Ui_fileparser import Ui_Dialog diff --git a/src/ui/dialogs/login.py b/src/ui/dialogs/login.py index 649cbaf..09b9d1f 100644 --- a/src/ui/dialogs/login.py +++ b/src/ui/dialogs/login.py @@ -5,7 +5,7 @@ import loguru from PySide6 import QtCore, QtWidgets from src import LOG_DIR, Icon -from src.backend.database import Database +from src.database import Database from .dialog_sources.login_ui import Ui_Dialog @@ -75,7 +75,7 @@ class LoginDialog(Ui_Dialog): hashed_password = hashlib.sha256(password.encode()).hexdigest() if len(self.db.getUsers()) == 0: - from src.backend.admin_console import AdminCommands + from src.admin import AdminCommands AdminCommands().create_admin() self.lresult = 1 # Indicate successful login diff --git a/src/ui/dialogs/newEdition.py b/src/ui/dialogs/newEdition.py index a103690..77fe6e1 100644 --- a/src/ui/dialogs/newEdition.py +++ b/src/ui/dialogs/newEdition.py @@ -1,7 +1,7 @@ from PySide6 import QtCore, QtWidgets -from src.backend.catalogue import Catalogue -from src.backend.database import Database +from src.services.catalogue import Catalogue +from src.database import Database from src.ui.dialogs.mail import Mail_Dialog from .dialog_sources.order_neweditions_ui import Ui_Dialog diff --git a/src/ui/dialogs/parsed_titles.py b/src/ui/dialogs/parsed_titles.py index dca3cdd..416f38b 100644 --- a/src/ui/dialogs/parsed_titles.py +++ b/src/ui/dialogs/parsed_titles.py @@ -4,7 +4,7 @@ import loguru from PySide6 import QtWidgets from src import LOG_DIR -from src.backend import AutoAdder +from src.background import AutoAdder from .dialog_sources.parsed_titles_ui import Ui_Form diff --git a/src/ui/dialogs/progress.py b/src/ui/dialogs/progress.py index cdeab3b..6abd9c2 100644 --- a/src/ui/dialogs/progress.py +++ b/src/ui/dialogs/progress.py @@ -5,9 +5,9 @@ from PySide6 import QtCore from PySide6.QtWidgets import QDialog, QPushButton, QVBoxLayout from qtqdm import Qtqdm, QtqdmProgressBar -from src.logic import BookData -from src.logic.lehmannsapi import LehmannsClient -from src.logic.SRU import SWB +from src.core.models import BookData +from src.services.lehmanns import LehmannsClient +from src.services.sru import SWB class CheckThread(QtCore.QThread): diff --git a/src/ui/userInterface.py b/src/ui/userInterface.py index efac025..3bf6da8 100644 --- a/src/ui/userInterface.py +++ b/src/ui/userInterface.py @@ -15,24 +15,27 @@ from PySide6.QtGui import QRegularExpressionValidator from PySide6.QtMultimedia import QAudioOutput, QMediaPlayer from src import Icon -from src.backend import ( +from src.database import Database +from src.background import ( AvailChecker, BookGrabber, - Database, DocumentationThread, NewEditionCheckerThread, ) -from src.backend.create_file import recreateFile -from src.backend.delete_temp_contents import delete_temp_contents as tempdelete -from src.logic import ( - APP_NRS, +from src.utils.files import recreateFile, delete_temp_contents as tempdelete +from src.core.models import ( Apparat, ApparatData, BookData, Prof, SemapDocument, Semester, +) +from src.core.constants import APP_NRS +from src.parsers import ( csv_to_list, +) +from src.logic import ( eml_to_semap, pdf_to_semap, word_to_semap, diff --git a/src/ui/widgets/MessageCalendar.py b/src/ui/widgets/MessageCalendar.py index cc03437..4c6b89f 100644 --- a/src/ui/widgets/MessageCalendar.py +++ b/src/ui/widgets/MessageCalendar.py @@ -5,7 +5,7 @@ from PySide6 import QtCore, QtWidgets from PySide6.QtCore import QDate from PySide6.QtGui import QColor, QPen -from src.backend import Database +from src.database import Database from src.shared.logging import log color = "#ddfb00" if darkdetect.isDark() else "#2204ff" diff --git a/src/ui/widgets/admin_create_user.py b/src/ui/widgets/admin_create_user.py index d9a0e68..84f4a16 100644 --- a/src/ui/widgets/admin_create_user.py +++ b/src/ui/widgets/admin_create_user.py @@ -1,7 +1,8 @@ from PySide6 import QtWidgets from PySide6.QtCore import Signal from .widget_sources.admin_create_user_ui import Ui_Dialog -from src.backend import AdminCommands, Database +from src.admin import AdminCommands +from src.database import Database class UserCreate(QtWidgets.QDialog, Ui_Dialog): diff --git a/src/ui/widgets/admin_edit_prof.py b/src/ui/widgets/admin_edit_prof.py index 083cded..d94aa5c 100644 --- a/src/ui/widgets/admin_edit_prof.py +++ b/src/ui/widgets/admin_edit_prof.py @@ -4,8 +4,8 @@ import loguru from PySide6 import QtWidgets from src import LOG_DIR -from src.backend import Database -from src.logic import Prof +from src.database import Database +from src.core.models import Prof from .widget_sources.admin_edit_prof_ui import Ui_Dialog # diff --git a/src/ui/widgets/admin_edit_user.py b/src/ui/widgets/admin_edit_user.py index bbf78ca..101911f 100644 --- a/src/ui/widgets/admin_edit_user.py +++ b/src/ui/widgets/admin_edit_user.py @@ -1,6 +1,7 @@ from PySide6 import QtWidgets -from src.backend import AdminCommands, Database +from src.admin import AdminCommands +from src.database import Database from .widget_sources.admin_edit_user_ui import Ui_Dialog diff --git a/src/ui/widgets/admin_query.py b/src/ui/widgets/admin_query.py index c4d6fe1..2c63ace 100644 --- a/src/ui/widgets/admin_query.py +++ b/src/ui/widgets/admin_query.py @@ -1,7 +1,7 @@ from PySide6 import QtCore, QtWidgets from src import Icon -from src.backend import Database +from src.database import Database from .widget_sources. import Ui_Form diff --git a/src/ui/widgets/calendar_entry.py b/src/ui/widgets/calendar_entry.py index 2dd68a1..e8327ae 100644 --- a/src/ui/widgets/calendar_entry.py +++ b/src/ui/widgets/calendar_entry.py @@ -2,7 +2,7 @@ from PySide6 import QtWidgets from PySide6.QtCore import Signal from src import Icon -from src.backend.database import Database +from src.database import Database from .widget_sources.calendar_entry_ui import Ui_Dialog diff --git a/src/ui/widgets/elsa_main.py b/src/ui/widgets/elsa_main.py index 6607446..4975b43 100644 --- a/src/ui/widgets/elsa_main.py +++ b/src/ui/widgets/elsa_main.py @@ -5,8 +5,10 @@ from PySide6.QtCore import QDate from PySide6.QtGui import QRegularExpressionValidator from src import Icon -from src.backend import Database, recreateElsaFile -from src.logic import Prof, Semester, elsa_word_to_csv +from src.database import Database +from src.utils.files import recreateElsaFile +from src.core.models import Prof, Semester +from src.logic import elsa_word_to_csv from src.shared.logging import log from src.ui.dialogs import ElsaAddEntry, popus_confirm from src.ui.widgets.filepicker import FilePicker diff --git a/src/ui/widgets/graph.py b/src/ui/widgets/graph.py index ad5d1d8..838744f 100644 --- a/src/ui/widgets/graph.py +++ b/src/ui/widgets/graph.py @@ -5,7 +5,7 @@ from PySide6 import QtCore, QtGui, QtWidgets from PySide6.QtCharts import QCategoryAxis, QChart, QChartView, QLineSeries, QValueAxis from PySide6.QtGui import QColor, QPainter, QPen -from src.logic.semester import Semester +from src.core.models import Semester def mergedicts(d1: dict[str, Any], d2: dict[str, Any]): @@ -101,7 +101,7 @@ class DataQtGraph(QtWidgets.QWidget): self.chart.createDefaultAxes() for entry in lst: - # print("entry:", entry) + print("entry:", entry) entryseries = QLineSeries() for x_val, y_val in zip(entry["x"], entry["y"]): # diff --git a/src/ui/widgets/new_edition_check.py b/src/ui/widgets/new_edition_check.py index 56a36b3..e903265 100644 --- a/src/ui/widgets/new_edition_check.py +++ b/src/ui/widgets/new_edition_check.py @@ -4,8 +4,8 @@ from PySide6 import QtWidgets from PySide6.QtCore import Qt from src import Icon -from src.backend.catalogue import Catalogue -from src.logic import BookData +from src.services.catalogue import Catalogue +from src.core.models import BookData from .widget_sources.new_edition_check_book_ui import ( Ui_Dialog as Ui_NewEditionCheckBook, diff --git a/src/ui/widgets/searchPage.py b/src/ui/widgets/searchPage.py index c8cb0f1..c415161 100644 --- a/src/ui/widgets/searchPage.py +++ b/src/ui/widgets/searchPage.py @@ -4,9 +4,9 @@ from natsort import natsorted from PySide6 import QtCore, QtGui, QtWidgets from PySide6.QtCore import Signal -from src.backend import Database -from src.logic import BookData, Prof, Semester, custom_sort, sort_semesters_list -from src.logic.dataclass import Apparat +from src.core.models import Apparat, BookData, Prof, Semester +from src.database import Database +from src.logic import custom_sort, sort_semesters_list from src.shared.logging import log from src.ui.dialogs import ApparatExtendDialog, Mail_Dialog, ReminderDialog from src.ui.widgets import DataQtGraph, StatusWidget @@ -374,6 +374,7 @@ class SearchStatisticPage(QtWidgets.QDialog, Ui_Dialog): "x": [i[0] for i in data], "y": {"Erstellt": [i[1] for i in data], "Gelöscht": [i[2] for i in data]}, } + log.debug(graph_data) graph = DataQtGraph( title="Erstellte und gelöschte Apparate", data=graph_data, diff --git a/src/ui/widgets/signature_update.py b/src/ui/widgets/signature_update.py index b0b49fe..92f4cf1 100644 --- a/src/ui/widgets/signature_update.py +++ b/src/ui/widgets/signature_update.py @@ -6,10 +6,10 @@ from queue import Empty, Queue from PySide6 import QtCore, QtWidgets from PySide6.QtMultimedia import QAudioOutput, QMediaPlayer -from src.backend.catalogue import Catalogue -from src.backend.database import Database -from src.backend.webadis import get_book_medianr -from src.logic.SRU import SWB +from src.services.catalogue import Catalogue +from src.database import Database +from src.services.webadis import get_book_medianr +from src.services.sru import SWB from src.shared.logging import log from .widget_sources.admin_update_signatures_ui import Ui_Dialog diff --git a/src/ui/widgets/welcome_wizard.py b/src/ui/widgets/welcome_wizard.py index 85418e5..c9a1700 100644 --- a/src/ui/widgets/welcome_wizard.py +++ b/src/ui/widgets/welcome_wizard.py @@ -5,7 +5,7 @@ from appdirs import AppDirs from PySide6 import QtCore, QtWidgets from src import settings -from src.backend import Database +from src.database import Database from src.shared.logging import log from .widget_sources.welcome_wizard_ui import Ui_Wizard @@ -80,7 +80,7 @@ class WelcomeWizard(QtWidgets.QWizard, Ui_Wizard): self.settings_database_name.setText("semesterapparate.db") def test_login_data(self): - from src.backend import AdminCommands + from src.admin import AdminCommands log.info("Testing login data for SAM user") db_path = ( @@ -109,7 +109,7 @@ class WelcomeWizard(QtWidgets.QWizard, Ui_Wizard): def create_sam_user(self): """Create a SAM user in the database.""" - from src.backend import AdminCommands + from src.admin import AdminCommands db_path = ( self.settings_database.text() + "/" + self.settings_database_name.text() diff --git a/src/utils/files.py b/src/utils/files.py new file mode 100644 index 0000000..12f0f81 --- /dev/null +++ b/src/utils/files.py @@ -0,0 +1,100 @@ +"""File operations and management utilities.""" + +import os +from pathlib import Path + +from src import LOG_DIR, settings +from src.database import Database +from src.shared.logging import log + + +def recreate_file(name: str, app_id: int, filetype: str, open_file: bool = True) -> Path: + """ + Recreate a file from the database and optionally open it. + + Args: + name: The filename selected by the user. + app_id: The ID of the apparatus. + filetype: The extension of the file to be created. + open_file: Determines if the file should be opened. Defaults to True. + + Returns: + Absolute path to the file. + """ + db = Database() + path = db.recreateFile(name, app_id, filetype=filetype) + path = Path(path) + log.info(f"File created: {path}") + + if open_file: + path = path.resolve() + if os.getenv("OS") == "Windows_NT": + os.startfile(path) + else: + os.system(f"open {path}") + + return path + + +# Legacy name for backwards compatibility +def recreateFile(name: str, app_id: int, filetype: str, open: bool = True) -> Path: + """Legacy function name - use recreate_file instead.""" + return recreate_file(name, app_id, filetype, open) + + +def recreate_elsa_file(filename: str, filetype: str, open_file: bool = True) -> Path: + """ + Recreate an ELSA file from the database and optionally open it. + + Args: + filename: The filename selected by the user. + filetype: The file extension. + open_file: Determines if the file should be opened. Defaults to True. + + Returns: + Absolute path to the file. + """ + if filename.startswith("(") and filename.endswith(")"): + filename = str(filename[1:-1].replace("'", "")) + + if not isinstance(filename, str): + raise ValueError("filename must be a string") + + db = Database() + path = db.recreateElsaFile(filename, filetype) + path = Path(path) + + if open_file: + path = path.resolve() + if os.getenv("OS") == "Windows_NT": + os.startfile(path) + else: + os.system(f"open {path}") + + return path + + +# Legacy name for backwards compatibility +def recreateElsaFile(filename: str, filetype: str, open: bool = True) -> Path: + """Legacy function name - use recreate_elsa_file instead.""" + return recreate_elsa_file(filename, filetype, open) + + +def delete_temp_contents() -> None: + """Delete the contents of the temp directory.""" + database = settings.database + path = database.temp.expanduser() + + for root, dirs, files in os.walk(path, topdown=False): + for file in files: + try: + os.remove(os.path.join(root, file)) + except Exception as e: + log.warning(f"Could not remove file {file}: {e}") + for dir in dirs: + try: + os.rmdir(os.path.join(root, dir)) + except Exception as e: + log.warning(f"Could not remove directory {dir}: {e}") + + log.info(f"Temp directory cleared: {path}") diff --git a/test.py b/test.py index 5a5da36..7a1ffda 100644 --- a/test.py +++ b/test.py @@ -1,33 +1,15 @@ -from src.logic.semester import Semester +from src.backend.catalogue import Catalogue +from src.logic.webrequest import BibTextTransformer, WebRequest -sem1 = Semester.from_string("WiSe 23/24") -print(sem1.value) -sem2 = Semester.from_string("SoSe 24") -print(sem2.value) -sem3 = Semester() -print(sem3.value) +cat = Catalogue() +result = cat.get_book("3825872475") +print(result) +web = WebRequest() +web.get_ppn("3825872475") +data = web.get_data_elsa() +print(data) +bib = BibTextTransformer() +bib.get_data(result) +book = bib.return_data() -print("Comparing Sem1 with sem2") -assert sem1.isPastSemester(sem2) is True -assert sem1.isFutureSemester(sem2) is False -assert sem1.isMatch(sem2) is False -print("Comparing Sem2 with sem1") -assert sem2.isPastSemester(sem1) is False -assert sem2.isFutureSemester(sem1) is True -assert sem2.isMatch(sem1) is False -print("Comparing Sem1 with sem1") -assert sem1.isPastSemester(sem1) is False -assert sem1.isFutureSemester(sem1) is False -assert sem1.isMatch(sem1) is True -print("Comparing Sem2 with sem2") -assert sem2.isPastSemester(sem2) is False -assert sem2.isFutureSemester(sem2) is False -assert sem2.isMatch(sem2) is True -print("Comparing Sem3 with sem3") -assert sem3.isPastSemester(sem3) is False -assert sem3.isFutureSemester(sem3) is False -assert sem3.isMatch(sem3) is True -print("Comparing Sem1 with sem3") -assert sem1.isPastSemester(sem3) is True -assert sem1.isFutureSemester(sem3) is False -assert sem1.isMatch(sem3) is False +print(book) diff --git a/tests/test_migrations_runner.py b/tests/test_migrations_runner.py index c86561c..2ffbcdb 100644 --- a/tests/test_migrations_runner.py +++ b/tests/test_migrations_runner.py @@ -1,7 +1,7 @@ import sqlite3 as sql from pathlib import Path -from src.backend.database import Database +from src.database import Database p = Path("devtests_test_migrations.db") if p.exists(): diff --git a/uv.lock b/uv.lock index 9ca2491..6605328 100644 --- a/uv.lock +++ b/uv.lock @@ -165,6 +165,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5", size = 161216 }, ] +[[package]] +name = "chardet" +version = "5.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/0d/f7b6ab21ec75897ed80c17d79b15951a719226b9fababf1e40ea74d69079/chardet-5.2.0.tar.gz", hash = "sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7", size = 2069618 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/6f/f5fbc992a329ee4e0f288c1fe0e2ad9485ed064cac731ed2fe47dcc38cbf/chardet-5.2.0-py3-none-any.whl", hash = "sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970", size = 199385 }, +] + [[package]] name = "charset-normalizer" version = "3.4.3" @@ -226,6 +235,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f1/f4/7b7fdbb613992013c4518a0bf8fee2915f79ec07bcfa6180569bca7fa8ef/comtypes-1.4.11-py3-none-any.whl", hash = "sha256:1760d5059ca7ca1d61b574c998378d879c271a86c41f88926619ea97497592bb", size = 246365 }, ] +[[package]] +name = "cssselect" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/0a/c3ea9573b1dc2e151abfe88c7fe0c26d1892fe6ed02d0cdb30f0d57029d5/cssselect-1.3.0.tar.gz", hash = "sha256:57f8a99424cfab289a1b6a816a43075a4b00948c86b4dcf3ef4ee7e15f7ab0c7", size = 42870 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/58/257350f7db99b4ae12b614a36256d9cc870d71d9e451e79c2dc3b23d7c3c/cssselect-1.3.0-py3-none-any.whl", hash = "sha256:56d1bf3e198080cc1667e137bc51de9cadfca259f03c2d4e09037b3e01e30f0d", size = 18786 }, +] + [[package]] name = "darkdetect" version = "0.8.0" @@ -820,6 +838,30 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191 }, ] +[[package]] +name = "pdfminer" +version = "20191125" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycryptodome" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/a3/155c5cde5f9c0b1069043b2946a93f54a41fd72cc19c6c100f6f2f5bdc15/pdfminer-20191125.tar.gz", hash = "sha256:9e700bc731300ed5c8936343c1dd4529638184198e54e91dd2b59b64a755dc01", size = 4173248 } + +[[package]] +name = "pdfquery" +version = "0.4.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "chardet" }, + { name = "cssselect" }, + { name = "lxml" }, + { name = "pdfminer" }, + { name = "pyquery" }, + { name = "roman" }, + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e5/ed/caf087d2d65ceef10fb117af79bbab50ea3a24ed8b1dc8abb0dc8039d2d3/pdfquery-0.4.3.tar.gz", hash = "sha256:a2a2974cb312fda4f569adc8d63377d25d5c6367240b4a7bfb165392c73e1dce", size = 17489 } + [[package]] name = "plaster" version = "1.1.2" @@ -882,6 +924,38 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ce/4f/5249960887b1fbe561d9ff265496d170b55a735b76724f10ef19f9e40716/prompt_toolkit-3.0.51-py3-none-any.whl", hash = "sha256:52742911fde84e2d423e2f9a4cf1de7d7ac4e51958f648d9540e0fb8db077b07", size = 387810 }, ] +[[package]] +name = "pycryptodome" +version = "3.23.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/a6/8452177684d5e906854776276ddd34eca30d1b1e15aa1ee9cefc289a33f5/pycryptodome-3.23.0.tar.gz", hash = "sha256:447700a657182d60338bab09fdb27518f8856aecd80ae4c6bdddb67ff5da44ef", size = 4921276 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/5d/bdb09489b63cd34a976cc9e2a8d938114f7a53a74d3dd4f125ffa49dce82/pycryptodome-3.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:0011f7f00cdb74879142011f95133274741778abba114ceca229adbf8e62c3e4", size = 2495152 }, + { url = "https://files.pythonhosted.org/packages/a7/ce/7840250ed4cc0039c433cd41715536f926d6e86ce84e904068eb3244b6a6/pycryptodome-3.23.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:90460fc9e088ce095f9ee8356722d4f10f86e5be06e2354230a9880b9c549aae", size = 1639348 }, + { url = "https://files.pythonhosted.org/packages/ee/f0/991da24c55c1f688d6a3b5a11940567353f74590734ee4a64294834ae472/pycryptodome-3.23.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4764e64b269fc83b00f682c47443c2e6e85b18273712b98aa43bcb77f8570477", size = 2184033 }, + { url = "https://files.pythonhosted.org/packages/54/16/0e11882deddf00f68b68dd4e8e442ddc30641f31afeb2bc25588124ac8de/pycryptodome-3.23.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb8f24adb74984aa0e5d07a2368ad95276cf38051fe2dc6605cbcf482e04f2a7", size = 2270142 }, + { url = "https://files.pythonhosted.org/packages/d5/fc/4347fea23a3f95ffb931f383ff28b3f7b1fe868739182cb76718c0da86a1/pycryptodome-3.23.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d97618c9c6684a97ef7637ba43bdf6663a2e2e77efe0f863cce97a76af396446", size = 2309384 }, + { url = "https://files.pythonhosted.org/packages/6e/d9/c5261780b69ce66d8cfab25d2797bd6e82ba0241804694cd48be41add5eb/pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9a53a4fe5cb075075d515797d6ce2f56772ea7e6a1e5e4b96cf78a14bac3d265", size = 2183237 }, + { url = "https://files.pythonhosted.org/packages/5a/6f/3af2ffedd5cfa08c631f89452c6648c4d779e7772dfc388c77c920ca6bbf/pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:763d1d74f56f031788e5d307029caef067febf890cd1f8bf61183ae142f1a77b", size = 2343898 }, + { url = "https://files.pythonhosted.org/packages/9a/dc/9060d807039ee5de6e2f260f72f3d70ac213993a804f5e67e0a73a56dd2f/pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:954af0e2bd7cea83ce72243b14e4fb518b18f0c1649b576d114973e2073b273d", size = 2269197 }, + { url = "https://files.pythonhosted.org/packages/f9/34/e6c8ca177cb29dcc4967fef73f5de445912f93bd0343c9c33c8e5bf8cde8/pycryptodome-3.23.0-cp313-cp313t-win32.whl", hash = "sha256:257bb3572c63ad8ba40b89f6fc9d63a2a628e9f9708d31ee26560925ebe0210a", size = 1768600 }, + { url = "https://files.pythonhosted.org/packages/e4/1d/89756b8d7ff623ad0160f4539da571d1f594d21ee6d68be130a6eccb39a4/pycryptodome-3.23.0-cp313-cp313t-win_amd64.whl", hash = "sha256:6501790c5b62a29fcb227bd6b62012181d886a767ce9ed03b303d1f22eb5c625", size = 1799740 }, + { url = "https://files.pythonhosted.org/packages/5d/61/35a64f0feaea9fd07f0d91209e7be91726eb48c0f1bfc6720647194071e4/pycryptodome-3.23.0-cp313-cp313t-win_arm64.whl", hash = "sha256:9a77627a330ab23ca43b48b130e202582e91cc69619947840ea4d2d1be21eb39", size = 1703685 }, + { url = "https://files.pythonhosted.org/packages/db/6c/a1f71542c969912bb0e106f64f60a56cc1f0fabecf9396f45accbe63fa68/pycryptodome-3.23.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:187058ab80b3281b1de11c2e6842a357a1f71b42cb1e15bce373f3d238135c27", size = 2495627 }, + { url = "https://files.pythonhosted.org/packages/6e/4e/a066527e079fc5002390c8acdd3aca431e6ea0a50ffd7201551175b47323/pycryptodome-3.23.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:cfb5cd445280c5b0a4e6187a7ce8de5a07b5f3f897f235caa11f1f435f182843", size = 1640362 }, + { url = "https://files.pythonhosted.org/packages/50/52/adaf4c8c100a8c49d2bd058e5b551f73dfd8cb89eb4911e25a0c469b6b4e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67bd81fcbe34f43ad9422ee8fd4843c8e7198dd88dd3d40e6de42ee65fbe1490", size = 2182625 }, + { url = "https://files.pythonhosted.org/packages/5f/e9/a09476d436d0ff1402ac3867d933c61805ec2326c6ea557aeeac3825604e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8987bd3307a39bc03df5c8e0e3d8be0c4c3518b7f044b0f4c15d1aa78f52575", size = 2268954 }, + { url = "https://files.pythonhosted.org/packages/f9/c5/ffe6474e0c551d54cab931918127c46d70cab8f114e0c2b5a3c071c2f484/pycryptodome-3.23.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa0698f65e5b570426fc31b8162ed4603b0c2841cbb9088e2b01641e3065915b", size = 2308534 }, + { url = "https://files.pythonhosted.org/packages/18/28/e199677fc15ecf43010f2463fde4c1a53015d1fe95fb03bca2890836603a/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:53ecbafc2b55353edcebd64bf5da94a2a2cdf5090a6915bcca6eca6cc452585a", size = 2181853 }, + { url = "https://files.pythonhosted.org/packages/ce/ea/4fdb09f2165ce1365c9eaefef36625583371ee514db58dc9b65d3a255c4c/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:156df9667ad9f2ad26255926524e1c136d6664b741547deb0a86a9acf5ea631f", size = 2342465 }, + { url = "https://files.pythonhosted.org/packages/22/82/6edc3fc42fe9284aead511394bac167693fb2b0e0395b28b8bedaa07ef04/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:dea827b4d55ee390dc89b2afe5927d4308a8b538ae91d9c6f7a5090f397af1aa", size = 2267414 }, + { url = "https://files.pythonhosted.org/packages/59/fe/aae679b64363eb78326c7fdc9d06ec3de18bac68be4b612fc1fe8902693c/pycryptodome-3.23.0-cp37-abi3-win32.whl", hash = "sha256:507dbead45474b62b2bbe318eb1c4c8ee641077532067fec9c1aa82c31f84886", size = 1768484 }, + { url = "https://files.pythonhosted.org/packages/54/2f/e97a1b8294db0daaa87012c24a7bb714147c7ade7656973fd6c736b484ff/pycryptodome-3.23.0-cp37-abi3-win_amd64.whl", hash = "sha256:c75b52aacc6c0c260f204cbdd834f76edc9fb0d8e0da9fbf8352ef58202564e2", size = 1799636 }, + { url = "https://files.pythonhosted.org/packages/18/3d/f9441a0d798bf2b1e645adc3265e55706aead1255ccdad3856dbdcffec14/pycryptodome-3.23.0-cp37-abi3-win_arm64.whl", hash = "sha256:11eeeb6917903876f134b56ba11abe95c0b0fd5e3330def218083c7d98bbcb3c", size = 1703675 }, + { url = "https://files.pythonhosted.org/packages/9f/7c/f5b0556590e7b4e710509105e668adb55aa9470a9f0e4dea9c40a4a11ce1/pycryptodome-3.23.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:350ebc1eba1da729b35ab7627a833a1a355ee4e852d8ba0447fafe7b14504d56", size = 1705791 }, + { url = "https://files.pythonhosted.org/packages/33/38/dcc795578d610ea1aaffef4b148b8cafcfcf4d126b1e58231ddc4e475c70/pycryptodome-3.23.0-pp27-pypy_73-win32.whl", hash = "sha256:93837e379a3e5fd2bb00302a47aee9fdf7940d83595be3915752c74033d17ca7", size = 1780265 }, +] + [[package]] name = "pydantic" version = "2.11.7" @@ -982,6 +1056,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/05/e7/df2285f3d08fee213f2d041540fa4fc9ca6c2d44cf36d3a035bf2a8d2bcc/pyparsing-3.2.3-py3-none-any.whl", hash = "sha256:a749938e02d6fd0b59b356ca504a24982314bb090c383e3cf201c95ef7e2bfcf", size = 111120 }, ] +[[package]] +name = "pyquery" +version = "2.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cssselect" }, + { name = "lxml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/48/79e774ea00b671d08867f06d9258203be81834236c150ac00e942d8fc4db/pyquery-2.0.1.tar.gz", hash = "sha256:0194bb2706b12d037db12c51928fe9ebb36b72d9e719565daba5a6c595322faf", size = 44999 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/f5/5067b48012967ea166b9bd0a015b69e0560e4c6e7c06f28d9bab8f9dd10b/pyquery-2.0.1-py3-none-any.whl", hash = "sha256:aedfa0bd0eb9afc94b3ddbec8f375a6362b32bc9662f46e3e0d866483f4771b0", size = 22573 }, +] + [[package]] name = "pyramid" version = "2.0.2" @@ -1210,6 +1297,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b6/c2/9fce4c8a9587c4e90500114d742fe8ef0fd92d7bad29d136bb9941add271/rich_click-1.8.9-py3-none-any.whl", hash = "sha256:c3fa81ed8a671a10de65a9e20abf642cfdac6fdb882db1ef465ee33919fbcfe2", size = 36082 }, ] +[[package]] +name = "roman" +version = "5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/30/86/8bdb59db4b7ea9a2bd93f8d25298981e09a4c9f4744cf4cbafa7ef6fee7b/roman-5.1.tar.gz", hash = "sha256:3a86572e9bc9183e771769601189e5fa32f1620ffeceebb9eca836affb409986", size = 8066 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/d0/27c9840ddaf331ace898c7f4aa1e1304a7acc22b844b5420fabb6d14c3a0/roman-5.1-py3-none-any.whl", hash = "sha256:bf595d8a9bc4a8e8b1dfa23e1d4def0251b03b494786df6b8c3d3f1635ce285a", size = 5825 }, +] + [[package]] name = "semesterapparatsmanager" version = "1.0.0" @@ -1231,6 +1327,7 @@ dependencies = [ { name = "omegaconf" }, { name = "openai" }, { name = "pandas" }, + { name = "pdfquery" }, { name = "playwright" }, { name = "pyramid" }, { name = "pyside6" }, @@ -1268,6 +1365,7 @@ requires-dist = [ { name = "omegaconf", specifier = ">=2.3.0" }, { name = "openai", specifier = ">=1.79.0" }, { name = "pandas", specifier = ">=2.2.3" }, + { name = "pdfquery", specifier = ">=0.4.3" }, { name = "playwright", specifier = ">=1.49.1" }, { name = "pyramid", specifier = ">=2.0.2" }, { name = "pyside6", specifier = ">=6.9.1" }, -- 2.49.1 From 6c6d140c2fb480381647ab2b52ec0145955a58c2 Mon Sep 17 00:00:00 2001 From: WorldTeacher Date: Mon, 24 Nov 2025 12:39:44 +0100 Subject: [PATCH 38/40] Refactor UI imports and clean up unused code - Removed unnecessary imports from various UI files to streamline the codebase. - Deleted the unused documentation viewer implementation in `documentationview.py`. - Cleaned up imports in `files.py` by removing `LOG_DIR` as it was not used. - Removed the temporary script `temp.py` as it was no longer needed. --- src/admin/commands.py | 1 - src/backend/delete_temp_contents.py | 1 - src/ui/dialogs/Ui_reminder.py | 2 +- src/ui/dialogs/confirm_extend_ui.py | 2 +- .../dialogs/dialog_sources/app_status_ui.py | 2 +- .../dialog_sources/confirm_extend_ui.py | 2 +- .../dialogs/dialog_sources/deletedialog_ui.py | 26 +++----- .../dialog_sources/elsa_add_table_entry_ui.py | 10 +-- .../dialogs/dialog_sources/mail_preview_ui.py | 13 +--- .../newMailTemplateDesigner_ui.py | 11 +--- .../dialog_sources/order_neweditions_ui.py | 29 ++++----- src/ui/dialogs/dialog_sources/reminder_ui.py | 2 +- src/ui/dialogs/ext_app.py | 2 +- src/ui/dialogs/mail_preview_ui.py | 2 +- src/ui/dialogs/progress.py | 3 +- src/ui/semesterapparat_ui_ui.py | 61 +++++++++---------- .../widget_sources/admin_create_user_ui.py | 2 +- .../widget_sources/admin_edit_prof_ui.py | 2 +- .../widget_sources/admin_edit_user_ui.py | 2 +- .../admin_update_signatures_ui.py | 12 +--- .../widget_sources/calendar_entry_ui.py | 2 +- .../widget_sources/elsa_maindialog_ui.py | 2 +- .../widgets/widget_sources/icon_widget_ui.py | 2 +- .../new_edition_check_book_ui.py | 10 +-- .../new_edition_check_found_result_ui.py | 13 +--- .../new_edition_check_selector_ui.py | 12 +--- .../widget_sources/new_edition_check_ui.py | 12 +--- .../search_statistic_page_ui.py | 34 +++++------ .../widget_sources/welcome_wizard_ui.py | 10 +-- src/utils/__init__.py | 9 ++- src/utils/docs_ui.py | 2 +- src/utils/documentationview.py | 46 -------------- src/utils/files.py | 2 +- src/utils/temp.py | 4 -- 34 files changed, 109 insertions(+), 238 deletions(-) delete mode 100644 src/utils/temp.py diff --git a/src/admin/commands.py b/src/admin/commands.py index bedf8ef..aa27ee0 100644 --- a/src/admin/commands.py +++ b/src/admin/commands.py @@ -3,7 +3,6 @@ import random from src.database import Database from src.shared.logging import log -from src import LOG_DIR # change passwords for apparats, change passwords for users, list users, create and delete users etc diff --git a/src/backend/delete_temp_contents.py b/src/backend/delete_temp_contents.py index 21f393c..a61062b 100644 --- a/src/backend/delete_temp_contents.py +++ b/src/backend/delete_temp_contents.py @@ -1,5 +1,4 @@ import os -from pathlib import Path from src import settings database = settings.database diff --git a/src/ui/dialogs/Ui_reminder.py b/src/ui/dialogs/Ui_reminder.py index 283e509..001303a 100644 --- a/src/ui/dialogs/Ui_reminder.py +++ b/src/ui/dialogs/Ui_reminder.py @@ -6,7 +6,7 @@ # run again. Do not edit this file unless you know what you are doing. -from PySide6 import QtCore, QtGui, QtWidgets +from PySide6 import QtCore, QtWidgets class Ui_Dialog(object): diff --git a/src/ui/dialogs/confirm_extend_ui.py b/src/ui/dialogs/confirm_extend_ui.py index cc2674e..a82c515 100644 --- a/src/ui/dialogs/confirm_extend_ui.py +++ b/src/ui/dialogs/confirm_extend_ui.py @@ -6,7 +6,7 @@ # run again. Do not edit this file unless you know what you are doing. -from PySide6 import QtCore, QtGui, QtWidgets +from PySide6 import QtCore, QtWidgets class Ui_extend_confirm(object): diff --git a/src/ui/dialogs/dialog_sources/app_status_ui.py b/src/ui/dialogs/dialog_sources/app_status_ui.py index d845f0e..9a913cd 100644 --- a/src/ui/dialogs/dialog_sources/app_status_ui.py +++ b/src/ui/dialogs/dialog_sources/app_status_ui.py @@ -6,7 +6,7 @@ # run again. Do not edit this file unless you know what you are doing. -from PySide6 import QtCore, QtGui, QtWidgets +from PySide6 import QtCore class Ui_Form(object): diff --git a/src/ui/dialogs/dialog_sources/confirm_extend_ui.py b/src/ui/dialogs/dialog_sources/confirm_extend_ui.py index bef4a6b..2eed1d9 100644 --- a/src/ui/dialogs/dialog_sources/confirm_extend_ui.py +++ b/src/ui/dialogs/dialog_sources/confirm_extend_ui.py @@ -6,7 +6,7 @@ # run again. Do not edit this file unless you know what you are doing. -from PySide6 import QtCore, QtGui, QtWidgets +from PySide6 import QtCore, QtWidgets class Ui_extend_confirm(object): diff --git a/src/ui/dialogs/dialog_sources/deletedialog_ui.py b/src/ui/dialogs/dialog_sources/deletedialog_ui.py index 878a448..138b6d6 100644 --- a/src/ui/dialogs/dialog_sources/deletedialog_ui.py +++ b/src/ui/dialogs/dialog_sources/deletedialog_ui.py @@ -8,17 +8,9 @@ ## WARNING! All changes made in this file will be lost when recompiling UI file! ################################################################################ -from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale, - QMetaObject, QObject, QPoint, QRect, - QSize, QTime, QUrl, Qt) -from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor, - QFont, QFontDatabase, QGradient, QIcon, - QImage, QKeySequence, QLinearGradient, QPainter, - QPalette, QPixmap, QRadialGradient, QTransform) -from PySide6.QtWidgets import (QApplication, QDialog, QHBoxLayout, QHeaderView, - QLabel, QLineEdit, QPushButton, QSizePolicy, - QSpacerItem, QTableWidget, QTableWidgetItem, QVBoxLayout, - QWidget) +from PySide6.QtCore import (QCoreApplication, QMetaObject) +from PySide6.QtWidgets import (QHBoxLayout, QLabel, QLineEdit, QPushButton, QSizePolicy, + QSpacerItem, QTableWidget, QTableWidgetItem, QVBoxLayout) class Ui_Dialog(object): def setupUi(self, Dialog): @@ -104,17 +96,17 @@ class Ui_Dialog(object): self.label.setText(QCoreApplication.translate("Dialog", u"Medium suchen", None)) self.lineEdit.setPlaceholderText(QCoreApplication.translate("Dialog", u"Titel/Signatursuche", None)) ___qtablewidgetitem = self.tableWidget.horizontalHeaderItem(1) - ___qtablewidgetitem.setText(QCoreApplication.translate("Dialog", u"Apparat", None)); + ___qtablewidgetitem.setText(QCoreApplication.translate("Dialog", u"Apparat", None)) ___qtablewidgetitem1 = self.tableWidget.horizontalHeaderItem(2) - ___qtablewidgetitem1.setText(QCoreApplication.translate("Dialog", u"Signatur", None)); + ___qtablewidgetitem1.setText(QCoreApplication.translate("Dialog", u"Signatur", None)) ___qtablewidgetitem2 = self.tableWidget.horizontalHeaderItem(3) - ___qtablewidgetitem2.setText(QCoreApplication.translate("Dialog", u"Titel", None)); + ___qtablewidgetitem2.setText(QCoreApplication.translate("Dialog", u"Titel", None)) ___qtablewidgetitem3 = self.tableWidget.horizontalHeaderItem(4) - ___qtablewidgetitem3.setText(QCoreApplication.translate("Dialog", u"Auflage", None)); + ___qtablewidgetitem3.setText(QCoreApplication.translate("Dialog", u"Auflage", None)) ___qtablewidgetitem4 = self.tableWidget.horizontalHeaderItem(5) - ___qtablewidgetitem4.setText(QCoreApplication.translate("Dialog", u"ISBN", None)); + ___qtablewidgetitem4.setText(QCoreApplication.translate("Dialog", u"ISBN", None)) ___qtablewidgetitem5 = self.tableWidget.horizontalHeaderItem(6) - ___qtablewidgetitem5.setText(QCoreApplication.translate("Dialog", u"ID", None)); + ___qtablewidgetitem5.setText(QCoreApplication.translate("Dialog", u"ID", None)) self.reset_btn.setText(QCoreApplication.translate("Dialog", u"Zur\u00fccksetzen", None)) self.delete_btn.setText(QCoreApplication.translate("Dialog", u"L\u00f6schen", None)) self.cancel_btn.setText(QCoreApplication.translate("Dialog", u"Abbrechen", None)) diff --git a/src/ui/dialogs/dialog_sources/elsa_add_table_entry_ui.py b/src/ui/dialogs/dialog_sources/elsa_add_table_entry_ui.py index a66c087..04c176a 100644 --- a/src/ui/dialogs/dialog_sources/elsa_add_table_entry_ui.py +++ b/src/ui/dialogs/dialog_sources/elsa_add_table_entry_ui.py @@ -8,14 +8,8 @@ ## WARNING! All changes made in this file will be lost when recompiling UI file! ################################################################################ -from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale, - QMetaObject, QObject, QPoint, QRect, - QSize, QTime, QUrl, Qt) -from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor, - QFont, QFontDatabase, QGradient, QIcon, - QImage, QKeySequence, QLinearGradient, QPainter, - QPalette, QPixmap, QRadialGradient, QTransform) -from PySide6.QtWidgets import (QAbstractButton, QApplication, QDialog, QDialogButtonBox, +from PySide6.QtCore import (QCoreApplication, QMetaObject, Qt) +from PySide6.QtWidgets import (QDialogButtonBox, QGridLayout, QGroupBox, QHBoxLayout, QLabel, QLineEdit, QPushButton, QRadioButton, QSizePolicy, QSpacerItem, QStackedWidget, QTextEdit, QToolButton, diff --git a/src/ui/dialogs/dialog_sources/mail_preview_ui.py b/src/ui/dialogs/dialog_sources/mail_preview_ui.py index 236064c..8fd1ef7 100644 --- a/src/ui/dialogs/dialog_sources/mail_preview_ui.py +++ b/src/ui/dialogs/dialog_sources/mail_preview_ui.py @@ -8,17 +8,10 @@ ## WARNING! All changes made in this file will be lost when recompiling UI file! ################################################################################ -from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale, - QMetaObject, QObject, QPoint, QRect, - QSize, QTime, QUrl, Qt) -from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor, - QFont, QFontDatabase, QGradient, QIcon, - QImage, QKeySequence, QLinearGradient, QPainter, - QPalette, QPixmap, QRadialGradient, QTransform) -from PySide6.QtWidgets import (QAbstractButton, QApplication, QComboBox, QDialog, - QDialogButtonBox, QGridLayout, QHBoxLayout, QLabel, +from PySide6.QtCore import (QCoreApplication, QMetaObject, QSize, Qt) +from PySide6.QtWidgets import (QComboBox, QDialogButtonBox, QGridLayout, QHBoxLayout, QLabel, QLineEdit, QPushButton, QRadioButton, QSizePolicy, - QSpacerItem, QTextEdit, QWidget) + QSpacerItem, QTextEdit) class Ui_eMailPreview(object): def setupUi(self, eMailPreview): diff --git a/src/ui/dialogs/dialog_sources/newMailTemplateDesigner_ui.py b/src/ui/dialogs/dialog_sources/newMailTemplateDesigner_ui.py index e5f3495..f13b04c 100644 --- a/src/ui/dialogs/dialog_sources/newMailTemplateDesigner_ui.py +++ b/src/ui/dialogs/dialog_sources/newMailTemplateDesigner_ui.py @@ -8,15 +8,8 @@ ## WARNING! All changes made in this file will be lost when recompiling UI file! ################################################################################ -from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale, - QMetaObject, QObject, QPoint, QRect, - QSize, QTime, QUrl, Qt) -from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor, - QFont, QFontDatabase, QGradient, QIcon, - QImage, QKeySequence, QLinearGradient, QPainter, - QPalette, QPixmap, QRadialGradient, QTransform) -from PySide6.QtWidgets import (QAbstractButton, QApplication, QComboBox, QDialog, - QDialogButtonBox, QGridLayout, QHBoxLayout, QLabel, +from PySide6.QtCore import (QCoreApplication, QMetaObject, Qt) +from PySide6.QtWidgets import (QComboBox, QDialogButtonBox, QGridLayout, QHBoxLayout, QLabel, QLineEdit, QPushButton, QSizePolicy, QSpacerItem, QTextEdit, QVBoxLayout, QWidget) diff --git a/src/ui/dialogs/dialog_sources/order_neweditions_ui.py b/src/ui/dialogs/dialog_sources/order_neweditions_ui.py index 0edcbf2..92f8cee 100644 --- a/src/ui/dialogs/dialog_sources/order_neweditions_ui.py +++ b/src/ui/dialogs/dialog_sources/order_neweditions_ui.py @@ -8,16 +8,9 @@ ## WARNING! All changes made in this file will be lost when recompiling UI file! ################################################################################ -from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale, - QMetaObject, QObject, QPoint, QRect, - QSize, QTime, QUrl, Qt) -from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor, - QFont, QFontDatabase, QGradient, QIcon, - QImage, QKeySequence, QLinearGradient, QPainter, - QPalette, QPixmap, QRadialGradient, QTransform) -from PySide6.QtWidgets import (QApplication, QDialog, QHBoxLayout, QHeaderView, - QPushButton, QSizePolicy, QSpacerItem, QTableWidget, - QTableWidgetItem, QVBoxLayout, QWidget) +from PySide6.QtCore import (QCoreApplication, QMetaObject) +from PySide6.QtWidgets import (QHBoxLayout, QPushButton, QSizePolicy, QSpacerItem, QTableWidget, + QTableWidgetItem, QVBoxLayout) class Ui_Dialog(object): def setupUi(self, Dialog): @@ -72,21 +65,21 @@ class Ui_Dialog(object): def retranslateUi(self, Dialog): Dialog.setWindowTitle(QCoreApplication.translate("Dialog", u"Dialog", None)) ___qtablewidgetitem = self.tableWidget.horizontalHeaderItem(0) - ___qtablewidgetitem.setText(QCoreApplication.translate("Dialog", u"Bestellen", None)); + ___qtablewidgetitem.setText(QCoreApplication.translate("Dialog", u"Bestellen", None)) ___qtablewidgetitem1 = self.tableWidget.horizontalHeaderItem(1) - ___qtablewidgetitem1.setText(QCoreApplication.translate("Dialog", u"Signatur", None)); + ___qtablewidgetitem1.setText(QCoreApplication.translate("Dialog", u"Signatur", None)) ___qtablewidgetitem2 = self.tableWidget.horizontalHeaderItem(2) - ___qtablewidgetitem2.setText(QCoreApplication.translate("Dialog", u"Titel", None)); + ___qtablewidgetitem2.setText(QCoreApplication.translate("Dialog", u"Titel", None)) ___qtablewidgetitem3 = self.tableWidget.horizontalHeaderItem(3) - ___qtablewidgetitem3.setText(QCoreApplication.translate("Dialog", u"ISBN", None)); + ___qtablewidgetitem3.setText(QCoreApplication.translate("Dialog", u"ISBN", None)) ___qtablewidgetitem4 = self.tableWidget.horizontalHeaderItem(4) - ___qtablewidgetitem4.setText(QCoreApplication.translate("Dialog", u"Autor", None)); + ___qtablewidgetitem4.setText(QCoreApplication.translate("Dialog", u"Autor", None)) ___qtablewidgetitem5 = self.tableWidget.horizontalHeaderItem(5) - ___qtablewidgetitem5.setText(QCoreApplication.translate("Dialog", u"Auflage", None)); + ___qtablewidgetitem5.setText(QCoreApplication.translate("Dialog", u"Auflage", None)) ___qtablewidgetitem6 = self.tableWidget.horizontalHeaderItem(6) - ___qtablewidgetitem6.setText(QCoreApplication.translate("Dialog", u"Standort", None)); + ___qtablewidgetitem6.setText(QCoreApplication.translate("Dialog", u"Standort", None)) ___qtablewidgetitem7 = self.tableWidget.horizontalHeaderItem(7) - ___qtablewidgetitem7.setText(QCoreApplication.translate("Dialog", u"Link", None)); + ___qtablewidgetitem7.setText(QCoreApplication.translate("Dialog", u"Link", None)) self.pushButton.setText(QCoreApplication.translate("Dialog", u"Bestellen", None)) # retranslateUi diff --git a/src/ui/dialogs/dialog_sources/reminder_ui.py b/src/ui/dialogs/dialog_sources/reminder_ui.py index 25b7bf8..feea85f 100644 --- a/src/ui/dialogs/dialog_sources/reminder_ui.py +++ b/src/ui/dialogs/dialog_sources/reminder_ui.py @@ -6,7 +6,7 @@ # run again. Do not edit this file unless you know what you are doing. -from PySide6 import QtCore, QtGui, QtWidgets +from PySide6 import QtCore, QtWidgets class Ui_Erinnerung(object): diff --git a/src/ui/dialogs/ext_app.py b/src/ui/dialogs/ext_app.py index 425067e..b6c80c7 100644 --- a/src/ui/dialogs/ext_app.py +++ b/src/ui/dialogs/ext_app.py @@ -6,7 +6,7 @@ # run again. Do not edit this file unless you know what you are doing. -from PySide6 import QtCore, QtGui, QtWidgets +from PySide6 import QtCore, QtWidgets class Ui_Frame(object): diff --git a/src/ui/dialogs/mail_preview_ui.py b/src/ui/dialogs/mail_preview_ui.py index da6b448..b633b07 100644 --- a/src/ui/dialogs/mail_preview_ui.py +++ b/src/ui/dialogs/mail_preview_ui.py @@ -6,7 +6,7 @@ # run again. Do not edit this file unless you know what you are doing. -from PySide6 import QtCore, QtGui, QtWidgets +from PySide6 import QtCore, QtWidgets class Ui_eMailPreview(object): diff --git a/src/ui/dialogs/progress.py b/src/ui/dialogs/progress.py index 6abd9c2..9464ff3 100644 --- a/src/ui/dialogs/progress.py +++ b/src/ui/dialogs/progress.py @@ -1,5 +1,4 @@ -from typing import List, Optional, Set, Union -import re +from typing import List from PySide6 import QtCore from PySide6.QtWidgets import QDialog, QPushButton, QVBoxLayout diff --git a/src/ui/semesterapparat_ui_ui.py b/src/ui/semesterapparat_ui_ui.py index adfe069..13d7936 100644 --- a/src/ui/semesterapparat_ui_ui.py +++ b/src/ui/semesterapparat_ui_ui.py @@ -8,18 +8,13 @@ ## WARNING! All changes made in this file will be lost when recompiling UI file! ################################################################################ -from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale, - QMetaObject, QObject, QPoint, QRect, - QSize, QTime, QUrl, Qt) -from PySide6.QtGui import (QAction, QBrush, QColor, QConicalGradient, - QCursor, QFont, QFontDatabase, QGradient, - QIcon, QImage, QKeySequence, QLinearGradient, - QPainter, QPalette, QPixmap, QRadialGradient, - QTransform) -from PySide6.QtWidgets import (QAbstractItemView, QAbstractScrollArea, QApplication, QCheckBox, +from PySide6.QtCore import (QCoreApplication, QMetaObject, QRect, + QSize, Qt) +from PySide6.QtGui import (QAction, QFont, QIcon) +from PySide6.QtWidgets import (QAbstractItemView, QAbstractScrollArea, QCheckBox, QComboBox, QFormLayout, QFrame, QGridLayout, - QGroupBox, QHBoxLayout, QHeaderView, QLabel, - QLineEdit, QMainWindow, QMenu, QMenuBar, + QGroupBox, QHBoxLayout, QLabel, + QLineEdit, QMenu, QMenuBar, QProgressBar, QPushButton, QSizePolicy, QSpacerItem, QStatusBar, QTabWidget, QTableWidget, QTableWidgetItem, QToolButton, QVBoxLayout, QWidget) @@ -210,13 +205,13 @@ class Ui_MainWindow(object): font2.setFamilies([u"Arial"]) font2.setPointSize(8) __qtablewidgetitem6 = QTableWidgetItem() - __qtablewidgetitem6.setFont(font2); + __qtablewidgetitem6.setFont(font2) self.document_list.setHorizontalHeaderItem(0, __qtablewidgetitem6) __qtablewidgetitem7 = QTableWidgetItem() - __qtablewidgetitem7.setFont(font2); + __qtablewidgetitem7.setFont(font2) self.document_list.setHorizontalHeaderItem(1, __qtablewidgetitem7) __qtablewidgetitem8 = QTableWidgetItem() - __qtablewidgetitem8.setFont(font2); + __qtablewidgetitem8.setFont(font2) self.document_list.setHorizontalHeaderItem(2, __qtablewidgetitem8) __qtablewidgetitem9 = QTableWidgetItem() self.document_list.setHorizontalHeaderItem(3, __qtablewidgetitem9) @@ -896,28 +891,28 @@ class Ui_MainWindow(object): self.create_new_app.setText(QCoreApplication.translate("MainWindow", u"neu. App anlegen", None)) self.cancel_active_selection.setText(QCoreApplication.translate("MainWindow", u"Auswahl abbrechen", None)) ___qtablewidgetitem = self.tableWidget_apparate.horizontalHeaderItem(0) - ___qtablewidgetitem.setText(QCoreApplication.translate("MainWindow", u"AppNr", None)); + ___qtablewidgetitem.setText(QCoreApplication.translate("MainWindow", u"AppNr", None)) ___qtablewidgetitem1 = self.tableWidget_apparate.horizontalHeaderItem(1) - ___qtablewidgetitem1.setText(QCoreApplication.translate("MainWindow", u"App Name", None)); + ___qtablewidgetitem1.setText(QCoreApplication.translate("MainWindow", u"App Name", None)) ___qtablewidgetitem2 = self.tableWidget_apparate.horizontalHeaderItem(2) - ___qtablewidgetitem2.setText(QCoreApplication.translate("MainWindow", u"Professor", None)); + ___qtablewidgetitem2.setText(QCoreApplication.translate("MainWindow", u"Professor", None)) ___qtablewidgetitem3 = self.tableWidget_apparate.horizontalHeaderItem(3) - ___qtablewidgetitem3.setText(QCoreApplication.translate("MainWindow", u"g\u00fcltig bis", None)); + ___qtablewidgetitem3.setText(QCoreApplication.translate("MainWindow", u"g\u00fcltig bis", None)) ___qtablewidgetitem4 = self.tableWidget_apparate.horizontalHeaderItem(4) - ___qtablewidgetitem4.setText(QCoreApplication.translate("MainWindow", u"Dauerapparat", None)); + ___qtablewidgetitem4.setText(QCoreApplication.translate("MainWindow", u"Dauerapparat", None)) ___qtablewidgetitem5 = self.tableWidget_apparate.horizontalHeaderItem(5) - ___qtablewidgetitem5.setText(QCoreApplication.translate("MainWindow", u"KontoNr", None)); + ___qtablewidgetitem5.setText(QCoreApplication.translate("MainWindow", u"KontoNr", None)) self.add_medium.setText(QCoreApplication.translate("MainWindow", u"Medien hinzuf\u00fcgen", None)) self.label.setText(QCoreApplication.translate("MainWindow", u" Medienliste", None)) self.app_group_box.setTitle(QCoreApplication.translate("MainWindow", u"SemesterApparatsdetails", None)) ___qtablewidgetitem6 = self.document_list.horizontalHeaderItem(0) - ___qtablewidgetitem6.setText(QCoreApplication.translate("MainWindow", u"Dokumentname", None)); + ___qtablewidgetitem6.setText(QCoreApplication.translate("MainWindow", u"Dokumentname", None)) ___qtablewidgetitem7 = self.document_list.horizontalHeaderItem(1) - ___qtablewidgetitem7.setText(QCoreApplication.translate("MainWindow", u"Dateityp", None)); + ___qtablewidgetitem7.setText(QCoreApplication.translate("MainWindow", u"Dateityp", None)) ___qtablewidgetitem8 = self.document_list.horizontalHeaderItem(2) - ___qtablewidgetitem8.setText(QCoreApplication.translate("MainWindow", u"Neu?", None)); + ___qtablewidgetitem8.setText(QCoreApplication.translate("MainWindow", u"Neu?", None)) ___qtablewidgetitem9 = self.document_list.horizontalHeaderItem(3) - ___qtablewidgetitem9.setText(QCoreApplication.translate("MainWindow", u"path", None)); + ___qtablewidgetitem9.setText(QCoreApplication.translate("MainWindow", u"path", None)) self.appname_mand.setText(QCoreApplication.translate("MainWindow", u"*", None)) self.profname_mand.setText(QCoreApplication.translate("MainWindow", u"*", None)) self.fach_mand.setText(QCoreApplication.translate("MainWindow", u"*", None)) @@ -995,25 +990,25 @@ class Ui_MainWindow(object): self.avail_status.setText(QCoreApplication.translate("MainWindow", u"TextLabel", None)) self.automation_add_selected_books.setText(QCoreApplication.translate("MainWindow", u"Ausgew\u00e4hlte als verf\u00fcgbar markieren", None)) ___qtablewidgetitem10 = self.tableWidget_apparat_media.horizontalHeaderItem(0) - ___qtablewidgetitem10.setText(QCoreApplication.translate("MainWindow", u"Buchtitel", None)); + ___qtablewidgetitem10.setText(QCoreApplication.translate("MainWindow", u"Buchtitel", None)) #if QT_CONFIG(tooltip) - ___qtablewidgetitem10.setToolTip(QCoreApplication.translate("MainWindow", u"Es kann sein, dass der Buchtitel leer ist, dies kommt vor, wenn der Titel nicht passend formatiert ist", None)); + ___qtablewidgetitem10.setToolTip(QCoreApplication.translate("MainWindow", u"Es kann sein, dass der Buchtitel leer ist, dies kommt vor, wenn der Titel nicht passend formatiert ist", None)) #endif // QT_CONFIG(tooltip) ___qtablewidgetitem11 = self.tableWidget_apparat_media.horizontalHeaderItem(1) - ___qtablewidgetitem11.setText(QCoreApplication.translate("MainWindow", u"Signatur", None)); + ___qtablewidgetitem11.setText(QCoreApplication.translate("MainWindow", u"Signatur", None)) ___qtablewidgetitem12 = self.tableWidget_apparat_media.horizontalHeaderItem(2) - ___qtablewidgetitem12.setText(QCoreApplication.translate("MainWindow", u"Auflage", None)); + ___qtablewidgetitem12.setText(QCoreApplication.translate("MainWindow", u"Auflage", None)) ___qtablewidgetitem13 = self.tableWidget_apparat_media.horizontalHeaderItem(3) - ___qtablewidgetitem13.setText(QCoreApplication.translate("MainWindow", u"Autor", None)); + ___qtablewidgetitem13.setText(QCoreApplication.translate("MainWindow", u"Autor", None)) ___qtablewidgetitem14 = self.tableWidget_apparat_media.horizontalHeaderItem(4) - ___qtablewidgetitem14.setText(QCoreApplication.translate("MainWindow", u"im Apparat?", None)); + ___qtablewidgetitem14.setText(QCoreApplication.translate("MainWindow", u"im Apparat?", None)) #if QT_CONFIG(tooltip) - ___qtablewidgetitem14.setToolTip(QCoreApplication.translate("MainWindow", u"Diese Angabe ist nicht zuverl\u00e4ssig. Ist das \u274c vorhanden, kann das Medium im Apparat sein, aber aufgrund eines Bugs nicht gefunden worden", None)); + ___qtablewidgetitem14.setToolTip(QCoreApplication.translate("MainWindow", u"Diese Angabe ist nicht zuverl\u00e4ssig. Ist das \u274c vorhanden, kann das Medium im Apparat sein, aber aufgrund eines Bugs nicht gefunden worden", None)) #endif // QT_CONFIG(tooltip) ___qtablewidgetitem15 = self.tableWidget_apparat_media.horizontalHeaderItem(5) - ___qtablewidgetitem15.setText(QCoreApplication.translate("MainWindow", u"Vorgemerkt", None)); + ___qtablewidgetitem15.setText(QCoreApplication.translate("MainWindow", u"Vorgemerkt", None)) ___qtablewidgetitem16 = self.tableWidget_apparat_media.horizontalHeaderItem(6) - ___qtablewidgetitem16.setText(QCoreApplication.translate("MainWindow", u"Link", None)); + ___qtablewidgetitem16.setText(QCoreApplication.translate("MainWindow", u"Link", None)) self.tabWidget.setTabText(self.tabWidget.indexOf(self.createApparat), QCoreApplication.translate("MainWindow", u"Anlegen", None)) self.tabWidget.setTabText(self.tabWidget.indexOf(self.search_statistics), QCoreApplication.translate("MainWindow", u"Suchen / Statistik", None)) self.tabWidget.setTabText(self.tabWidget.indexOf(self.elsatab), QCoreApplication.translate("MainWindow", u"ELSA", None)) diff --git a/src/ui/widgets/widget_sources/admin_create_user_ui.py b/src/ui/widgets/widget_sources/admin_create_user_ui.py index 935fc8b..5cecc1f 100644 --- a/src/ui/widgets/widget_sources/admin_create_user_ui.py +++ b/src/ui/widgets/widget_sources/admin_create_user_ui.py @@ -6,7 +6,7 @@ # run again. Do not edit this file unless you know what you are doing. -from PySide6 import QtCore, QtGui, QtWidgets +from PySide6 import QtCore, QtWidgets class Ui_Dialog(object): diff --git a/src/ui/widgets/widget_sources/admin_edit_prof_ui.py b/src/ui/widgets/widget_sources/admin_edit_prof_ui.py index 3cbb6ce..2724e9d 100644 --- a/src/ui/widgets/widget_sources/admin_edit_prof_ui.py +++ b/src/ui/widgets/widget_sources/admin_edit_prof_ui.py @@ -6,7 +6,7 @@ # run again. Do not edit this file unless you know what you are doing. -from PySide6 import QtCore, QtGui, QtWidgets +from PySide6 import QtCore, QtWidgets class Ui_Dialog(object): diff --git a/src/ui/widgets/widget_sources/admin_edit_user_ui.py b/src/ui/widgets/widget_sources/admin_edit_user_ui.py index a33fce7..efc212a 100644 --- a/src/ui/widgets/widget_sources/admin_edit_user_ui.py +++ b/src/ui/widgets/widget_sources/admin_edit_user_ui.py @@ -6,7 +6,7 @@ # run again. Do not edit this file unless you know what you are doing. -from PySide6 import QtCore, QtGui, QtWidgets +from PySide6 import QtCore, QtWidgets class Ui_Dialog(object): diff --git a/src/ui/widgets/widget_sources/admin_update_signatures_ui.py b/src/ui/widgets/widget_sources/admin_update_signatures_ui.py index 6ce7841..3cf93a6 100644 --- a/src/ui/widgets/widget_sources/admin_update_signatures_ui.py +++ b/src/ui/widgets/widget_sources/admin_update_signatures_ui.py @@ -8,16 +8,10 @@ ## WARNING! All changes made in this file will be lost when recompiling UI file! ################################################################################ -from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale, - QMetaObject, QObject, QPoint, QRect, - QSize, QTime, QUrl, Qt) -from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor, - QFont, QFontDatabase, QGradient, QIcon, - QImage, QKeySequence, QLinearGradient, QPainter, - QPalette, QPixmap, QRadialGradient, QTransform) -from PySide6.QtWidgets import (QApplication, QDialog, QFormLayout, QLabel, +from PySide6.QtCore import (QCoreApplication, QMetaObject) +from PySide6.QtWidgets import (QFormLayout, QLabel, QProgressBar, QPushButton, QSizePolicy, QSpacerItem, - QSpinBox, QVBoxLayout, QWidget) + QSpinBox, QVBoxLayout) class Ui_Dialog(object): def setupUi(self, Dialog): diff --git a/src/ui/widgets/widget_sources/calendar_entry_ui.py b/src/ui/widgets/widget_sources/calendar_entry_ui.py index 8041d1f..f0e43b6 100644 --- a/src/ui/widgets/widget_sources/calendar_entry_ui.py +++ b/src/ui/widgets/widget_sources/calendar_entry_ui.py @@ -6,7 +6,7 @@ # run again. Do not edit this file unless you know what you are doing. -from PySide6 import QtCore, QtGui, QtWidgets +from PySide6 import QtCore, QtWidgets class Ui_Dialog(object): diff --git a/src/ui/widgets/widget_sources/elsa_maindialog_ui.py b/src/ui/widgets/widget_sources/elsa_maindialog_ui.py index 0edfbe5..f342b08 100644 --- a/src/ui/widgets/widget_sources/elsa_maindialog_ui.py +++ b/src/ui/widgets/widget_sources/elsa_maindialog_ui.py @@ -6,7 +6,7 @@ # run again. Do not edit this file unless you know what you are doing. -from PySide6 import QtCore, QtGui, QtWidgets +from PySide6 import QtCore, QtWidgets class Ui_Dialog(object): diff --git a/src/ui/widgets/widget_sources/icon_widget_ui.py b/src/ui/widgets/widget_sources/icon_widget_ui.py index 66b7475..10c966d 100644 --- a/src/ui/widgets/widget_sources/icon_widget_ui.py +++ b/src/ui/widgets/widget_sources/icon_widget_ui.py @@ -6,7 +6,7 @@ # run again. Do not edit this file unless you know what you are doing. -from PySide6 import QtCore, QtGui, QtWidgets +from PySide6 import QtCore, QtWidgets class Ui_Dialog(object): diff --git a/src/ui/widgets/widget_sources/new_edition_check_book_ui.py b/src/ui/widgets/widget_sources/new_edition_check_book_ui.py index d99db66..c9defd8 100644 --- a/src/ui/widgets/widget_sources/new_edition_check_book_ui.py +++ b/src/ui/widgets/widget_sources/new_edition_check_book_ui.py @@ -8,14 +8,8 @@ ## WARNING! All changes made in this file will be lost when recompiling UI file! ################################################################################ -from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale, - QMetaObject, QObject, QPoint, QRect, - QSize, QTime, QUrl, Qt) -from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor, - QFont, QFontDatabase, QGradient, QIcon, - QImage, QKeySequence, QLinearGradient, QPainter, - QPalette, QPixmap, QRadialGradient, QTransform) -from PySide6.QtWidgets import (QApplication, QDialog, QFormLayout, QGridLayout, +from PySide6.QtCore import (QCoreApplication, QMetaObject) +from PySide6.QtWidgets import (QFormLayout, QGridLayout, QHBoxLayout, QLabel, QLineEdit, QPushButton, QSizePolicy, QStackedWidget, QWidget) diff --git a/src/ui/widgets/widget_sources/new_edition_check_found_result_ui.py b/src/ui/widgets/widget_sources/new_edition_check_found_result_ui.py index 8e2d17f..faff39f 100644 --- a/src/ui/widgets/widget_sources/new_edition_check_found_result_ui.py +++ b/src/ui/widgets/widget_sources/new_edition_check_found_result_ui.py @@ -8,16 +8,9 @@ ## WARNING! All changes made in this file will be lost when recompiling UI file! ################################################################################ -from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale, - QMetaObject, QObject, QPoint, QRect, - QSize, QTime, QUrl, Qt) -from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor, - QFont, QFontDatabase, QGradient, QIcon, - QImage, QKeySequence, QLinearGradient, QPainter, - QPalette, QPixmap, QRadialGradient, QTransform) -from PySide6.QtWidgets import (QApplication, QCheckBox, QDialog, QFormLayout, - QLabel, QLineEdit, QSizePolicy, QSpacerItem, - QWidget) +from PySide6.QtCore import (QCoreApplication, QMetaObject, Qt) +from PySide6.QtWidgets import (QCheckBox, QFormLayout, + QLabel, QLineEdit, QSizePolicy, QSpacerItem) class Ui_Dialog(object): def setupUi(self, Dialog): diff --git a/src/ui/widgets/widget_sources/new_edition_check_selector_ui.py b/src/ui/widgets/widget_sources/new_edition_check_selector_ui.py index a70b5df..7c1c70f 100644 --- a/src/ui/widgets/widget_sources/new_edition_check_selector_ui.py +++ b/src/ui/widgets/widget_sources/new_edition_check_selector_ui.py @@ -8,15 +8,9 @@ ## WARNING! All changes made in this file will be lost when recompiling UI file! ################################################################################ -from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale, - QMetaObject, QObject, QPoint, QRect, - QSize, QTime, QUrl, Qt) -from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor, - QFont, QFontDatabase, QGradient, QIcon, - QImage, QKeySequence, QLinearGradient, QPainter, - QPalette, QPixmap, QRadialGradient, QTransform) -from PySide6.QtWidgets import (QApplication, QDialog, QHBoxLayout, QLabel, - QPushButton, QSizePolicy, QVBoxLayout, QWidget) +from PySide6.QtCore import (QCoreApplication, QMetaObject) +from PySide6.QtWidgets import (QHBoxLayout, QLabel, + QPushButton, QVBoxLayout) class Ui_Dialog(object): def setupUi(self, Dialog): diff --git a/src/ui/widgets/widget_sources/new_edition_check_ui.py b/src/ui/widgets/widget_sources/new_edition_check_ui.py index d214764..281569b 100644 --- a/src/ui/widgets/widget_sources/new_edition_check_ui.py +++ b/src/ui/widgets/widget_sources/new_edition_check_ui.py @@ -8,15 +8,9 @@ ## WARNING! All changes made in this file will be lost when recompiling UI file! ################################################################################ -from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale, - QMetaObject, QObject, QPoint, QRect, - QSize, QTime, QUrl, Qt) -from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor, - QFont, QFontDatabase, QGradient, QIcon, - QImage, QKeySequence, QLinearGradient, QPainter, - QPalette, QPixmap, QRadialGradient, QTransform) -from PySide6.QtWidgets import (QApplication, QDialog, QFrame, QHBoxLayout, - QProgressBar, QPushButton, QSizePolicy, QStackedWidget, +from PySide6.QtCore import (QCoreApplication, QMetaObject) +from PySide6.QtWidgets import (QFrame, QHBoxLayout, + QProgressBar, QPushButton, QStackedWidget, QVBoxLayout, QWidget) class Ui_Dialog(object): diff --git a/src/ui/widgets/widget_sources/search_statistic_page_ui.py b/src/ui/widgets/widget_sources/search_statistic_page_ui.py index 8a49705..c98ce4a 100644 --- a/src/ui/widgets/widget_sources/search_statistic_page_ui.py +++ b/src/ui/widgets/widget_sources/search_statistic_page_ui.py @@ -8,16 +8,10 @@ ## WARNING! All changes made in this file will be lost when recompiling UI file! ################################################################################ -from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale, - QMetaObject, QObject, QPoint, QRect, - QSize, QTime, QUrl, Qt) -from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor, - QFont, QFontDatabase, QGradient, QIcon, - QImage, QKeySequence, QLinearGradient, QPainter, - QPalette, QPixmap, QRadialGradient, QTransform) -from PySide6.QtWidgets import (QAbstractItemView, QApplication, QCheckBox, QComboBox, - QDialog, QFrame, QGridLayout, QHBoxLayout, - QHeaderView, QLabel, QLayout, QLineEdit, +from PySide6.QtCore import (QCoreApplication, QMetaObject, QSize, Qt) +from PySide6.QtWidgets import (QAbstractItemView, QCheckBox, QComboBox, + QFrame, QGridLayout, QHBoxLayout, + QLabel, QLayout, QLineEdit, QPushButton, QSizePolicy, QSpacerItem, QStackedWidget, QTabWidget, QTableWidget, QTableWidgetItem, QVBoxLayout, QWidget) @@ -438,26 +432,26 @@ class Ui_Dialog(object): self.btn_notify_for_deletion.setText(QCoreApplication.translate("Dialog", u"Ausgew\u00e4hlte Benachrichtigen", None)) self.btn_extendSelection.setText(QCoreApplication.translate("Dialog", u"Ausgew\u00e4hlte Verl\u00e4ngern", None)) ___qtablewidgetitem = self.tableWidget.horizontalHeaderItem(1) - ___qtablewidgetitem.setText(QCoreApplication.translate("Dialog", u"Apparatsname", None)); + ___qtablewidgetitem.setText(QCoreApplication.translate("Dialog", u"Apparatsname", None)) ___qtablewidgetitem1 = self.tableWidget.horizontalHeaderItem(2) - ___qtablewidgetitem1.setText(QCoreApplication.translate("Dialog", u"Apparatsnummer", None)); + ___qtablewidgetitem1.setText(QCoreApplication.translate("Dialog", u"Apparatsnummer", None)) ___qtablewidgetitem2 = self.tableWidget.horizontalHeaderItem(3) - ___qtablewidgetitem2.setText(QCoreApplication.translate("Dialog", u"Person", None)); + ___qtablewidgetitem2.setText(QCoreApplication.translate("Dialog", u"Person", None)) ___qtablewidgetitem3 = self.tableWidget.horizontalHeaderItem(4) - ___qtablewidgetitem3.setText(QCoreApplication.translate("Dialog", u"Fach", None)); + ___qtablewidgetitem3.setText(QCoreApplication.translate("Dialog", u"Fach", None)) ___qtablewidgetitem4 = self.statistics_table.horizontalHeaderItem(0) - ___qtablewidgetitem4.setText(QCoreApplication.translate("Dialog", u"Semester", None)); + ___qtablewidgetitem4.setText(QCoreApplication.translate("Dialog", u"Semester", None)) ___qtablewidgetitem5 = self.statistics_table.horizontalHeaderItem(1) - ___qtablewidgetitem5.setText(QCoreApplication.translate("Dialog", u"Zugang", None)); + ___qtablewidgetitem5.setText(QCoreApplication.translate("Dialog", u"Zugang", None)) ___qtablewidgetitem6 = self.statistics_table.horizontalHeaderItem(2) - ___qtablewidgetitem6.setText(QCoreApplication.translate("Dialog", u"Abgang", None)); + ___qtablewidgetitem6.setText(QCoreApplication.translate("Dialog", u"Abgang", None)) self.tabWidget_3.setTabText(self.tabWidget_3.indexOf(self.statistic_table), QCoreApplication.translate("Dialog", u"Tabelle", None)) self.tabWidget_3.setTabText(self.tabWidget_3.indexOf(self.graph_table), QCoreApplication.translate("Dialog", u"Erstellte und gel\u00f6schte Semesterapparate", None)) ___qtablewidgetitem7 = self.book_search_result.horizontalHeaderItem(0) - ___qtablewidgetitem7.setText(QCoreApplication.translate("Dialog", u"Titel", None)); + ___qtablewidgetitem7.setText(QCoreApplication.translate("Dialog", u"Titel", None)) ___qtablewidgetitem8 = self.book_search_result.horizontalHeaderItem(1) - ___qtablewidgetitem8.setText(QCoreApplication.translate("Dialog", u"Signatur", None)); + ___qtablewidgetitem8.setText(QCoreApplication.translate("Dialog", u"Signatur", None)) ___qtablewidgetitem9 = self.book_search_result.horizontalHeaderItem(2) - ___qtablewidgetitem9.setText(QCoreApplication.translate("Dialog", u"Apparat", None)); + ___qtablewidgetitem9.setText(QCoreApplication.translate("Dialog", u"Apparat", None)) # retranslateUi diff --git a/src/ui/widgets/widget_sources/welcome_wizard_ui.py b/src/ui/widgets/widget_sources/welcome_wizard_ui.py index 1d87792..98fd103 100644 --- a/src/ui/widgets/widget_sources/welcome_wizard_ui.py +++ b/src/ui/widgets/widget_sources/welcome_wizard_ui.py @@ -8,14 +8,8 @@ ## WARNING! All changes made in this file will be lost when recompiling UI file! ################################################################################ -from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale, - QMetaObject, QObject, QPoint, QRect, - QSize, QTime, QUrl, Qt) -from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor, - QFont, QFontDatabase, QGradient, QIcon, - QImage, QKeySequence, QLinearGradient, QPainter, - QPalette, QPixmap, QRadialGradient, QTransform) -from PySide6.QtWidgets import (QApplication, QCheckBox, QComboBox, QFormLayout, +from PySide6.QtCore import (QCoreApplication, QMetaObject, Qt) +from PySide6.QtWidgets import (QCheckBox, QComboBox, QFormLayout, QGridLayout, QHBoxLayout, QLabel, QLineEdit, QPushButton, QSizePolicy, QSpacerItem, QTextEdit, QToolButton, QVBoxLayout, QWidget, QWizard, diff --git a/src/utils/__init__.py b/src/utils/__init__.py index 0eafea8..cdb6ec6 100644 --- a/src/utils/__init__.py +++ b/src/utils/__init__.py @@ -1,3 +1,10 @@ +__all__ = [ + "Icon", + "SemesterDocument", + "app_sort", + "name_sort", +] + from .icon import Icon -from .sortgenerator import app_sort, name_sort from .richtext import SemesterDocument +from .sortgenerator import app_sort, name_sort diff --git a/src/utils/docs_ui.py b/src/utils/docs_ui.py index 1f0523a..49dab04 100644 --- a/src/utils/docs_ui.py +++ b/src/utils/docs_ui.py @@ -6,7 +6,7 @@ # run again. Do not edit this file unless you know what you are doing. -from PySide6 import QtCore, QtGui, QtWidgets +from PySide6 import QtCore, QtWidgets class Ui_Dialog(object): diff --git a/src/utils/documentationview.py b/src/utils/documentationview.py index ca180f2..e69de29 100644 --- a/src/utils/documentationview.py +++ b/src/utils/documentationview.py @@ -1,46 +0,0 @@ -import os -import sys - -# from PySide6 import Webview -from PySide6.QtWidgets import QApplication, QMainWindow, QTabWidget - -documentation_path = "docs" - - -class DocumentationViewer(QMainWindow): - def __init__(self): - super().__init__() - self.setWindowTitle("Documentation Viewer") - self.setGeometry(100, 100, 800, 600) - - self.tabs = QTabWidget() - self.setCentralWidget(self.tabs) - - self.set_documentation_tabs() - - def set_documentation_tabs(self): - files = [ - os.path.join(documentation_path, file) - for file in os.listdir(documentation_path) - if file.endswith(".html") - ] - for file in files: - with open(file, "r") as f: - html_content = f.read() - tab_name = os.path.basename(file).split(".")[0] - self.load_documentation(tab_name, html_content) - - def load_documentation( - self, - tab_name="Documentation", - html_content="

    Documentation

    Your HTML documentation content goes here.

    ", - ): - # open documentation - name = tab_name - - -if __name__ == "__main__": - app = QApplication(sys.argv) - viewer = DocumentationViewer() - viewer.show() - sys.exit(app.exec()) diff --git a/src/utils/files.py b/src/utils/files.py index 12f0f81..cd18bca 100644 --- a/src/utils/files.py +++ b/src/utils/files.py @@ -3,7 +3,7 @@ import os from pathlib import Path -from src import LOG_DIR, settings +from src import settings from src.database import Database from src.shared.logging import log diff --git a/src/utils/temp.py b/src/utils/temp.py deleted file mode 100644 index 59eaa09..0000000 --- a/src/utils/temp.py +++ /dev/null @@ -1,4 +0,0 @@ -template = "{{sap{}_prof}} ({{sap_{}_name}})" -result = "\n".join(template.format(i, i) for i in range(1, 201)) - -print(result) -- 2.49.1 From 2d54d64a46063c5d48e71c849f7645e31ef1445a Mon Sep 17 00:00:00 2001 From: WorldTeacher Date: Mon, 24 Nov 2025 12:40:32 +0100 Subject: [PATCH 39/40] sort imports --- src/ui/widgets/elsa_main.py | 159 ++++++++++++++++++++---------------- 1 file changed, 90 insertions(+), 69 deletions(-) diff --git a/src/ui/widgets/elsa_main.py b/src/ui/widgets/elsa_main.py index 4975b43..bca36ef 100644 --- a/src/ui/widgets/elsa_main.py +++ b/src/ui/widgets/elsa_main.py @@ -5,14 +5,14 @@ from PySide6.QtCore import QDate from PySide6.QtGui import QRegularExpressionValidator from src import Icon -from src.database import Database -from src.utils.files import recreateElsaFile from src.core.models import Prof, Semester +from src.database import Database from src.logic import elsa_word_to_csv from src.shared.logging import log from src.ui.dialogs import ElsaAddEntry, popus_confirm from src.ui.widgets.filepicker import FilePicker from src.ui.widgets.graph import DataQtGraph +from src.utils.files import recreateElsaFile from .widget_sources.elsa_maindialog_ui import Ui_Dialog @@ -22,7 +22,7 @@ class ElsaDialog(QtWidgets.QDialog, Ui_Dialog): super().__init__() self.setupUi(self) self.table_elsa_list.setContextMenuPolicy( - QtCore.Qt.ContextMenuPolicy.CustomContextMenu + QtCore.Qt.ContextMenuPolicy.CustomContextMenu, ) self.table_elsa_list.customContextMenuRequested.connect(self.elsa_context_menu) # elsa buttons @@ -52,11 +52,11 @@ class ElsaDialog(QtWidgets.QDialog, Ui_Dialog): # set header to occupy the whole width and auto scale based on table width self.elsa_statistics_table.horizontalHeader().setStretchLastSection(True) self.elsa_statistics_table.horizontalHeader().setSectionResizeMode( - QtWidgets.QHeaderView.ResizeMode.Stretch + QtWidgets.QHeaderView.ResizeMode.Stretch, ) # if table size gets smaller, set horitzontal headers text to be left aligned self.elsa_statistics_table.horizontalHeader().setDefaultAlignment( - QtCore.Qt.AlignmentFlag.AlignLeft + QtCore.Qt.AlignmentFlag.AlignLeft, ) # self.table_elsa_list. Icon("person", self.prof_icon) @@ -65,13 +65,13 @@ class ElsaDialog(QtWidgets.QDialog, Ui_Dialog): self.newProf_mail.setValidator( QRegularExpressionValidator( QtCore.QRegularExpression( - r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}" - ) - ) + r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}", + ), + ), ) self.newProf_telnr.setValidator(QtGui.QIntValidator()) self.newProf_telnr.setValidator( - QtGui.QRegularExpressionValidator(QtCore.QRegularExpression(r"^\d{3,14}")) + QtGui.QRegularExpressionValidator(QtCore.QRegularExpression(r"^\d{3,14}")), ) ##Variables @@ -111,7 +111,7 @@ class ElsaDialog(QtWidgets.QDialog, Ui_Dialog): self.elsa_save.setToolTip("Bitte erst Daten eingeben") self.newProf_mail.setToolTip("Bitte geben Sie eine gültige E-Mail ein") self.newProf_telnr.setToolTip( - "Bitte geben Sie eine gültige Telefonnummer ein" + "Bitte geben Sie eine gültige Telefonnummer ein", ) def checkProf(self): @@ -131,9 +131,7 @@ class ElsaDialog(QtWidgets.QDialog, Ui_Dialog): def getProfs(self): profs = self.db.getProfs() - profs = [ - ("{}, {}".format(prof.lastname, prof.firstname), prof.id) for prof in profs - ] + profs = [(f"{prof.lastname}, {prof.firstname}", prof.id) for prof in profs] profs = list(set(profs)) profs.sort() @@ -142,7 +140,6 @@ class ElsaDialog(QtWidgets.QDialog, Ui_Dialog): def elsa_context_menu(self, position): QtWidgets.QMenu() # TODO: add functions - pass def elsa_table_entry(self): data = ElsaAddEntry() @@ -240,7 +237,8 @@ class ElsaDialog(QtWidgets.QDialog, Ui_Dialog): self.db.createProf(profdata) prof_id = self.db.getProfId(profdata) self.profs.append( - "f{}, {}".format(profdata.lastname, profdata.firstname), prof_id + f"f{profdata.lastname}, {profdata.firstname}", + prof_id, ) elsa_id = self.db.createElsaApparat( date, @@ -318,14 +316,20 @@ class ElsaDialog(QtWidgets.QDialog, Ui_Dialog): for document in documents: self.dokument_list_elsa.insertRow(0) self.dokument_list_elsa.setItem( - 0, 0, QtWidgets.QTableWidgetItem(document[0]) + 0, + 0, + QtWidgets.QTableWidgetItem(document[0]), ) self.dokument_list_elsa.setItem( - 0, 1, QtWidgets.QTableWidgetItem(document[1]) + 0, + 1, + QtWidgets.QTableWidgetItem(document[1]), ) self.dokument_list_elsa.setItem(0, 2, QtWidgets.QTableWidgetItem("❌")) self.dokument_list_elsa.setItem( - 0, 3, QtWidgets.QTableWidgetItem("Database") + 0, + 3, + QtWidgets.QTableWidgetItem("Database"), ) scans = self.db.getElsaMedia(elsa_id) if scans == []: @@ -340,19 +344,27 @@ class ElsaDialog(QtWidgets.QDialog, Ui_Dialog): def setElsaRow(self, scan): self.table_elsa_list.insertRow(0) self.table_elsa_list.setItem( - 0, 0, QtWidgets.QTableWidgetItem(scan["work_author"]) + 0, + 0, + QtWidgets.QTableWidgetItem(scan["work_author"]), ) self.table_elsa_list.setItem( - 0, 1, QtWidgets.QTableWidgetItem(scan["section_author"]) + 0, + 1, + QtWidgets.QTableWidgetItem(scan["section_author"]), ) self.table_elsa_list.setItem(0, 2, QtWidgets.QTableWidgetItem(scan["year"])) self.table_elsa_list.setItem(0, 3, QtWidgets.QTableWidgetItem(scan["issue"])) self.table_elsa_list.setItem(0, 4, QtWidgets.QTableWidgetItem(scan["edition"])) self.table_elsa_list.setItem( - 0, 5, QtWidgets.QTableWidgetItem(scan["work_title"]) + 0, + 5, + QtWidgets.QTableWidgetItem(scan["work_title"]), ) self.table_elsa_list.setItem( - 0, 6, QtWidgets.QTableWidgetItem(scan["chapter_title"]) + 0, + 6, + QtWidgets.QTableWidgetItem(scan["chapter_title"]), ) self.table_elsa_list.setItem( 0, @@ -361,10 +373,14 @@ class ElsaDialog(QtWidgets.QDialog, Ui_Dialog): ) self.table_elsa_list.setItem(0, 8, QtWidgets.QTableWidgetItem(scan["location"])) self.table_elsa_list.setItem( - 0, 9, QtWidgets.QTableWidgetItem(scan["publisher"]) + 0, + 9, + QtWidgets.QTableWidgetItem(scan["publisher"]), ) self.table_elsa_list.setItem( - 0, 10, QtWidgets.QTableWidgetItem(scan["signature"]) + 0, + 10, + QtWidgets.QTableWidgetItem(scan["signature"]), ) self.table_elsa_list.setItem(0, 11, QtWidgets.QTableWidgetItem(scan["type"])) @@ -391,51 +407,52 @@ class ElsaDialog(QtWidgets.QDialog, Ui_Dialog): def parseDokumentElsa(self): if self.dokument_list_elsa.rowCount() == 0: return - else: - # get the file path of the selected file based on it's row - row = self.dokument_list_elsa.currentRow() - file = self.dokument_list_elsa.item(row, 3).text() - file_location = file - if file == "Database": - filename = self.dokument_list_elsa.item(row, 0).text() - filetype = self.dokument_list_elsa.item(row, 1).text() + # get the file path of the selected file based on it's row + row = self.dokument_list_elsa.currentRow() + file = self.dokument_list_elsa.item(row, 3).text() + file_location = file + if file == "Database": + filename = self.dokument_list_elsa.item(row, 0).text() + filetype = self.dokument_list_elsa.item(row, 1).text() - file = recreateElsaFile( - filename=filename, filetype=filetype, open=False - ) - data, _ = elsa_word_to_csv(file) - elsa_id = self.db.getElsaId( - self.db.getProfId(Prof(fullname=self.elsa_prof.currentText())), - self.elsa_semester.text(), - self.elsa_date.text(), + file = recreateElsaFile( + filename=filename, + filetype=filetype, + open=False, ) - log.debug( - f"elsa_id: {elsa_id}, prof: {self.elsa_prof.currentText()}, semester: {self.elsa_semester.text()}, date: {self.elsa_date.text()}" + data, _ = elsa_word_to_csv(file) + elsa_id = self.db.getElsaId( + self.db.getProfId(Prof(fullname=self.elsa_prof.currentText())), + self.elsa_semester.text(), + self.elsa_date.text(), + ) + log.debug( + f"elsa_id: {elsa_id}, prof: {self.elsa_prof.currentText()}, semester: {self.elsa_semester.text()}, date: {self.elsa_date.text()}", + ) + if file_location != "Database": + self.db.insertElsaFile( + [ + { + "name": file.split("/")[-1], + "path": file, + "type": file.split(".")[-1], + }, + ], + elsa_id, ) - if file_location != "Database": - self.db.insertElsaFile( - [ - { - "name": file.split("/")[-1], - "path": file, - "type": file.split(".")[-1], - } - ], - elsa_id, - ) - for row in data: - if self.seperateEntries.isChecked(): - if ";" in row["pages"]: - count = row["pages"].split(";") - for i in range(len(count)): - row["pages"] = count[i] - self.setElsaRow( - row, - ) - self.db.addElsaMedia(row, elsa_id) - else: - self.setElsaRow(row) - self.db.addElsaMedia(row, elsa_id) + for row in data: + if self.seperateEntries.isChecked(): + if ";" in row["pages"]: + count = row["pages"].split(";") + for i in range(len(count)): + row["pages"] = count[i] + self.setElsaRow( + row, + ) + self.db.addElsaMedia(row, elsa_id) + else: + self.setElsaRow(row) + self.db.addElsaMedia(row, elsa_id) self.quote_entry.setEnabled(True) @@ -454,7 +471,7 @@ class ElsaDialog(QtWidgets.QDialog, Ui_Dialog): self.elsa_cancel_create.click() try: self.elsa_statistics.removeTab(1) - except: + except IndexError: log.debug("No tab to remove") self.elsa_table.setRowCount(0) elsa_apparats = self.db.getElsaApparats() @@ -486,10 +503,14 @@ class ElsaDialog(QtWidgets.QDialog, Ui_Dialog): for i in range(len(self.graph_data["x"])): self.elsa_statistics_table.insertRow(0) self.elsa_statistics_table.setItem( - 0, 0, QtWidgets.QTableWidgetItem(self.graph_data["x"][i]) + 0, + 0, + QtWidgets.QTableWidgetItem(self.graph_data["x"][i]), ) self.elsa_statistics_table.setItem( - 0, 1, QtWidgets.QTableWidgetItem(str(self.graph_data["y"][i])) + 0, + 1, + QtWidgets.QTableWidgetItem(str(self.graph_data["y"][i])), ) self.elsa_statistics.addTab(graph, "Graph") -- 2.49.1 From 84b00409f71f36ad475c21ca9815c5c7844647cd Mon Sep 17 00:00:00 2001 From: WorldTeacher Date: Mon, 24 Nov 2025 13:52:24 +0100 Subject: [PATCH 40/40] test release workflow --- .gitea/workflows/release.yml | 230 ++++++++++++++++++++++++++++++----- 1 file changed, 197 insertions(+), 33 deletions(-) diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index 892b841..7b84059 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -1,59 +1,223 @@ +name: Build and Release + on: workflow_dispatch: inputs: - release_notes: - description: Release notes (use \n for newlines) - type: string - required: false github_release: - description: 'Create Gitea Release' + description: "Create Gitea Release" default: true type: boolean + prerelease: + description: "Is this a prerelease?" + default: false + type: boolean bump: - description: 'Bump type' + description: "Bump type" required: false - default: 'patch' + default: "patch" type: choice options: - - 'major' - - 'minor' - - 'patch' + - "major" + - "minor" + - "patch" +env: + BASE_URL: "http://192.168.178.110:3000" jobs: - bump: + prepare: runs-on: ubuntu-latest + outputs: + version: ${{ steps.bump.outputs.version }} + tag: ${{ steps.bump.outputs.tag }} + changelog: ${{ steps.build_changelog.outputs.changelog }} + steps: - - name: Checkout - uses: actions/checkout@v4 + - name: Checkout code + uses: actions/checkout@v5 with: fetch-depth: 0 - - name: Install UV + fetch-tags: true + + - name: Install uv uses: astral-sh/setup-uv@v5 + - name: Set up Python - run: uv python install + uses: actions/setup-python@v5 + with: + # Uses the version specified in pyproject.toml + python-version-file: "pyproject.toml" + + - name: Set Git identity + run: | + git config user.name "Gitea CI" + git config user.email "ci@git.theprivateserver.de" + + - name: Bump version + id: bump + run: | + uv tool install bump-my-version + + uv tool run bump-my-version bump "${{ github.event.inputs.bump }}" + version="$(uv tool run bump-my-version show current_version)" + + echo "VERSION=$version" >> "$GITHUB_ENV" + echo "version=$version" >> "$GITHUB_OUTPUT" + echo "tag=v$version" >> "$GITHUB_OUTPUT" + # no env needed here, uv handles the Python it installs + + - name: Push changes + uses: ad-m/github-push-action@master + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + branch: ${{ github.ref }} + + - name: Build Changelog + id: build_changelog + uses: https://github.com/mikepenz/release-changelog-builder-action@v6.0.1 + with: + platform: "gitea" + baseURL: "${{ env.BASE_URL }}" + configuration: ".gitea/changelog_config.json" + env: + GITHUB_TOKEN: ${{ secrets.GITEA_TOKEN }} + + build-linux: + needs: prepare + runs-on: ubuntu-latest + env: + VERSION: ${{ needs.prepare.outputs.version }} + TAG_NAME: ${{ needs.prepare.outputs.tag }} + + steps: + - name: Checkout code + uses: actions/checkout@v5 + with: + fetch-depth: 0 + fetch-tags: true + + - name: Install uv + uses: astral-sh/setup-uv@v5 + + - name: Set up Python + uses: actions/setup-python@v5 with: python-version-file: "pyproject.toml" - - name: Install dependencies - run: uv sync --locked --all-extras --dev - - name: Install Bump tool - run: uv tool install bump-my-version - - name: Bump version - id: bump_version + + - name: Install all dependencies + run: uv sync --all-groups + + - name: Build Linux release with Nuitka run: | - uv tool run bump-my-version bump ${{ github.event.inputs.bump }} --tag --allow-dirty - - name: Add release notes - id: add_release_notes + uv run python -m nuitka \ + --standalone \ + --output-dir=dist \ + --include-data-dir=./config=config \ + --include-data-dir=./site=site \ + --include-data-dir=./icons=icons \ + --include-data-dir=./mail_vorlagen=mail_vorlagen \ + --enable-plugin=pyside6 \ + --product-name=SemesterApparatsManager \ + --product-version=${VERSION} \ + --output-filename=SAM \ + main.py + + - name: Prepare Linux Release Artifact run: | - echo "RELEASE_NOTES<> $GITHUB_ENV - echo "${{ github.event.inputs.release_notes }}" >> $GITHUB_ENV - echo "EOF" >> $GITHUB_ENV - - name: Create Gitea Release + mkdir -p releases + cd dist/SemesterApparatsManager.dist + zip -r "../../releases/SAM-linux-v${VERSION}.zip" * + cd ../../ + + - name: Create / Update Gitea Release (Linux asset + changelog) if: ${{ github.event.inputs.github_release == 'true' }} - uses: softprops/action-gh-release@v1 + uses: softprops/action-gh-release@v2 with: - tag_name: - release_name: Release ${{ github.sha }} - body: ${{ env.RELEASE_NOTES }} + tag_name: ${{ env.TAG_NAME }} + name: Release ${{ env.TAG_NAME }} + body: ${{ needs.prepare.outputs.changelog }} draft: false - prerelease: false + prerelease: ${{ github.event.inputs.prerelease }} + make_latest: true + files: | + releases/SAM-linux-v${{ env.VERSION }}.zip + env: + GITHUB_TOKEN: ${{ secrets.TOKEN }} + GITHUB_REPOSITORY: ${{ github.repository }} + + build-windows: + needs: [prepare, build-linux] + runs-on: windows-latest + env: + VERSION: ${{ needs.prepare.outputs.version }} + TAG_NAME: ${{ needs.prepare.outputs.tag }} + UV_PATH: 'C:\Users\gitea_runner_windows\.local\bin\uv.exe' + UV_NO_PROJECT: "1" + UV_NO_CONFIG: "1" + + steps: + - name: Checkout code + uses: actions/checkout@v5 + + - name: Ensure Python via uv + shell: powershell + run: | + if (-not (Test-Path $env:UV_PATH)) { + Write-Error "uv not found at $env:UV_PATH" + exit 1 + } + + $version = "3.12" + Write-Host "Checking for Python $version via uv..." + $exists = & $env:UV_PATH python list | Select-String $version -Quiet + + if (-not $exists) { + Write-Host "Python $version not found; installing with uv..." + & $env:UV_PATH python install $version + } else { + Write-Host "Python $version already installed in uv." + } + + - name: Install build dependencies + shell: powershell + run: | + & $env:UV_PATH sync --all-groups + + - name: Build Windows release with Nuitka + shell: powershell + run: | + & $env:UV_PATH run --python 3.12 python -m nuitka ` + --standalone ` + --assume-yes-for-downloads ` + --output-dir=dist ` + --mingw64 ` + --include-data-dir=./config=config ` + --include-data-dir=./site=site ` + --include-data-dir=./icons=icons ` + --include-data-dir=./mail_vorlagen=mail_vorlagen ` + --enable-plugin=pyside6 ` + --product-name=SemesterApparatsManager ` + --product-version=${env:VERSION} ` + --output-filename=SAM.exe ` + main.py + + - name: Prepare Windows Release Artifact + shell: powershell + run: | + New-Item -ItemType Directory -Force -Path releases | Out-Null + Set-Location dist + Compress-Archive -Path * -DestinationPath "..\releases\SAM-windows-v${env:VERSION}.zip" -Force + Set-Location .. + + - name: Attach Windows asset to Gitea Release + if: ${{ github.event.inputs.github_release == 'true' }} + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ env.TAG_NAME }} + draft: false + prerelease: ${{ github.event.inputs.prerelease }} + files: | + releases/SAM-windows-v${{ env.VERSION }}.zip + env: + GITHUB_TOKEN: ${{ secrets.TOKEN }} + # GITHUB_REPOSITORY: ${{ github.repository }} -- 2.49.1