From 7136823584fa1727bc3eb40ed4c81b7375c66af1 Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Wed, 7 Jun 2023 18:35:02 +0100 Subject: [PATCH 01/18] Start working on new dialog to select data to open --- damnit/gui/main_window.py | 7 ++ damnit/gui/open_dialog.py | 50 ++++++++++ damnit/gui/open_dialog.ui | 185 +++++++++++++++++++++++++++++++++++ damnit/gui/open_dialog_ui.py | 77 +++++++++++++++ 4 files changed, 319 insertions(+) create mode 100644 damnit/gui/open_dialog.py create mode 100644 damnit/gui/open_dialog.ui create mode 100644 damnit/gui/open_dialog_ui.py diff --git a/damnit/gui/main_window.py b/damnit/gui/main_window.py index c2c072a4..2cb85f4e 100644 --- a/damnit/gui/main_window.py +++ b/damnit/gui/main_window.py @@ -33,6 +33,7 @@ from .plot import Canvas, Plot from .user_variables import AddUserVariableDialog from .editor import Editor, ContextTestResult +from .open_dialog import OpenDBDialog log = logging.getLogger(__name__) @@ -1033,6 +1034,12 @@ def run_app(context_dir, connect_to_kafka=True): application = QtWidgets.QApplication(sys.argv) application.setStyle(TableViewStyle()) + if context_dir is None: + dialog = OpenDBDialog() + if dialog.exec() == QtWidgets.QDialog.Rejected: + return 0 + context_dir = dialog.get_chosen_dir() + window = MainWindow(context_dir=context_dir, connect_to_kafka=connect_to_kafka) window.show() return application.exec() diff --git a/damnit/gui/open_dialog.py b/damnit/gui/open_dialog.py new file mode 100644 index 00000000..39593615 --- /dev/null +++ b/damnit/gui/open_dialog.py @@ -0,0 +1,50 @@ +import os.path + +from extra_data.read_machinery import find_proposal +from PyQt5.QtGui import QIntValidator +from PyQt5.QtWidgets import QDialog, QFileDialog, QDialogButtonBox + +from .open_dialog_ui import Ui_Dialog + +class OpenDBDialog(QDialog): + def __init__(self): + super().__init__() + self.ui = Ui_Dialog() + self.ui.setupUi(self) + self.ui.buttonBox.button(QDialogButtonBox.StandardButton.Ok).setEnabled(False) + self.ui.proposal_rb.toggled.connect(self.update_ok) + self.ui.proposal_edit.textChanged(self.update_ok) + self.ui.folder_edit.textChanged(self.update_ok) + self.ui.browse_button.clicked.connect(self.browse_for_folder) + self.ui.proposal_edit.setValidator(QIntValidator(1000, 999999)) + self.ui.proposal_edit.setFocus() + + def get_proposal_dir(self): + if not self.ui.proposal_edit.hasAcceptableInput(): + return None + prop_no = int(self.ui.proposal_edit.text()) + return find_proposal(f"p{prop_no:06}") + + def update_ok(self): + dir = self.get_chosen_dir() + valid = (dir is not None) and os.path.isdir(dir) + self.ui.buttonBox.button(QDialogButtonBox.StandardButton.Ok).setEnabled(valid) + + def browse_for_folder(self): + path = QFileDialog.getExistingDirectory() + if path: + self.ui.folder_edit.setText() + + def get_chosen_dir(self): + if self.ui.proposal_rb.isChecked(): + return self.get_proposal_dir() + else: + return self.ui.folder_edit.text() + +def select_amore_dir(): + dlg = QDialog() + ui = Ui_Dialog() + ui.setupUi(dlg) + ui.browse_button.clicked.connect() + + dlg.exec() diff --git a/damnit/gui/open_dialog.ui b/damnit/gui/open_dialog.ui new file mode 100644 index 00000000..cd485772 --- /dev/null +++ b/damnit/gui/open_dialog.ui @@ -0,0 +1,185 @@ + + + Dialog + + + + 0 + 0 + 400 + 265 + + + + Dialog + + + + + + Please select the data to open: + + + + + + + + + Open proposal number: + + + true + + + + + + + 009999 + + + + + + + + + + + Open DAMNIT folder: + + + + + + + + + false + + + + + + + false + + + Browse + + + + + + + + + <html><head/><body><p>DAMNIT will open an existing database if this folder contains <span style=" font-family:'Source Code Pro';">runs.sqlite</span> and <span style=" font-family:'Source Code Pro';">context.py</span>, or create a new database if not.</p></body></html> + + + Qt::RichText + + + true + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + buttonBox + accepted() + Dialog + accept() + + + 224 + 272 + + + 157 + 274 + + + + + buttonBox + rejected() + Dialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + proposal_rb + toggled(bool) + proposal_edit + setEnabled(bool) + + + 62 + 63 + + + 224 + 67 + + + + + folder_rb + toggled(bool) + folder_edit + setEnabled(bool) + + + 64 + 145 + + + 83 + 214 + + + + + folder_rb + toggled(bool) + browse_button + setEnabled(bool) + + + 323 + 147 + + + 347 + 197 + + + + + diff --git a/damnit/gui/open_dialog_ui.py b/damnit/gui/open_dialog_ui.py new file mode 100644 index 00000000..0b99b1f4 --- /dev/null +++ b/damnit/gui/open_dialog_ui.py @@ -0,0 +1,77 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'open_dialog.ui' +# +# Created by: PyQt5 UI code generator 5.15.9 +# +# WARNING: Any manual changes made to this file will be lost when pyuic5 is +# run again. Do not edit this file unless you know what you are doing. + + +from PyQt5 import QtCore, QtGui, QtWidgets + + +class Ui_Dialog(object): + def setupUi(self, Dialog): + Dialog.setObjectName("Dialog") + Dialog.resize(400, 265) + self.verticalLayout = QtWidgets.QVBoxLayout(Dialog) + self.verticalLayout.setObjectName("verticalLayout") + self.label = QtWidgets.QLabel(Dialog) + self.label.setObjectName("label") + self.verticalLayout.addWidget(self.label) + self.horizontalLayout = QtWidgets.QHBoxLayout() + self.horizontalLayout.setObjectName("horizontalLayout") + self.proposal_rb = QtWidgets.QRadioButton(Dialog) + self.proposal_rb.setChecked(True) + self.proposal_rb.setObjectName("proposal_rb") + self.horizontalLayout.addWidget(self.proposal_rb) + self.proposal_edit = QtWidgets.QLineEdit(Dialog) + self.proposal_edit.setObjectName("proposal_edit") + self.horizontalLayout.addWidget(self.proposal_edit) + self.verticalLayout.addLayout(self.horizontalLayout) + self.verticalLayout_2 = QtWidgets.QVBoxLayout() + self.verticalLayout_2.setObjectName("verticalLayout_2") + self.folder_rb = QtWidgets.QRadioButton(Dialog) + self.folder_rb.setObjectName("folder_rb") + self.verticalLayout_2.addWidget(self.folder_rb) + self.horizontalLayout_2 = QtWidgets.QHBoxLayout() + self.horizontalLayout_2.setObjectName("horizontalLayout_2") + self.folder_edit = QtWidgets.QLineEdit(Dialog) + self.folder_edit.setEnabled(False) + self.folder_edit.setObjectName("folder_edit") + self.horizontalLayout_2.addWidget(self.folder_edit) + self.browse_button = QtWidgets.QPushButton(Dialog) + self.browse_button.setEnabled(False) + self.browse_button.setObjectName("browse_button") + self.horizontalLayout_2.addWidget(self.browse_button) + self.verticalLayout_2.addLayout(self.horizontalLayout_2) + self.label_2 = QtWidgets.QLabel(Dialog) + self.label_2.setTextFormat(QtCore.Qt.RichText) + self.label_2.setWordWrap(True) + self.label_2.setObjectName("label_2") + self.verticalLayout_2.addWidget(self.label_2) + self.verticalLayout.addLayout(self.verticalLayout_2) + self.buttonBox = QtWidgets.QDialogButtonBox(Dialog) + self.buttonBox.setOrientation(QtCore.Qt.Horizontal) + self.buttonBox.setStandardButtons(QtWidgets.QDialogButtonBox.Cancel|QtWidgets.QDialogButtonBox.Ok) + self.buttonBox.setObjectName("buttonBox") + self.verticalLayout.addWidget(self.buttonBox) + + self.retranslateUi(Dialog) + self.buttonBox.accepted.connect(Dialog.accept) # type: ignore + self.buttonBox.rejected.connect(Dialog.reject) # type: ignore + self.proposal_rb.toggled['bool'].connect(self.proposal_edit.setEnabled) # type: ignore + self.folder_rb.toggled['bool'].connect(self.folder_edit.setEnabled) # type: ignore + self.folder_rb.toggled['bool'].connect(self.browse_button.setEnabled) # type: ignore + QtCore.QMetaObject.connectSlotsByName(Dialog) + + def retranslateUi(self, Dialog): + _translate = QtCore.QCoreApplication.translate + Dialog.setWindowTitle(_translate("Dialog", "Dialog")) + self.label.setText(_translate("Dialog", "Please select the data to open:")) + self.proposal_rb.setText(_translate("Dialog", "Open proposal number:")) + self.proposal_edit.setInputMask(_translate("Dialog", "009999")) + self.folder_rb.setText(_translate("Dialog", "Open DAMNIT folder:")) + self.browse_button.setText(_translate("Dialog", "Browse")) + self.label_2.setText(_translate("Dialog", "

