Ejemplo n.º 1
0
 def do_open_ds_view(self):
     """Opens current url in the data store view."""
     try:
         self.ds_view = DataStoreForm(self._project.db_mngr,
                                      (self._sa_url, self.name))
     except spinedb_api.SpineDBAPIError as e:
         self._logger.msg_error.emit(e.msg)
         return
     self.ds_view.destroyed.connect(self._handle_ds_view_destroyed)
     self.ds_view.show()
    def setUp(self):
        """Overridden method. Runs before each test. Makes instances of DataStoreForm classes."""
        with mock.patch("spinetoolbox.spine_db_manager.DiffDatabaseMapping") as mock_DiffDBMapping, mock.patch(
            "spinetoolbox.widgets.data_store_widget.DataStoreForm.restore_ui"
        ):
            self.db_mngr = SpineDBManager(None, None)

            def DiffDBMapping_side_effect(url, upgrade=False, codename=None):
                mock_db_map = mock.MagicMock()
                mock_db_map.codename = codename
                return mock_db_map

            mock_DiffDBMapping.side_effect = DiffDBMapping_side_effect
            self.tree_view_form = DataStoreForm(self.db_mngr, ("mock_url", "mock_db"))
            self.mock_db_map = self.tree_view_form.db_map
Ejemplo n.º 3
0
def _make_pivot_proxy_model():
    """Returns a prefilled PivotTableModel."""
    db_mngr = MagicMock()
    db_mngr.get_value.side_effect = lambda db_map, item_type, id_, field, role: id_
    mock_db_map = Mock()
    mock_db_map.codename = "codename"
    db_mngr.get_db_map_for_listener.side_effect = lambda *args, **kwargs: mock_db_map
    db_mngr.undo_action.__getitem__.side_effect = lambda key: QAction()
    db_mngr.redo_action.__getitem__.side_effect = lambda key: QAction()
    data_store_widget = DataStoreForm(db_mngr, ("sqlite://", "codename"))
    data_store_widget.create_header_widget = lambda *args, **kwargs: None
    model = data_store_widget.pivot_table_model
    data = {
        ('1', 'int_col'):
        '-3',
        ('2', 'int_col'):
        '-1',
        ('3', 'int_col'):
        '2',
        ('1', 'float_col'):
        '1.1',
        ('2', 'float_col'):
        '1.2',
        ('3', 'float_col'):
        '1.3',
        ('1', 'time_series_col'):
        '{"type": "time_series", "data": {"2019-07-10T13:00": 2.3, "2019-07-10T13:20": 5.0}}',
        (
            '2',
            'time_series_col',
        ):
        '{"type": "time_series", "index": {"start": "2019-07-10T13:00", "resolution": "20 minutes"}, "data": [3.3, 4.0]}',
        ('3', 'time_series_col'):
        '{"type": "time_series", "data": {"2019-07-10T13:00": 4.3, "2019-07-10T13:20": 3.0}}',
    }
    index_ids = ['rows', 'col_types']
    model.reset_model(data, index_ids, ['rows'], ['col_types'], [], ())
    model.fetchMore(QModelIndex())
    return data_store_widget.pivot_table_proxy
Ejemplo n.º 4
0
def main(argv):
    """Launches Data Store view as it's own application.

    Args:
        argv (list): Command line arguments
    """
    if not pyside2_version_check():
        return 0
    if not spinedb_api_version_check():
        return 0
    try:
        path = argv[1]
    except IndexError:
        return 0
    app = QApplication(argv)
    QFontDatabase.addApplicationFont(":/fonts/fontawesome5-solid-webfont.ttf")
    locale.setlocale(locale.LC_NUMERIC, 'C')
    url = f"sqlite:///{path}"
    db_mngr = SpineDBManager(SimpleLogger(), None)
    tree = DataStoreForm(db_mngr, (url, "main"))
    tree.show()
    return_code = app.exec_()
    return return_code
Ejemplo n.º 5
0
    def setUp(self):
        """Overridden method. Runs before each test. Makes instance of DataStoreForm class."""
        with mock.patch(
                "spinetoolbox.spine_db_manager.QMessageBox"
        ), mock.patch(
                "spinetoolbox.widgets.data_store_widget.DataStoreForm.restore_ui"
        ):
            self.mock_db_mngr = mock.MagicMock()

            def get_db_map_for_listener_side_effect(listener,
                                                    url,
                                                    codename=None):
                mock_db_map = mock.MagicMock()
                mock_db_map.codename = codename
                return mock_db_map

            self.mock_db_mngr.get_db_map_for_listener.side_effect = get_db_map_for_listener_side_effect
            self.mock_db_mngr.undo_action.__getitem__.side_effect = lambda key: QAction(
            )
            self.mock_db_mngr.redo_action.__getitem__.side_effect = lambda key: QAction(
            )
            self.ds_view_form = DataStoreForm(self.mock_db_mngr,
                                              ("mock_url", "mock_db"))
            self.mock_db_map = self.ds_view_form.db_map
 def setUp(self):
     db_mngr = MagicMock()
     db_mngr.get_value.side_effect = lambda db_map, item_type, id_, field, role: id_
     db_mngr.get_item.side_effect = lambda db_map, item_type, id_: {
         "name": id_,
         "parameter_name": id_
     }
     mock_db_map = Mock()
     mock_db_map.codename = "codename"
     db_mngr.get_db_map_for_listener.side_effect = lambda *args, **kwargs: mock_db_map
     db_mngr.undo_action.__getitem__.side_effect = lambda key: QAction()
     db_mngr.redo_action.__getitem__.side_effect = lambda key: QAction()
     tabular_view = DataStoreForm(db_mngr, ("sqlite://", "codename"))
     self._model = PivotTableModel(tabular_view)
     data = {
         ('row1', 'col1'): '1',
         ('row2', 'col1'): '3',
         ('row1', 'col2'): '5',
         ('row2', 'col2'): '7'
     }
     index_names = ['rows', 'cols']
     self._model.reset_model(data, index_names, ['rows'], ['cols'], [], ())
     self._model.fetchMore(QModelIndex())
