Exemple #1
0
class ImportViewInterface(QObject):
    """
    interface class defining signals and slots for all import views
    """

    def __init__(self, dock_widget: GeologicalDataProcessingDockWidget) -> None:
        """
        Initialize the view
        :param dock_widget: current GeologicalDataProcessingDockWidget instance
        """

        self.logger = QGISLogHandler(self.__class__.__name__)
        self.__combos = dict()
        self._dwg = dock_widget

        # initialize user interface
        self._import_service = ImportService.get_instance()
        self._import_service.reset_import.connect(self.reset_import)
        self._import_service.import_columns_changed.connect(self._on_import_columns_changed)

        self._table_view: QTableView or None = None
        self._only_number_in_table_view: bool = False
        self._table_model: PropertyImportModel = PropertyImportModel()
        self._dwg.start_import_button.clicked.connect(self._on_start_import)
        self._controller_thread: ImportControllersInterface or None = None

        super().__init__()

    def _connect_combo_listener(self):
        """
        connects all combobox elements to the on_selection_changed slot
        :return: Nothing
        """
        [combo.currentIndexChanged.connect(self.on_selection_changed) for combo in self.__combos]

    @property
    def combobox_names(self) -> Dict:
        """
        Returns a dictionary of comboboxes for the current view
        :return: returns a dictionary of comboboxes for the current view
        """
        return self.__combos

    @combobox_names.setter
    def combobox_names(self, combo_dict: Dict) -> None:
        """
        Sets a new dictionary to the combobox list. Disconnects the old ones and connects the new to the
        on_selection_changed slot
        :param combo_dict: dictionary with new combobox elements
        :return: Nothing
        :raises TypeError: if a dictionary value is not an instance of QComboBox or a key is not a str. Sets an empty
        dictionary instead
        """

        self._disconnect_selection_changed()

        for key in combo_dict:
            if not isinstance(key, str):
                self.__combos = dict()
                raise TypeError("{} is not a string".format(str(key)))
            if not isinstance(combo_dict[key], QComboBox):
                self.__combos = dict()
                raise TypeError("{} is not an instance of QComboBox".format(str(key)))

        self.__combos = combo_dict
        self._connect_selection_changed()

    @property
    def table_view(self) -> QTableView or None:
        return self._table_view

    @table_view.setter
    def table_view(self, widget: QTableView) -> None:
        if not isinstance(widget, QTableView):
            raise TypeError("submitted object is not of type QTableView: {}", str(widget))

        self._table_model.clear()
        self._table_view = widget
        self._table_view.setModel(self._table_model)

        if self._only_number_in_table_view:
            self._table_view.setItemDelegate(LogImportDelegate())
        else:
            self._table_view.setItemDelegate(PropertyImportDelegate())
        # self._table_view.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeToContents)
        # self._table_view.horizontalHeader().setSectionResizeMode(0, QHeaderView.Stretch)
        self._table_view.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)

    @property
    def dockwidget(self) -> GeologicalDataProcessingDockWidget:
        """
        Returns the current dockwidget
        :return: the current dockwidget
        """
        return self._dwg

    #
    # signals
    #

    selection_changed = pyqtSignal(list)
    """data changed signal which gives the index or name of the changed column and the newly selected text"""

    start_import = pyqtSignal()
    """signal to start the data import through an ImportController Thread"""

    #
    # slots
    #

    def on_selection_changed(self, _=None) -> None:
        """
        Emits the combobox_changed signal with a list of changed text
        :return: Nothing
        """
        selection_list = [self.combobox_names[key].currentText() for key in self.combobox_names]
        self.selection_changed.emit(selection_list)

        if self._table_view is None:
            self.logger.debug("No list widget specified for additional columns")
            return

        if self._only_number_in_table_view:
            cols = diff(self._import_service.number_columns, selection_list)
        else:
            cols = diff(self._import_service.selectable_columns, selection_list)

        self.logger.debug("selectable_columns: " + str(self._import_service.selectable_columns))
        self.logger.debug("selected_cols: " + str(selection_list))
        self.logger.debug("additional cols: " + str(cols))

        if self._table_view is not None:
            self._table_view.show()
            self._table_view.setEnabled(True)
            self._table_view.clearSelection()

        self._table_model.clear()
        for col in cols:
            property_type = PropertyTypes.FLOAT if col in self._import_service.number_columns else PropertyTypes.STRING
            self._table_model.add(PropertyImportData(name=col[0], unit=col[1], property_type=property_type))

    def _on_import_columns_changed(self) -> None:
        """
        change the import columns
        :return: Nothing
        """
        self.logger.debug("(Interface) _on_import_columns_changed")
        self._connect_selection_changed()
        self.on_selection_changed()

    def _on_start_import(self) -> None:
        self.logger.debug("(Interface) _on_start_import")
        self._update_progress_bar(0)
        self._dwg.progress_bar_layout.setVisible(True)

    def _on_import_failed(self, msg: str) -> None:
        self.logger.debug("(Interface) _on_import_failed")
        self.__import_finished()
        self.logger.error("Import failed", msg, to_messagebar=True)

    def _on_import_finished_with_warnings(self, msg: str) -> None:
        self.logger.debug("(Interface) _on_import_finished_with_warnings")
        self.logger.warn("Import finished with warnings", msg, to_messagebar=True)
        self.__import_finished()

    def _on_import_successful(self):
        self.logger.debug("(Interface) _on_import_successful")
        self.logger.info("Import successful", to_messagebar=True)
        self.__import_finished()

    def _on_cancel_import(self):
        self._controller_thread.cancel_import("Import canceled by user")

    def __import_finished(self):
        self._dwg.progress_bar_layout.setVisible(False)
        self._disconnect_thread()

        self._controller_thread.wait(2000)
        self._controller_thread = None

    #
    # public functions
    #

    def combobox_data(self, index: int or str) -> str:
        """
        Returns the currently selected item of the gui element with the given index
        :param index: index of the requested gui element as integer or string
        :return: Returns the data at the given index
        :raises IndexError: if index is not part in the available list
        """

        if isinstance(index, int):
            index = [self.combobox_names.keys()][index]
        else:
            index = str(index)

        if index not in self.combobox_names:
            raise IndexError("{} is not available".format(index))

        return self.combobox_names[index].currentText()

    def get_name(self, index: int) -> str or None:
        """
        Returns the name of the combobox with the given index
        :param index: index of the requested combobox
        :return: Returns the name of the combobox with the given index
        :raises IndexError: if the requested index is not in the list
        :raises ValueError: if the index is not convertible to an integer
        """
        index = int(index)

        if 0 <= index < len(self.combobox_names.keys()):
            return list(self.combobox_names.keys())[0]

    def get_names(self):
        """
        Returns a list of the combobox names
        :return: Returns a list of the combobox names
        """
        return list(self.combobox_names.keys())

    def set_combobox_data(self, index: int or str, values: List[str], default_index: int = 0) -> None:
        """
        Sets the committed values list to the gui combobox elements for the given index
        :param index: index of the requested gui element as integer or string
        :param values: new values for the combo boxes as a list of strings
        :param default_index: default selected index. If no default value is given, or the index is not part of the
                              list, the first entry will be selected by default
        :return: Returns, if the data setting was successful
        :raises IndexError: if index is not part in the available list
        :raises TypeError: if default_index is not an instance of int
        """

        if isinstance(index, int):
            index = [self.combobox_names.keys()][index]
        else:
            index = str(index)

        if index not in self.combobox_names:
            raise IndexError("{} is not available".format(index))

        if not isinstance(default_index, int):
            raise TypeError("default_index({}) is not an instance of int!".format(default_index))

        self.combobox_names[index].clear()
        for item in values:
            self.combobox_names[index].addItem(str(item))

        if not (0 <= default_index <= len(values)):
            default_index = 0
        self.combobox_names[index].setCurrentIndex(default_index)

    def reset_import(self) -> None:
        """
        Clears all import combo boxes, in case of a failure
        :return: Nothing
        """
        [self.set_combobox_data(name, []) for name in self.get_names()]

    def get_property_columns(self) -> List[PropertyImportData]:
        selection = set([x.row() for x in self._table_view.selectedIndexes()])
        self.logger.debug("selected rows indices: {}".format(selection))
        erg = [self._table_model.row(x) for x in selection]
        self.logger.debug("Selection:")
        [self.logger.debug("\t{}".format(x)) for x in erg]
        return erg

    #
    # protected functions
    #

    def _update_progress_bar(self, value):
        """
        slot to set the current progress bar value
        :param value: value in percent
        :return: nothing
        """
        self.logger.debug("Update progressbar with value {} called".format(value))
        if value < 0:
            self.dockwidget.progress_bar.setValue(0)
        elif value > 100:
            self.dockwidget.progress_bar.setValue(100)
        else:
            self.dockwidget.progress_bar.setValue(int(value))

    def _connect_selection_changed(self):
        self.logger.debug("_connect_selection_changed")
        [self.__combos[key].currentTextChanged.connect(self.on_selection_changed) for key in self.__combos]

    def _disconnect_selection_changed(self):
        for key in self.__combos:
            try:
                self.__combos[key].currentTextChanged.disconnect(self.on_selection_changed)
            except TypeError:
                # not connected
                pass

    def _connect_thread(self):
        self._controller_thread.import_finished.connect(self._on_import_successful)
        self._controller_thread.import_failed.connect(self._on_import_failed)
        self._controller_thread.import_finished_with_warnings.connect(self._on_import_finished_with_warnings)
        self._controller_thread.update_progress.connect(self._update_progress_bar)
        self._dwg.cancel_import.clicked.connect(self._on_cancel_import)

    def _disconnect_thread(self):
        self._controller_thread.import_finished.disconnect(self._on_import_successful)
        self._controller_thread.import_failed.disconnect(self._on_import_failed)
        self._controller_thread.import_finished_with_warnings.disconnect(self._on_import_finished_with_warnings)
        self._controller_thread.update_progress.disconnect(self._update_progress_bar)
        self._dwg.cancel_import.clicked.disconnect(self._on_cancel_import)
    def check_required_modules():
        """
        Check all module requirements
        :return: True is all modules with required versions were found, else false
        """
        logger = QGISLogHandler("ModuleService")

        # don't display logging to QGIS
        safed_iface = logger.qgis_iface
        logger.qgis_iface = None

        logger.info("Checking required modules")

        python = ModuleService.get_python()

        cmd = [python, "-m", "pip", "list", "--format", "json", "--user"]

        try:
            logger.info("Checking package versions")
            logger.info("CMD: {}".format(" ".join(cmd)))

            result = run(cmd, stdout=PIPE, stderr=STDOUT, check=True)
            logger.info("run pip info successful")
            packages = json.loads(result.stdout.decode())

        except CalledProcessError as e:
            # restore QGIS logging
            logger.qgis_iface = safed_iface
            logger.error("pip info request failed!")
            logger.error("RETURN-CODE: {} - CMD: {}".format(e.returncode, e.cmd))
            logger.error("OUTPUT: {}".format(e.output))
            return False

        ModuleService.modules = list()
        for module in module_list:
            module_found = False
            for package in packages:
                if package["name"] != module:
                    continue
                module_found = True

                logger.debug("found module {} [{}]".format(package["name"], package["version"]))

                v1 = version.parse(package["version"])
                v2 = version.parse(module_list[module])
                if v1 < v2:
                    logger.warn("Module version [{}] differs from required version [{}]".format(v1, v2))
                    ModuleService.modules.append("{}=={}".format(module, module_list[module]))

            if not module_found:
                logger.debug("Module not found, adding {}=={} to install list".format(module, module_list[module]))
                ModuleService.modules.append("{}=={}".format(module, module_list[module]))

        if len(ModuleService.modules) > 0:
            # restore QGIS logging
            logger.qgis_iface = safed_iface
            logger.info("Missing packages found: {}".format(ModuleService.modules))
            return False

        logger.info("All packages up to date")

        # restore QGIS logging
        logger.qgis_iface = safed_iface
        return True
