def getNewAuthWindow(self): if self.auth_window: if self.auth_window.authenticated(): self.on_actionLogout_triggered() del self.auth_window self.auth_window = \ EmbeddedAuthWindow(config=self.uploader.server, cookie_persistence=self.cookie_persistence, authentication_success_callback=self.onLoginSuccess) self.ui.actionLogin.setEnabled(True)
def getNewAuthWindow(self): if self.auth_window: if self.auth_window.authenticated(): self.on_actionLogout_triggered() self.auth_window.deleteLater() self.auth_window = \ EmbeddedAuthWindow(self, config=self.uploader.server, cookie_persistence= self.uploader.server.get("cookie_persistence", self.cookie_persistence), authentication_success_callback=self.onLoginSuccess, log_level=logging.getLogger().getEffectiveLevel()) self.ui.actionLogin.setEnabled(True)
def __init__(self, config_path=None): super(MainWindow, self).__init__() self.ui = MainWindowUI(self) self.configure(config_path) self.authWindow = EmbeddedAuthWindow( self, config=self.config.get("server"), cookie_persistence=False, authentication_success_callback=self.onLoginSuccess) self.getSession() if not self.identity: self.ui.actionLaunch.setEnabled(False) self.ui.actionRefresh.setEnabled(False) self.ui.actionOptions.setEnabled(False) self.ui.actionLogout.setEnabled(False)
class MainWindow(QMainWindow): config = None credential = None config_path = None store = None catalog = None identity = None attributes = None server = None tempdir = None progress_update_signal = pyqtSignal(str) use_3D_viewer = False curator_mode = False def __init__(self, config_path=None): super(MainWindow, self).__init__() self.ui = MainWindowUI(self) self.configure(config_path) self.authWindow = EmbeddedAuthWindow( self, config=self.config.get("server"), cookie_persistence=False, authentication_success_callback=self.onLoginSuccess) self.getSession() if not self.identity: self.ui.actionLaunch.setEnabled(False) self.ui.actionRefresh.setEnabled(False) self.ui.actionOptions.setEnabled(False) self.ui.actionLogout.setEnabled(False) def configure(self, config_path): # configure logging self.ui.logTextBrowser.widget.log_update_signal.connect(self.updateLog) self.ui.logTextBrowser.setFormatter( logging.Formatter("%(asctime)s - %(levelname)s - %(message)s")) logging.getLogger().addHandler(self.ui.logTextBrowser) logging.getLogger().setLevel(logging.INFO) # configure Ermrest/Hatrac if not config_path: config_path = os.path.join( os.path.expanduser( os.path.normpath("~/.deriva/synapse/synspy-launcher")), "config.json") self.config_path = config_path config = read_config(self.config_path, create_default=True, default=DEFAULT_CONFIG) protocol = config["server"]["protocol"] self.server = config["server"]["host"] catalog_id = config["server"]["catalog_id"] session_config = config.get("session") self.catalog = ErmrestCatalog(protocol, self.server, catalog_id, self.credential, session_config=session_config) self.store = HatracStore(protocol, self.server, self.credential, session_config=session_config) # create working dir (tempdir) self.tempdir = tempfile.mkdtemp(prefix="synspy_") # determine viewer mode self.use_3D_viewer = True if config.get( "viewer_mode", "2d").lower() == "3d" else False # curator mode? curator_mode = config.get("curator_mode") if not curator_mode: config["curator_mode"] = False self.curator_mode = config.get("curator_mode") # save config self.config = config write_config(self.config_path, self.config) def getSession(self): qApp.setOverrideCursor(Qt.WaitCursor) self.updateStatus("Validating session.") queryTask = SessionQueryTask(self.catalog) queryTask.status_update_signal.connect(self.onSessionResult) queryTask.query() def onLoginSuccess(self, **kwargs): self.authWindow.hide() self.credential = kwargs["credential"] self.catalog.set_credentials(self.credential, self.server) self.store.set_credentials(self.credential, self.server) self.getSession() def enableControls(self): self.ui.actionLaunch.setEnabled(True) self.ui.actionRefresh.setEnabled(True) self.ui.actionOptions.setEnabled(self.authWindow.authenticated()) self.ui.actionLogin.setEnabled(not self.authWindow.authenticated()) self.ui.actionLogout.setEnabled(self.authWindow.authenticated()) self.ui.actionExit.setEnabled(True) self.ui.workList.setEnabled(True) def disableControls(self): self.ui.actionLaunch.setEnabled(False) self.ui.actionRefresh.setEnabled(False) self.ui.actionOptions.setEnabled(False) self.ui.actionLogin.setEnabled(False) self.ui.actionLogout.setEnabled(False) self.ui.actionExit.setEnabled(False) self.ui.workList.setEnabled(False) def closeEvent(self, event=None): self.disableControls() self.cancelTasks() shutil.rmtree(self.tempdir) if event: event.accept() def cancelTasks(self): Request.shutdown() self.statusBar().showMessage( "Waiting for background tasks to terminate...") while True: qApp.processEvents() if QThreadPool.globalInstance().waitForDone(10): break self.statusBar().showMessage( "All background tasks terminated successfully") def is_curator(self): for attr in self.attributes: if attr.get('id') == CURATORS: return True return False def displayWorklist(self, worklist): keys = [ "RID", "RCT", "Source Image", "Classifier", "Due Date", "Accepted?", "Status", "URL", "Npz URL", "ZYX Slice", "Segmentation Mode", "Segments URL", "Segments Filtered URL", "Subject", ] self.ui.workList.clear() self.ui.workList.setRowCount(0) self.ui.workList.setColumnCount(0) displayed = [ "RID", "RCT", "Segmentation Mode", "Classifier", "Due Date", "Accepted?", "Status" ] self.ui.workList.setRowCount(len(worklist)) self.ui.workList.setColumnCount(len(keys)) self.ui.workList.removeAction(self.ui.markIncompleteAction) if self.is_curator() and self.curator_mode: self.ui.workList.addAction(self.ui.markIncompleteAction) rows = 0 for row in worklist: value = row.get("Status") if not (value == "analysis pending" or value == "analysis in progress") \ and not (self.is_curator() and self.curator_mode): self.ui.workList.hideRow(rows) cols = 0 for key in keys: item = QTableWidgetItem() if key == "Classifier": value = "%s (%s)" % (row['user'][0]['Full_Name'], row['user'][0]['Display_Name']) item.setData(Qt.UserRole, row['Classifier']) elif key == "URL" or key == "Subject": value = row["source_image"][0].get(key) else: value = row.get(key) if isinstance(value, bool): value = str(value) if isinstance(value, str) and key == 'RCT': value = value.replace( 'T', ' ')[0:19] # drop fractional seconds and TZ if isinstance(value, str): item.setText(value) item.setToolTip(value) self.ui.workList.setItem(rows, cols, item) cols += 1 rows += 1 cols = 0 for key in keys: if key not in displayed: self.ui.workList.hideColumn(cols) cols += 1 self.ui.workList.setHorizontalHeaderLabels(keys) # add header names self.ui.workList.horizontalHeader().setDefaultAlignment( Qt.AlignLeft) # set alignment for col in range(len(displayed)): self.ui.workList.resizeColumnToContents(col) self.ui.workList.sortByColumn(2, Qt.DescendingOrder) def getCacheDir(self): cwd = os.getcwd() cache_dir = os.path.expanduser(self.config.get("cache_dir", cwd)) if not os.path.isdir(cache_dir): try: os.makedirs(cache_dir) except OSError as error: if error.errno != errno.EEXIST: logging.error(format_exception(error)) cache_dir = cwd return cache_dir def downloadCallback(self, **kwargs): status = kwargs.get("progress") if status: self.progress_update_signal.emit(status) return True def uploadCallback(self, **kwargs): completed = kwargs.get("completed") total = kwargs.get("total") file_path = kwargs.get("file_path") if completed and total: file_path = " [%s]" % os.path.basename( file_path) if file_path else "" status = "Uploading file%s: %d%% complete" % ( file_path, round(((completed / total) % 100) * 100)) else: summary = kwargs.get("summary", "") file_path = "Uploaded file: [%s] " % os.path.basename( file_path) if file_path else "" status = file_path # + summary if status: self.progress_update_signal.emit(status) return True def serverProblemMessageBox(self, text, detail): msg = QMessageBox() msg.setIcon(QMessageBox.Warning) msg.setWindowTitle("Confirm Action") msg.setText(text) msg.setInformativeText( detail + "\n\nWould you like to remove this item from the current worklist?" ) msg.setStandardButtons(QMessageBox.Yes | QMessageBox.No) ret = msg.exec_() if ret == QMessageBox.No: return else: row = self.ui.workList.getCurrentTableRow() self.ui.workList.removeRow(row) return def retrieveFiles(self): # if there is an existing segments file, download it first, otherwise just initiate the input file download seg_mode = self.ui.workList.getCurrentTableItemTextByName( "Segmentation Mode") segments_url = self.ui.workList.getCurrentTableItemTextByName( "Segments Filtered URL") if segments_url: segments_filename = 'ROI_%s_%s_only.csv' % ( self.ui.workList.getCurrentTableItemTextByName("RID"), seg_mode) segments_destfile = os.path.abspath( os.path.join(self.tempdir, segments_filename)) self.updateStatus("Downloading file: [%s]" % segments_destfile) downloadTask = FileRetrieveTask(self.store) downloadTask.status_update_signal.connect( self.onRetrieveAnalysisFileResult) self.progress_update_signal.connect(self.updateProgress) downloadTask.retrieve(segments_url, destfile=segments_destfile, progress_callback=self.downloadCallback) else: self.retrieveInputFile() def retrieveInputFile(self): # get the main TIFF file for analysis if not already cached if self.use_3D_viewer: url = self.ui.workList.getCurrentTableItemTextByName("URL") filename = 'Image_%s.ome.tiff' % self.ui.workList.getCurrentTableItemTextByName( "Source Image") else: url = self.ui.workList.getCurrentTableItemTextByName("Npz URL") filename = 'ROI_%s.npz' % self.ui.workList.getCurrentTableItemTextByName( "RID") destfile = os.path.abspath(os.path.join(self.getCacheDir(), filename)) if not url and not self.use_3D_viewer: self.resetUI( "Unable to launch 2D viewer due to missing NPZ file for %s." % self.ui.workList.getCurrentTableItemTextByName("RID")) self.serverProblemMessageBox( "2D viewer requires NPZ data to be present!", "The launcher is currently configured to execute the 2D viewer, which requires NPZ files for input. " + "No NPZ file could be found on the server for this task.") return if not os.path.isfile(destfile): self.updateStatus("Downloading file: [%s]" % destfile) downloadTask = FileRetrieveTask(self.store) downloadTask.status_update_signal.connect( self.onRetrieveInputFileResult) self.progress_update_signal.connect(self.updateProgress) downloadTask.retrieve(url, destfile=destfile, progress_callback=self.downloadCallback) else: self.onRetrieveInputFileResult( True, "The file [%s] already exists" % destfile, None, destfile) def getSubprocessPath(self): executable = "synspy-viewer" if self.use_3D_viewer else "synspy-viewer2d" base_path = None return os.path.normpath(resource_path(executable, base_path)) def executeViewer(self, file_path): self.updateStatus("Executing viewer...") env = os.environ env["SYNSPY_AUTO_DUMP_LOAD"] = "true" env["DUMP_PREFIX"] = "./ROI_%s" % self.ui.workList.getCurrentTableItemTextByName( "RID") env["ZYX_SLICE"] = self.ui.workList.getCurrentTableItemTextByName( "ZYX Slice") env["ZYX_IMAGE_GRID"] = "0.4, 0.26, 0.26" env["SYNSPY_DETECT_NUCLEI"] = str( "nucleic" == self.ui.workList.getCurrentTableItemTextByName( "Segmentation Mode")).lower() output_path = os.path.join(os.path.dirname(self.config_path), "viewer.log") classifier = self.ui.workList.getTableItemByName( self.ui.workList.getCurrentTableRow(), "Classifier").data(Qt.UserRole) viewerTask = ViewerTask(self.getSubprocessPath(), self.identity == classifier, proc_output_path=output_path) viewerTask.status_update_signal.connect(self.onSubprocessExecuteResult) viewerTask.run(file_path, self.tempdir, env) def uploadAnalysisResult(self, update_state): qApp.setOverrideCursor(Qt.WaitCursor) # generate hatrac upload params basename = "ROI_%s" % self.ui.workList.getCurrentTableItemTextByName( "RID") match = r"%s_.*\.csv$" % basename output_files = [ f for f in os.listdir(self.tempdir) if os.path.isfile(os.path.join(self.tempdir, f)) and re.match(match, f) ] if not output_files: self.resetUI( "Could not locate output file from viewer subprocess -- aborting." ) return seg_mode = self.ui.workList.getCurrentTableItemTextByName( "Segmentation Mode") if seg_mode == "synaptic": extension = "_synaptic_only.csv" elif seg_mode == "nucleic": extension = "_nucleic_only.csv" else: self.updateStatus("Unknown segmentation mode \"%s\" -- aborting." % seg_mode) return file_name = basename + extension hatrac_path = HATRAC_UPDATE_URL_TEMPLATE % \ (self.ui.workList.getCurrentTableItemTextByName("Subject"), file_name) file_path = os.path.abspath(os.path.join(self.tempdir, file_name)) # upload to object store self.updateStatus("Uploading file %s to server..." % file_name) self.progress_update_signal.connect(self.updateProgress) uploadTask = FileUploadTask(self.store) uploadTask.status_update_signal.connect(self.onUploadFileResult) uploadTask.upload(hatrac_path, file_path, update_state, callback=self.uploadCallback) def markIncomplete(self): RID = self.ui.workList.getCurrentTableItemTextByName("RID") body = [{"RID": RID, "Status": "analysis in progress"}] self.updateStatus("Updating task status for %s..." % RID) updateTask = CatalogUpdateTask(self.catalog) updateTask.status_update_signal.connect(self.onCatalogUpdateResult) updateTask.update(WORKLIST_STATUS_UPDATE, json=body) @pyqtSlot() def taskTriggered(self): self.ui.logTextBrowser.widget.clear() self.disableControls() @pyqtSlot(str) def updateProgress(self, status): self.statusBar().showMessage(status) @pyqtSlot(str, str) def updateStatus(self, status, detail=None): logging.info(status + ((": %s" % detail) if detail else "")) self.statusBar().showMessage(status) @pyqtSlot(str, str) def resetUI(self, status, detail=None): qApp.restoreOverrideCursor() self.updateStatus(status, detail) self.enableControls() @pyqtSlot(str) def updateLog(self, text): self.ui.logTextBrowser.widget.appendPlainText(text) @pyqtSlot(bool, str, str, object) def onSessionResult(self, success, status, detail, result): qApp.restoreOverrideCursor() if success: self.identity = result["client"]["id"] self.attributes = result["attributes"] display_name = result["client"]["full_name"] self.setWindowTitle( "%s (%s - %s)" % (self.windowTitle(), self.server, display_name)) self.ui.actionLaunch.setEnabled(True) self.ui.actionLogout.setEnabled(True) self.ui.actionLogin.setEnabled(False) if not self.is_curator(): self.curator_mode = self.config["curator_mode"] = False self.on_actionRefresh_triggered() else: self.updateStatus("Login required.") @pyqtSlot() def on_actionLaunch_triggered(self): self.disableControls() qApp.setOverrideCursor(Qt.WaitCursor) # create working dir (tempdir) if self.tempdir: shutil.rmtree(self.tempdir) self.tempdir = tempfile.mkdtemp(prefix="synspy_") self.retrieveFiles() @pyqtSlot(bool, str, str, str) def onRetrieveAnalysisFileResult(self, success, status, detail, file_path): if not success: try: os.remove(file_path) except Exception as e: logging.warning("Unable to remove file [%s]: %s" % (file_path, format_exception(e))) self.resetUI(status, detail) self.serverProblemMessageBox( "Unable to download required input file", "The in-progress analysis file was not downloaded successfully." ) return self.retrieveInputFile() @pyqtSlot(bool, str, str, str) def onRetrieveInputFileResult(self, success, status, detail, file_path): if not success: try: os.remove(file_path) except Exception as e: logging.warning("Unable to remove file [%s]: %s" % (file_path, format_exception(e))) self.resetUI(status, detail) self.serverProblemMessageBox( "Unable to download required input file", "The image input file was not downloaded successfully.") return self.executeViewer(file_path) @pyqtSlot(bool, str, str, bool) def onSubprocessExecuteResult(self, success, status, detail, is_owner): qApp.restoreOverrideCursor() if not success: self.resetUI(status, detail) return if not is_owner or self.curator_mode: self.resetUI(status, detail) return # prompt for save/complete/discard msg = QMessageBox() msg.setIcon(QMessageBox.Information) msg.setWindowTitle("Confirm Action") msg.setText("How would you like to proceed?") msg.setInformativeText( "Select \"Save Progress\" to save your progress and upload the output to the server.\n\n" "Select \"Complete\" to upload the output to the server and mark this task as completed.\n\n" "Select \"Discard\" to abort the process and leave the task state unchanged." ) saveButton = msg.addButton("Save Progress", QMessageBox.ActionRole) completeButton = msg.addButton("Complete", QMessageBox.ActionRole) discardButton = msg.addButton("Discard", QMessageBox.RejectRole) msg.exec_() if msg.clickedButton() == discardButton: self.resetUI("Aborted.") return update_state = None if msg.clickedButton() == saveButton: update_state = ("incomplete", "analysis in progress") elif msg.clickedButton() == completeButton: update_state = ("complete", "analysis complete") self.uploadAnalysisResult(update_state) @pyqtSlot(bool, str, str, object) def onUploadFileResult(self, success, status, detail, result): if not success: self.resetUI(status, detail) self.serverProblemMessageBox( "Unable to upload required file(s)", "One or more required files were not uploaded successfully.") return state = result[0] RID = self.ui.workList.getCurrentTableItemTextByName("RID") body = [{ "RID": RID, "Segments Filtered URL": result[1], "Status": state[1] }] self.updateStatus("Updating task status for %s..." % RID) updateTask = CatalogUpdateTask(self.catalog) updateTask.status_update_signal.connect(self.onCatalogUpdateResult) updateTask.update(WORKLIST_UPDATE, json=body) @pyqtSlot(bool, str, str, object) def onCatalogUpdateResult(self, success, status, detail, result): if not success: self.resetUI(status, detail) self.serverProblemMessageBox( "Unable to update catalog data", "The catalog state was not updated successfully.") return qApp.restoreOverrideCursor() self.on_actionRefresh_triggered() @pyqtSlot() def on_actionRefresh_triggered(self): if not self.identity: self.updateStatus("Unable to get worklist -- not logged in.") return qApp.setOverrideCursor(Qt.WaitCursor) self.disableControls() self.updateStatus("Refreshing worklist...") queryTask = CatalogQueryTask(self.catalog) queryTask.status_update_signal.connect(self.onRefreshResult) if self.is_curator() and self.curator_mode: queryTask.query(WORKLIST_CURATOR_QUERY) else: queryTask.query(WORKLIST_QUERY % urlquote(self.identity, "")) @pyqtSlot(bool, str, str, object) def onRefreshResult(self, success, status, detail, result): if success: self.displayWorklist(result) self.resetUI("Ready.") else: self.resetUI(status, detail) if (self.ui.workList.rowCount() > 0) and self.identity: self.ui.actionLaunch.setEnabled(True) else: self.ui.actionLaunch.setEnabled(False) @pyqtSlot() def on_actionLogin_triggered(self): self.authWindow.show() self.authWindow.login() @pyqtSlot() def on_actionLogout_triggered(self): self.authWindow.logout() self.setWindowTitle("%s %s" % (self.ui.title, synspy_version)) self.ui.workList.clearContents() self.ui.workList.setRowCount(0) self.identity = None self.ui.actionLaunch.setEnabled(False) self.ui.actionLogout.setEnabled(False) self.ui.actionLogin.setEnabled(True) @pyqtSlot() def on_actionHelp_triggered(self): pass @pyqtSlot() def on_actionOptions_triggered(self): OptionsDialog.getOptions(self) @pyqtSlot() def on_actionExit_triggered(self): self.closeEvent() QCoreApplication.quit()
class UploadWindow(QMainWindow): progress_update_signal = pyqtSignal(str) def __init__(self, uploader, config_file=None, credential_file=None, hostname=None, window_title=None, cookie_persistence=True): super(UploadWindow, self).__init__() qApp.aboutToQuit.connect(self.quitEvent) assert uploader is not None self.uploader = None self.auth_window = None self.identity = None self.current_path = None self.uploading = False self.save_progress_on_cancel = False self.ui = UploadWindowUI(self) self.ui.title = window_title if window_title else "Deriva Upload Utility %s" % __version__ self.setWindowTitle(self.ui.title) self.config_file = config_file self.credential_file = credential_file self.cookie_persistence = cookie_persistence self.show() self.configure(uploader, hostname) def configure(self, uploader, hostname): # if a hostname has been provided, it overrides whatever default host a given uploader is configured for server = None if hostname: server = dict() if hostname.startswith("http"): url = urllib.parse.urlparse(hostname) server["protocol"] = url.scheme server["host"] = url.netloc else: server["protocol"] = "https" server["host"] = hostname # instantiate the uploader... # if an uploader instance does not have a default host configured, prompt the user to configure one if self.uploader: del self.uploader self.uploader = uploader(self.config_file, self.credential_file, server, dcctx_cid="gui/DerivaUploadGUI") if not self.uploader.server: if not self.checkValidServer(): return else: self.uploader.setServer(server) self.setWindowTitle("%s (%s)" % (self.ui.title, self.uploader.server["host"])) self.getNewAuthWindow() if not self.checkVersion(): return self.getSession() def getNewAuthWindow(self): if self.auth_window: if self.auth_window.authenticated(): self.on_actionLogout_triggered() self.auth_window.deleteLater() self.auth_window = \ EmbeddedAuthWindow(self, config=self.uploader.server, cookie_persistence= self.uploader.server.get("cookie_persistence", self.cookie_persistence), authentication_success_callback=self.onLoginSuccess, log_level=logging.getLogger().getEffectiveLevel()) self.ui.actionLogin.setEnabled(True) def getSession(self): qApp.setOverrideCursor(Qt.WaitCursor) logging.debug("Validating session: %s" % self.uploader.server["host"]) queryTask = SessionQueryTask(self.uploader) queryTask.status_update_signal.connect(self.onSessionResult) queryTask.query() def onLoginSuccess(self, **kwargs): self.auth_window.hide() self.uploader.setCredentials(kwargs["credential"]) self.getSession() def enableControls(self): self.ui.actionUpload.setEnabled(self.canUpload()) self.ui.actionRescan.setEnabled(self.current_path is not None and self.auth_window.authenticated()) self.ui.actionCancel.setEnabled(False) self.ui.actionOptions.setEnabled(True) self.ui.actionLogin.setEnabled(not self.auth_window.authenticated()) self.ui.actionLogout.setEnabled(self.auth_window.authenticated()) self.ui.actionExit.setEnabled(True) self.ui.browseButton.setEnabled(True) def disableControls(self): self.ui.actionUpload.setEnabled(False) self.ui.actionRescan.setEnabled(False) self.ui.actionOptions.setEnabled(False) self.ui.actionLogin.setEnabled(False) self.ui.actionLogout.setEnabled(False) self.ui.actionExit.setEnabled(False) self.ui.browseButton.setEnabled(False) def closeEvent(self, event=None): self.disableControls() if self.uploading: self.cancelTasks(self.cancelConfirmation()) if event: event.accept() def checkValidServer(self): self.restoreCursor() if self.uploader.server and self.uploader.server.get("host"): return True msg = QMessageBox() msg.setIcon(QMessageBox.Warning) msg.setWindowTitle("No Server Configured") msg.setText("Add server configuration now?") msg.setStandardButtons(QMessageBox.Yes | QMessageBox.No) ret = msg.exec_() if ret == QMessageBox.Yes: self.on_actionOptions_triggered() else: return False def checkAllowSessionCaching(self): client_settings = self.uploader.config.get("client_settings") if not client_settings: return allow_session_caching = stob(client_settings.get("allow_session_caching", True)) cookie_persistence = self.uploader.server.get("cookie_persistence", False) if cookie_persistence != allow_session_caching: if not allow_session_caching: self.uploader.server["cookie_persistence"] = False servers = list() for server in self.uploader.getServers(): if server.get("host", "") != self.uploader.server.get("host"): servers.append(server) servers.append(self.uploader.server) setServers = getattr(self.uploader, "setServers", None) if callable(setServers): setServers(servers) def onServerChanged(self, server): if server is None or server == self.uploader.server: return qApp.setOverrideCursor(Qt.WaitCursor) self.uploader.setServer(server) self.restoreCursor() if not self.checkValidServer(): return self.setWindowTitle("%s (%s)" % (self.ui.title, self.uploader.server["host"])) self.getNewAuthWindow() self.getSession() def cancelTasks(self, save_progress): qApp.setOverrideCursor(Qt.WaitCursor) self.save_progress_on_cancel = save_progress self.uploader.cancel() Task.shutdown_all() self.statusBar().showMessage("Waiting for background tasks to terminate...") while True: qApp.processEvents() if QThreadPool.globalInstance().waitForDone(10): break self.uploading = False self.statusBar().showMessage("All background tasks terminated successfully") self.restoreCursor() def uploadCallback(self, **kwargs): completed = kwargs.get("completed") total = kwargs.get("total") file_path = kwargs.get("file_path") file_name = os.path.basename(file_path) if file_path else "" job_info = kwargs.get("job_info", {}) job_info.update() if completed and total: file_name = " [%s]" % file_name job_info.update({"completed": completed, "total": total, "host": kwargs.get("host")}) status = "Uploading file%s: %d%% complete" % (file_name, round(((completed / total) % 100) * 100)) self.uploader.setTransferState(file_path, job_info) else: summary = kwargs.get("summary", "") file_name = "Uploaded file: [%s] " % file_name status = file_name # + summary if status: self.progress_update_signal.emit(status) if self.uploader.cancelled: if self.save_progress_on_cancel: return -1 else: return False return True def restoreCursor(self): qApp.restoreOverrideCursor() qApp.processEvents() def statusCallback(self, **kwargs): status = kwargs.get("status") self.progress_update_signal.emit(status) def displayUploads(self, upload_list): keys = ["State", "Status", "File"] hidden = ["State"] self.ui.uploadList.setRowCount(len(upload_list)) self.ui.uploadList.setColumnCount(len(keys)) rows = 0 for row in upload_list: cols = 0 for key in keys: item = QTableWidgetItem() value = row.get(key) text = str(value) or "" item.setText(text) item.setToolTip("<span>" + text + "</span>") self.ui.uploadList.setItem(rows, cols, item) if key in hidden: self.ui.uploadList.hideColumn(cols) cols += 1 rows += 1 self.ui.uploadList.setHorizontalHeaderLabels(keys) # add header names self.ui.uploadList.horizontalHeader().setDefaultAlignment(Qt.AlignLeft) # set alignment self.ui.uploadList.resizeColumnToContents(0) def canUpload(self): return (self.ui.uploadList.rowCount() > 0) and self.auth_window.authenticated() def checkVersion(self): if not self.uploader.isVersionCompatible(): self.updateStatus("Version incompatibility detected", "Current version: %s, required version: %s" % ( self.uploader.getVersion(), self.uploader.getVersionCompatibility())) self.disableControls() self.ui.actionExit.setEnabled(True) self.ui.actionOptions.setEnabled(True) self.updateConfirmation() return False self.checkAllowSessionCaching() self.resetUI("Ready...") return True def updateConfirmation(self): url = self.uploader.config.get("version_update_url") if not url: return self.restoreCursor() msg = QMessageBox() msg.setIcon(QMessageBox.Warning) msg.setWindowTitle("Version incompatibility detected!") msg.setText("Current version: %s\nRequired version: %s\n\nLaunch browser and download required version?" % (self.uploader.getVersion(), self.uploader.getVersionCompatibility())) msg.setInformativeText( "Selecting \"Yes\" will close the application and launch an external web browser which will take you to a " "download page where you can get the required version of this software.") msg.setStandardButtons(QMessageBox.Yes | QMessageBox.No) ret = msg.exec_() if ret == QMessageBox.Yes: webbrowser.open_new(url) self.deleteLater() def updateConfig(self): qApp.setOverrideCursor(Qt.WaitCursor) configUpdateTask = ConfigUpdateTask(self.uploader) configUpdateTask.status_update_signal.connect(self.onUpdateConfigResult) configUpdateTask.update_config() def scanDirectory(self, reset=False): self.uploader.reset() scanTask = ScanDirectoryTask(self.uploader) scanTask.status_update_signal.connect(self.onScanResult) scanTask.scan(self.current_path) @pyqtSlot(str) def updateProgress(self, status): if status: self.statusBar().showMessage(status) else: self.displayUploads(self.uploader.getFileStatusAsArray()) @pyqtSlot(str, str) def updateStatus(self, status, detail=None, success=True): msg = status + ((": %s" % detail) if detail else "") logging.info(msg) if success else logging.error(msg) self.statusBar().showMessage(status) @pyqtSlot(str, str) def resetUI(self, status, detail=None, success=True): self.updateStatus(status, detail, success) self.enableControls() @pyqtSlot(str) def updateLog(self, text): self.ui.logTextBrowser.widget.appendPlainText(text) @pyqtSlot(bool, str, str, object) def onSessionResult(self, success, status, detail, result): self.restoreCursor() if success: self.identity = result["client"]["id"] display_name = result["client"]["full_name"] self.setWindowTitle("%s (%s - %s)" % (self.ui.title, self.uploader.server["host"], display_name)) self.ui.actionLogout.setEnabled(True) self.ui.actionLogin.setEnabled(False) if self.current_path: self.ui.actionRescan.setEnabled(True) self.ui.actionUpload.setEnabled(True) self.updateStatus("Logged in.") self.updateConfig() else: self.updateStatus("Login required.") @pyqtSlot(bool, str, str, object) def onUpdateConfigResult(self, success, status, detail, result): self.restoreCursor() if not success: self.resetUI(status, detail) return if not result: return confirm_updates = stob(self.uploader.server.get("confirm_updates", False)) if confirm_updates: msg = QMessageBox() msg.setIcon(QMessageBox.Information) msg.setWindowTitle("Updated Configuration Available") msg.setText("Apply updated configuration?") msg.setInformativeText( "Selecting \"Yes\" will apply the latest configuration from the server and overwrite the existing " "default configuration file.\n\nSelecting \"No\" will ignore these updates and continue to use the " "existing configuration.\n\nYou should always apply the latest configuration changes from the server " "unless you understand the risk involved with using a potentially out-of-date configuration.") msg.setStandardButtons(QMessageBox.Yes | QMessageBox.No) ret = msg.exec_() if ret == QMessageBox.No: return write_config(self.uploader.getDeployedConfigFilePath(), result) self.uploader.initialize(cleanup=False) if not self.checkVersion(): return self.on_actionRescan_triggered() @pyqtSlot() def on_actionBrowse_triggered(self): dialog = QFileDialog() path = dialog.getExistingDirectory(self, "Select Directory", self.current_path, QFileDialog.ShowDirsOnly) if not path: return self.current_path = path self.ui.pathTextBox.setText(os.path.normpath(self.current_path)) self.scanDirectory() @pyqtSlot() def on_actionRescan_triggered(self): if not self.current_path: return self.scanDirectory() @pyqtSlot(bool, str, str, object) def onScanResult(self, success, status, detail, result): self.restoreCursor() if success: self.displayUploads(self.uploader.getFileStatusAsArray()) self.ui.actionUpload.setEnabled(self.canUpload()) self.resetUI("Ready...") if self.uploading: self.on_actionUpload_triggered() else: self.resetUI(status, detail, success) @pyqtSlot() def on_actionUpload_triggered(self): if not self.uploading: if self.uploader.cancelled: self.uploading = True self.on_actionRescan_triggered() return self.disableControls() self.ui.actionCancel.setEnabled(True) self.save_progress_on_cancel = False qApp.setOverrideCursor(Qt.WaitCursor) self.uploading = True self.updateStatus("Uploading...") self.progress_update_signal.connect(self.updateProgress) uploadTask = UploadFilesTask(self.uploader) uploadTask.status_update_signal.connect(self.onUploadResult) uploadTask.upload(status_callback=self.statusCallback, file_callback=self.uploadCallback) @pyqtSlot(bool, str, str, object) def onUploadResult(self, success, status, detail, result): self.restoreCursor() self.uploading = False self.displayUploads(self.uploader.getFileStatusAsArray()) if success: self.resetUI("Ready.") else: self.resetUI(status, detail, success) @pyqtSlot() def on_actionCancel_triggered(self): self.cancelTasks(self.cancelConfirmation()) self.restoreCursor() self.displayUploads(self.uploader.getFileStatusAsArray()) self.resetUI("Ready.") @pyqtSlot() def on_actionLogin_triggered(self): if not self.auth_window: if self.checkValidServer(): self.getNewAuthWindow() else: return self.auth_window.show() self.auth_window.login() @pyqtSlot() def on_actionLogout_triggered(self): self.setWindowTitle("%s (%s)" % (self.ui.title, self.uploader.server["host"])) self.auth_window.logout() self.identity = None self.ui.actionUpload.setEnabled(False) self.ui.actionRescan.setEnabled(False) self.ui.actionLogout.setEnabled(False) self.ui.actionLogin.setEnabled(True) self.updateStatus("Logged out.") @pyqtSlot() def on_actionOptions_triggered(self): OptionsDialog.getOptions(self) @pyqtSlot() def on_actionHelp_triggered(self): pass @pyqtSlot() def on_actionExit_triggered(self): self.closeEvent() qApp.quit() def quitEvent(self): if self.auth_window: self.auth_window.logout(self.logoutConfirmation()) qApp.closeAllWindows() self.deleteLater() def logoutConfirmation(self): if self.auth_window and (not self.auth_window.authenticated() or not self.auth_window.cookie_persistence): return msg = QMessageBox() msg.setIcon(QMessageBox.Warning) msg.setWindowTitle("Confirm Action") msg.setText("Do you wish to completely logout of the system?") msg.setInformativeText("Selecting \"Yes\" will clear the login state and invalidate the current user identity." "\n\nSelecting \"No\" will keep your current identity cached, which will allow you to " "log back in without authenticating until your session expires.\n\nNOTE: Select \"Yes\" " "if this is a shared system using a single user account.") msg.setStandardButtons(QMessageBox.Yes | QMessageBox.No) ret = msg.exec_() if ret == QMessageBox.Yes: return True return False def cancelConfirmation(self): self.restoreCursor() msg = QMessageBox() msg.setIcon(QMessageBox.Warning) msg.setWindowTitle("Confirm Action") msg.setText("Save progress for the current upload?") msg.setInformativeText("Selecting \"Yes\" will attempt to resume this transfer from the point where it was " "cancelled.\n\nSelecting \"No\" will require the transfer to be started over from the " "beginning of file.") msg.setStandardButtons(QMessageBox.Yes | QMessageBox.No) ret = msg.exec_() if ret == QMessageBox.Yes: return True return False