import sys 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, Semester from src.logic import Prof, custom_sort, sort_semesters_list from src.ui.dialogs import ApparatExtendDialog, Mail_Dialog, ReminderDialog from src.ui.widgets import DataQtGraph, StatusWidget 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): super().__init__(parent) class SearchStatisticPage(QtWidgets.QDialog, Ui_Dialog): apparat_open = Signal(str) reloadSignal = Signal() refreshSignal = Signal() updateCalendar = Signal(int, list) def __init__(self): log.info("SearchStatisticPage started") super().__init__() self.setupUi(self) self.book_search_result.horizontalHeader().setSectionResizeMode( QtWidgets.QHeaderView.ResizeMode.Stretch ) self.check_deletable.stateChanged.connect(self.gridchange) self.tableWidget.horizontalHeader().setSectionResizeMode( QtWidgets.QHeaderView.ResizeMode.Stretch ) self.btn_del_select_apparats.clicked.connect(self.delete_selected_apparats) self.statistics_table.doubleClicked.connect(self.display_detailed_data) self.tabWidget_2.currentChanged.connect(self.tabW2_changed) self.btn_search.clicked.connect(self.statistics) self.tableWidget.customContextMenuRequested.connect( self.statistics_table_context_menu ) # statistic side buttons self.btn_notify_for_deletion.clicked.connect(self.notify_for_deletion) self.btn_notify_for_deletion.setEnabled(False) self.btn_del_select_apparats.setEnabled(False) self.tableWidget.resizeColumnsToContents() self.tableWidget.resizeRowsToContents() self.db = Database() self.appnrs = self.db.getUnavailableApparatNumbers() self.box_appnrs.addItems(str(i) for i in self.appnrs) self.splitter = QtWidgets.QSplitter(QtCore.Qt.Orientation.Horizontal) # insert splitter in apparatResult to allow resizing of the columns self.splitter.addWidget(self.app_results) self.splitter.addWidget(self.stats) self.apparatResult.layout().removeWidget(self.stats) self.apparatResult.layout().removeWidget(self.app_results) self.apparatResult.layout().addWidget(self.splitter) # set tableWidget column 0 to be 50px wide self.tableWidget.setColumnWidth(0, 50) self.semester = Semester().value self.search_by_signature.returnPressed.connect(self.search_book) self.search_by_title.returnPressed.connect(self.search_book) self.populate_tab() def restore_apparat(self): selected_rows = self.tableWidget.selectionModel().selectedRows() apparats = [] if len(selected_rows) == 0: # get position of right click row = self.tableWidget.currentRow() apparats.append(self.tableWidget.item(row, 1).text()) else: for row in selected_rows: apparats.append(self.tableWidget.item(row.row(), 1).text()) for apparat in apparats: apparat_id = self.db.getApparatId(apparat) self.db.restoreApparat(apparat_id, apparat) # remove the red color from the row # get row where the apparat is row = self.tableWidget.findItems(apparat, QtCore.Qt.MatchFlag.MatchExactly)[ 0 ].row() for j in range(5): self.tableWidget.item(row, j).setBackground(QtGui.QColor(255, 255, 255)) self.reloadSignal.emit() def statistics_table_context_menu(self, position): menu = QtWidgets.QMenu() restore_action = menu.addAction("Wiederherstellen") extend_action = menu.addAction("Verlängern") remind_action = menu.addAction("Erinnerung") menu.addAction(restore_action) restore_action.triggered.connect(self.restore_apparat) extend_action.triggered.connect(self.extend_apparat) remind_action.triggered.connect(self.reminder) menu.exec(self.tableWidget.mapToGlobal(position)) def extend_apparat(self): extend = ApparatExtendDialog() extend.exec() if extend.result() == QtWidgets.QDialog.DialogCode.Accepted: data = extend.get_data() log.debug(data) app_name = self.tableWidget.item(self.tableWidget.currentRow(), 1).text() app_id = self.db.getApparatId(app_name) self.db.setNewSemesterDate(app_id, data["semester"], data["dauerapp"]) # remove the row self.tableWidget.removeRow(self.tableWidget.currentRow()) self.refreshSignal.emit() def reminder(self): log.info("Opening reminder dialog") reminder = ReminderDialog() reminder.exec() tableposition = self.tableWidget.currentRow() appnr = self.tableWidget.item(tableposition, 2).text() if reminder.result() == QtWidgets.QDialog.DialogCode.Accepted: data = reminder.return_message() # #print(data) self.db.addMessage( data, "admin", appnr, ) self.updateCalendar.emit(data) log.info("committed message to database") def tabW2_changed(self): if self.tabWidget_2.currentIndex() == 0: self.stackedWidget_4.setCurrentIndex(0) else: self.stackedWidget_4.setCurrentIndex(1) def search_book(self): self.no_result.setText("") self.book_search_result.setRowCount(0) signature = self.search_by_signature.text() title = self.search_by_title.text() params = { "signature": signature if signature != "" else None, "title": title if title != "" else None, } params = {key: value for key, value in params.items() if value is not None} log.info(params) retdata = self.db.searchBook(params) log.info(retdata) if retdata == [] or retdata is None: self.no_result.setText("Keine Ergebnisse gefunden") return for book in retdata: log.debug(book) self.book_search_result.insertRow(0) self.book_search_result.setItem( 0, 0, QtWidgets.QTableWidgetItem(book[0].title) ) self.book_search_result.setItem( 0, 1, QtWidgets.QTableWidgetItem(book[0].signature) ) # #print(book[1]) self.book_search_result.setItem( 0, 2, QtWidgets.QTableWidgetItem( self.db.fetch_one( "SELECT semesterapparat.appnr || ' (' || semesterapparat.name || ')' AS formatted_result from semesterapparat WHERE semesterapparat.appnr = ?", (book[1],), )[0], ), ) def notify_for_deletion(self): # get all selected apparats selected_apparats: list[dict] = [] for i in range(self.tableWidget.rowCount()): data = {} if self.tableWidget.cellWidget(i, 0).isChecked(): data["app_id"] = self.tableWidget.item(i, 2).text() data["app_name"] = self.tableWidget.item(i, 1).text() data["prof_name"] = self.tableWidget.item(i, 3).text() selected_apparats.append(data) # delete all selected apparats log.debug(selected_apparats) dialogs = [] for i in selected_apparats: app_id = i["app_id"] app_name = i["app_name"] prof_name = i["prof_name"] prof_mail = self.db.getProfData(prof_name).mail self.mail_thread = Mail_Dialog( app_id=app_id, app_name=app_name, prof_name=prof_name, prof_mail=prof_mail, app_subject="", default_mail="Information bezüglich der Auflösung des Semesterapparates", ) dialogs.append(self.mail_thread) for dialog in dialogs: dialog.exec() self.btn_notify_for_deletion.setEnabled(False) def setStatisticTableSize(self): # # #print(self.statistics_table.size(), self.statistics_table.rowCount()) size = self.statistics_table.size() h = size.height() w = size.width() rows = self.statistics_table.rowCount() rowheight = round(h / rows) - 5 header_width = round(w / 3) - 5 for i in range(3): self.statistics_table.setColumnWidth(i, header_width) for i in range(rows): self.statistics_table.setRowHeight(i, rowheight) def __table_or_graph(self): return self.tabWidget_3.currentIndex() def gridchange(self): if self.check_deletable.isChecked(): self.box_semester.setEnabled(False) self.box_appnrs.setEnabled(False) self.box_appnrs.clear() self.box_person.setEnabled(False) self.box_person.clear() self.box_fach.setEnabled(False) self.box_fach.clear() self.box_erstellsemester.setEnabled(False) self.box_erstellsemester.clear() self.box_dauerapp.setEnabled(False) self.box_dauerapp.clear() self.populate_tab(self.__table_or_graph()) else: self.box_semester.setEnabled(True) self.box_appnrs.setEnabled(True) self.box_person.setEnabled(True) self.box_fach.setEnabled(True) self.box_erstellsemester.setEnabled(True) self.box_dauerapp.setEnabled(True) def populate_tab(self, table_or_graph=0): log.info("populate_tab started") # add default values to the dropdowns self.box_appnrs.clear() self.box_appnrs.addItem("") self.box_appnrs.setCurrentText("") self.box_person.clear() self.box_person.addItem("") self.box_person.setCurrentText("") self.box_fach.clear() self.box_fach.addItem("") self.box_fach.setCurrentText("") self.box_erstellsemester.clear() self.box_erstellsemester.addItem("") self.box_erstellsemester.setCurrentText("") self.box_semester.clear() self.box_semester.addItem("") self.box_semester.setCurrentText("") self.box_dauerapp.clear() self.box_dauerapp.addItems(["Ja", "Nein", ""]) self.box_dauerapp.setCurrentText("") # add custom vaules appnrs = self.appnrs 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) self.box_person.addItems( [f"{person.lastname}, {person.firstname}" for person in persons] ) self.box_fach.addItems(subject[1] for subject in self.db.getSubjects()) semester = self.db.getSemesters() semester = sort_semesters_list(semester) self.box_erstellsemester.addItems(semester) self.box_semester.addItems(semester) self.statistics_table.setRowCount(0) # set data for table and graph in tab 2 tableWidget_3 data = self.db.getApparatCountBySemester() data = custom_sort(data) # self.tabWidget_3.clear() self.tabWidget_3.removeTab(1) self.statistics_table.setRowCount(len(data)) for i in range(len(data)): self.statistics_table.setItem(i, 0, QtWidgets.QTableWidgetItem(data[i][0])) self.statistics_table.setItem( i, 1, QtWidgets.QTableWidgetItem(str(data[i][1])) ) self.statistics_table.setItem( i, 2, QtWidgets.QTableWidgetItem(str(data[i][2])) ) self.statistics_table.resizeColumnsToContents() self.statistics_table.resizeRowsToContents() # self.setStatisticTableSize() # get height of the table # create graph graph_data = { "x": [i[0] for i in data], "y": {"Erstellt": [i[1] for i in data], "Gelöscht": [i[2] for i in data]}, } graph = DataQtGraph( title="Erstellte und gelöschte Apparate", data=graph_data, generateMissing=False, y_label="Anzahl der Apparate", ) # place the graph into tabWidget_3 self.tabWidget_3.addTab(graph, "Graph") self.tabWidget_3.setCurrentIndex(table_or_graph) log.info("populate_tab finished") def delete_selected_apparats(self): # get all selected apparats selected_apparats = [] 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_apparat_rows.append(i) # delete all selected apparats # # #print(selected_apparats) log.info(f"Deleting apparats: {selected_apparats}") for apparat in selected_apparats: self.db.deleteApparat(apparat, self.semester) for row in selected_apparat_rows: # set the background of the row to red for j in range(5): self.tableWidget.item(row, j).setBackground(QtGui.QColor(235, 74, 71)) # refresh the table self.populate_tab(self.__table_or_graph()) self.btn_del_select_apparats.setEnabled(False) def statistics(self): """Generate the statistics based on the selected filters.""" self.tableWidget.setRowCount(0) active_semseter = self.semester self.db_err_message.setText("") self.btn_del_select_apparats.setEnabled(False) self.btn_notify_for_deletion.setEnabled(False) params = { "appnr": ( self.box_appnrs.currentText() if self.box_appnrs.currentText() != "" else None ), "prof_id": ( self.db.getProfId(Prof(fullname=self.box_person.currentText())) if self.box_person.currentText() != "" else None ), "fach": ( self.box_fach.currentText() if self.box_fach.currentText() != "" else None ), "erstellsemester": ( self.box_erstellsemester.currentText() if self.box_erstellsemester.currentText() != "" else None ), "dauer": ( "1" if self.box_dauerapp.currentText() == "Ja" else "0" if self.box_dauerapp.currentText() == "Nein" else None ), "endsemester": ( self.box_semester.currentText() if self.box_semester.currentText() != "" else None ), "deletable": "True" if self.check_deletable.isChecked() else None, "deletesemester": active_semseter, } params = { key: value for key, value in params.items() if value is not None } # remove empty lines to prevent errors params = { key: value for key, value in params.items() if value != "Alle" } # remove empty lines to prevent errors result = self.db.statistic_request(**params) # add QTableWidgetItems to the table # self.tableWidget.setRowCount(len(result)) if len(result) == 0: self.db_err_message.setText("Keine Ergebnisse gefunden") return 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) self.tableWidget.setRowCount(len(data)) if len(data) > 0: self.btn_del_select_apparats.setEnabled(True) self.btn_notify_for_deletion.setEnabled(True) for i in range(len(data)): # set the items 0 = clickable checkbox, 1 = appname, 2 = profname, 3 = fach self.tableWidget.setItem(i, 0, QtWidgets.QTableWidgetItem("")) self.tableWidget.setItem(i, 1, QtWidgets.QTableWidgetItem(data[i][1])) # set tooltip for the apparat name self.tableWidget.item(i, 1).setToolTip(data[i][1]) self.tableWidget.setItem(i, 2, QtWidgets.QTableWidgetItem(str(data[i][4]))) self.tableWidget.setItem(i, 3, QtWidgets.QTableWidgetItem(data[i][2])) self.tableWidget.setItem(i, 4, QtWidgets.QTableWidgetItem(data[i][3])) # replace the 0 with a checkbox checkbox = QtWidgets.QCheckBox() checkbox.setChecked(False) self.tableWidget.setCellWidget(i, 0, checkbox) # if i[9] is 1, set the background of the row to red if int(data[i][9]) == 1: for j in range(5): self.tableWidget.item(i, j).setBackground(QtGui.QColor(235, 74, 71)) # disable the checkbox self.tableWidget.cellWidget(i, 0).setEnabled(False) # set the tooltip self.tableWidget.cellWidget(i, 0).setToolTip( "Dieser Semesterapparat kann nicht gelöscht werden, da er bereits gelöscht wurde" ) # # remove empty rows for row in range(self.tableWidget.rowCount()): if self.tableWidget.item(row, 1) is None: self.tableWidget.removeRow(row) def display_detailed_data(self): selected_semester = self.statistics_table.item( self.statistics_table.currentRow(), 0 ).text() # get all apparats from the selected semester data = self.db.getApparatsBySemester(selected_semester) # replace keys for german names # split to two lists created = {"Erstellt": data["created"]} deleted = {"Gelöscht": data["deleted"]} created_status = StatusWidget(created, selected_semester) deleted_status = StatusWidget(deleted, selected_semester) created_status.setSizePolicy( QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Expanding, ) deleted_status.setSizePolicy( QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Expanding, ) # add them to the gridLayout_4 if self.dataLayout.count() > 0: self.dataLayout.itemAt(0).widget().deleteLater() self.dataLayout.itemAt(1).widget().deleteLater() self.dataLayout.addWidget(created_status) self.dataLayout.addWidget(deleted_status) # self.setStatisticTableSize() created_status.person_double_clicked.connect(self.open_apparat) created_status.setToolTip("Doppelklick um den Semesterapparat zu öffnen") deleted_status.setToolTip("Nur zur Übersicht") # set deleted_status background to slightly gray def open_apparat(self, *args): header = "created" # in *args parent_depth = args[2] apparat = args[1] if header == "deleted" and parent_depth == 2: log.warning("Semesterapparat wurde bereits gelöscht") QtWidgets.QMessageBox.information( self, "Information", f"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 self.tableWidget.setRowCount(0) name = apparat.split("(")[0].strip() prof_id = self.db.getProfId({"profname": name}) apparats = self.db.getApparatsByProf(prof_id) for app in apparats: # set the items 0 = clickable checkbox, 1 = appname, 2 = profname, 3 = fach # insert new row self.tableWidget.insertRow(0) self.tableWidget.setItem(0, 0, QtWidgets.QTableWidgetItem("")) self.tableWidget.setItem(0, 1, QtWidgets.QTableWidgetItem(app.name)) self.tableWidget.setItem( 0, 2, QtWidgets.QTableWidgetItem(str(app.appnr)) ) self.tableWidget.setItem(0, 3, QtWidgets.QTableWidgetItem(name)) self.tableWidget.setItem(0, 4, QtWidgets.QTableWidgetItem(app.subject)) # replace the 0 with a checkbox checkbox = QtWidgets.QCheckBox() checkbox.setChecked(False) self.tableWidget.setCellWidget(0, 0, checkbox) # if i[9] is 1, set the background of the row to red if int(app.deleted) == 1: for j in range(5): self.tableWidget.item(0, j).setBackground( QtGui.QColor(235, 74, 71) ) # disable the checkbox self.tableWidget.cellWidget(0, 0).setEnabled(False) # set the tooltip self.tableWidget.cellWidget(0, 0).setToolTip( "Dieser Semesterapparat kann nicht gelöscht werden, da er bereits gelöscht wurde" ) elif parent_depth == 2: # #print("depth", parent_depth) # apparat selected case - open the apparat in the frame self.apparat_open.emit(apparat) return def emit_signal(self, *args): # #print("emit_signal", *args) self.apparat_open.emit(args[1]) def show_ui(): app = QtWidgets.QApplication([]) window = SearchStatisticPage() window.show() app.exec()