Esempio n. 1
0
    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)
Esempio n. 3
0
 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)
Esempio n. 4
0
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