DAMNIT will open an existing database if this folder contains runs.sqlite and context.py, or create a new database if not.

")) From e754ebc99254863007151d2cef376fcdbd53c105 Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Wed, 7 Jun 2023 18:40:54 +0100 Subject: [PATCH 02/18] Remove inputMask from proposal edit widget --- damnit/gui/open_dialog.ui | 6 +----- damnit/gui/open_dialog_ui.py | 1 - 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/damnit/gui/open_dialog.ui b/damnit/gui/open_dialog.ui index cd485772..b2cd4cef 100644 --- a/damnit/gui/open_dialog.ui +++ b/damnit/gui/open_dialog.ui @@ -34,11 +34,7 @@ - - - 009999 - - + diff --git a/damnit/gui/open_dialog_ui.py b/damnit/gui/open_dialog_ui.py index 0b99b1f4..a038b02d 100644 --- a/damnit/gui/open_dialog_ui.py +++ b/damnit/gui/open_dialog_ui.py @@ -71,7 +71,6 @@ def retranslateUi(self, Dialog): Dialog.setWindowTitle(_translate("Dialog", "Dialog")) self.label.setText(_translate("Dialog", "Please select the data to open:")) self.proposal_rb.setText(_translate("Dialog", "Open proposal number:")) - self.proposal_edit.setInputMask(_translate("Dialog", "009999")) self.folder_rb.setText(_translate("Dialog", "Open DAMNIT folder:")) self.browse_button.setText(_translate("Dialog", "Browse")) self.label_2.setText(_translate("Dialog", "