Ejemplo n.º 7
0
class TestAddItemsDialog(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        """Overridden method. Runs once before all tests in this class."""
        try:
            cls.app = QApplication().processEvents()
        except RuntimeError:
            pass
        logging.basicConfig(
            stream=sys.stderr,
            level=logging.DEBUG,
            format='%(asctime)s %(levelname)s: %(message)s',
            datefmt='%Y-%m-%d %H:%M:%S',
        )

    def setUp(self):
        """Overridden method. Runs before each test. Makes instance of DataStoreForm class."""
        with mock.patch(
                "spinetoolbox.spine_db_manager.QMessageBox"
        ), mock.patch(
                "spinetoolbox.widgets.data_store_widget.DataStoreForm.restore_ui"
        ):
            self.mock_db_mngr = mock.MagicMock()

            def get_db_map_for_listener_side_effect(listener,
                                                    url,
                                                    codename=None):
                mock_db_map = mock.MagicMock()
                mock_db_map.codename = codename
                return mock_db_map

            self.mock_db_mngr.get_db_map_for_listener.side_effect = get_db_map_for_listener_side_effect
            self.mock_db_mngr.undo_action.__getitem__.side_effect = lambda key: QAction(
            )
            self.mock_db_mngr.redo_action.__getitem__.side_effect = lambda key: QAction(
            )
            self.ds_view_form = DataStoreForm(self.mock_db_mngr,
                                              ("mock_url", "mock_db"))
            self.mock_db_map = self.ds_view_form.db_map

    def tearDown(self):
        """Overridden method. Runs after each test.
        Use this to free resources after a test if needed.
        """
        with mock.patch(
                "spinetoolbox.widgets.data_store_widget.DataStoreForm.save_window_state"
        ) as mock_save_w_s:
            self.ds_view_form.close()
            mock_save_w_s.assert_called_once()
        self.ds_view_form.deleteLater()
        self.ds_view_form = None
        try:
            os.remove('mock_db.sqlite')
        except OSError:
            pass

    def test_add_object_classes(self):
        """Test object classes are added through the manager when accepting the dialog."""
        dialog = AddObjectClassesDialog(self.ds_view_form, self.mock_db_mngr,
                                        self.mock_db_map)
        model = dialog.model
        header = model.header
        model.fetchMore()
        self.assertEqual(
            header,
            ['object class name', 'description', 'display icon', 'databases'])
        indexes = [
            model.index(0, header.index(field))
            for field in ('object class name', 'databases')
        ]
        values = ['fish', 'mock_db']
        model.batch_set_data(indexes, values)

        def _add_object_classes(db_map_data):
            self.assertTrue(self.mock_db_map in db_map_data)
            data = db_map_data[self.mock_db_map]
            self.assertEqual(len(data), 1)
            item = data[0]
            self.assertTrue("name" in item)
            self.assertEqual(item["name"], "fish")

        self.mock_db_mngr.add_object_classes.side_effect = _add_object_classes
        dialog.accept()
        self.mock_db_mngr.add_object_classes.assert_called_once()

    def test_do_not_add_object_classes_with_invalid_db(self):
        """Test object classes aren't added when the database is not correct."""
        dialog = AddObjectClassesDialog(self.ds_view_form, self.mock_db_mngr,
                                        self.mock_db_map)
        self.ds_view_form.msg_error = mock.NonCallableMagicMock()
        self.ds_view_form.msg_error.attach_mock(mock.MagicMock(), "emit")
        model = dialog.model
        header = model.header
        model.fetchMore()
        self.assertEqual(
            header,
            ['object class name', 'description', 'display icon', 'databases'])
        indexes = [
            model.index(0, header.index(field))
            for field in ('object class name', 'databases')
        ]
        values = ['fish', 'gibberish']
        model.batch_set_data(indexes, values)
        dialog.accept()
        self.mock_db_mngr.add_object_classes.assert_not_called()
        self.ds_view_form.msg_error.emit.assert_called_with(
            "Invalid database 'gibberish' at row 1")
Ejemplo n.º 8
0
 def _make_view_window(self, db_maps):
     try:
         return DataStoreForm(self._project.db_mngr, *db_maps)
     except SpineDBAPIError as e:
         self._logger.msg_error.emit(e.msg)
Ejemplo n.º 9
0
class DataStore(ProjectItem):
    def __init__(self,
                 name,
                 description,
                 x,
                 y,
                 toolbox,
                 project,
                 logger,
                 url=None):
        """Data Store class.

        Args:
            name (str): Object name
            description (str): Object description
            x (float): Initial X coordinate of item icon
            y (float): Initial Y coordinate of item icon
            toolbox (ToolboxUI): QMainWindow instance
            project (SpineToolboxProject): the project this item belongs to
            logger (LoggerInterface): a logger instance
            url (str or dict): SQLAlchemy url
        """
        super().__init__(name, description, x, y, project, logger)
        if url is None:
            url = dict()
        if url and not isinstance(url["database"], str):
            url["database"] = deserialize_path(url["database"],
                                               self._project.project_dir)
        self._toolbox = toolbox
        self._url = self.parse_url(url)
        self._sa_url = None
        self.ds_view = None
        self._for_spine_model_checkbox_state = Qt.Unchecked
        # Make logs directory for this Data Store
        self.logs_dir = os.path.join(self.data_dir, "logs")
        try:
            create_dir(self.logs_dir)
        except OSError:
            self._logger.msg_error.emit(
                f"[OSError] Creating directory {self.logs_dir} failed. Check permissions."
            )

    @staticmethod
    def item_type():
        """See base class."""
        return "Data Store"

    @staticmethod
    def category():
        """See base class."""
        return "Data Stores"

    def parse_url(self, url):
        """Return a complete url dictionary from the given dict or string"""
        base_url = dict(dialect=None,
                        username=None,
                        password=None,
                        host=None,
                        port=None,
                        database=None)
        if isinstance(url, dict):
            if "database" in url and url["database"] is not None:
                if url["database"].lower().endswith(".sqlite"):
                    # Convert relative database path back to absolute
                    abs_path = os.path.abspath(
                        os.path.join(self._project.project_dir,
                                     url["database"]))
                    url["database"] = abs_path
            base_url.update(url)
        return base_url

    def make_signal_handler_dict(self):
        """Returns a dictionary of all shared signals and their handlers.
        This is to enable simpler connecting and disconnecting."""
        s = super().make_signal_handler_dict()
        s[self._properties_ui.toolButton_ds_open_dir.
          clicked] = lambda checked=False: self.open_directory()
        s[self._properties_ui.pushButton_ds_view.clicked] = self.open_ds_view
        s[self._properties_ui.toolButton_open_sqlite_file.
          clicked] = self.open_sqlite_file
        s[self._properties_ui.pushButton_create_new_spine_db.
          clicked] = self.create_new_spine_database
        s[self._properties_ui.toolButton_copy_url.clicked] = self.copy_url
        s[self._properties_ui.comboBox_dialect.
          activated[str]] = self.refresh_dialect
        s[self._properties_ui.lineEdit_database.
          file_dropped] = self.set_path_to_sqlite_file
        s[self._properties_ui.lineEdit_username.
          editingFinished] = self.refresh_username
        s[self._properties_ui.lineEdit_password.
          editingFinished] = self.refresh_password
        s[self._properties_ui.lineEdit_host.
          editingFinished] = self.refresh_host
        s[self._properties_ui.lineEdit_port.
          editingFinished] = self.refresh_port
        s[self._properties_ui.lineEdit_database.
          editingFinished] = self.refresh_database
        return s

    def activate(self):
        """Load url into selections and connect signals."""
        self._properties_ui.label_ds_name.setText(self.name)
        self._properties_ui.checkBox_for_spine_model.setCheckState(
            self._for_spine_model_checkbox_state)
        self.load_url_into_selections(
        )  # Do this before connecting signals or funny things happen
        super().connect_signals()

    def deactivate(self):
        """Disconnect signals."""
        self._for_spine_model_checkbox_state = self._properties_ui.checkBox_for_spine_model.checkState(
        )
        if not super().disconnect_signals():
            logging.error("Item %s deactivation failed", self.name)
            return False
        return True

    def url(self):
        """Return the url attribute, for saving the project."""
        return self._url

    def _update_sa_url(self, log_errors=True):
        self._sa_url = self._make_url(log_errors=log_errors)

    @busy_effect
    def _make_url(self, log_errors=True):
        """Returns a sqlalchemy url from the current url attribute or None if not valid."""
        if not self._url:
            if log_errors:
                self._logger.msg_error.emit(
                    f"No URL specified for <b>{self.name}</b>. Please specify one and try again"
                )
            return None
        try:
            url_copy = dict(self._url)
            dialect = url_copy.pop("dialect")
            if not dialect:
                if log_errors:
                    self._logger.msg_error.emit(
                        f"Unable to generate URL from <b>{self.name}</b> selections: invalid dialect {dialect}. "
                        "<br>Please select a new dialect and try again.")
                return None
            if dialect == 'sqlite':
                url = URL('sqlite', **url_copy)  # pylint: disable=unexpected-keyword-arg
            else:
                db_api = spinedb_api.SUPPORTED_DIALECTS[dialect]
                drivername = f"{dialect}+{db_api}"
                url = URL(drivername, **url_copy)  # pylint: disable=unexpected-keyword-arg
        except Exception as e:  # pylint: disable=broad-except
            # This is in case one of the keys has invalid format
            if log_errors:
                self._logger.msg_error.emit(
                    f"Unable to generate URL from <b>{self.name}</b> selections: {e} "
                    "<br>Please make new selections and try again.")
            return None
        if not url.database:
            if log_errors:
                self._logger.msg_error.emit(
                    f"Unable to generate URL from <b>{self.name}</b> selections: database missing. "
                    "<br>Please select a database and try again.")
            return None
        # Small hack to make sqlite file paths relative to this DS directory
        # TODO: Check if this is still needed
        if dialect == "sqlite" and not os.path.isabs(url.database):
            url.database = os.path.join(self.data_dir, url.database)
            self._properties_ui.lineEdit_database.setText(url.database)
        # Final check
        try:
            engine = create_engine(url)
            with engine.connect():
                pass
        except Exception as e:  # pylint: disable=broad-except
            if log_errors:
                self._logger.msg_error.emit(
                    f"Unable to generate URL from <b>{self.name}</b> selections: {e} "
                    "<br>Please make new selections and try again.")
            return None
        return url

    def project(self):
        """Returns current project or None if no project open."""
        return self._project

    @Slot("QString", name="set_path_to_sqlite_file")
    def set_path_to_sqlite_file(self, file_path):
        """Set path to SQLite file."""
        abs_path = os.path.abspath(file_path)
        self._properties_ui.lineEdit_database.setText(abs_path)
        self.set_url_key("database", abs_path)

    @Slot(bool, name='open_sqlite_file')
    def open_sqlite_file(self, checked=False):
        """Open file browser where user can select the path to an SQLite
        file that they want to use."""
        # noinspection PyCallByClass, PyTypeChecker, PyArgumentList
        answer = QFileDialog.getOpenFileName(self._toolbox,
                                             'Select SQLite file',
                                             self.data_dir)
        file_path = answer[0]
        if not file_path:  # Cancel button clicked
            return
        # Update UI
        self.set_path_to_sqlite_file(file_path)

    def load_url_into_selections(self):
        """Load url attribute into shared widget selections.
        Used when activating the item, and creating a new Spine db."""
        # TODO: Test what happens when Tool item calls this and this item is selected.
        self._properties_ui.comboBox_dialect.setCurrentIndex(-1)
        self._properties_ui.comboBox_dsn.setCurrentIndex(-1)
        self._properties_ui.lineEdit_host.clear()
        self._properties_ui.lineEdit_port.clear()
        self._properties_ui.lineEdit_database.clear()
        self._properties_ui.lineEdit_username.clear()
        self._properties_ui.lineEdit_password.clear()
        if not self._url:
            return
        dialect = self._url["dialect"]
        self.enable_dialect(dialect)
        self._properties_ui.comboBox_dialect.setCurrentText(dialect)
        if self._url["host"]:
            self._properties_ui.lineEdit_host.setText(self._url["host"])
        if self._url["port"]:
            self._properties_ui.lineEdit_port.setText(str(self._url["port"]))
        if self._url["database"]:
            abs_db_path = os.path.abspath(self._url["database"])
            self._properties_ui.lineEdit_database.setText(abs_db_path)
        if self._url["username"]:
            self._properties_ui.lineEdit_username.setText(
                self._url["username"])
        if self._url["password"]:
            self._properties_ui.lineEdit_password.setText(
                self._url["password"])

    def set_url_key(self, key, value):
        """Set url key to value."""
        self._url[key] = value
        self.item_changed.emit()

    @Slot(name="refresh_host")
    def refresh_host(self):
        """Refresh host from selections."""
        host = self._properties_ui.lineEdit_host.text()
        self.set_url_key("host", host)

    @Slot(name="refresh_port")
    def refresh_port(self):
        """Refresh port from selections."""
        port = self._properties_ui.lineEdit_port.text()
        self.set_url_key("port", port)

    @Slot(name="refresh_database")
    def refresh_database(self):
        """Refresh database from selections."""
        database = self._properties_ui.lineEdit_database.text()
        self.set_url_key("database", database)

    @Slot(name="refresh_username")
    def refresh_username(self):
        """Refresh username from selections."""
        username = self._properties_ui.lineEdit_username.text()
        self.set_url_key("username", username)

    @Slot(name="refresh_password")
    def refresh_password(self):
        """Refresh password from selections."""
        password = self._properties_ui.lineEdit_password.text()
        self.set_url_key("password", password)

    @Slot("QString", name="refresh_dialect")
    def refresh_dialect(self, dialect):
        self.set_url_key("dialect", dialect)
        self.enable_dialect(dialect)

    def enable_dialect(self, dialect):
        """Enable the given dialect in the item controls."""
        if dialect == 'sqlite':
            self.enable_sqlite()
        elif dialect == 'mssql':
            import pyodbc  # pylint: disable=import-outside-toplevel

            dsns = pyodbc.dataSources()
            # Collect dsns which use the msodbcsql driver
            mssql_dsns = list()
            for key, value in dsns.items():
                if 'msodbcsql' in value.lower():
                    mssql_dsns.append(key)
            if mssql_dsns:
                self._properties_ui.comboBox_dsn.clear()
                self._properties_ui.comboBox_dsn.addItems(mssql_dsns)
                self._properties_ui.comboBox_dsn.setCurrentIndex(-1)
                self.enable_mssql()
            else:
                msg = "Please create a SQL Server ODBC Data Source first."
                self._logger.msg_warning.emit(msg)
        else:
            self.enable_common()

    def enable_no_dialect(self):
        """Adjust widget enabled status to default when no dialect is selected."""
        self._properties_ui.comboBox_dialect.setEnabled(True)
        self._properties_ui.comboBox_dsn.setEnabled(False)
        self._properties_ui.toolButton_open_sqlite_file.setEnabled(False)
        self._properties_ui.lineEdit_host.setEnabled(False)
        self._properties_ui.lineEdit_port.setEnabled(False)
        self._properties_ui.lineEdit_database.setEnabled(False)
        self._properties_ui.lineEdit_username.setEnabled(False)
        self._properties_ui.lineEdit_password.setEnabled(False)

    def enable_mssql(self):
        """Adjust controls to mssql connection specification."""
        self._properties_ui.comboBox_dsn.setEnabled(True)
        self._properties_ui.toolButton_open_sqlite_file.setEnabled(False)
        self._properties_ui.lineEdit_host.setEnabled(False)
        self._properties_ui.lineEdit_port.setEnabled(False)
        self._properties_ui.lineEdit_database.setEnabled(False)
        self._properties_ui.lineEdit_username.setEnabled(True)
        self._properties_ui.lineEdit_password.setEnabled(True)
        self._properties_ui.lineEdit_host.clear()
        self._properties_ui.lineEdit_port.clear()
        self._properties_ui.lineEdit_database.clear()

    def enable_sqlite(self):
        """Adjust controls to sqlite connection specification."""
        self._properties_ui.comboBox_dsn.setEnabled(False)
        self._properties_ui.comboBox_dsn.setCurrentIndex(-1)
        self._properties_ui.toolButton_open_sqlite_file.setEnabled(True)
        self._properties_ui.lineEdit_host.setEnabled(False)
        self._properties_ui.lineEdit_port.setEnabled(False)
        self._properties_ui.lineEdit_database.setEnabled(True)
        self._properties_ui.lineEdit_username.setEnabled(False)
        self._properties_ui.lineEdit_password.setEnabled(False)
        self._properties_ui.lineEdit_host.clear()
        self._properties_ui.lineEdit_port.clear()
        self._properties_ui.lineEdit_username.clear()
        self._properties_ui.lineEdit_password.clear()

    def enable_common(self):
        """Adjust controls to 'common' connection specification."""
        self._properties_ui.comboBox_dsn.setEnabled(False)
        self._properties_ui.comboBox_dsn.setCurrentIndex(-1)
        self._properties_ui.toolButton_open_sqlite_file.setEnabled(False)
        self._properties_ui.lineEdit_host.setEnabled(True)
        self._properties_ui.lineEdit_port.setEnabled(True)
        self._properties_ui.lineEdit_database.setEnabled(True)
        self._properties_ui.lineEdit_username.setEnabled(True)
        self._properties_ui.lineEdit_password.setEnabled(True)

    @Slot(bool)
    def open_ds_view(self, checked=False):
        """Opens current url in the data store view."""
        self._update_sa_url()
        if not self._sa_url:
            return
        if self.ds_view:
            # If the db_url is the same, just raise the current form
            if self.ds_view.db_url == (self._sa_url, self.name):
                if self.ds_view.windowState() & Qt.WindowMinimized:
                    # Remove minimized status and restore window with the previous state (maximized/normal state)
                    self.ds_view.setWindowState(self.ds_view.windowState()
                                                & ~Qt.WindowMinimized
                                                | Qt.WindowActive)
                    self.ds_view.activateWindow()
                else:
                    self.ds_view.raise_()
                return
            self.ds_view.close()
        self.do_open_ds_view()

    @busy_effect
    def do_open_ds_view(self):
        """Opens current url in the data store view."""
        try:
            self.ds_view = DataStoreForm(self._project.db_mngr,
                                         (self._sa_url, self.name))
        except spinedb_api.SpineDBAPIError as e:
            self._logger.msg_error.emit(e.msg)
            return
        self.ds_view.destroyed.connect(self._handle_ds_view_destroyed)
        self.ds_view.show()

    @Slot()
    def _handle_ds_view_destroyed(self):
        self.ds_view = None

    def data_files(self):
        """Return a list of files that are in this items data directory."""
        if not os.path.isdir(self.data_dir):
            return None
        return os.listdir(self.data_dir)

    @Slot(bool)
    def copy_url(self, checked=False):
        """Copy db url to clipboard."""
        self._update_sa_url()
        if not self._sa_url:
            return
        self._sa_url.password = None
        QApplication.clipboard().setText(str(self._sa_url))
        self._logger.msg.emit(
            f"Database url <b>{self._sa_url}</b> copied to clipboard")

    @Slot(bool, name="create_new_spine_database")
    def create_new_spine_database(self, checked=False):
        """Create new (empty) Spine database."""
        for_spine_model = self._properties_ui.checkBox_for_spine_model.isChecked(
        )
        # Try to make an url from the current status
        self._update_sa_url(log_errors=False)
        if not self._sa_url:
            self._logger.msg_warning.emit(
                f"Unable to generate URL from <b>{self.name}</b> selections. Defaults will be used..."
            )
            dialect = "sqlite"
            database = os.path.abspath(
                os.path.join(self.data_dir, self.name + ".sqlite"))
            self._properties_ui.comboBox_dialect.setCurrentText(dialect)
            self._properties_ui.lineEdit_database.setText(database)
            self._url["dialect"] = dialect
            self._url["database"] = database
            self.item_changed.emit()
        self._project.db_mngr.create_new_spine_database(
            self._sa_url, for_spine_model)

    def update_name_label(self):
        """Update Data Store tab name label. Used only when renaming project items."""
        self._properties_ui.label_ds_name.setText(self.name)

    def _do_handle_dag_changed(self, resources):
        """See base class."""
        self._update_sa_url(log_errors=False)
        if not self._sa_url:
            self.add_notification(
                "The URL for this Data Store is not correctly set. Set it in the Data Store Properties panel."
            )

    def item_dict(self):
        """Returns a dictionary corresponding to this item."""
        d = super().item_dict()
        d["url"] = dict(self.url())
        db = d["url"]["database"]
        # If database key is a file, change the path to relative
        if d["url"]["dialect"] == "sqlite" and db is not None:
            d["url"]["database"] = serialize_path(db,
                                                  self._project.project_dir)
        return d

    @staticmethod
    def upgrade_from_no_version_to_version_1(item_name, old_item_dict,
                                             old_project_dir):
        """See base class."""
        new_data_store = dict(old_item_dict)
        if "reference" in new_data_store:
            url_path = new_data_store["reference"]
            url = {
                "dialect": "sqlite",
                "username": None,
                "host": None,
                "port": None
            }
        else:
            url = new_data_store["url"]
            url_path = url["database"]
        if not url_path:
            url["database"] = None
        else:
            serialized_url_path = serialize_path(url_path, old_project_dir)
            if serialized_url_path["relative"]:
                serialized_url_path["path"] = os.path.join(
                    ".spinetoolbox", "items", serialized_url_path["path"])
            url["database"] = serialized_url_path
        new_data_store["url"] = url
        return new_data_store

    @staticmethod
    def custom_context_menu(parent, pos):
        """Returns the context menu for this item.

        Args:
            parent (QWidget): The widget that is controlling the menu
            pos (QPoint): Position on screen
        """
        return DataStoreContextMenu(parent, pos)

    def apply_context_menu_action(self, parent, action):
        """Applies given action from context menu. Implement in subclasses as needed.

        Args:
            parent (QWidget): The widget that is controlling the menu
            action (str): The selected action
        """
        super().apply_context_menu_action(parent, action)
        if action == "Open view...":
            self.open_ds_view()

    def rename(self, new_name):
        """Rename this item.

        Args:
            new_name (str): New name
        Returns:
            bool: True if renaming succeeded, False otherwise
        """
        old_data_dir = os.path.abspath(
            self.data_dir)  # Old data_dir before rename
        success = super().rename(new_name)
        if not success:
            return False
        # For a Data Store, logs_dir must be updated and the database line edit may need to be updated
        db_dir, db_filename = os.path.split(
            os.path.abspath(
                self._properties_ui.lineEdit_database.text().strip()))
        # If dialect is sqlite and db line edit refers to a file in the old data_dir, db line edit needs updating
        if self._properties_ui.comboBox_dialect.currentText(
        ) == "sqlite" and db_dir == old_data_dir:
            new_db_path = os.path.join(
                self.data_dir,
                db_filename)  # Note. data_dir has been updated at this point
            # Check that the db was moved successfully to the new data_dir
            if os.path.exists(new_db_path):
                self.set_path_to_sqlite_file(new_db_path)
        # Update logs dir
        self.logs_dir = os.path.join(self.data_dir, "logs")
        return True

    def tear_down(self):
        """Tears down this item. Called by toolbox just before closing.
        Closes the DataStoreForm instance opened by this item.
        """
        if self.ds_view:
            self.ds_view.close()

    def notify_destination(self, source_item):
        """See base class."""
        if source_item.item_type() == "Importer":
            self._logger.msg.emit(
                f"Link established. Mappings generated by <b>{source_item.name}</b> will be "
                f"imported in <b>{self.name}</b> when executing.")
        elif source_item.item_type() in ["Data Connection", "Tool"]:
            # Does this type of link do anything?
            self._logger.msg.emit("Link established.")
        else:
            super().notify_destination(source_item)

    @staticmethod
    def default_name_prefix():
        """see base class"""
        return "Data Store"

    def output_resources_backward(self):
        """See base class."""
        return self.output_resources_forward()

    def output_resources_forward(self):
        """See base class."""
        self._update_sa_url(log_errors=False)
        if self._sa_url:
            resource = ProjectItemResource(self,
                                           "database",
                                           url=str(self._sa_url))
            return [resource]
        self.add_notification(
            "The URL for this Data Store is not correctly set. Set it in the Data Store Properties panel."
        )
        return list()
class TestTreeViewForm(
    TestTreeViewFormAddMixin,
    TestTreeViewFormUpdateMixin,
    TestTreeViewFormRemoveMixin,
    TestTreeViewFormFilterMixin,
    unittest.TestCase,
):
    @staticmethod
    def _object_class(*args):
        return dict(zip(["id", "name", "description", "display_order", "display_icon"], args))

    @staticmethod
    def _object(*args):
        return dict(zip(["id", "class_id", "name", "description"], args))

    @staticmethod
    def _relationship_class(*args):
        return dict(zip(["id", "name", "object_class_id_list", "object_class_name_list"], args))

    @staticmethod
    def _relationship(*args):
        return dict(zip(["id", "class_id", "name", "class_name", "object_id_list", "object_name_list"], args))

    @staticmethod
    def _object_parameter_definition(*args):
        d = dict(zip(["id", "object_class_id", "object_class_name", "parameter_name"], args))
        d["name"] = d["parameter_name"]
        return d

    @staticmethod
    def _relationship_parameter_definition(*args):
        d = dict(
            zip(
                [
                    "id",
                    "relationship_class_id",
                    "relationship_class_name",
                    "object_class_id_list",
                    "object_class_name_list",
                    "parameter_name",
                ],
                args,
            )
        )
        d["name"] = d["parameter_name"]
        return d

    @staticmethod
    def _object_parameter_value(*args):
        return dict(
            zip(
                [
                    "id",
                    "object_class_id",
                    "object_class_name",
                    "object_id",
                    "object_name",
                    "parameter_id",
                    "parameter_name",
                    "value",
                ],
                args,
            )
        )

    @staticmethod
    def _relationship_parameter_value(*args):
        return dict(
            zip(
                [
                    "id",
                    "relationship_class_id",
                    "relationship_class_name",
                    "object_class_id_list",
                    "object_class_name_list",
                    "relationship_id",
                    "object_id_list",
                    "object_name_list",
                    "parameter_id",
                    "parameter_name",
                    "value",
                ],
                args,
            )
        )

    @classmethod
    def setUpClass(cls):
        """Overridden method. Runs once before all tests in this class."""
        try:
            cls.app = QApplication().processEvents()
        except RuntimeError:
            pass
        logging.basicConfig(
            stream=sys.stderr,
            level=logging.DEBUG,
            format='%(asctime)s %(levelname)s: %(message)s',
            datefmt='%Y-%m-%d %H:%M:%S',
        )
        cls.create_mock_dataset()

    @classmethod
    def create_mock_dataset(cls):
        cls.fish_class = cls._object_class(1, "fish", "A fish.", 1, None)
        cls.dog_class = cls._object_class(2, "dog", "A dog.", 3, None)
        cls.fish_dog_class = cls._relationship_class(
            3,
            "fish__dog",
            str(cls.fish_class["id"]) + "," + str(cls.dog_class["id"]),
            cls.fish_class["name"] + "," + cls.dog_class["name"],
        )
        cls.dog_fish_class = cls._relationship_class(
            4,
            "dog__fish",
            str(cls.dog_class["id"]) + "," + str(cls.fish_class["id"]),
            cls.dog_class["name"] + "," + cls.fish_class["name"],
        )
        cls.nemo_object = cls._object(1, cls.fish_class["id"], 'nemo', 'The lost one.')
        cls.pluto_object = cls._object(2, cls.dog_class["id"], 'pluto', "Mickey's.")
        cls.scooby_object = cls._object(3, cls.dog_class["id"], 'scooby', 'Scooby-Dooby-Doo.')
        cls.pluto_nemo_rel = cls._relationship(
            4,
            cls.dog_fish_class["id"],
            "dog__fish_pluto__nemo",
            cls.dog_fish_class["name"],
            str(cls.pluto_object["id"]) + "," + str(cls.nemo_object["id"]),
            cls.pluto_object["name"] + "," + cls.nemo_object["name"],
        )
        cls.nemo_pluto_rel = cls._relationship(
            5,
            cls.fish_dog_class["id"],
            "fish__dog_nemo__pluto",
            cls.fish_dog_class["name"],
            str(cls.nemo_object["id"]) + "," + str(cls.pluto_object["id"]),
            cls.nemo_object["name"] + "," + cls.pluto_object["name"],
        )
        cls.nemo_scooby_rel = cls._relationship(
            6,
            cls.fish_dog_class["id"],
            "fish__dog_nemo__scooby",
            cls.fish_dog_class["name"],
            str(cls.nemo_object["id"]) + "," + str(cls.scooby_object["id"]),
            cls.nemo_object["name"] + "," + cls.scooby_object["name"],
        )
        cls.water_parameter = cls._object_parameter_definition(1, cls.fish_class["id"], cls.fish_class["name"], "water")
        cls.breed_parameter = cls._object_parameter_definition(2, cls.dog_class["id"], cls.dog_class["name"], "breed")
        cls.relative_speed_parameter = cls._relationship_parameter_definition(
            3,
            cls.fish_dog_class["id"],
            cls.fish_dog_class["name"],
            cls.fish_dog_class["object_class_id_list"],
            cls.fish_dog_class["object_class_name_list"],
            "relative_speed",
        )
        cls.combined_mojo_parameter = cls._relationship_parameter_definition(
            4,
            cls.dog_fish_class["id"],
            cls.dog_fish_class["name"],
            cls.dog_fish_class["object_class_id_list"],
            cls.dog_fish_class["object_class_name_list"],
            "combined_mojo",
        )
        cls.nemo_water = cls._object_parameter_value(
            1,
            cls.water_parameter["object_class_id"],
            cls.water_parameter["object_class_name"],
            cls.nemo_object["id"],
            cls.nemo_object["name"],
            cls.water_parameter["id"],
            cls.water_parameter["parameter_name"],
            '"salt"',
        )
        cls.pluto_breed = cls._object_parameter_value(
            2,
            cls.breed_parameter["object_class_id"],
            cls.breed_parameter["object_class_name"],
            cls.pluto_object["id"],
            cls.pluto_object["name"],
            cls.breed_parameter["id"],
            cls.breed_parameter["parameter_name"],
            '"bloodhound"',
        )
        cls.scooby_breed = cls._object_parameter_value(
            3,
            cls.breed_parameter["object_class_id"],
            cls.breed_parameter["object_class_name"],
            cls.scooby_object["id"],
            cls.scooby_object["name"],
            cls.breed_parameter["id"],
            cls.breed_parameter["parameter_name"],
            '"great dane"',
        )
        cls.nemo_pluto_relative_speed = cls._relationship_parameter_value(
            4,
            cls.relative_speed_parameter["relationship_class_id"],
            cls.relative_speed_parameter["relationship_class_name"],
            cls.relative_speed_parameter["object_class_id_list"],
            cls.relative_speed_parameter["object_class_name_list"],
            cls.nemo_pluto_rel["id"],
            cls.nemo_pluto_rel["object_id_list"],
            cls.nemo_pluto_rel["object_name_list"],
            cls.relative_speed_parameter["id"],
            cls.relative_speed_parameter["parameter_name"],
            "-1",
        )
        cls.nemo_scooby_relative_speed = cls._relationship_parameter_value(
            5,
            cls.relative_speed_parameter["relationship_class_id"],
            cls.relative_speed_parameter["relationship_class_name"],
            cls.relative_speed_parameter["object_class_id_list"],
            cls.relative_speed_parameter["object_class_name_list"],
            cls.nemo_scooby_rel["id"],
            cls.nemo_scooby_rel["object_id_list"],
            cls.nemo_scooby_rel["object_name_list"],
            cls.relative_speed_parameter["id"],
            cls.relative_speed_parameter["parameter_name"],
            "5",
        )
        cls.pluto_nemo_combined_mojo = cls._relationship_parameter_value(
            6,
            cls.combined_mojo_parameter["relationship_class_id"],
            cls.combined_mojo_parameter["relationship_class_name"],
            cls.combined_mojo_parameter["object_class_id_list"],
            cls.combined_mojo_parameter["object_class_name_list"],
            cls.pluto_nemo_rel["id"],
            cls.pluto_nemo_rel["object_id_list"],
            cls.pluto_nemo_rel["object_name_list"],
            cls.combined_mojo_parameter["id"],
            cls.combined_mojo_parameter["parameter_name"],
            "100",
        )

    def setUp(self):
        """Overridden method. Runs before each test. Makes instances of DataStoreForm classes."""
        with mock.patch("spinetoolbox.spine_db_manager.DiffDatabaseMapping") as mock_DiffDBMapping, mock.patch(
            "spinetoolbox.widgets.data_store_widget.DataStoreForm.restore_ui"
        ):
            self.db_mngr = SpineDBManager(None, None)

            def DiffDBMapping_side_effect(url, upgrade=False, codename=None):
                mock_db_map = mock.MagicMock()
                mock_db_map.codename = codename
                return mock_db_map

            mock_DiffDBMapping.side_effect = DiffDBMapping_side_effect
            self.tree_view_form = DataStoreForm(self.db_mngr, ("mock_url", "mock_db"))
            self.mock_db_map = self.tree_view_form.db_map

    def tearDown(self):
        """Overridden method. Runs after each test.
        Use this to free resources after a test if needed.
        """
        with mock.patch(
            "spinetoolbox.widgets.data_store_widget.DataStoreForm.save_window_state"
        ) as mock_save_w_s, mock.patch("spinetoolbox.spine_db_manager.QMessageBox"):
            self.tree_view_form.close()
            mock_save_w_s.assert_called_once()
        self.tree_view_form.deleteLater()
        self.tree_view_form = None

    def put_mock_object_classes_in_db_mngr(self):
        """Put fish and dog object classes in the db mngr."""

        def _get_object_classes(db_map):
            self.db_mngr.cache_items("object class", {db_map: [self.fish_class, self.dog_class]})
            return [self.fish_class, self.dog_class]

        self.db_mngr.get_object_classes = _get_object_classes

    def put_mock_objects_in_db_mngr(self):
        """Put nemo, pluto and scooby objects in the db mngr."""

        def _get_objects(db_map, class_id=None):
            self.db_mngr.cache_items("object", {db_map: [self.nemo_object, self.pluto_object, self.scooby_object]})
            if class_id == self.fish_class["id"]:
                return [self.nemo_object]
            if class_id == self.dog_class["id"]:
                return [self.pluto_object, self.scooby_object]
            if class_id is None:
                return [self.nemo_object, self.pluto_object, self.scooby_object]
            return []

        self.db_mngr.get_objects = _get_objects

    def put_mock_relationship_classes_in_db_mngr(self):
        """Put dog__fish and fish__dog relationship classes in the db mngr."""

        def _get_relationship_classes(db_map, ids=None, object_class_id=None):
            self.db_mngr.cache_items("relationship class", {db_map: [self.fish_dog_class, self.dog_fish_class]})
            if object_class_id in (self.fish_class["id"], self.dog_class["id"]):
                return [self.fish_dog_class, self.dog_fish_class]
            if object_class_id is None:
                return [self.fish_dog_class, self.dog_fish_class]
            return []

        self.db_mngr.get_relationship_classes = _get_relationship_classes

    def put_mock_relationships_in_db_mngr(self):
        """Put pluto_nemo, nemo_pluto and nemo_scooby relationships in the db mngr."""

        def _get_relationships(db_map, class_id=None, object_id=None):
            self.db_mngr.cache_items(
                "relationship", {db_map: [self.pluto_nemo_rel, self.nemo_pluto_rel, self.nemo_scooby_rel]}
            )
            if class_id == self.dog_fish_class["id"]:
                if object_id in (self.nemo_object["id"], self.pluto_object["id"]):
                    return [self.pluto_nemo_rel]
            if class_id == self.fish_dog_class["id"]:
                if object_id == self.nemo_object["id"]:
                    return [self.nemo_pluto_rel, self.nemo_scooby_rel]
                if object_id == self.pluto_object["id"]:
                    return [self.nemo_pluto_rel]
                if object_id == self.scooby_object["id"]:
                    return [self.nemo_scooby_rel]
            if class_id is None and object_id is None:
                return [self.pluto_nemo_rel, self.nemo_pluto_rel, self.nemo_scooby_rel]
            return []

        self.db_mngr.get_relationships = _get_relationships

    def put_mock_object_parameter_definitions_in_db_mngr(self):
        """Put water and breed object parameter definitions in the db mngr."""

        def _get_object_parameter_definitions(db_map, ids=None, object_class_id=None):
            self.db_mngr.cache_items("parameter definition", {db_map: [self.water_parameter, self.breed_parameter]})
            if object_class_id == self.fish_class["id"]:
                return [self.water_parameter]
            if object_class_id == self.dog_class["id"]:
                return [self.breed_parameter]
            if object_class_id is None:
                return [self.water_parameter, self.breed_parameter]
            return []

        self.db_mngr.get_object_parameter_definitions = _get_object_parameter_definitions

    def put_mock_relationship_parameter_definitions_in_db_mngr(self):
        """Put relative speed and combined mojo relationship parameter definitions in the db mngr."""

        def _get_relationship_parameter_definitions(db_map, ids=None, relationship_class_id=None):
            self.db_mngr.cache_items(
                "parameter definition", {db_map: [self.relative_speed_parameter, self.combined_mojo_parameter]}
            )
            if relationship_class_id == self.fish_dog_class["id"]:
                return [self.relative_speed_parameter]
            if relationship_class_id == self.dog_fish_class["id"]:
                return [self.combined_mojo_parameter]
            if relationship_class_id is None:
                return [self.relative_speed_parameter, self.combined_mojo_parameter]
            return []

        self.db_mngr.get_relationship_parameter_definitions = _get_relationship_parameter_definitions

    def put_mock_object_parameter_values_in_db_mngr(self):
        """Put some object parameter values in the db mngr."""

        def _get_object_parameter_values(db_map, ids=None, object_class_id=None):
            self.db_mngr.cache_items(
                "parameter value", {db_map: [self.nemo_water, self.pluto_breed, self.scooby_breed]}
            )
            if object_class_id == self.fish_class["id"]:
                return [self.nemo_water]
            if object_class_id == self.dog_class["id"]:
                return [self.pluto_breed, self.scooby_breed]
            if object_class_id is None:
                return [self.nemo_water, self.pluto_breed, self.scooby_breed]
            return []

        self.db_mngr.get_object_parameter_values = _get_object_parameter_values

    def put_mock_relationship_parameter_values_in_db_mngr(self):
        """Put some relationship parameter values in the db mngr."""

        def _get_relationship_parameter_values(db_map, ids=None, relationship_class_id=None):
            self.db_mngr.cache_items(
                "parameter value",
                {
                    db_map: [
                        self.nemo_pluto_relative_speed,
                        self.nemo_scooby_relative_speed,
                        self.pluto_nemo_combined_mojo,
                    ]
                },
            )
            if relationship_class_id == self.fish_dog_class["id"]:
                return [self.nemo_pluto_relative_speed, self.nemo_scooby_relative_speed]
            if relationship_class_id == self.dog_fish_class["id"]:
                return [self.pluto_nemo_combined_mojo]
            if relationship_class_id is None:
                return [self.nemo_pluto_relative_speed, self.nemo_scooby_relative_speed, self.pluto_nemo_combined_mojo]
            return []

        self.db_mngr.get_relationship_parameter_values = _get_relationship_parameter_values

    def put_mock_dataset_in_db_mngr(self):
        """Put mock dataset in the db mngr."""
        self.put_mock_object_classes_in_db_mngr()
        self.put_mock_objects_in_db_mngr()
        self.put_mock_relationship_classes_in_db_mngr()
        self.put_mock_relationships_in_db_mngr()
        self.put_mock_object_parameter_definitions_in_db_mngr()
        self.put_mock_relationship_parameter_definitions_in_db_mngr()
        self.put_mock_object_parameter_values_in_db_mngr()
        self.put_mock_relationship_parameter_values_in_db_mngr()

    def test_set_object_parameter_definition_defaults(self):
        """Test that defaults are set in object parameter definition models according the object tree selection."""
        self.put_mock_object_classes_in_db_mngr()
        self.tree_view_form.init_models()
        for item in self.tree_view_form.object_tree_model.visit_all():
            item.fetch_more()
        # Select fish item in object tree
        root_item = self.tree_view_form.object_tree_model.root_item
        fish_item = root_item.child(0)
        fish_index = self.tree_view_form.object_tree_model.index_from_item(fish_item)
        self.tree_view_form.ui.treeView_object.setCurrentIndex(fish_index)
        self.tree_view_form.ui.treeView_object.selectionModel().select(fish_index, QItemSelectionModel.Select)
        # Check default in object parameter definition
        model = self.tree_view_form.object_parameter_definition_model
        model.empty_model.fetchMore()
        h = model.header.index
        row_data = []
        for row in range(model.rowCount()):
            row_data.append(tuple(model.index(row, h(field)).data() for field in ("object_class_name", "database")))
        self.assertTrue(("fish", "mock_db") in row_data)

    @unittest.skip("TODO")
    def test_set_object_parameter_value_defaults(self):
        """Test that defaults are set in relationship parameter definition
        models according the object tree selection.
        """
        self.fail()

    @unittest.skip("TODO")
    def test_set_relationship_parameter_definition_defaults(self):
        """Test that defaults are set in relationship parameter definition
        models according the object tree selection.
        """
        self.fail()

    @unittest.skip("TODO")
    def test_set_relationship_parameter_value_defaults(self):
        """Test that defaults are set in relationship parameter definition
        models according the object tree selection.
        """
        self.fail()