Exemple #3
0
class ImportControllersInterface(QThread):
    """
    Basic interface for all import_tests controller
    """

    def __init__(self, data: Dict, selection: Dict, properties: List[PropertyImportData]) -> None:
        """
        :param data: import data parsed from the file to import
        :param selection: dictionary of selected columns
        """
        super().__init__()

        self._logger = QGISLogHandler(self.__class__.__name__)
        self._data: Dict = data
        self._selection: Dict = selection
        self._properties: List[PropertyImportData] = properties
        self._mutex = QMutex()
        self._cancel = False
        self._message = ""

    def run(self) -> None:
        """
        Thread execution function to import data
        :return: Nothing
        """
        pass

    #
    # signals
    #

    update_progress = pyqtSignal(int)
    """update progress bar signal. Committed value has to be between 0 and 100"""

    import_finished = pyqtSignal()
    """signal emitted, when the import process has finished"""

    import_finished_with_warnings = pyqtSignal(str)
    """signal emitted, when the import process has finished with warnings"""

    import_failed = pyqtSignal(str)
    """signal emitted, when the import process was canceled or failed through a call of the cancel_import slot"""

    #
    # slots
    #

    def cancel_import(self, msg: str = "") -> None:
        """
        slot for canceling the import process. A trigger variable will hint the importer to stop at the next
        possibility. This should ensure the finalization of all started write processes and therefore the integrity of
        all database objects.

        The :func:`~GeologicalDataProcessing.controller.import_controller.ImportControllersInterface.import_cancelled`
        signal will be sent, if the import process was successfully cancelled.
        :return: Nothing
        """
        self._cancel = True
        if msg == "":
            self._message = "Import canceled"
        else:
            self._message = msg

    def _import_done(self, future: Future = None) -> None:
        """
        function called, when import is done or canceled
        :param future: import executing future object
        :return: nothing
        """
        self._logger.debug("import done")

        self._view.dockwidget.progress_bar_layout.setVisible(False)
        self._view.dockwidget.cancel_import.clicked.disconnect(self._stop_import)
        if (future is not None) and future.cancelled():
            self._logger.warn("future run finished, import cancelled!")
        elif future is not None:
            self._logger.info("future run finished, import successful")

        self._logger.info("QThread finished")
        if self.__thread is not None:
            self._logger.debug("waiting for the end...")
            self.__thread.wait()
            self._logger.debug("at the end...")
            self.__thread = None