DAMNIT will open an existing database if this folder contains runs.sqlite and context.py, or create a new database if not.

")) From bfc4ba63fa0bd357457e101056c18815bfc0e618 Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Thu, 8 Jun 2023 12:31:54 +0100 Subject: [PATCH 03/18] Find proposal dirs in a thread --- damnit/gui/open_dialog.py | 47 ++++++++++++++++++++++++++++++--------- 1 file changed, 36 insertions(+), 11 deletions(-) diff --git a/damnit/gui/open_dialog.py b/damnit/gui/open_dialog.py index 39593615..31b34714 100644 --- a/damnit/gui/open_dialog.py +++ b/damnit/gui/open_dialog.py @@ -1,33 +1,58 @@ import os.path from extra_data.read_machinery import find_proposal -from PyQt5.QtGui import QIntValidator +from PyQt5.QtCore import QObject, QThread, pyqtSignal from PyQt5.QtWidgets import QDialog, QFileDialog, QDialogButtonBox from .open_dialog_ui import Ui_Dialog +class ProposalFinder(QObject): + find_result = pyqtSignal(str, str) + + def find_proposal(self, propnum: str): + if str.isdecimal() and len(str) >= 4 + try: + dir = find_proposal(f"p{int(propnum):06}") + except: + dir = '' + else: + dir = '' + self.find_result.emit(propnum, dir) + class OpenDBDialog(QDialog): + proposal_num_changed = pyqtSignal(str) + proposal_dir = '' + def __init__(self): super().__init__() self.ui = Ui_Dialog() self.ui.setupUi(self) self.ui.buttonBox.button(QDialogButtonBox.StandardButton.Ok).setEnabled(False) self.ui.proposal_rb.toggled.connect(self.update_ok) - self.ui.proposal_edit.textChanged(self.update_ok) self.ui.folder_edit.textChanged(self.update_ok) self.ui.browse_button.clicked.connect(self.browse_for_folder) - self.ui.proposal_edit.setValidator(QIntValidator(1000, 999999)) self.ui.proposal_edit.setFocus() - def get_proposal_dir(self): - if not self.ui.proposal_edit.hasAcceptableInput(): - return None - prop_no = int(self.ui.proposal_edit.text()) - return find_proposal(f"p{prop_no:06}") + self.proposal_finder_thread = QThread() + self.proposal_finder = ProposalFinder() + self.proposal_finder.moveToThread(self.proposal_finder_thread) + self.ui.proposal_edit.textChanged(self.proposal_finder.find_proposal) + self.proposal_finder.find_result.connect(self.proposal_dir_result) + self.finished.connect(self.proposal_finder_thread.quit) + + def proposal_dir_result(self, propnum, dir): + if propnum != self.ui.proposal_edit.text(): + return # Text field has been changed + self.proposal_dir = dir + if self.ui.proposal_rb.isChecked(): + self.ui.buttonBox.button(QDialogButtonBox.StandardButton.Ok).setEnabled(bool(dir)) + def update_ok(self): - dir = self.get_chosen_dir() - valid = (dir is not None) and os.path.isdir(dir) + if self.ui.proposal_rb.isChecked(): + valid = bool(self.proposal_dir) + else: + valid = os.path.isdir(self.ui.folder_edit.text()) self.ui.buttonBox.button(QDialogButtonBox.StandardButton.Ok).setEnabled(valid) def browse_for_folder(self): @@ -37,7 +62,7 @@ def browse_for_folder(self): def get_chosen_dir(self): if self.ui.proposal_rb.isChecked(): - return self.get_proposal_dir() + return os.path.join(self.proposal_dir, "usr/Shared/amore") else: return self.ui.folder_edit.text() From 159dd8c559c2516e4a2797171e40711290d532fd Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Thu, 8 Jun 2023 13:37:00 +0200 Subject: [PATCH 04/18] Various minor fixes --- damnit/gui/open_dialog.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/damnit/gui/open_dialog.py b/damnit/gui/open_dialog.py index 31b34714..dc306d33 100644 --- a/damnit/gui/open_dialog.py +++ b/damnit/gui/open_dialog.py @@ -10,7 +10,7 @@ class ProposalFinder(QObject): find_result = pyqtSignal(str, str) def find_proposal(self, propnum: str): - if str.isdecimal() and len(str) >= 4 + if propnum.isdecimal() and len(propnum) >= 4: try: dir = find_proposal(f"p{int(propnum):06}") except: @@ -29,16 +29,17 @@ def __init__(self): self.ui.setupUi(self) self.ui.buttonBox.button(QDialogButtonBox.StandardButton.Ok).setEnabled(False) self.ui.proposal_rb.toggled.connect(self.update_ok) - self.ui.folder_edit.textChanged(self.update_ok) + self.ui.folder_edit.textChanged.connect(self.update_ok) self.ui.browse_button.clicked.connect(self.browse_for_folder) self.ui.proposal_edit.setFocus() self.proposal_finder_thread = QThread() self.proposal_finder = ProposalFinder() self.proposal_finder.moveToThread(self.proposal_finder_thread) - self.ui.proposal_edit.textChanged(self.proposal_finder.find_proposal) + self.ui.proposal_edit.textChanged.connect(self.proposal_finder.find_proposal) self.proposal_finder.find_result.connect(self.proposal_dir_result) self.finished.connect(self.proposal_finder_thread.quit) + self.proposal_finder_thread.start() def proposal_dir_result(self, propnum, dir): if propnum != self.ui.proposal_edit.text(): From 705f6ee466177f21714ecbbfb2d70c6d87e8ae7a Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Thu, 8 Jun 2023 13:58:09 +0200 Subject: [PATCH 05/18] Simplify validity checking a bit --- damnit/gui/open_dialog.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/damnit/gui/open_dialog.py b/damnit/gui/open_dialog.py index dc306d33..6938a413 100644 --- a/damnit/gui/open_dialog.py +++ b/damnit/gui/open_dialog.py @@ -45,9 +45,7 @@ def proposal_dir_result(self, propnum, dir): if propnum != self.ui.proposal_edit.text(): return # Text field has been changed self.proposal_dir = dir - if self.ui.proposal_rb.isChecked(): - self.ui.buttonBox.button(QDialogButtonBox.StandardButton.Ok).setEnabled(bool(dir)) - + self.update_ok() def update_ok(self): if self.ui.proposal_rb.isChecked(): From 50d89eec56eaaaa55cc8f045478f46df12bba19a Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Thu, 8 Jun 2023 13:02:28 +0100 Subject: [PATCH 06/18] Use pathlib instead of os.path --- damnit/gui/open_dialog.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/damnit/gui/open_dialog.py b/damnit/gui/open_dialog.py index 6938a413..745c126a 100644 --- a/damnit/gui/open_dialog.py +++ b/damnit/gui/open_dialog.py @@ -1,4 +1,4 @@ -import os.path +from pathlib import Path from extra_data.read_machinery import find_proposal from PyQt5.QtCore import QObject, QThread, pyqtSignal @@ -41,7 +41,7 @@ def __init__(self): self.finished.connect(self.proposal_finder_thread.quit) self.proposal_finder_thread.start() - def proposal_dir_result(self, propnum, dir): + def proposal_dir_result(self, propnum: str, dir: str): if propnum != self.ui.proposal_edit.text(): return # Text field has been changed self.proposal_dir = dir @@ -51,7 +51,7 @@ def update_ok(self): if self.ui.proposal_rb.isChecked(): valid = bool(self.proposal_dir) else: - valid = os.path.isdir(self.ui.folder_edit.text()) + valid = Path(self.ui.folder_edit.text()).is_dir() self.ui.buttonBox.button(QDialogButtonBox.StandardButton.Ok).setEnabled(valid) def browse_for_folder(self): @@ -61,9 +61,9 @@ def browse_for_folder(self): def get_chosen_dir(self): if self.ui.proposal_rb.isChecked(): - return os.path.join(self.proposal_dir, "usr/Shared/amore") + return Path(self.proposal_dir, "usr/Shared/amore") else: - return self.ui.folder_edit.text() + return Path(self.ui.folder_edit.text()) def select_amore_dir(): dlg = QDialog() From 6585dc356b7baa26ef08a31de944229c09b23e69 Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Wed, 21 Jun 2023 17:12:16 +0200 Subject: [PATCH 07/18] Fix setting selected folder path --- damnit/gui/open_dialog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/damnit/gui/open_dialog.py b/damnit/gui/open_dialog.py index 745c126a..8640b79b 100644 --- a/damnit/gui/open_dialog.py +++ b/damnit/gui/open_dialog.py @@ -57,7 +57,7 @@ def update_ok(self): def browse_for_folder(self): path = QFileDialog.getExistingDirectory() if path: - self.ui.folder_edit.setText() + self.ui.folder_edit.setText(path) def get_chosen_dir(self): if self.ui.proposal_rb.isChecked(): From 43700e15243b535c2b739862997c6804564166fd Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Wed, 5 Jul 2023 14:23:21 +0100 Subject: [PATCH 08/18] Remove unused function --- damnit/gui/open_dialog.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/damnit/gui/open_dialog.py b/damnit/gui/open_dialog.py index 8640b79b..08b9bf5b 100644 --- a/damnit/gui/open_dialog.py +++ b/damnit/gui/open_dialog.py @@ -64,11 +64,3 @@ def get_chosen_dir(self): return Path(self.proposal_dir, "usr/Shared/amore") else: return Path(self.ui.folder_edit.text()) - -def select_amore_dir(): - dlg = QDialog() - ui = Ui_Dialog() - ui.setupUi(dlg) - ui.browse_button.clicked.connect() - - dlg.exec() From d3e47937918e5bcd166f8e0f72dcc3ecc139578c Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Wed, 5 Jul 2023 15:09:10 +0100 Subject: [PATCH 09/18] Prompt for creating database & starting backend, use dialog from menu bar --- damnit/gui/main_window.py | 108 ++++++++++++++++---------------------- damnit/gui/open_dialog.py | 10 +++- 2 files changed, 53 insertions(+), 65 deletions(-) diff --git a/damnit/gui/main_window.py b/damnit/gui/main_window.py index 2cb85f4e..4103b298 100644 --- a/damnit/gui/main_window.py +++ b/damnit/gui/main_window.py @@ -190,71 +190,16 @@ def _menu_bar_help(self) -> None: dialog.exec() def _menu_bar_autoconfigure(self) -> None: - proposal_dir = "" - - # If we're on a system with access to GPFS, prompt for the proposal - # number so we can preset the prompt for the AMORE directory. - if self.gpfs_accessible(): - prompt = True - while prompt: - prop_no, prompt = QtWidgets.QInputDialog.getInt(self, "Select proposal", - "Which proposal is this for?") - if not prompt: - break - - proposal = f"p{prop_no:06}" - try: - proposal_dir = find_proposal(proposal) - prompt = False - except Exception: - button = QtWidgets.QMessageBox.warning(self, "Bad proposal number", - "Could not find a proposal with this number, try again?", - buttons=QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No) - if button != QtWidgets.QMessageBox.Yes: - prompt = False - else: - prop_no = None - - # By convention the AMORE directory is often stored at usr/Shared/amore, - # so if this directory exists, then we use it. - standard_path = Path(proposal_dir) / "usr/Shared/amore" - if standard_path.is_dir() and db_path(standard_path).is_file(): - path = standard_path - else: - # Helper lambda to open a prompt for the user - prompt_for_path = lambda: QFileDialog.getExistingDirectory(self, - "Select context directory", - proposal_dir) - - if self.gpfs_accessible() and prop_no is not None: - button = QMessageBox.question(self, "Database not found", - f"Proposal {prop_no} does not have an AMORE database, " \ - "would you like to create one and start the backend?") - if button == QMessageBox.Yes: - initialize_and_start_backend(standard_path, prop_no) - path = standard_path - else: - # Otherwise, we prompt the user - path = prompt_for_path() - else: - path = prompt_for_path() - - # If we found a database, make sure we're working with a Path object - if path: - path = Path(path) - else: - # Otherwise just return + dialog = OpenDBDialog(self) + if dialog.exec() == QtWidgets.QDialog.Rejected: + return + context_dir = dialog.get_chosen_dir() + prop_no = dialog.get_proposal_num() + if not prompt_setup_db_and_backend(context_dir, prop_no, parent=self): + # User said no to setting up a new database return - # Check if the backend is running - if not backend_is_running(path): - button = QMessageBox.question(self, "Backend not running", - "The AMORE backend is not running, would you like to start it? " \ - "This is only necessary if new runs are expected.") - if button == QMessageBox.Yes: - initialize_and_start_backend(path) - - self.autoconfigure(Path(path), proposal=prop_no) + self.autoconfigure(context_dir, proposal=prop_no) def gpfs_accessible(self): return os.path.isdir("/gpfs/exfel/exp") @@ -1027,6 +972,39 @@ def drawControl(self, element, option, painter, widget=None): super().drawControl(element, option, painter, widget) +def prompt_setup_db_and_backend(context_dir: Path, prop_no=None, parent=None): + if not db_path(context_dir).is_file(): + + button = QMessageBox.question( + parent, "Database not found", + f"{context_dir} does not contain a DAMNIT database, " + "would you like to create one and start the backend?" + ) + if button != QMessageBox.Yes: + return False + + + if prop_no is None: + prop_no, ok = QtWidgets.QInputDialog.getInt( + parent, "Select proposal", "Which proposal is this for?" + ) + if not ok: + return False + initialize_and_start_backend(context_dir, prop_no) + + # Check if the backend is running + elif not backend_is_running(context_dir): + button = QMessageBox.question( + parent, "Backend not running", + "The DAMNIT backend is not running, would you like to start it? " + "This is only necessary if new runs are expected." + ) + if button == QMessageBox.Yes: + initialize_and_start_backend(context_dir, prop_no) + + return True + + def run_app(context_dir, connect_to_kafka=True): QtWidgets.QApplication.setAttribute( QtCore.Qt.ApplicationAttribute.AA_DontUseNativeMenuBar @@ -1039,6 +1017,10 @@ def run_app(context_dir, connect_to_kafka=True): if dialog.exec() == QtWidgets.QDialog.Rejected: return 0 context_dir = dialog.get_chosen_dir() + prop_no = dialog.get_proposal_num() + if not prompt_setup_db_and_backend(context_dir, prop_no): + # User said no to setting up a new database + return 0 window = MainWindow(context_dir=context_dir, connect_to_kafka=connect_to_kafka) window.show() diff --git a/damnit/gui/open_dialog.py b/damnit/gui/open_dialog.py index 08b9bf5b..d92dbd60 100644 --- a/damnit/gui/open_dialog.py +++ b/damnit/gui/open_dialog.py @@ -1,4 +1,5 @@ from pathlib import Path +from typing import Optional from extra_data.read_machinery import find_proposal from PyQt5.QtCore import QObject, QThread, pyqtSignal @@ -23,8 +24,8 @@ class OpenDBDialog(QDialog): proposal_num_changed = pyqtSignal(str) proposal_dir = '' - def __init__(self): - super().__init__() + def __init__(self, parent=None): + super().__init__(parent) self.ui = Ui_Dialog() self.ui.setupUi(self) self.ui.buttonBox.button(QDialogButtonBox.StandardButton.Ok).setEnabled(False) @@ -64,3 +65,8 @@ def get_chosen_dir(self): return Path(self.proposal_dir, "usr/Shared/amore") else: return Path(self.ui.folder_edit.text()) + + def get_proposal_num(self) -> Optional[int]: + if self.ui.proposal_rb.isChecked(): + return int(self.ui.proposal_edit.text()) + return None From 929c7dbf63710047b1fd492a94ac16935d093f8a Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Wed, 5 Jul 2023 15:35:52 +0100 Subject: [PATCH 10/18] Fix test for automatically creating database & starting backend --- damnit/gui/main_window.py | 12 ++++-------- damnit/gui/open_dialog.py | 7 +++++++ tests/test_gui.py | 13 +++++++++++-- 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/damnit/gui/main_window.py b/damnit/gui/main_window.py index 4103b298..e8777af3 100644 --- a/damnit/gui/main_window.py +++ b/damnit/gui/main_window.py @@ -190,11 +190,9 @@ def _menu_bar_help(self) -> None: dialog.exec() def _menu_bar_autoconfigure(self) -> None: - dialog = OpenDBDialog(self) - if dialog.exec() == QtWidgets.QDialog.Rejected: + context_dir, prop_no = OpenDBDialog.run_get_result(parent=self) + if context_dir is None: return - context_dir = dialog.get_chosen_dir() - prop_no = dialog.get_proposal_num() if not prompt_setup_db_and_backend(context_dir, prop_no, parent=self): # User said no to setting up a new database return @@ -1013,11 +1011,9 @@ def run_app(context_dir, connect_to_kafka=True): application.setStyle(TableViewStyle()) if context_dir is None: - dialog = OpenDBDialog() - if dialog.exec() == QtWidgets.QDialog.Rejected: + context_dir, prop_no = OpenDBDialog.run_get_result() + if context_dir is None: return 0 - context_dir = dialog.get_chosen_dir() - prop_no = dialog.get_proposal_num() if not prompt_setup_db_and_backend(context_dir, prop_no): # User said no to setting up a new database return 0 diff --git a/damnit/gui/open_dialog.py b/damnit/gui/open_dialog.py index d92dbd60..5dcaebd7 100644 --- a/damnit/gui/open_dialog.py +++ b/damnit/gui/open_dialog.py @@ -42,6 +42,13 @@ def __init__(self, parent=None): self.finished.connect(self.proposal_finder_thread.quit) self.proposal_finder_thread.start() + @staticmethod + def run_get_result(parent=None) -> (Optional[str], Optional[int]): + dlg = OpenDBDialog(parent) + if dlg.exec() == QDialog.Rejected: + return None, None + return dlg.get_chosen_dir(), dlg.get_proposal_num() + def proposal_dir_result(self, propnum: str, dir: str): if propnum != self.ui.proposal_edit.text(): return # Text field has been changed diff --git a/tests/test_gui.py b/tests/test_gui.py index 451de8a9..63adb7c0 100644 --- a/tests/test_gui.py +++ b/tests/test_gui.py @@ -307,7 +307,7 @@ def helper_patch(): # p1234, and the user always wants to create a database and start the # backend. with (patch.object(win, "gpfs_accessible", return_value=True), - patch.object(QInputDialog, "getInt", return_value=(1234, True)), + patch(f"{pkg}.OpenDBDialog.run_get_result", return_value=(db_dir, 1234)), patch(f"{pkg}.find_proposal", return_value=tmp_path), patch.object(QMessageBox, "question", return_value=QMessageBox.Yes), patch(f"{pkg}.initialize_and_start_backend") as initialize_and_start_backend, @@ -327,6 +327,15 @@ def helper_patch(): db_dir.mkdir(parents=True) db_path(db_dir).touch() + # Autoconfigure with database present & backend 'running': + with (helper_patch() as initialize_and_start_backend, + patch(f"{pkg}.backend_is_running", return_value=True)): + win._menu_bar_autoconfigure() + + # We expect the database to be initialized and the backend started + win.autoconfigure.assert_called_once_with(db_dir, proposal=1234) + initialize_and_start_backend.assert_not_called() + # Autoconfigure again, the GUI should start the backend again with (helper_patch() as initialize_and_start_backend, patch(f"{pkg}.backend_is_running", return_value=False)): @@ -334,7 +343,7 @@ def helper_patch(): # This time the database is already initialized win.autoconfigure.assert_called_once_with(db_dir, proposal=1234) - initialize_and_start_backend.assert_called_once_with(db_dir) + initialize_and_start_backend.assert_called_once_with(db_dir, 1234) def test_user_vars(mock_ctx_user, mock_user_vars, mock_db, qtbot): From f931f80040457f758d6002e7ea780c7919089f24 Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Wed, 5 Jul 2023 16:04:30 +0100 Subject: [PATCH 11/18] Add test for OpenDBDialog --- damnit/gui/open_dialog.py | 2 +- tests/test_gui.py | 29 ++++++++++++++++++++++++++++- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/damnit/gui/open_dialog.py b/damnit/gui/open_dialog.py index 5dcaebd7..9b3255b7 100644 --- a/damnit/gui/open_dialog.py +++ b/damnit/gui/open_dialog.py @@ -43,7 +43,7 @@ def __init__(self, parent=None): self.proposal_finder_thread.start() @staticmethod - def run_get_result(parent=None) -> (Optional[str], Optional[int]): + def run_get_result(parent=None) -> (Optional[Path], Optional[int]): dlg = OpenDBDialog(parent) if dlg.exec() == QDialog.Rejected: return None, None diff --git a/tests/test_gui.py b/tests/test_gui.py index 63adb7c0..2840db53 100644 --- a/tests/test_gui.py +++ b/tests/test_gui.py @@ -8,13 +8,14 @@ import numpy as np import pandas as pd from PyQt5.QtCore import Qt -from PyQt5.QtWidgets import QMessageBox, QInputDialog, QDialog, QStyledItemDelegate, QLineEdit +from PyQt5.QtWidgets import QMessageBox, QFileDialog, QDialog, QStyledItemDelegate, QLineEdit from damnit.ctxsupport.ctxrunner import ContextFile, Results from damnit.backend.db import db_path from damnit.backend.extract_data import add_to_db from damnit.gui.editor import ContextTestResult from damnit.gui.main_window import MainWindow, Settings, AddUserVariableDialog +from damnit.gui.open_dialog import OpenDBDialog # Check if a PID exists by using `kill -0` @@ -695,3 +696,29 @@ def get_index(title, row=0): # Edit a standalone comment comment_index = get_index("Comment", row=1) win.table.setData(comment_index, "Foo", Qt.EditRole) + + +def test_open_dialog(mock_db, qtbot): + db_dir, db = mock_db + dlg = OpenDBDialog() + + # Test supplying a proposal number: + with patch("damnit.gui.open_dialog.find_proposal", return_value=str(db_dir)): + with qtbot.waitSignal(dlg.proposal_finder.find_result): + dlg.ui.proposal_edit.setText('1234') + with qtbot.waitSignal(dlg.proposal_finder_thread.finished): + dlg.accept() + + assert dlg.get_chosen_dir() == db_dir / 'usr/Shared/amore' + assert dlg.get_proposal_num() == 1234 + + # Test selecting a folder: + dlg = OpenDBDialog() + dlg.ui.folder_rb.setChecked(True) + with patch.object(QFileDialog, 'getExistingDirectory', return_value=str(db_dir)): + dlg.ui.browse_button.click() + with qtbot.waitSignal(dlg.proposal_finder_thread.finished): + dlg.accept() + + assert dlg.get_chosen_dir() == db_dir + assert dlg.get_proposal_num() is None From 9cc07fa4b6f6a586f6e62f624ebebedb979dbba3 Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Wed, 5 Jul 2023 17:38:16 +0100 Subject: [PATCH 12/18] Make path joining more explicit Co-authored-by: James Wrigley --- damnit/gui/open_dialog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/damnit/gui/open_dialog.py b/damnit/gui/open_dialog.py index 9b3255b7..791d1205 100644 --- a/damnit/gui/open_dialog.py +++ b/damnit/gui/open_dialog.py @@ -69,7 +69,7 @@ def browse_for_folder(self): def get_chosen_dir(self): if self.ui.proposal_rb.isChecked(): - return Path(self.proposal_dir, "usr/Shared/amore") + return Path(self.proposal_dir) / "usr/Shared/amore" else: return Path(self.ui.folder_edit.text()) From 6d1cc278ef0a213f1932b9ffc047a253bfb72b70 Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Thu, 6 Jul 2023 11:19:21 +0100 Subject: [PATCH 13/18] Fix: avoid destroying QThread object before it can finish --- damnit/gui/main_window.py | 6 ++++-- damnit/gui/open_dialog.py | 8 +++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/damnit/gui/main_window.py b/damnit/gui/main_window.py index e8777af3..8225f6e2 100644 --- a/damnit/gui/main_window.py +++ b/damnit/gui/main_window.py @@ -190,7 +190,8 @@ def _menu_bar_help(self) -> None: dialog.exec() def _menu_bar_autoconfigure(self) -> None: - context_dir, prop_no = OpenDBDialog.run_get_result(parent=self) + open_dialog = OpenDBDialog(self) + context_dir, prop_no = open_dialog.run_get_result() if context_dir is None: return if not prompt_setup_db_and_backend(context_dir, prop_no, parent=self): @@ -1011,7 +1012,8 @@ def run_app(context_dir, connect_to_kafka=True): application.setStyle(TableViewStyle()) if context_dir is None: - context_dir, prop_no = OpenDBDialog.run_get_result() + open_dialog = OpenDBDialog() + context_dir, prop_no = open_dialog.run_get_result() if context_dir is None: return 0 if not prompt_setup_db_and_backend(context_dir, prop_no): diff --git a/damnit/gui/open_dialog.py b/damnit/gui/open_dialog.py index 791d1205..53e2a757 100644 --- a/damnit/gui/open_dialog.py +++ b/damnit/gui/open_dialog.py @@ -42,12 +42,10 @@ def __init__(self, parent=None): self.finished.connect(self.proposal_finder_thread.quit) self.proposal_finder_thread.start() - @staticmethod - def run_get_result(parent=None) -> (Optional[Path], Optional[int]): - dlg = OpenDBDialog(parent) - if dlg.exec() == QDialog.Rejected: + def run_get_result(self) -> (Optional[Path], Optional[int]): + if self.exec() == QDialog.Rejected: return None, None - return dlg.get_chosen_dir(), dlg.get_proposal_num() + return self.get_chosen_dir(), self.get_proposal_num() def proposal_dir_result(self, propnum: str, dir: str): if propnum != self.ui.proposal_edit.text(): From 08413fdadaaed59527b0c43a0fe6b8f66b0483e9 Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Thu, 6 Jul 2023 13:09:43 +0100 Subject: [PATCH 14/18] Don't start thread unless dialog is shown --- damnit/gui/open_dialog.py | 5 +++-- tests/test_gui.py | 2 ++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/damnit/gui/open_dialog.py b/damnit/gui/open_dialog.py index 53e2a757..f3967239 100644 --- a/damnit/gui/open_dialog.py +++ b/damnit/gui/open_dialog.py @@ -34,15 +34,16 @@ def __init__(self, parent=None): self.ui.browse_button.clicked.connect(self.browse_for_folder) self.ui.proposal_edit.setFocus() - self.proposal_finder_thread = QThread() + self.proposal_finder_thread = QThread(parent=parent) self.proposal_finder = ProposalFinder() self.proposal_finder.moveToThread(self.proposal_finder_thread) self.ui.proposal_edit.textChanged.connect(self.proposal_finder.find_proposal) self.proposal_finder.find_result.connect(self.proposal_dir_result) self.finished.connect(self.proposal_finder_thread.quit) - self.proposal_finder_thread.start() + self.proposal_finder_thread.finished.connect(self.proposal_finder_thread.deleteLater) def run_get_result(self) -> (Optional[Path], Optional[int]): + self.proposal_finder_thread.start() if self.exec() == QDialog.Rejected: return None, None return self.get_chosen_dir(), self.get_proposal_num() diff --git a/tests/test_gui.py b/tests/test_gui.py index 2840db53..7501f7cb 100644 --- a/tests/test_gui.py +++ b/tests/test_gui.py @@ -701,6 +701,7 @@ def get_index(title, row=0): def test_open_dialog(mock_db, qtbot): db_dir, db = mock_db dlg = OpenDBDialog() + dlg.proposal_finder_thread.start() # Test supplying a proposal number: with patch("damnit.gui.open_dialog.find_proposal", return_value=str(db_dir)): @@ -714,6 +715,7 @@ def test_open_dialog(mock_db, qtbot): # Test selecting a folder: dlg = OpenDBDialog() + dlg.proposal_finder_thread.start() dlg.ui.folder_rb.setChecked(True) with patch.object(QFileDialog, 'getExistingDirectory', return_value=str(db_dir)): dlg.ui.browse_button.click() From eca1a8b31c408f53f8a6f13823c79ede5b592b21 Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Thu, 6 Jul 2023 13:28:08 +0100 Subject: [PATCH 15/18] Simpler way to wait for thread to finish in test --- tests/test_gui.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_gui.py b/tests/test_gui.py index 7501f7cb..3dc3d49d 100644 --- a/tests/test_gui.py +++ b/tests/test_gui.py @@ -707,8 +707,8 @@ def test_open_dialog(mock_db, qtbot): with patch("damnit.gui.open_dialog.find_proposal", return_value=str(db_dir)): with qtbot.waitSignal(dlg.proposal_finder.find_result): dlg.ui.proposal_edit.setText('1234') - with qtbot.waitSignal(dlg.proposal_finder_thread.finished): - dlg.accept() + dlg.accept() + dlg.proposal_finder_thread.wait(2000) assert dlg.get_chosen_dir() == db_dir / 'usr/Shared/amore' assert dlg.get_proposal_num() == 1234 @@ -719,8 +719,8 @@ def test_open_dialog(mock_db, qtbot): dlg.ui.folder_rb.setChecked(True) with patch.object(QFileDialog, 'getExistingDirectory', return_value=str(db_dir)): dlg.ui.browse_button.click() - with qtbot.waitSignal(dlg.proposal_finder_thread.finished): - dlg.accept() + dlg.accept() + dlg.proposal_finder_thread.wait(2000) assert dlg.get_chosen_dir() == db_dir assert dlg.get_proposal_num() is None From 086eec960894f48cd9cfde33066e055ac2ba8130 Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Thu, 6 Jul 2023 13:51:21 +0100 Subject: [PATCH 16/18] Remove some unused imports --- damnit/gui/main_window.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/damnit/gui/main_window.py b/damnit/gui/main_window.py index 8225f6e2..ca88c789 100644 --- a/damnit/gui/main_window.py +++ b/damnit/gui/main_window.py @@ -15,11 +15,10 @@ from pandas.api.types import infer_dtype from kafka.errors import NoBrokersAvailable -from extra_data.read_machinery import find_proposal from PyQt5 import QtCore, QtGui, QtWidgets, QtSvg from PyQt5.QtCore import Qt -from PyQt5.QtWidgets import QFileDialog, QMessageBox, QTabWidget +from PyQt5.QtWidgets import QMessageBox, QTabWidget from PyQt5.Qsci import QsciScintilla, QsciLexerPython from ..backend.db import db_path, DamnitDB From 04fa3345146b693f9b6fc9ad73fdb9c081d82f74 Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Thu, 6 Jul 2023 13:54:45 +0100 Subject: [PATCH 17/18] Remove unused mocking --- tests/test_gui.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/test_gui.py b/tests/test_gui.py index 3dc3d49d..6d7d2df1 100644 --- a/tests/test_gui.py +++ b/tests/test_gui.py @@ -307,9 +307,7 @@ def helper_patch(): # Patch things such that the GUI thinks we're on GPFS trying to open # p1234, and the user always wants to create a database and start the # backend. - with (patch.object(win, "gpfs_accessible", return_value=True), - patch(f"{pkg}.OpenDBDialog.run_get_result", return_value=(db_dir, 1234)), - patch(f"{pkg}.find_proposal", return_value=tmp_path), + with (patch(f"{pkg}.OpenDBDialog.run_get_result", return_value=(db_dir, 1234)), patch.object(QMessageBox, "question", return_value=QMessageBox.Yes), patch(f"{pkg}.initialize_and_start_backend") as initialize_and_start_backend, patch.object(win, "autoconfigure")): From d7125566ddc3563db48809f49ab5ae88eec629ba Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Thu, 6 Jul 2023 13:56:08 +0100 Subject: [PATCH 18/18] Remove unused method --- damnit/gui/main_window.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/damnit/gui/main_window.py b/damnit/gui/main_window.py index ca88c789..88d6b379 100644 --- a/damnit/gui/main_window.py +++ b/damnit/gui/main_window.py @@ -199,9 +199,6 @@ def _menu_bar_autoconfigure(self) -> None: self.autoconfigure(context_dir, proposal=prop_no) - def gpfs_accessible(self): - return os.path.isdir("/gpfs/exfel/exp") - def save_settings(self): self._settings_db_path.parent.mkdir(parents=True, exist_ok=True)