diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6e7458f4b..5072ec659 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -17,6 +17,12 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.1.0/)
### Security
+## [0.9.1]
+
+### Removed
+
+- Removed support for MSSQL dialect. It did not work anyway.
+
## [0.9.0]
Dropped support for Python 3.8.
diff --git a/pyproject.toml b/pyproject.toml
index 4a5232eae..f194bb2b7 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -16,7 +16,7 @@ dependencies = [
"PySide6 >= 6.5.0, != 6.5.3, != 6.6.3, != 6.7.0, < 6.8",
"jupyter_client >=6.0",
"qtconsole >=5.1",
- "spinedb_api>=0.32.0",
+ "spinedb_api>=0.32.1",
"spine_engine>=0.25.0",
"numpy >=1.20.2",
"matplotlib >= 3.5",
@@ -26,7 +26,7 @@ dependencies = [
"Pygments >=2.8",
"jill >=0.9.2",
"pyzmq >=21.0",
- "spine_items>=0.23.0",
+ "spine_items>=0.23.1",
]
[project.urls]
diff --git a/spinetoolbox/database_display_names.py b/spinetoolbox/database_display_names.py
new file mode 100644
index 000000000..4b6ed677d
--- /dev/null
+++ b/spinetoolbox/database_display_names.py
@@ -0,0 +1,120 @@
+######################################################################################################################
+# Copyright (C) 2017-2022 Spine project consortium
+# Copyright Spine Toolbox contributors
+# This file is part of Spine Toolbox.
+# Spine Toolbox is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General
+# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option)
+# any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General
+# Public License for more details. You should have received a copy of the GNU Lesser General Public License along with
+# this program. If not, see .
+######################################################################################################################
+
+"""This module contains functionality to manage database display names."""
+import hashlib
+import pathlib
+from PySide6.QtCore import QObject, Signal, Slot
+from sqlalchemy.engine.url import URL, make_url
+
+
+class NameRegistry(QObject):
+ display_name_changed = Signal(str, str)
+ """Emitted when the display name of a database changes."""
+
+ def __init__(self, parent=None):
+ """
+ Args:
+ parent (QObject, optional): parent object
+ """
+ super().__init__(parent)
+ self._names_by_url: dict[str, set[str]] = {}
+
+ @Slot(str, str)
+ def register(self, db_url, name):
+ """Registers a new name for given database URL.
+
+ Args:
+ db_url (URL or str): database URL
+ name (str): name to register
+ """
+ url = str(db_url)
+ if url in self._names_by_url and name in self._names_by_url[url]:
+ return
+ self._names_by_url.setdefault(url, set()).add(name)
+ self.display_name_changed.emit(url, self.display_name(db_url))
+
+ @Slot(str, str)
+ def unregister(self, db_url, name):
+ """Removes a name from the registry.
+
+ Args:
+ db_url (URL or str): database URL
+ name (str): name to remove
+ """
+ url = str(db_url)
+ names = self._names_by_url[url]
+ old_name = self.display_name(url) if len(names) in (1, 2) else None
+ names.remove(name)
+ if old_name is not None:
+ new_name = self.display_name(url)
+ self.display_name_changed.emit(url, new_name)
+
+ def display_name(self, db_url):
+ """Makes display name for a database.
+
+ Args:
+ db_url (URL or str): database URL
+
+ Returns:
+ str: display name
+ """
+ try:
+ registered_names = self._names_by_url[str(db_url)]
+ except KeyError:
+ return suggest_display_name(db_url)
+ else:
+ if len(registered_names) == 1:
+ return next(iter(registered_names))
+ return suggest_display_name(db_url)
+
+ def display_name_iter(self, db_maps):
+ """Yields database mapping display names.
+
+ Args:
+ db_maps (Iterable of DatabaseMapping): database mappings
+
+ Yields:
+ str: display name
+ """
+ yield from (self.display_name(db_map.sa_url) for db_map in db_maps)
+
+ def map_display_names_to_db_maps(self, db_maps):
+ """Returns a dictionary that maps display names to database mappings.
+
+ Args:
+ db_maps (Iterable of DatabaseMapping): database mappings
+
+ Returns:
+ dict: database mappings keyed by display names
+ """
+ return {self.display_name(db_map.sa_url): db_map for db_map in db_maps}
+
+
+def suggest_display_name(db_url):
+ """Returns a short name for the database mapping.
+
+ Args:
+ db_url (URL or str): database URL
+
+ Returns:
+ str: suggested name for the database for display purposes.
+ """
+ if not isinstance(db_url, URL):
+ db_url = make_url(db_url)
+ if not db_url.drivername.startswith("sqlite"):
+ return db_url.database
+ if db_url.database is not None:
+ return pathlib.Path(db_url.database).stem
+ hashing = hashlib.sha1()
+ hashing.update(bytes(str(id(db_url)), "utf-8"))
+ return hashing.hexdigest()
diff --git a/spinetoolbox/multi_tab_windows.py b/spinetoolbox/multi_tab_windows.py
new file mode 100644
index 000000000..cd5fc6716
--- /dev/null
+++ b/spinetoolbox/multi_tab_windows.py
@@ -0,0 +1,71 @@
+######################################################################################################################
+# Copyright (C) 2017-2022 Spine project consortium
+# Copyright Spine Toolbox contributors
+# This file is part of Spine Toolbox.
+# Spine Toolbox is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General
+# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option)
+# any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General
+# Public License for more details. You should have received a copy of the GNU Lesser General Public License along with
+# this program. If not, see .
+######################################################################################################################
+
+"""Contains functionality to keep track on open MultiTabWindow instances."""
+from spinetoolbox.widgets.multi_tab_window import MultiTabWindow
+
+
+class MultiTabWindowRegistry:
+ """Registry that holds multi tab windows."""
+
+ def __init__(self):
+ self._multi_tab_windows: list[MultiTabWindow] = []
+
+ def has_windows(self):
+ """Tests if there are any windows registered.
+
+ Returns:
+ bool: True if editor windows exist, False otherwise
+ """
+ return bool(self._multi_tab_windows)
+
+ def windows(self):
+ """Returns a list of multi tab windows.
+
+ Returns:
+ list of MultiTabWindow: windows
+ """
+ return list(self._multi_tab_windows)
+
+ def tabs(self):
+ """Returns a list of tabs across all windows.
+
+ Returns:
+ list of QWidget: tab widgets
+ """
+ return [
+ window.tab_widget.widget(k) for window in self._multi_tab_windows for k in range(window.tab_widget.count())
+ ]
+
+ def register_window(self, window):
+ """Registers a new multi tab window.
+
+ Args:
+ window (MultiTabWindow): window to register
+ """
+ self._multi_tab_windows.append(window)
+
+ def unregister_window(self, window):
+ """Removes multi tab window from the registry.
+
+ Args:
+ window (MultiTabWindow): window to unregister
+ """
+ self._multi_tab_windows.remove(window)
+
+ def get_some_window(self):
+ """Returns a random multi tab window or None if none is available.
+
+ Returns:
+ MultiTabWindow: editor window
+ """
+ return self._multi_tab_windows[0] if self._multi_tab_windows else None
diff --git a/spinetoolbox/plotting.py b/spinetoolbox/plotting.py
index 1c5f3cd90..26af616ef 100644
--- a/spinetoolbox/plotting.py
+++ b/spinetoolbox/plotting.py
@@ -692,12 +692,13 @@ def plot_pivot_table_selection(model, model_indexes, plot_widget=None):
return plot_data(data_list, plot_widget)
-def plot_db_mngr_items(items, db_maps, plot_widget=None):
+def plot_db_mngr_items(items, db_maps, db_name_registry, plot_widget=None):
"""Returns a plot widget with plots of database manager parameter value items.
Args:
items (list of dict): parameter value items
db_maps (list of DatabaseMapping): database mappings corresponding to items
+ db_name_registry (NameRegistry): database display name registry
plot_widget (PlotWidget, optional): widget to add plots to
"""
if not items:
@@ -707,13 +708,13 @@ def plot_db_mngr_items(items, db_maps, plot_widget=None):
root_node = TreeNode("database")
for item, db_map in zip(items, db_maps):
value = from_database(item["value"], item["type"])
+ db_name = db_name_registry.display_name(db_map.sa_url)
if value is None:
continue
try:
leaf_content = _convert_to_leaf(value)
except PlottingError as error:
- raise PlottingError(f"Failed to plot value in {db_map.codename}: {error}") from error
- db_name = db_map.codename
+ raise PlottingError(f"Failed to plot value in {db_name}: {error}") from error
parameter_name = item["parameter_definition_name"]
entity_byname = item["entity_byname"]
if not isinstance(entity_byname, tuple):
diff --git a/spinetoolbox/spine_db_commands.py b/spinetoolbox/spine_db_commands.py
index a7467f851..2495e1444 100644
--- a/spinetoolbox/spine_db_commands.py
+++ b/spinetoolbox/spine_db_commands.py
@@ -107,7 +107,7 @@ def __init__(self, db_mngr, db_map, item_type, data, check=True, **kwargs):
self.redo_data = data
self.undo_ids = None
self._check = check
- self.setText(f"add {item_type} items to {db_map.codename}")
+ self.setText(f"add {item_type} items to {db_mngr.name_registry.display_name(db_map.sa_url)}")
def redo(self):
super().redo()
@@ -143,7 +143,7 @@ def __init__(self, db_mngr, db_map, item_type, data, check=True, **kwargs):
if self.redo_data == self.undo_data:
self.setObsolete(True)
self._check = check
- self.setText(f"update {item_type} items in {db_map.codename}")
+ self.setText(f"update {item_type} items in {self.db_mngr.name_registry.display_name(db_map.sa_url)}")
def redo(self):
super().redo()
@@ -183,7 +183,7 @@ def __init__(self, db_mngr, db_map, item_type, data, check=True, **kwargs):
self.redo_update_data = None
self.undo_remove_ids = None
self.undo_update_data = None
- self.setText(f"update {item_type} items in {db_map.codename}")
+ self.setText(f"update {item_type} items in {self.db_mngr.name_registry.display_name(db_map.sa_url)}")
def redo(self):
super().redo()
@@ -225,7 +225,7 @@ def __init__(self, db_mngr, db_map, item_type, ids, check=True, **kwargs):
self.item_type = item_type
self.ids = ids
self._check = check
- self.setText(f"remove {item_type} items from {db_map.codename}")
+ self.setText(f"remove {item_type} items from {self.db_mngr.name_registry.display_name(db_map.sa_url)}")
def redo(self):
super().redo()
diff --git a/spinetoolbox/spine_db_editor/editors.py b/spinetoolbox/spine_db_editor/editors.py
new file mode 100644
index 000000000..d3b2f7d09
--- /dev/null
+++ b/spinetoolbox/spine_db_editor/editors.py
@@ -0,0 +1,16 @@
+######################################################################################################################
+# Copyright (C) 2017-2022 Spine project consortium
+# Copyright Spine Toolbox contributors
+# This file is part of Spine Toolbox.
+# Spine Toolbox is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General
+# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option)
+# any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General
+# Public License for more details. You should have received a copy of the GNU Lesser General Public License along with
+# this program. If not, see .
+######################################################################################################################
+
+"""Contains Spine Database editor's window registry."""
+from spinetoolbox.multi_tab_windows import MultiTabWindowRegistry
+
+db_editor_registry = MultiTabWindowRegistry()
diff --git a/spinetoolbox/spine_db_editor/graphics_items.py b/spinetoolbox/spine_db_editor/graphics_items.py
index a37da029f..ffc036858 100644
--- a/spinetoolbox/spine_db_editor/graphics_items.py
+++ b/spinetoolbox/spine_db_editor/graphics_items.py
@@ -157,7 +157,7 @@ def display_data(self):
@property
def display_database(self):
- return ",".join([db_map.codename for db_map in self.db_maps])
+ return ", ".join(self.db_mngr.name_registry.display_name_iter(self.db_maps))
@property
def db_maps(self):
@@ -370,7 +370,7 @@ def default_parameter_data(self):
return {
"entity_class_name": self.entity_class_name,
"entity_byname": DB_ITEM_SEPARATOR.join(self.byname),
- "database": self.first_db_map.codename,
+ "database": self.db_mngr.name_registry.display_name(self.first_db_map.sa_url),
}
def shape(self):
@@ -667,7 +667,7 @@ def _populate_connect_entities_menu(self, menu):
for name, db_map_ent_clss in self._db_map_entity_class_lists.items():
for db_map, ent_cls in db_map_ent_clss:
icon = self.db_mngr.entity_class_icon(db_map, ent_cls["id"])
- action_name = name + "@" + db_map.codename
+ action_name = name + "@" + self.db_mngr.name_registry.display_name(db_map.sa_url)
enabled = set(ent_cls["dimension_id_list"]) <= entity_class_ids_in_graph.get(db_map, set())
action_name_icon_enabled.append((action_name, icon, enabled))
for action_name, icon, enabled in sorted(action_name_icon_enabled):
@@ -702,7 +702,11 @@ def _start_connecting_entities(self, action):
class_name, db_name = action.text().split("@")
db_map_ent_cls_lst = self._db_map_entity_class_lists[class_name]
db_map, ent_cls = next(
- iter((db_map, ent_cls) for db_map, ent_cls in db_map_ent_cls_lst if db_map.codename == db_name)
+ iter(
+ (db_map, ent_cls)
+ for db_map, ent_cls in db_map_ent_cls_lst
+ if self.db_mngr.name_registry.display_name(db_map.sa_url) == db_name
+ )
)
self._spine_db_editor.start_connecting_entities(db_map, ent_cls, self)
diff --git a/spinetoolbox/spine_db_editor/main.py b/spinetoolbox/spine_db_editor/main.py
index 11e8819d7..7fc105f1f 100644
--- a/spinetoolbox/spine_db_editor/main.py
+++ b/spinetoolbox/spine_db_editor/main.py
@@ -29,9 +29,9 @@ def main():
editor = MultiSpineDBEditor(db_mngr)
if args.separate_tabs:
for url in args.url:
- editor.add_new_tab({url: None})
+ editor.add_new_tab([url])
else:
- editor.add_new_tab({url: None for url in args.url})
+ editor.add_new_tab(args.url)
editor.show()
return_code = app.exec()
return return_code
diff --git a/spinetoolbox/spine_db_editor/mvcmodels/alternative_item.py b/spinetoolbox/spine_db_editor/mvcmodels/alternative_item.py
index c1f11c2df..c4c7535be 100644
--- a/spinetoolbox/spine_db_editor/mvcmodels/alternative_item.py
+++ b/spinetoolbox/spine_db_editor/mvcmodels/alternative_item.py
@@ -20,10 +20,6 @@
class DBItem(EmptyChildMixin, FetchMoreMixin, StandardDBItem):
"""A root item representing a db."""
- @property
- def item_type(self):
- return "db"
-
@property
def fetch_item_type(self):
return "alternative"
@@ -38,13 +34,8 @@ def _make_child(self, id_):
class AlternativeItem(GrayIfLastMixin, EditableMixin, LeafItem):
"""An alternative leaf item."""
- @property
- def item_type(self):
- return "alternative"
-
- @property
- def icon_code(self):
- return _ALTERNATIVE_ICON
+ item_type = "alternative"
+ icon_code = _ALTERNATIVE_ICON
def tool_tip(self, column):
if column == 0 and self.id:
diff --git a/spinetoolbox/spine_db_editor/mvcmodels/alternative_model.py b/spinetoolbox/spine_db_editor/mvcmodels/alternative_model.py
index 845be17d5..e797bcc84 100644
--- a/spinetoolbox/spine_db_editor/mvcmodels/alternative_model.py
+++ b/spinetoolbox/spine_db_editor/mvcmodels/alternative_model.py
@@ -24,7 +24,7 @@ class AlternativeModel(TreeModelBase):
"""A model to display alternatives in a tree view."""
def _make_db_item(self, db_map):
- return DBItem(self, db_map)
+ return DBItem(self, db_map, self.db_mngr.name_registry)
def mimeData(self, indexes):
"""Stores selected indexes into MIME data.
diff --git a/spinetoolbox/spine_db_editor/mvcmodels/empty_models.py b/spinetoolbox/spine_db_editor/mvcmodels/empty_models.py
index e4663f4fd..c3830d73f 100644
--- a/spinetoolbox/spine_db_editor/mvcmodels/empty_models.py
+++ b/spinetoolbox/spine_db_editor/mvcmodels/empty_models.py
@@ -11,6 +11,7 @@
######################################################################################################################
"""Empty models for dialogs as well as parameter definitions and values."""
+from typing import ClassVar
from PySide6.QtCore import Qt
from ...helpers import DB_ITEM_SEPARATOR, rows_to_row_count_tuples
from ...mvcmodels.empty_row_model import EmptyRowModel
@@ -22,6 +23,9 @@
class EmptyModelBase(EmptyRowModel):
"""Base class for all empty models that go in a CompoundModelBase subclass."""
+ item_type: ClassVar[str] = None
+ can_be_filtered = False
+
def __init__(self, parent):
"""
Args:
@@ -33,10 +37,6 @@ def __init__(self, parent):
self.entity_class_id = None
self._db_map_entities_to_add = {}
- @property
- def item_type(self):
- raise NotImplementedError()
-
@property
def field_map(self):
return self._parent.field_map
@@ -68,7 +68,7 @@ def add_items_to_db(self, db_map_data):
def _notify_about_added_entities(self):
editor = self.parent().parent()
- popup = AddedEntitiesPopup(editor, self._db_map_entities_to_add)
+ popup = AddedEntitiesPopup(editor, self.db_mngr.name_registry, self._db_map_entities_to_add)
popup.show()
def _clean_to_be_added_entities(self, db_map_items):
@@ -91,10 +91,6 @@ def _make_unique_id(self, item):
which rows have been added and thus need to be removed."""
raise NotImplementedError()
- @property
- def can_be_filtered(self):
- return False
-
def accepted_rows(self):
return range(self.rowCount())
@@ -109,8 +105,8 @@ def handle_items_added(self, db_map_data):
Finds and removes model items that were successfully added to the db."""
added_ids = set()
for db_map, items in db_map_data.items():
+ database = self.db_mngr.name_registry.display_name(db_map.sa_url)
for item in items:
- database = db_map.codename
unique_id = (database, *self._make_unique_id(item))
added_ids.add(unique_id)
removed_rows = []
@@ -167,8 +163,13 @@ def _make_db_map_data(self, rows):
db_map_data = {}
for item in items:
database = item.pop("database")
- db_map = next(iter(x for x in self.db_mngr.db_maps if x.codename == database), None)
- if not db_map:
+ try:
+ db_map = next(
+ iter(
+ x for x in self.db_mngr.db_maps if self.db_mngr.name_registry.display_name(x.sa_url) == database
+ )
+ )
+ except StopIteration:
continue
item = {k: v for k, v in item.items() if v is not None}
db_map_data.setdefault(db_map, []).append(item)
@@ -177,7 +178,10 @@ def _make_db_map_data(self, rows):
def data(self, index, role=Qt.ItemDataRole.DisplayRole):
if role == DB_MAP_ROLE:
database = self.data(index, Qt.ItemDataRole.DisplayRole)
- return next(iter(x for x in self.db_mngr.db_maps if x.codename == database), None)
+ return next(
+ iter(x for x in self.db_mngr.db_maps if self.db_mngr.name_registry.display_name(x.sa_url) == database),
+ None,
+ )
return super().data(index, role)
@@ -244,9 +248,7 @@ def _entity_class_name_candidates_by_entity(db_map, item):
class EmptyParameterDefinitionModel(SplitValueAndTypeMixin, ParameterMixin, EmptyModelBase):
"""An empty parameter_definition model."""
- @property
- def item_type(self):
- return "parameter_definition"
+ item_type = "parameter_definition"
def _make_unique_id(self, item):
return tuple(item.get(x) for x in ("entity_class_name", "name"))
@@ -268,9 +270,7 @@ class EmptyParameterValueModel(
):
"""An empty parameter_value model."""
- @property
- def item_type(self):
- return "parameter_value"
+ item_type = "parameter_value"
@staticmethod
def _check_item(item):
@@ -309,9 +309,7 @@ def _entity_class_name_candidates(self, db_map, item):
class EmptyEntityAlternativeModel(MakeEntityOnTheFlyMixin, EntityMixin, EmptyModelBase):
- @property
- def item_type(self):
- return "entity_alternative"
+ item_type = "entity_alternative"
@staticmethod
def _check_item(item):
diff --git a/spinetoolbox/spine_db_editor/mvcmodels/entity_tree_item.py b/spinetoolbox/spine_db_editor/mvcmodels/entity_tree_item.py
index 9886a48cf..6abbd9310 100644
--- a/spinetoolbox/spine_db_editor/mvcmodels/entity_tree_item.py
+++ b/spinetoolbox/spine_db_editor/mvcmodels/entity_tree_item.py
@@ -116,7 +116,10 @@ def _children_sort_key(self):
def default_parameter_data(self):
"""Return data to put as default in a parameter table when this item is selected."""
- return {"entity_class_name": self.name, "database": self.first_db_map.codename}
+ return {
+ "entity_class_name": self.name,
+ "database": self.db_mngr.name_registry.display_name(self.first_db_map.sa_url),
+ }
@property
def display_data(self):
@@ -260,12 +263,13 @@ def set_data(self, column, value, role):
def default_parameter_data(self):
"""Return data to put as default in a parameter table when this item is selected."""
item = self.db_map_data(self.first_db_map)
+ db_name = self.db_mngr.name_registry.display_name(self.first_db_map.sa_url)
if not item:
- return {"database": self.first_db_map.codename}
+ return {"database": db_name}
return {
"entity_class_name": item["entity_class_name"],
"entity_byname": DB_ITEM_SEPARATOR.join(item["entity_byname"]),
- "database": self.first_db_map.codename,
+ "database": db_name,
}
def is_valid(self):
diff --git a/spinetoolbox/spine_db_editor/mvcmodels/frozen_table_model.py b/spinetoolbox/spine_db_editor/mvcmodels/frozen_table_model.py
index f439ae0e5..1329e956c 100644
--- a/spinetoolbox/spine_db_editor/mvcmodels/frozen_table_model.py
+++ b/spinetoolbox/spine_db_editor/mvcmodels/frozen_table_model.py
@@ -336,7 +336,7 @@ def _tooltip_from_data(self, row, column):
elif header == "index":
tool_tip = str(value[1])
elif header == "database":
- tool_tip = value.codename
+ tool_tip = self.db_mngr.name_registry.display_name(value.sa_url)
elif header == "entity":
db_map, id_ = value
tool_tip = self.db_mngr.get_item(db_map, "entity", id_).get("description")
@@ -365,7 +365,7 @@ def _name_from_data(self, value, header):
if header == "index":
return str(value[1])
if header == "database":
- return value.codename
+ return self.db_mngr.name_registry.display_name(value.sa_url)
db_map, id_ = value
item = self.db_mngr.get_item(db_map, "entity", id_)
return item.get("name")
diff --git a/spinetoolbox/spine_db_editor/mvcmodels/metadata_table_model_base.py b/spinetoolbox/spine_db_editor/mvcmodels/metadata_table_model_base.py
index 241d3179c..c84e9d22f 100644
--- a/spinetoolbox/spine_db_editor/mvcmodels/metadata_table_model_base.py
+++ b/spinetoolbox/spine_db_editor/mvcmodels/metadata_table_model_base.py
@@ -127,7 +127,7 @@ def data(self, index, role=Qt.ItemDataRole.DisplayRole):
if role == Qt.ItemDataRole.DisplayRole:
if column == Column.DB_MAP:
db_map = self._data[row][column] if row < len(self._data) else self._adder_row[column]
- return db_map.codename if db_map is not None else ""
+ return self._db_mngr.name_registry.display_name(db_map.sa_url) if db_map is not None else ""
return self._data[row][column] if row < len(self._data) else self._adder_row[column]
if (
role == Qt.ItemDataRole.BackgroundRole
@@ -208,7 +208,7 @@ def batch_set_data(self, indexes, values):
columns = []
previous_values = []
data_length = len(self._data)
- available_codenames = {db_map.codename: db_map for db_map in self._db_maps}
+ available_codenames = self._db_mngr.name_registry.map_display_names_to_db_maps(self._db_maps)
reserved = self._reserved_metadata()
for index, value in zip(indexes, values):
if not self.flags(index) & Qt.ItemIsEditable:
@@ -440,35 +440,33 @@ def _remove_data(self, db_map_data, id_column):
self._data = self._data[:row] + self._data[row + count :]
self.endRemoveRows()
- def sort(self, column, order=Qt.AscendingOrder):
+ def sort(self, column, order=Qt.SortOrder.AscendingOrder):
if not self._data or column < 0:
return
def db_map_sort_key(row):
db_map = row[Column.DB_MAP]
- return db_map.codename if db_map is not None else ""
+ return self._db_mngr.name_registry.display_name(db_map.sa_url) if db_map is not None else ""
sort_key = itemgetter(column) if column != Column.DB_MAP else db_map_sort_key
- self._data.sort(key=sort_key, reverse=order == Qt.DescendingOrder)
+ self._data.sort(key=sort_key, reverse=order == Qt.SortOrder.DescendingOrder)
top_left = self.index(0, 0)
bottom_right = self.index(len(self._data) - 1, Column.DB_MAP)
self.dataChanged.emit(top_left, bottom_right, [Qt.ItemDataRole.DisplayRole, Qt.ItemDataRole.BackgroundRole])
- def _find_db_map(self, codename):
- """Finds database mapping with given codename.
+ def _find_db_map(self, name):
+ """Finds database mapping with given name.
Args:
- codename (str): database mapping's code name
+ name (str): database mapping's name
Returns:
- DiffDatabaseMapping: database mapping or None if not found
+ DatabaseMapping: database mapping or None if not found
"""
- match = None
- for db_map in self._db_maps:
- if codename == db_map.codename:
- match = db_map
- break
- return match
+ return next(
+ iter(db_map for db_map in self._db_maps if name == self._db_mngr.name_registry.display_name(db_map.sa_url)),
+ None,
+ )
def _reserved_metadata(self):
"""Collects metadata names and values that are already in database.
diff --git a/spinetoolbox/spine_db_editor/mvcmodels/multi_db_tree_item.py b/spinetoolbox/spine_db_editor/mvcmodels/multi_db_tree_item.py
index 5c8a329be..e265e7017 100644
--- a/spinetoolbox/spine_db_editor/mvcmodels/multi_db_tree_item.py
+++ b/spinetoolbox/spine_db_editor/mvcmodels/multi_db_tree_item.py
@@ -117,7 +117,7 @@ def display_data(self):
@property
def display_database(self):
"""Returns the database for display."""
- return ",".join([db_map.codename for db_map in self.db_maps])
+ return ", ".join(self.model.db_mngr.name_registry.display_name_iter(self._db_map_ids))
@property
def display_icon(self):
@@ -489,7 +489,7 @@ def data(self, column, role=Qt.ItemDataRole.DisplayRole):
def default_parameter_data(self):
"""Returns data to set as default in a parameter table when this item is selected."""
- return {"database": self.first_db_map.codename}
+ return {"database": self.db_mngr.name_registry.display_name(self.first_db_map.sa_url)}
def tear_down(self):
super().tear_down()
diff --git a/spinetoolbox/spine_db_editor/mvcmodels/parameter_value_list_item.py b/spinetoolbox/spine_db_editor/mvcmodels/parameter_value_list_item.py
index 2e7af66b5..77bfa7f2b 100644
--- a/spinetoolbox/spine_db_editor/mvcmodels/parameter_value_list_item.py
+++ b/spinetoolbox/spine_db_editor/mvcmodels/parameter_value_list_item.py
@@ -30,10 +30,6 @@
class DBItem(EmptyChildMixin, FetchMoreMixin, StandardDBItem):
"""An item representing a db."""
- @property
- def item_type(self):
- return "db"
-
@property
def fetch_item_type(self):
return "parameter_value_list"
@@ -50,9 +46,7 @@ class ListItem(
):
"""A list item."""
- @property
- def item_type(self):
- return "parameter_value_list"
+ item_type = "parameter_value_list"
@property
def fetch_item_type(self):
@@ -95,9 +89,7 @@ def update_item_in_db(self, db_item):
class ValueItem(GrayIfLastMixin, EditableMixin, LeafItem):
- @property
- def item_type(self):
- return "list_value"
+ item_type = "list_value"
def data(self, column, role=Qt.ItemDataRole.DisplayRole):
if role == Qt.ItemDataRole.DisplayRole and not self.id:
diff --git a/spinetoolbox/spine_db_editor/mvcmodels/parameter_value_list_model.py b/spinetoolbox/spine_db_editor/mvcmodels/parameter_value_list_model.py
index b2d704dcf..f922fa353 100644
--- a/spinetoolbox/spine_db_editor/mvcmodels/parameter_value_list_model.py
+++ b/spinetoolbox/spine_db_editor/mvcmodels/parameter_value_list_model.py
@@ -20,7 +20,7 @@ class ParameterValueListModel(TreeModelBase):
"""A model to display parameter_value_list data in a tree view."""
def _make_db_item(self, db_map):
- return DBItem(self, db_map)
+ return DBItem(self, db_map, self.db_mngr.name_registry)
def columnCount(self, parent=QModelIndex()):
"""Returns the number of columns under the given parent. Always 1."""
diff --git a/spinetoolbox/spine_db_editor/mvcmodels/pivot_table_models.py b/spinetoolbox/spine_db_editor/mvcmodels/pivot_table_models.py
index 5802ceb67..554cd0c1d 100644
--- a/spinetoolbox/spine_db_editor/mvcmodels/pivot_table_models.py
+++ b/spinetoolbox/spine_db_editor/mvcmodels/pivot_table_models.py
@@ -278,7 +278,7 @@ class TopLeftDatabaseHeaderItem(TopLeftHeaderItem):
def __init__(self, model):
super().__init__(model)
- self._suggested_codename = None
+ self._suggested_db_name = None
@property
def header_type(self):
@@ -290,7 +290,7 @@ def name(self):
def header_data(self, header_id, role=Qt.ItemDataRole.DisplayRole):
"""See base class."""
- return header_id.codename
+ return self._model.db_mngr.name_registry.display_name(header_id.sa_url)
def update_data(self, db_map_data):
"""See base class."""
@@ -300,17 +300,17 @@ def add_data(self, names, db_map):
"""See base class."""
return False
- def set_data(self, codename):
- """Sets database mapping's codename.
+ def set_data(self, name):
+ """Sets database mapping's name.
Args:
- codename (str): database codename
+ name (str): database name
Returns:
- bool: True if codename was acceptable, False otherwise
+ bool: True if name was acceptable, False otherwise
"""
- if any(db_map.codename == codename for db_map in self.model.db_maps):
- self._suggested_codename = codename
+ if any(self._model.db_mngr.name_registry.display_name(db_map.sa_url) == name for db_map in self._model.db_maps):
+ self._suggested_db_name = name
return True
return False
@@ -320,23 +320,23 @@ def take_suggested_db_map(self):
Returns:
DatabaseMapping: database mapping
"""
- if self._suggested_codename is not None:
+ if self._suggested_db_name is not None:
for db_map in self.model.db_maps:
- if db_map.codename == self._suggested_codename:
- self._suggested_codename = None
+ if self._model.db_mngr.name_registry.display_name(db_map.sa_url) == self._suggested_db_name:
+ self._suggested_db_name = None
return db_map
- raise RuntimeError(f"Logic error: no such database mapping `{self._suggested_codename}`")
+ raise RuntimeError(f"Logic error: no such database mapping `{self._suggested_db_name}`")
return next(iter(self.model.db_maps))
- def suggest_db_map_codename(self):
- """Suggests a database mapping codename.
+ def suggest_db_map_name(self):
+ """Suggests a database mapping name.
Returns:
- str: codename
+ str: database display name
"""
- if self._suggested_codename is not None:
- return self._suggested_codename
- return next(iter(self.model.db_maps)).codename
+ if self._suggested_db_name is not None:
+ return self._suggested_db_name
+ return self._model.db_mngr.name_registry.display_name(next(iter(self.model.db_maps)).sa_url)
class PivotTableModelBase(QAbstractTableModel):
@@ -837,14 +837,14 @@ def data(self, index, role=Qt.ItemDataRole.DisplayRole):
with suppress(ValueError):
database_header_column = self.model.pivot_rows.index("database")
if index.column() == database_header_column:
- return self.top_left_headers["database"].suggest_db_map_codename()
+ return self.top_left_headers["database"].suggest_db_map_name()
elif (
self.emptyColumnCount() > 0 and index.column() == self.headerColumnCount() + self.dataColumnCount()
):
with suppress(ValueError):
database_header_row = self.model.pivot_columns.index("database")
if index.row() == database_header_row:
- return self.top_left_headers["database"].suggest_db_map_codename()
+ return self.top_left_headers["database"].suggest_db_map_name()
return None
if role == Qt.ItemDataRole.FontRole and self.index_in_top_left(index):
font = QFont()
@@ -1200,7 +1200,7 @@ def all_header_names(self, index):
entity_names = [self.db_mngr.get_item(db_map, "entity", id_)["name"] for id_ in entity_ids]
parameter_name = self.db_mngr.get_item(db_map, "parameter_definition", parameter_id).get("name", "")
alternative_name = self.db_mngr.get_item(db_map, "alternative", alternative_id).get("name", "")
- return entity_names, parameter_name, alternative_name, db_map.codename
+ return entity_names, parameter_name, alternative_name, self.db_mngr.name_registry.display_name(db_map.sa_url)
def index_name(self, index):
"""Returns a string that concatenates the object and parameter names corresponding to the given data index.
diff --git a/spinetoolbox/spine_db_editor/mvcmodels/scenario_item.py b/spinetoolbox/spine_db_editor/mvcmodels/scenario_item.py
index 4a3ec4437..fcf0cd770 100644
--- a/spinetoolbox/spine_db_editor/mvcmodels/scenario_item.py
+++ b/spinetoolbox/spine_db_editor/mvcmodels/scenario_item.py
@@ -27,10 +27,6 @@
class ScenarioDBItem(EmptyChildMixin, FetchMoreMixin, StandardDBItem):
"""A root item representing a db."""
- @property
- def item_type(self):
- return "db"
-
@property
def fetch_item_type(self):
return "scenario"
@@ -45,18 +41,13 @@ def _make_child(self, id_):
class ScenarioItem(GrayIfLastMixin, EditableMixin, EmptyChildMixin, FetchMoreMixin, BoldTextMixin, LeafItem):
"""A scenario leaf item."""
- @property
- def item_type(self):
- return "scenario"
+ item_type = "scenario"
+ icon_code = _SCENARIO_ICON
@property
def fetch_item_type(self):
return "scenario_alternative"
- @property
- def icon_code(self):
- return _SCENARIO_ICON
-
def tool_tip(self, column):
if column == 0 and not self.id:
return "
Note: Scenario names longer than 20 characters might appear shortened in generated files.
"
@@ -125,9 +116,7 @@ def _make_child(self, id_):
class ScenarioAlternativeItem(GrayIfLastMixin, EditableMixin, LeafItem):
"""A scenario alternative leaf item."""
- @property
- def item_type(self):
- return "scenario_alternative"
+ item_type = "scenario_alternative"
def tool_tip(self, column):
if column == 0:
diff --git a/spinetoolbox/spine_db_editor/mvcmodels/scenario_model.py b/spinetoolbox/spine_db_editor/mvcmodels/scenario_model.py
index ac574a019..4ab0d3eb0 100644
--- a/spinetoolbox/spine_db_editor/mvcmodels/scenario_model.py
+++ b/spinetoolbox/spine_db_editor/mvcmodels/scenario_model.py
@@ -24,7 +24,7 @@ class ScenarioModel(TreeModelBase):
"""A model to display scenarios in a tree view."""
def _make_db_item(self, db_map):
- return ScenarioDBItem(self, db_map)
+ return ScenarioDBItem(self, db_map, self.db_mngr.name_registry)
def supportedDropActions(self):
return Qt.DropAction.CopyAction | Qt.DropAction.MoveAction
diff --git a/spinetoolbox/spine_db_editor/mvcmodels/single_models.py b/spinetoolbox/spine_db_editor/mvcmodels/single_models.py
index 885fb1871..c43e843ec 100644
--- a/spinetoolbox/spine_db_editor/mvcmodels/single_models.py
+++ b/spinetoolbox/spine_db_editor/mvcmodels/single_models.py
@@ -48,6 +48,7 @@ class SingleModelBase(HalfSortedTableModel):
item_type: ClassVar[str] = NotImplemented
group_fields: ClassVar[Iterable[str]] = ()
+ can_be_filtered = True
def __init__(self, parent, db_map, entity_class_id, committed, lazy=False):
"""
@@ -67,7 +68,9 @@ def __init__(self, parent, db_map, entity_class_id, committed, lazy=False):
def __lt__(self, other):
if self.entity_class_name == other.entity_class_name:
- return self.db_map.codename < other.db_map.codename
+ return self.db_mngr.name_registry.display_name(
+ self.db_map.sa_url
+ ) < self.db_mngr.name_registry.display_name(other.db_map.sa_url)
keys = {}
for side, model in {"left": self, "right": other}.items():
dim = len(model.dimension_id_list)
@@ -113,10 +116,6 @@ def dimension_id_list(self):
def fixed_fields(self):
return ["entity_class_name", "database"]
- @property
- def can_be_filtered(self):
- return True
-
def _mapped_field(self, field):
return self.field_map.get(field, field)
@@ -202,7 +201,7 @@ def data(self, index, role=Qt.ItemDataRole.DisplayRole):
return FIXED_FIELD_COLOR
if role in (Qt.ItemDataRole.DisplayRole, Qt.ItemDataRole.EditRole, Qt.ItemDataRole.ToolTipRole):
if field == "database":
- return self.db_map.codename
+ return self.db_mngr.name_registry.display_name(self.db_map.sa_url)
id_ = self._main_data[index.row()]
item = self.db_mngr.get_item(self.db_map, self.item_type, id_)
if role == Qt.ItemDataRole.ToolTipRole:
diff --git a/spinetoolbox/spine_db_editor/mvcmodels/tree_item_utility.py b/spinetoolbox/spine_db_editor/mvcmodels/tree_item_utility.py
index cb05c4eba..17369a576 100644
--- a/spinetoolbox/spine_db_editor/mvcmodels/tree_item_utility.py
+++ b/spinetoolbox/spine_db_editor/mvcmodels/tree_item_utility.py
@@ -11,6 +11,7 @@
######################################################################################################################
"""A tree model for parameter_value lists."""
+from typing import ClassVar
from PySide6.QtCore import Qt
from PySide6.QtGui import QBrush, QFont, QGuiApplication, QIcon
from spinetoolbox.fetch_parent import FlexibleFetchParent
@@ -21,9 +22,8 @@
class StandardTreeItem(TreeItem):
"""A tree item that fetches their children as they are inserted."""
- @property
- def item_type(self):
- return None
+ item_type: ClassVar[str] = None
+ icon_code: ClassVar[str] = None
@property
def db_mngr(self):
@@ -33,10 +33,6 @@ def db_mngr(self):
def display_data(self):
return None
- @property
- def icon_code(self):
- return None
-
def tool_tip(self, column):
return None
@@ -223,19 +219,18 @@ def handle_items_updated(self, db_map_data):
class StandardDBItem(SortChildrenMixin, StandardTreeItem):
"""An item representing a db."""
- def __init__(self, model, db_map):
- """Init class.
+ item_type = "db"
+ def __init__(self, model, db_map, db_name_registry):
+ """
Args:
- model (MinimalTreeModel)
- db_map (DatabaseMapping)
+ model (MinimalTreeModel): tree model
+ db_map (DatabaseMapping): database mapping
+ db_name_registry (NameRegistry): database display name registry
"""
super().__init__(model)
self.db_map = db_map
-
- @property
- def item_type(self):
- return "db"
+ self._db_name_registry = db_name_registry
def data(self, column, role=Qt.ItemDataRole.DisplayRole):
"""Shows Spine icon for fun."""
@@ -244,7 +239,7 @@ def data(self, column, role=Qt.ItemDataRole.DisplayRole):
if role == Qt.ItemDataRole.DecorationRole:
return QIcon(":/symbols/Spine_symbol.png")
if role in (Qt.ItemDataRole.DisplayRole, Qt.ItemDataRole.EditRole):
- return self.db_map.codename
+ return self._db_name_registry.display_name(self.db_map.sa_url)
class LeafItem(StandardTreeItem):
@@ -260,10 +255,6 @@ def __init__(self, model, identifier=None):
def _make_item_data(self):
return {"name": f"Type new {self.item_type} name here...", "description": ""}
- @property
- def item_type(self):
- raise NotImplementedError()
-
@property
def db_map(self):
return self.parent_item.db_map
diff --git a/spinetoolbox/spine_db_editor/widgets/add_items_dialogs.py b/spinetoolbox/spine_db_editor/widgets/add_items_dialogs.py
index a7764234b..a97618ad6 100644
--- a/spinetoolbox/spine_db_editor/widgets/add_items_dialogs.py
+++ b/spinetoolbox/spine_db_editor/widgets/add_items_dialogs.py
@@ -145,11 +145,11 @@ def __init__(self, parent, db_mngr, *db_maps):
Args:
parent (SpineDBEditor)
db_mngr (SpineDBManager)
- *db_maps: DiffDatabaseMapping instances
+ *db_maps: DatabaseMapping instances
"""
super().__init__(parent, db_mngr)
self.db_maps = db_maps
- self.keyed_db_maps = {x.codename: x for x in db_maps}
+ self.keyed_db_maps = db_mngr.name_registry.map_display_names_to_db_maps(db_maps)
self.remove_rows_button = QToolButton(self)
self.remove_rows_button.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
self.remove_rows_button.setText("Remove selected rows")
@@ -170,10 +170,8 @@ def remove_selected_rows(self, checked=True):
self.model.removeRows(row, 1)
def all_databases(self, row):
- """Returns a list of db names available for a given row.
- Used by delegates.
- """
- return [x.codename for x in self.db_maps]
+ """Returns a list of db names available for a given row."""
+ return [self.db_mngr.name_registry.display_name(x.sa_url) for x in self.db_maps]
class AddEntityClassesDialog(ShowIconColorEditorMixin, GetEntityClassesMixin, AddItemsDialog):
@@ -211,7 +209,7 @@ def __init__(self, parent, item, db_mngr, *db_maps, force_default=False):
labels = ["dimension name (1)"] if dimension_one_name is not None else []
labels += ["entity class name", "description", "display icon", "active by default", "databases"]
self.model.set_horizontal_header_labels(labels)
- db_names = ",".join(x.codename for x in item.db_maps)
+ db_names = ", ".join(db_mngr.name_registry.display_name_iter(item.db_maps))
self.default_display_icon = None
self.model.set_default_row(
**{
@@ -310,7 +308,7 @@ def accept(self):
db_names = row_data[db_column]
if db_names is None:
db_names = ""
- for db_name in db_names.split(","):
+ for db_name in db_names.split(", "):
if db_name not in self.keyed_db_maps:
self.parent().msg_error.emit(f"Invalid database {db_name} at row {i + 1}")
return
@@ -501,7 +499,7 @@ def _class_key_to_str(self, key, *db_maps):
class_name = self.db_map_ent_cls_lookup[db_maps[0]][key]["name"]
if len(db_maps) == len(self.db_maps):
return class_name
- return class_name + "@(" + ", ".join(db_map.codename for db_map in db_maps) + ")"
+ return class_name + "@(" + ", ".join(self.db_mngr.name_registry.display_name_iter(db_maps)) + ")"
def _accepts_class(self, ent_cls):
if self.entity_class is None:
@@ -519,7 +517,7 @@ def _do_reset_model(self):
header = self.dimension_name_list + ("entity name", "alternative", "entity group", "databases")
self.model.set_horizontal_header_labels(header)
default_db_maps = [db_map for db_map, keys in self.db_map_ent_cls_lookup.items() if self.class_key in keys]
- db_names = ",".join([db_name for db_name, db_map in self.keyed_db_maps.items() if db_map in default_db_maps])
+ db_names = ", ".join([db_name for db_name, db_map in self.keyed_db_maps.items() if db_map in default_db_maps])
alt_selection_model = self.parent().ui.alternative_tree_view.selectionModel()
alt_selection = alt_selection_model.selection()
selected_alt_name = None
@@ -553,7 +551,7 @@ def append_db_codenames(self, name, db_maps):
"""
if len(db_maps) == len(self.parent().db_maps):
return name
- return name + "@(" + ", ".join(db_map.codename for db_map in db_maps) + ")"
+ return name + "@(" + ", ".join(self.db_mngr.name_registry.display_name_iter(db_maps)) + ")"
def get_db_map_data(self):
db_map_data = {}
@@ -570,7 +568,7 @@ def get_db_map_data(self):
db_names = row_data[db_column]
if db_names is None:
db_names = ""
- for db_name in db_names.split(","):
+ for db_name in db_names.split(", "):
if db_name not in self.keyed_db_maps:
self.parent().msg_error.emit(f"Invalid database {db_name} at row {i + 1}")
return
@@ -636,7 +634,7 @@ def make_entity_alternatives(self, entities):
entity_name = row_data[name_column]
entity = entities[entity_name]
db_names = row_data[db_column]
- for db_name in db_names.split(","):
+ for db_name in db_names.split(", "):
db_map = self.keyed_db_maps[db_name]
entity_alternatives.setdefault(db_map, []).append(
{
@@ -664,7 +662,7 @@ def make_entity_groups(self, entities):
entity = entities[entity_name]
class_name = entity["entity_class_name"]
db_names = row_data[db_column]
- for db_name in db_names.split(","):
+ for db_name in db_names.split(", "):
db_map = self.keyed_db_maps[db_name]
db_map_data.setdefault(db_map, {}).setdefault("entities", set()).add((class_name, entity_group))
db_map_data.setdefault(db_map, {}).setdefault("entity_groups", set()).add(
@@ -725,8 +723,9 @@ def __init__(self, parent, item, db_mngr, *db_maps):
self.existing_items_model = MinimalTableModel(self, lazy=False)
self.new_items_model = MinimalTableModel(self, lazy=False)
self.model.sub_models = [self.new_items_model, self.existing_items_model]
- self.db_combo_box.addItems([db_map.codename for db_map in db_maps])
- self.reset_entity_class_combo_box(db_maps[0].codename)
+ names = list(db_mngr.name_registry.display_name_iter(db_maps))
+ self.db_combo_box.addItems(names)
+ self.reset_entity_class_combo_box(names[0])
self.connect_signals()
def _populate_layout(self):
@@ -891,7 +890,7 @@ def __init__(self, parent, entity_class_item, db_mngr, *db_maps):
self.db_mngr = db_mngr
self.db_maps = db_maps
self.db_map = db_maps[0]
- self.db_maps_by_codename = {db_map.codename: db_map for db_map in db_maps}
+ self.db_maps_by_db_name = db_mngr.name_registry.map_display_names_to_db_maps(db_maps)
self.db_combo_box = QComboBox(self)
self.header_widget = QWidget(self)
self.group_name_line_edit = QLineEdit(self)
@@ -938,7 +937,7 @@ def __init__(self, parent, entity_class_item, db_mngr, *db_maps):
layout.addWidget(self.members_tree, 1, 2)
layout.addWidget(self.button_box, 2, 0, 1, 3)
self.setAttribute(Qt.WA_DeleteOnClose)
- self.db_combo_box.addItems(list(self.db_maps_by_codename))
+ self.db_combo_box.addItems(list(self.db_maps_by_db_name))
self.db_map_entity_ids = {
db_map: {
x["name"]: x["id"]
@@ -958,7 +957,7 @@ def connect_signals(self):
self.remove_button.clicked.connect(self.remove_members)
def reset_list_widgets(self, database):
- self.db_map = self.db_maps_by_codename[database]
+ self.db_map = self.db_maps_by_db_name[database]
entity_ids = self.db_map_entity_ids[self.db_map]
members = []
non_members = []
@@ -1015,7 +1014,7 @@ def __init__(self, parent, entity_class_item, db_mngr, *db_maps):
self.setWindowTitle("Add entity group")
self.group_name_line_edit.setFocus()
self.group_name_line_edit.setPlaceholderText("Type group name here")
- self.reset_list_widgets(db_maps[0].codename)
+ self.reset_list_widgets(self.db_mngr.name_registry.display_name(db_maps[0].sa_url))
self.connect_signals()
def initial_member_ids(self):
@@ -1071,7 +1070,7 @@ def __init__(self, parent, entity_item, db_mngr, *db_maps):
self.group_name_line_edit.setReadOnly(True)
self.group_name_line_edit.setText(entity_item.name)
self.entity_item = entity_item
- self.reset_list_widgets(db_maps[0].codename)
+ self.reset_list_widgets(self.db_mngr.name_registry.display_name(db_maps[0].sa_url))
self.connect_signals()
def _entity_groups(self):
diff --git a/spinetoolbox/spine_db_editor/widgets/commit_viewer.py b/spinetoolbox/spine_db_editor/widgets/commit_viewer.py
index 94510aecc..0ffab7fa3 100644
--- a/spinetoolbox/spine_db_editor/widgets/commit_viewer.py
+++ b/spinetoolbox/spine_db_editor/widgets/commit_viewer.py
@@ -251,7 +251,7 @@ def __init__(self, qsettings, db_mngr, *db_maps, parent=None):
self._current_index = 0
for db_map in self._db_maps:
widget = _DBCommitViewer(self._db_mngr, db_map)
- tab_widget.addTab(widget, db_map.codename)
+ tab_widget.addTab(widget, db_mngr.name_registry.display_name(db_map.sa_url))
restore_ui(self, self._qsettings, "commitViewer")
self._qsettings.beginGroup("commitViewer")
current = self.centralWidget().widget(self._current_index)
diff --git a/spinetoolbox/spine_db_editor/widgets/custom_delegates.py b/spinetoolbox/spine_db_editor/widgets/custom_delegates.py
index 98ec8ffb7..525c25a5b 100644
--- a/spinetoolbox/spine_db_editor/widgets/custom_delegates.py
+++ b/spinetoolbox/spine_db_editor/widgets/custom_delegates.py
@@ -275,7 +275,10 @@ class DatabaseNameDelegate(TableDelegate):
def createEditor(self, parent, option, index):
"""Returns editor."""
editor = SearchBarEditor(self.parent(), parent)
- editor.set_data(index.data(Qt.ItemDataRole.DisplayRole), [x.codename for x in self.db_mngr.db_maps])
+ editor.set_data(
+ index.data(Qt.ItemDataRole.DisplayRole),
+ list(self.db_mngr.name_registry.display_name_iter(self.db_mngr.db_maps)),
+ )
editor.data_committed.connect(lambda *_: self._close_editor(editor, index))
return editor
@@ -758,7 +761,7 @@ def _create_alternative_editor(self, parent, index):
dbs_by_alternative_name = {}
database_column = self.parent().model.horizontal_header_labels().index("databases")
database_index = index.model().index(index.row(), database_column)
- databases = database_index.data(Qt.ItemDataRole.DisplayRole).split(",")
+ databases = database_index.data(Qt.ItemDataRole.DisplayRole).split(", ")
for db_map_codename in databases: # Filter possible alternatives based on selected databases
db_map = self.parent().keyed_db_maps[db_map_codename]
alternatives = self.parent().db_mngr.get_items(db_map, "alternative")
@@ -781,11 +784,12 @@ def _create_entity_group_editor(self, parent, index):
"""
database_column = self.parent().model.horizontal_header_labels().index("databases")
database_index = index.model().index(index.row(), database_column)
- databases = database_index.data(Qt.ItemDataRole.DisplayRole).split(",")
+ databases = database_index.data(Qt.ItemDataRole.DisplayRole).split(", ")
entity_class = self.parent().class_item
- dbs_by_entity_group = {} # A mapping from entity_group to db_map(s)
+ dbs_by_entity_group = {}
for db_map in entity_class.db_maps:
- if db_map.codename not in databases: # Allow groups that are in selected DBs under "databases" -column.
+ if parent.db_mngr.name_registry.display_name(db_map.sa_url) not in databases:
+ # Allow groups that are in selected DBs under "databases" column.
continue
class_item = self.parent().db_mngr.get_item_by_field(db_map, "entity_class", "name", entity_class.name)
if not class_item:
@@ -814,7 +818,7 @@ def _create_database_editor(self, parent, index):
"""
editor = CheckListEditor(parent)
all_databases = self.parent().all_databases(index.row())
- databases = index.data(Qt.ItemDataRole.DisplayRole).split(",")
+ databases = index.data(Qt.ItemDataRole.DisplayRole).split(", ")
editor.set_data(all_databases, databases)
return editor
diff --git a/spinetoolbox/spine_db_editor/widgets/custom_menus.py b/spinetoolbox/spine_db_editor/widgets/custom_menus.py
index 90cd7e3d2..94fa04ecd 100644
--- a/spinetoolbox/spine_db_editor/widgets/custom_menus.py
+++ b/spinetoolbox/spine_db_editor/widgets/custom_menus.py
@@ -28,11 +28,12 @@ class AutoFilterMenu(FilterMenuBase):
def __init__(self, parent, db_mngr, db_maps, item_type, field, show_empty=True):
"""
Args:
- parent (SpineDBEditor)
+ parent (SpineDBEditor): parent widget
db_mngr (SpineDBManager)
db_maps (Sequence of DatabaseMapping)
item_type (str)
field (str): the field name
+ show_empty (bool)
"""
super().__init__(parent)
self._item_type = item_type
@@ -62,12 +63,12 @@ def set_filter_rejected_values(self, rejected_values):
def _get_value(self, item, db_map):
if self._field == "database":
- return db_map.codename
+ return self._db_mngr.name_registry.display_name(db_map.sa_url)
return item[self._field]
def _get_display_value(self, item, db_map):
if self._field in ("value", "default_value"):
- return self._db_mngr.get_value(db_map, item, role=Qt.DisplayRole)
+ return self._db_mngr.get_value(db_map, item, role=Qt.ItemDataRole.DisplayRole)
if self._field == "entity_byname":
return DB_ITEM_SEPARATOR.join(item[self._field])
return self._get_value(item, db_map) or "(empty)"
@@ -216,20 +217,21 @@ def emit_filter_changed(self, valid_values):
self.filterChanged.emit(self._identifier, valid_values, self._filter.has_filter())
-class TabularViewCodenameFilterMenu(TabularViewFilterMenuBase):
- """Filter menu to filter database codenames in Pivot table."""
+class TabularViewDatabaseNameFilterMenu(TabularViewFilterMenuBase):
+ """Filter menu to filter database names in Pivot table."""
- def __init__(self, parent, db_maps, identifier, show_empty=True):
+ def __init__(self, parent, db_maps, identifier, db_name_registry, show_empty=True):
"""
Args:
parent (SpineDBEditor): parent widget
db_maps (Sequence of DatabaseMapping): database mappings
identifier (str): header identifier
+ db_name_registry (NameRegistry): database display name registry
show_empty (bool): if True, an empty row will be added to the end of the item list
"""
super().__init__(parent, identifier)
self._set_up(SimpleFilterCheckboxListModel, self, show_empty=show_empty)
- self._filter.set_filter_list([db_map.codename for db_map in db_maps])
+ self._filter.set_filter_list(list(db_name_registry.display_name_iter(db_maps)))
def emit_filter_changed(self, valid_values):
"""See base class."""
diff --git a/spinetoolbox/spine_db_editor/widgets/custom_qwidgets.py b/spinetoolbox/spine_db_editor/widgets/custom_qwidgets.py
index 4358c8b6e..a36415b41 100644
--- a/spinetoolbox/spine_db_editor/widgets/custom_qwidgets.py
+++ b/spinetoolbox/spine_db_editor/widgets/custom_qwidgets.py
@@ -150,8 +150,7 @@ def __init__(self, file_path, progress, db_editor):
@Slot(bool)
def open_file(self, checked=False):
- codename = os.path.splitext(self.file_name)[0]
- self.db_editor._open_sqlite_url(self.url, codename)
+ self.db_editor.add_new_tab(self.url)
class ShootingLabel(QLabel):
@@ -459,14 +458,14 @@ def selections(self):
class AddedEntitiesPopup(QDialog):
"""Class for showing automatically added entities"""
- def __init__(self, parent, added_entities):
+ def __init__(self, parent, db_name_registry, added_entities):
super().__init__(parent)
self.setWindowTitle("Added Entities")
self._textEdit = QTextEdit(self)
self._text = None
self._entity_names = None
self._create_entity_names(added_entities)
- self._create_text()
+ self._create_text(db_name_registry)
self._textEdit.setHtml(self._text)
self._textEdit.setReadOnly(True)
self._textEdit.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
@@ -484,10 +483,10 @@ def __init__(self, parent, added_entities):
self.setSizeGripEnabled(True)
self.resize(400, 400)
- def _create_text(self):
+ def _create_text(self, db_name_registry):
lines = []
for db_map, classes in self._entity_names.items():
- lines.append(f"{db_map.codename}:")
+ lines.append(f"{db_name_registry.display_name(db_map.sa_url)}:")
for cls_name, ent_names in classes.items():
lines.append(f"- {cls_name}:
")
for ent_name in ent_names:
diff --git a/spinetoolbox/spine_db_editor/widgets/edit_or_remove_items_dialogs.py b/spinetoolbox/spine_db_editor/widgets/edit_or_remove_items_dialogs.py
index 4bd3ffde6..389ea3ce5 100644
--- a/spinetoolbox/spine_db_editor/widgets/edit_or_remove_items_dialogs.py
+++ b/spinetoolbox/spine_db_editor/widgets/edit_or_remove_items_dialogs.py
@@ -32,11 +32,9 @@ def __init__(self, parent, db_mngr):
self.items = []
def all_databases(self, row):
- """Returns a list of db names available for a given row.
- Used by delegates.
- """
+ """Returns a list of db names available for a given row."""
item = self.items[row]
- return [db_map.codename for db_map in item.db_maps]
+ return list(self.db_mngr.name_registry.display_name_iter(item.db_maps))
class EditEntityClassesDialog(ShowIconColorEditorMixin, EditOrRemoveItemsDialog):
@@ -89,8 +87,15 @@ def accept(self):
db_names = ""
item = self.items[i]
db_maps = []
- for database in db_names.split(","):
- db_map = next((db_map for db_map in item.db_maps if db_map.codename == database), None)
+ for database in db_names.split(", "):
+ db_map = next(
+ (
+ db_map
+ for db_map in item.db_maps
+ if self.db_mngr.name_registry.display_name(db_map.sa_url) == database
+ ),
+ None,
+ )
if db_map is None:
self.parent().msg_error.emit(f"Invalid database {database} at row {i + 1}")
return
@@ -137,7 +142,7 @@ def __init__(self, parent, db_mngr, selected, class_key):
self.table_view.setItemDelegate(ManageEntitiesDelegate(self))
self.connect_signals()
self.db_maps = set(db_map for item in selected for db_map in item.db_maps)
- self.keyed_db_maps = {x.codename: x for x in self.db_maps}
+ self.keyed_db_maps = self.db_mngr.name_registry.map_display_names_to_db_maps(self.db_maps)
self.class_key = class_key
self.model.set_horizontal_header_labels(
[x + " byname" for x in self.dimension_name_list] + ["entity name", "databases"]
@@ -174,9 +179,16 @@ def accept(self):
if db_names is None:
db_names = ""
db_maps = []
- for database in db_names.split(","):
- db_map = next((db_map for db_map in item.db_maps if db_map.codename == database), None)
- if db_map is None:
+ for database in db_names.split(", "):
+ try:
+ db_map = next(
+ (
+ db_map
+ for db_map in item.db_maps
+ if self.db_mngr.name_registry.display_name(db_map.sa_url) == database
+ )
+ )
+ except StopIteration:
self.parent().msg_error.emit(f"Invalid database {database} at row {i + 1}")
return
db_maps.append(db_map)
@@ -187,7 +199,7 @@ def accept(self):
entity_classes = self.db_map_ent_cls_lookup[db_map]
if (self.class_key) not in entity_classes:
self.parent().msg_error.emit(
- f"Invalid entity class '{self.class_name}' for db '{db_map.codename}' at row {i + 1}"
+ f"Invalid entity class '{self.class_name}' for db '{self.db_mngr.name_registry.display_name(db_map.sa_url)}' at row {i + 1}"
)
return
ent_cls = entity_classes[self.class_key]
@@ -198,7 +210,7 @@ def accept(self):
for dimension_id, element_name in zip(dimension_id_list, element_name_list):
if (dimension_id, element_name) not in entities:
self.parent().msg_error.emit(
- f"Invalid entity '{element_name}' for db '{db_map.codename}' at row {i + 1}"
+ f"Invalid entity '{element_name}' for db '{self.db_mngr.name_registry.display_name(db_map.sa_url)}' at row {i + 1}"
)
return
element_id = entities[dimension_id, element_name]["id"]
@@ -248,8 +260,15 @@ def accept(self):
db_names = ""
item = self.items[i]
db_maps = []
- for database in db_names.split(","):
- db_map = next((db_map for db_map in item.db_maps if db_map.codename == database), None)
+ for database in db_names.split(", "):
+ db_map = next(
+ (
+ db_map
+ for db_map in item.db_maps
+ if self.db_mngr.name_registry.display_name(db_map.sa_url) == database
+ ),
+ None,
+ )
if db_map is None:
self.parent().msg_error.emit(f"Invalid database {database} at row {i + 1}")
return
@@ -282,7 +301,7 @@ def __init__(self, parent, entity_class_item, db_mngr, *db_maps):
combobox.setCurrentText(superclass_subclass["superclass_name"])
else:
combobox.setCurrentIndex(0)
- self._tab_widget.addTab(combobox, db_map.codename)
+ self._tab_widget.addTab(combobox, self.db_mngr.name_registry.display_name(db_map.sa_url))
self.connect_signals()
self.setWindowTitle(f"Select {self._subclass_name}'s superclass")
diff --git a/spinetoolbox/spine_db_editor/widgets/graph_view_mixin.py b/spinetoolbox/spine_db_editor/widgets/graph_view_mixin.py
index 1cb17cff6..a5428f125 100644
--- a/spinetoolbox/spine_db_editor/widgets/graph_view_mixin.py
+++ b/spinetoolbox/spine_db_editor/widgets/graph_view_mixin.py
@@ -661,7 +661,7 @@ def get_entity_key(self, db_map_entity_id):
entity = self.db_mngr.get_item(db_map, "entity", entity_id)
key = (entity["entity_class_name"], entity["dimension_name_list"], entity["entity_byname"])
if not self.ui.graphicsView.get_property("merge_dbs"):
- key += (db_map.codename,)
+ key += (self.db_mngr.name_registry.display_name(db_map.sa_url),)
return key
def _update_entity_element_inds(self, db_map_element_id_lists):
diff --git a/spinetoolbox/spine_db_editor/widgets/manage_items_dialogs.py b/spinetoolbox/spine_db_editor/widgets/manage_items_dialogs.py
index 6d3ba4722..b776dc5dc 100644
--- a/spinetoolbox/spine_db_editor/widgets/manage_items_dialogs.py
+++ b/spinetoolbox/spine_db_editor/widgets/manage_items_dialogs.py
@@ -167,7 +167,7 @@ def entity_class_name_list(self, row):
"""
db_column = self.model.header.index("databases")
db_names = self.model._main_data[row][db_column]
- db_maps = [self.keyed_db_maps[x] for x in db_names.split(",") if x in self.keyed_db_maps]
+ db_maps = [self.keyed_db_maps[x] for x in db_names.split(", ") if x in self.keyed_db_maps]
return self._entity_class_name_list_from_db_maps(*db_maps)
def _entity_class_name_list_from_db_maps(self, *db_maps):
@@ -234,7 +234,7 @@ def alternative_name_list(self, row):
"""
db_column = self.model.header.index("databases")
db_names = self.model._main_data[row][db_column]
- db_maps = [self.keyed_db_maps[x] for x in db_names.split(",") if x in self.keyed_db_maps]
+ db_maps = [self.keyed_db_maps[x] for x in db_names.split(", ") if x in self.keyed_db_maps]
return sorted(set(x for db_map in db_maps for x in self.db_map_alt_id_lookup[db_map]))
def entity_name_list(self, row, column):
@@ -243,7 +243,7 @@ def entity_name_list(self, row, column):
"""
db_column = self.model.header.index("databases")
db_names = self.model._main_data[row][db_column]
- db_maps = [self.keyed_db_maps[x] for x in db_names.split(",") if x in self.keyed_db_maps]
+ db_maps = [self.keyed_db_maps[x] for x in db_names.split(", ") if x in self.keyed_db_maps]
entity_name_lists = []
for db_map in db_maps:
entity_classes = self.db_map_ent_cls_lookup[db_map]
diff --git a/spinetoolbox/spine_db_editor/widgets/mass_select_items_dialogs.py b/spinetoolbox/spine_db_editor/widgets/mass_select_items_dialogs.py
index 349332612..eeeb1b390 100644
--- a/spinetoolbox/spine_db_editor/widgets/mass_select_items_dialogs.py
+++ b/spinetoolbox/spine_db_editor/widgets/mass_select_items_dialogs.py
@@ -22,11 +22,12 @@ class _SelectDatabases(QWidget):
checked_state_changed = Signal(int)
- def __init__(self, db_maps, checked_states, parent):
+ def __init__(self, db_maps, checked_states, db_name_registry, parent):
"""
Args:
db_maps (tuple of DatabaseMapping): database maps
checked_states (dict, optional): mapping from item name to check state boolean
+ db_name_registry (NameRegistry): database display name registry
parent (QWidget): parent widget
"""
super().__init__(parent)
@@ -34,7 +35,9 @@ def __init__(self, db_maps, checked_states, parent):
self._ui = Ui_Form()
self._ui.setupUi(self)
- self._check_boxes = {db_map: QCheckBox(db_map.codename, self) for db_map in db_maps}
+ self._check_boxes = {
+ db_map: QCheckBox(db_name_registry.display_name(db_map.sa_url), self) for db_map in db_maps
+ }
add_check_boxes(
self._check_boxes,
checked_states,
@@ -80,7 +83,9 @@ def __init__(self, parent, db_mngr, *db_maps, stored_state, ok_button_text):
database_checked_states = (
stored_state["databases"] if stored_state is not None else {db_map: True for db_map in db_maps}
)
- self._database_check_boxes_widget = _SelectDatabases(tuple(db_maps), database_checked_states, self)
+ self._database_check_boxes_widget = _SelectDatabases(
+ tuple(db_maps), database_checked_states, db_mngr.name_registry, self
+ )
self._database_check_boxes_widget.checked_state_changed.connect(self._handle_check_box_state_changed)
self._ui.root_layout.insertWidget(0, self._database_check_boxes_widget)
diff --git a/spinetoolbox/spine_db_editor/widgets/multi_spine_db_editor.py b/spinetoolbox/spine_db_editor/widgets/multi_spine_db_editor.py
index c2feec59e..d4027c267 100644
--- a/spinetoolbox/spine_db_editor/widgets/multi_spine_db_editor.py
+++ b/spinetoolbox/spine_db_editor/widgets/multi_spine_db_editor.py
@@ -19,6 +19,7 @@
from ...helpers import CharIconEngine, open_url
from ...widgets.multi_tab_window import MultiTabWindow
from ...widgets.settings_widget import SpineDBEditorSettingsWidget
+from ..editors import db_editor_registry
from .custom_qwidgets import OpenFileButton, OpenSQLiteFileButton, ShootingLabel
from .spine_db_editor import SpineDBEditor
@@ -26,11 +27,11 @@
class MultiSpineDBEditor(MultiTabWindow):
"""Database editor's tabbed main window."""
- def __init__(self, db_mngr, db_url_codenames=None):
+ def __init__(self, db_mngr, db_urls=None):
"""
Args:
db_mngr (SpineDBManager): database manager
- db_url_codenames (dict, optional): mapping from database URL to its codename
+ db_urls (Iterable of str, optional): URLs of database to load
"""
super().__init__(db_mngr.qsettings, "spineDBEditor")
self.db_mngr = db_mngr
@@ -42,9 +43,10 @@ def __init__(self, db_mngr, db_url_codenames=None):
self.setStatusBar(_CustomStatusBar(self))
self.statusBar().hide()
self.tab_load_success = True
- if db_url_codenames is not None:
- if not self.add_new_tab(db_url_codenames, window=True):
+ if db_urls is not None:
+ if not self.add_new_tab(db_urls):
self.tab_load_success = False
+ db_editor_registry.register_window(self)
def _make_other(self):
return MultiSpineDBEditor(self.db_mngr)
@@ -84,10 +86,10 @@ def _disconnect_tab_signals(self, index):
tab.ui.actionClose.triggered.disconnect(self.handle_close_request_from_tab)
return True
- def _make_new_tab(self, db_url_codenames=None, window=False): # pylint: disable=arguments-differ
+ def _make_new_tab(self, db_urls=None): # pylint: disable=arguments-differ
"""Makes a new tab, if successful return the tab, returns None otherwise"""
tab = SpineDBEditor(self.db_mngr)
- if not tab.load_db_urls(db_url_codenames, create=True, window=window):
+ if not tab.load_db_urls(db_urls if db_urls is not None else [], create=True):
return
return tab
@@ -102,7 +104,7 @@ def show_plus_button_context_menu(self, global_pos):
return
menu = QMenu(self)
for name, url in ds_urls.items():
- action = menu.addAction(name, lambda name=name, url=url: self.db_mngr.open_db_editor({url: name}, True))
+ action = menu.addAction(name, lambda url=url: open_db_editor([url], self.db_mngr, True))
action.setEnabled(url is not None and is_url_validated[name])
menu.popup(global_pos)
menu.aboutToHide.connect(menu.deleteLater)
@@ -114,13 +116,11 @@ def make_context_menu(self, index):
tab = self.tab_widget.widget(index)
menu.addSeparator()
menu.addAction(tab.toolbar.reload_action)
- db_url_codenames = tab.db_url_codenames
+ db_urls = tab.db_urls
menu.addAction(
QIcon(CharIconEngine("\uf24d")),
"Duplicate",
- lambda _=False, index=index + 1, db_url_codenames=db_url_codenames: self.insert_new_tab(
- index, db_url_codenames
- ),
+ lambda _=False, index=index + 1, db_urls=db_urls: self.insert_new_tab(index, db_urls),
)
return menu
@@ -160,10 +160,6 @@ def insert_open_file_button(self, file_path, progress, is_sqlite):
button = (OpenSQLiteFileButton if is_sqlite else OpenFileButton)(file_path, progress, self)
self._insert_statusbar_button(button)
- def _open_sqlite_url(self, url, codename):
- """Opens sqlite url."""
- self.add_new_tab({url: codename})
-
@Slot(bool)
def show_user_guide(self, checked=False):
"""Opens Spine db editor documentation page in browser."""
@@ -171,6 +167,11 @@ def show_user_guide(self, checked=False):
if not open_url(doc_url):
self.msg_error.emit(f"Unable to open url {doc_url}")
+ def closeEvent(self, event):
+ super().closeEvent(event)
+ if event.isAccepted():
+ db_editor_registry.unregister_window(self)
+
class _CustomStatusBar(QStatusBar):
def __init__(self, parent=None):
@@ -198,3 +199,49 @@ def __init__(self, parent=None):
self.insertPermanentWidget(0, self._hide_button)
self.setSizeGripEnabled(False)
self._hide_button.clicked.connect(self.hide)
+
+
+def _get_existing_spine_db_editor(db_urls):
+ """Returns existing editor window and tab or None for given database URLs.
+
+ Args:
+ db_urls (Sequence of str): database URLs
+
+ Returns:
+ tuple: editor window and tab or None if not found
+ """
+ for multi_db_editor in db_editor_registry.windows():
+ for k in range(multi_db_editor.tab_widget.count()):
+ db_editor = multi_db_editor.tab_widget.widget(k)
+ if db_editor.db_urls and all(url in db_urls for url in db_editor.db_urls):
+ return multi_db_editor, db_editor
+ return None
+
+
+def open_db_editor(db_urls, db_mngr, reuse_existing_editor):
+ """Opens a SpineDBEditor with given urls.
+
+ Optionally uses an existing MultiSpineDBEditor if any.
+ Also, if the same urls are open in an existing SpineDBEditor, just raises that one
+ instead of creating another.
+
+ Args:
+ db_urls (Iterable of str): URLs of databases to open
+ db_mngr (SpineDBManager): database manager
+ reuse_existing_editor (bool): if True and the same URL is already open, just raise the existing window
+ """
+ multi_db_editor = db_editor_registry.get_some_window() if reuse_existing_editor else None
+ if multi_db_editor is None:
+ multi_db_editor = MultiSpineDBEditor(db_mngr, db_urls)
+ if multi_db_editor.tab_load_success:
+ multi_db_editor.show()
+ return
+ existing = _get_existing_spine_db_editor(list(map(str, db_urls)))
+ if existing is None:
+ multi_db_editor.add_new_tab(db_urls)
+ else:
+ multi_db_editor, db_editor = existing
+ multi_db_editor.set_current_tab(db_editor)
+ if multi_db_editor.isMinimized():
+ multi_db_editor.showNormal()
+ multi_db_editor.activateWindow()
diff --git a/spinetoolbox/spine_db_editor/widgets/spine_db_editor.py b/spinetoolbox/spine_db_editor/widgets/spine_db_editor.py
index 748414173..fd647cb0c 100644
--- a/spinetoolbox/spine_db_editor/widgets/spine_db_editor.py
+++ b/spinetoolbox/spine_db_editor/widgets/spine_db_editor.py
@@ -75,8 +75,8 @@ def __init__(self, db_mngr):
from ..ui.spine_db_editor_window import Ui_MainWindow # pylint: disable=import-outside-toplevel
self.db_mngr = db_mngr
- self.db_maps = []
- self.db_urls = []
+ self.db_maps: list[DatabaseMapping] = []
+ self.db_urls: list[str] = []
self._history = []
self.recent_dbs_menu = RecentDatabasesPopupMenu(self)
self._change_notifiers = []
@@ -124,37 +124,38 @@ def toolbox(self):
def settings_subgroup(self):
return ";".join(self.db_urls)
- @property
- def db_names(self):
- return ", ".join([f"{db_map.codename}" for db_map in self.db_maps])
-
@property
def first_db_map(self):
return self.db_maps[0]
- @property
- def db_url_codenames(self):
- return {db_map.db_url: db_map.codename for db_map in self.db_maps}
-
@staticmethod
def is_db_map_editor():
- """Always returns True as SpineDBEditors are truly database editors.
+ """Always returns True as SpineDBEditors are truly database editors."""
+ return True
- Unless, of course, the database can one day be opened in read-only mode.
- In that case this method should return False.
+ @Slot(str, str)
+ def _update_title(self, url, name):
+ """Updates window title if database display name has changed.
- Returns:
- bool: Always True
+ Args:
+ url (str): database url
+ name (str): database display name
"""
- return True
+ if not any(str(db_map.sa_url) == url for db_map in self.db_maps):
+ return
+ self._reset_window_title()
+
+ def _reset_window_title(self):
+ """Sets new window title according to open databases."""
+ self.setWindowTitle(", ".join(self.db_mngr.name_registry.display_name_iter(self.db_maps)))
- def load_db_urls(self, db_url_codenames, create=False, update_history=True, window=False):
+ def load_db_urls(self, db_urls, create=False, update_history=True):
self.ui.actionImport.setEnabled(False)
self.ui.actionExport.setEnabled(False)
self.ui.actionMass_remove_items.setEnabled(False)
self.ui.actionVacuum.setEnabled(False)
self.toolbar.reload_action.setEnabled(False)
- if not db_url_codenames:
+ if not db_urls:
return True
if not self.tear_down():
return False
@@ -163,10 +164,8 @@ def load_db_urls(self, db_url_codenames, create=False, update_history=True, wind
self.db_maps = []
self._changelog.clear()
self._purge_change_notifiers()
- for url, codename in db_url_codenames.items():
- db_map = self.db_mngr.get_db_map(
- url, self, codename=None, create=create, window=window, force_upgrade_prompt=True
- )
+ for url in db_urls:
+ db_map = self.db_mngr.get_db_map(url, self, create=create, force_upgrade_prompt=True)
if db_map is not None:
self.db_maps.append(db_map)
if not self.db_maps:
@@ -184,9 +183,9 @@ def load_db_urls(self, db_url_codenames, create=False, update_history=True, wind
self.db_mngr.register_listener(self, *self.db_maps)
self.init_models()
self.init_add_undo_redo_actions()
- self.setWindowTitle(f"{self.db_names}") # This sets the tab name, just in case
+ self._reset_window_title()
if update_history:
- self.add_urls_to_history(self.db_url_codenames)
+ self.add_urls_to_history()
self.update_last_view()
self.restore_ui(self.last_view, fresh=True)
self.update_commit_enabled()
@@ -200,19 +199,16 @@ def show_recent_db(self):
self.recent_dbs_menu = RecentDatabasesPopupMenu(self)
self.ui.actionOpen_recent.setMenu(self.recent_dbs_menu)
- def add_urls_to_history(self, db_urls):
- """Adds urls to history.
-
- Args:
- db_urls (dict)
- """
+ def add_urls_to_history(self):
+ """Adds current urls to history."""
opened_names = set()
for row in self._history:
for name in row:
opened_names.add(name)
- for db_url, name in db_urls.items():
+ for url in self.db_urls:
+ name = self.db_mngr.name_registry.display_name(url)
if name not in opened_names:
- self._history.insert(0, {name: db_url})
+ self._history.insert(0, {name: url})
def init_add_undo_redo_actions(self):
new_undo_action = self.db_mngr.undo_action[self.first_db_map]
@@ -228,8 +224,8 @@ def open_db_file(self, _=False):
self.qsettings.endGroup()
if not file_path:
return
- url = "sqlite:///" + file_path
- self.load_db_urls({url: None})
+ url = "sqlite:///" + os.path.normcase(file_path)
+ self.load_db_urls([url])
@Slot(bool)
def add_db_file(self, _=False):
@@ -240,10 +236,8 @@ def add_db_file(self, _=False):
self.qsettings.endGroup()
if not file_path:
return
- url = "sqlite:///" + file_path
- db_url_codenames = self.db_url_codenames
- db_url_codenames[url] = None
- self.load_db_urls(db_url_codenames)
+ url = "sqlite:///" + os.path.normcase(file_path)
+ self.load_db_urls(self.db_urls + [url])
@Slot(bool)
def create_db_file(self, _=False):
@@ -258,8 +252,8 @@ def create_db_file(self, _=False):
os.remove(file_path)
except OSError:
pass
- url = "sqlite:///" + file_path
- self.load_db_urls({url: None}, create=True)
+ url = "sqlite:///" + os.path.normcase(file_path)
+ self.load_db_urls([url], create=True)
def reset_docks(self):
"""Resets the layout of the dock widgets for this URL"""
@@ -284,13 +278,12 @@ def _browse_commits(self):
def connect_signals(self):
"""Connects signals to slots."""
- # Message signals
self.msg.connect(self.add_message)
self.msg_error.connect(self.err_msg.showMessage)
self.db_mngr.items_added.connect(self._handle_items_added)
self.db_mngr.items_updated.connect(self._handle_items_updated)
self.db_mngr.items_removed.connect(self._handle_items_removed)
- # Menu actions
+ self.db_mngr.name_registry.display_name_changed.connect(self._update_title)
self.ui.actionCommit.triggered.connect(self.commit_session)
self.ui.actionRollback.triggered.connect(self.rollback_session)
self.ui.actionView_history.triggered.connect(self._browse_commits)
@@ -310,7 +303,7 @@ def vacuum(self, _checked=False):
msg = "Vacuum finished"
for db_map in self.db_maps:
freed, unit = vacuum(db_map.db_url)
- msg += f"- {freed} {unit} freed from {db_map.codename}
"
+ msg += f"- {freed} {unit} freed from {self.db_mngr.name_registry.display_name(db_map.sa_url)}
"
msg += "
"
self.msg.emit(msg)
@@ -555,7 +548,7 @@ def duplicate_scenario(self, db_map, scen_id):
Duplicates a scenario.
Args:
- db_map (DiffDatabaseMapping)
+ db_map (DatabaseMapping)
scen_id (int)
"""
orig_name = self.db_mngr.get_item(db_map, "scenario", scen_id)["name"]
@@ -597,7 +590,7 @@ def commit_session(self, checked=False):
dirty_db_maps = self.db_mngr.dirty(*self.db_maps)
if not dirty_db_maps:
return
- db_names = ", ".join([db_map.codename for db_map in dirty_db_maps])
+ db_names = ", ".join(self.db_mngr.name_registry.display_name_iter(dirty_db_maps))
commit_msg = self._get_commit_msg(db_names)
if not commit_msg:
return
@@ -609,7 +602,7 @@ def rollback_session(self, checked=False):
dirty_db_maps = self.db_mngr.dirty(*self.db_maps)
if not dirty_db_maps:
return
- db_names = ", ".join([db_map.codename for db_map in dirty_db_maps])
+ db_names = ", ".join(self.db_mngr.name_registry.display_name_iter(dirty_db_maps))
if not self._get_rollback_confirmation(db_names):
return
self.db_mngr.rollback_session(*dirty_db_maps)
@@ -618,7 +611,7 @@ def receive_session_committed(self, db_maps, cookie):
db_maps = set(self.db_maps) & set(db_maps)
if not db_maps:
return
- db_names = ", ".join([x.codename for x in db_maps])
+ db_names = ", ".join(self.db_mngr.name_registry.display_name_iter(db_maps))
if cookie is self:
msg = f"All changes in {db_names} committed successfully."
self.msg.emit(msg)
@@ -631,7 +624,7 @@ def receive_session_rolled_back(self, db_maps):
db_maps = set(self.db_maps) & set(db_maps)
if not db_maps:
return
- db_names = ", ".join([x.codename for x in db_maps])
+ db_names = ", ".join(self.db_mngr.name_registry.display_name_iter(db_maps))
msg = f"All changes in {db_names} rolled back successfully."
self.msg.emit(msg)
@@ -681,7 +674,9 @@ def receive_error_msg(self, db_map_error_log):
for db_map, error_log in db_map_error_log.items():
if isinstance(error_log, str):
error_log = [error_log]
- msg = "From " + db_map.codename + ":" + format_string_list(error_log)
+ msg = (
+ "From " + self.db_mngr.name_registry.display_name(db_map.sa_url) + ": " + format_string_list(error_log)
+ )
msgs.append(msg)
self.msg_error.emit(format_string_list(msgs))
@@ -769,7 +764,7 @@ def tear_down(self):
answer = self._prompt_to_commit_changes()
if answer == QMessageBox.StandardButton.Cancel:
return False
- db_names = ", ".join([db_map.codename for db_map in dirty_db_maps])
+ db_names = ", ".join(self.db_mngr.name_registry.display_name_iter(dirty_db_maps))
if answer == QMessageBox.StandardButton.Save:
commit_dirty = True
commit_msg = self._get_commit_msg(db_names)
@@ -781,7 +776,7 @@ def tear_down(self):
self, *self.db_maps, dirty_db_maps=dirty_db_maps, commit_dirty=commit_dirty, commit_msg=commit_msg
)
if failed_db_maps:
- msg = f"Failed to commit {[db_map.codename for db_map in failed_db_maps]}"
+ msg = f"Failed to commit {list(self.db_mngr.name_registry.display_name_iter(failed_db_maps))}"
self.db_mngr.receive_error_msg({i: [msg] for i in failed_db_maps})
return False
return True
@@ -872,6 +867,7 @@ def closeEvent(self, event):
event.ignore()
return
self.save_window_state()
+ self.db_mngr.name_registry.display_name_changed.disconnect(self._update_title)
super().closeEvent(event)
@staticmethod
@@ -907,11 +903,11 @@ class SpineDBEditor(TabularViewMixin, GraphViewMixin, StackedViewMixin, TreeView
pinned_values_updated = Signal(list)
- def __init__(self, db_mngr, db_url_codenames=None):
- """Initializes everything.
-
+ def __init__(self, db_mngr, db_urls=None):
+ """
Args:
db_mngr (SpineDBManager): The manager to use
+ db_urls (Iterable of str, optional): URLs of databases to load
"""
super().__init__(db_mngr)
self._original_size = None
@@ -926,8 +922,8 @@ def __init__(self, db_mngr, db_url_codenames=None):
self.connect_signals()
self.apply_stacked_style()
self.set_db_column_visibility(False)
- if db_url_codenames is not None:
- self.load_db_urls(db_url_codenames)
+ if db_urls is not None:
+ self.load_db_urls(db_urls)
def set_db_column_visibility(self, visible):
"""Set the visibility of the database -column in all the views it is present"""
diff --git a/spinetoolbox/spine_db_editor/widgets/stacked_view_mixin.py b/spinetoolbox/spine_db_editor/widgets/stacked_view_mixin.py
index 9197dd936..b1702a809 100644
--- a/spinetoolbox/spine_db_editor/widgets/stacked_view_mixin.py
+++ b/spinetoolbox/spine_db_editor/widgets/stacked_view_mixin.py
@@ -22,9 +22,7 @@
class StackedViewMixin:
- """
- Provides stacked parameter tables for the Spine db editor.
- """
+ """Provides stacked parameter tables for the Spine db editor."""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -102,7 +100,7 @@ def _set_default_parameter_data(self, index=None):
"""
if index is None or not index.isValid():
default_db_map = next(iter(self.db_maps))
- default_data = {"database": default_db_map.codename}
+ default_data = {"database": self.db_mngr.name_registry.display_name(default_db_map.sa_url)}
else:
item = index.model().item_from_index(index)
default_db_map = item.first_db_map
diff --git a/spinetoolbox/spine_db_editor/widgets/tabular_view_mixin.py b/spinetoolbox/spine_db_editor/widgets/tabular_view_mixin.py
index 06ff27058..2435eb265 100644
--- a/spinetoolbox/spine_db_editor/widgets/tabular_view_mixin.py
+++ b/spinetoolbox/spine_db_editor/widgets/tabular_view_mixin.py
@@ -27,7 +27,7 @@
PivotTableSortFilterProxy,
ScenarioAlternativePivotTableModel,
)
-from .custom_menus import TabularViewCodenameFilterMenu, TabularViewDBItemFilterMenu
+from .custom_menus import TabularViewDatabaseNameFilterMenu, TabularViewDBItemFilterMenu
from .tabular_view_header_widget import TabularViewHeaderWidget
@@ -402,7 +402,9 @@ def create_filter_menu(self, identifier):
"""
if identifier not in self.filter_menus:
if identifier == "database":
- menu = TabularViewCodenameFilterMenu(self, self.db_maps, identifier, show_empty=False)
+ menu = TabularViewDatabaseNameFilterMenu(
+ self, self.db_maps, identifier, self.db_mngr.name_registry, show_empty=False
+ )
else:
header = self.pivot_table_model.top_left_headers[identifier]
if header.header_type == "parameter":
diff --git a/spinetoolbox/spine_db_editor/widgets/toolbar.py b/spinetoolbox/spine_db_editor/widgets/toolbar.py
index 22aad619c..4341f451e 100644
--- a/spinetoolbox/spine_db_editor/widgets/toolbar.py
+++ b/spinetoolbox/spine_db_editor/widgets/toolbar.py
@@ -111,8 +111,8 @@ def create_button_for_action(self, action):
self.addWidget(tool_button)
def _show_url_codename_widget(self):
- """Shows the url codename widget"""
- dialog = _URLDialog(self._db_editor.db_url_codenames, parent=self)
+ """Shows the url widget"""
+ dialog = _URLDialog(self._db_editor.db_urls, self._db_editor.db_mngr.name_registry, self)
dialog.show()
@Slot(bool)
@@ -163,6 +163,7 @@ def __init__(self, db_mngr, db_map, parent=None):
super().__init__(parent=parent)
layout = QHBoxLayout(self)
self._offset = 0
+ self._db_mngr = db_mngr
self._db_map = db_map
self._filter_widgets = []
active_filter_configs = {cfg["type"]: cfg for cfg in filter_configs(db_map.db_url)}
@@ -181,7 +182,7 @@ def filtered_url_codename(self):
if not filter_config_:
continue
url = append_filter_config(url, filter_config_)
- return url, self._db_map.codename
+ return url, self._db_mngr.name_registry.display_name(self._db_map.sa_url)
def sizeHint(self):
size = super().sizeHint()
@@ -206,7 +207,7 @@ def __init__(self, db_mngr, db_maps, parent=None):
self.header().hide()
self._filter_arrays = []
for db_map in db_maps:
- top_level_item = QTreeWidgetItem([db_map.codename])
+ top_level_item = QTreeWidgetItem([db_mngr.name_registry.display_name(db_map.sa_url)])
self.addTopLevelItem(top_level_item)
child = QTreeWidgetItem()
top_level_item.addChild(child)
@@ -262,13 +263,13 @@ def accept(self):
class _URLDialog(QDialog):
- """Class for showing URLs and codenames in the database"""
+ """Class for showing URLs and database names in the editor"""
- def __init__(self, url_codenames, parent=None):
+ def __init__(self, urls, name_registry, parent=None):
super().__init__(parent=parent, f=Qt.Popup)
self.textEdit = QTextEdit(self)
self.textEdit.setObjectName("textEdit")
- text = "
".join([f"{codename}: {url}" for url, codename in url_codenames.items()])
+ text = "
".join([f"{name_registry.display_name(url)}: {url}" for url in urls])
self.textEdit.setHtml(text)
self.textEdit.setReadOnly(True)
self.textEdit.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
diff --git a/spinetoolbox/spine_db_manager.py b/spinetoolbox/spine_db_manager.py
index 3b38e6d23..1c06c3a1a 100644
--- a/spinetoolbox/spine_db_manager.py
+++ b/spinetoolbox/spine_db_manager.py
@@ -15,8 +15,7 @@
import json
import os
from PySide6.QtCore import QObject, Qt, Signal, Slot
-from PySide6.QtGui import QWindow
-from PySide6.QtWidgets import QApplication, QMessageBox, QWidget
+from PySide6.QtWidgets import QApplication, QMessageBox
from sqlalchemy.engine.url import URL
from spinedb_api import (
Array,
@@ -48,6 +47,7 @@
split_value_and_type,
)
from spinedb_api.spine_io.exporters.excel import export_spine_database_to_xlsx
+from spinetoolbox.database_display_names import NameRegistry
from .helpers import busy_effect, plain_to_tool_tip
from .mvcmodels.shared import INVALID_TYPE, PARAMETER_TYPE_VALIDATION_ROLE, PARSED_ROLE, TYPE_NOT_VALIDATED, VALID_TYPE
from .parameter_type_validation import ParameterTypeValidator, ValidationKey
@@ -58,7 +58,6 @@
RemoveItemsCommand,
UpdateItemsCommand,
)
-from .spine_db_editor.widgets.multi_spine_db_editor import MultiSpineDBEditor
from .spine_db_icon_manager import SpineDBIconManager
from .spine_db_worker import SpineDBWorker
from .widgets.options_dialog import OptionsDialog
@@ -114,6 +113,7 @@ def __init__(self, settings, parent, synchronous=False):
super().__init__(parent)
self.qsettings = settings
self._db_maps = {}
+ self.name_registry = NameRegistry(self)
self._workers = {}
self.listeners = {}
self.undo_stack = {}
@@ -196,7 +196,7 @@ def register_fetch_parent(self, db_map, parent):
worker.register_fetch_parent(parent)
def can_fetch_more(self, db_map, parent):
- """Whether or not we can fetch more items of given type from given db.
+ """Whether we can fetch more items of given type from given db.
Args:
db_map (DatabaseMapping)
@@ -333,6 +333,7 @@ def close_session(self, url):
worker.clean_up()
del self._validated_values["parameter_definition"][id(db_map)]
del self._validated_values["parameter_value"][id(db_map)]
+ self.undo_stack[db_map].cleanChanged.disconnect()
del self.undo_stack[db_map]
del self.undo_action[db_map]
del self.redo_action[db_map]
@@ -342,15 +343,13 @@ def close_all_sessions(self):
for url in list(self._db_maps):
self.close_session(url)
- def get_db_map(self, url, logger, window=False, codename=None, create=False, force_upgrade_prompt=False):
+ def get_db_map(self, url, logger, create=False, force_upgrade_prompt=False):
"""Returns a DatabaseMapping instance from url if possible, None otherwise.
If needed, asks the user to upgrade to the latest db version.
Args:
url (str, URL)
logger (LoggerInterface)
- window (bool)
- codename (str, optional)
create (bool)
force_upgrade_prompt (bool)
@@ -360,8 +359,6 @@ def get_db_map(self, url, logger, window=False, codename=None, create=False, for
url = str(url)
db_map = self._db_maps.get(url)
if db_map is not None:
- if not window and codename is not None and db_map.codename != codename:
- return None
return db_map
try:
prompt_data = DatabaseMapping.get_upgrade_db_prompt_data(url, create=create)
@@ -380,7 +377,7 @@ def get_db_map(self, url, logger, window=False, codename=None, create=False, for
return None
else:
kwargs = {}
- kwargs.update(codename=codename, create=create)
+ kwargs["create"] = create
try:
return self._do_get_db_map(url, **kwargs)
except SpineDBAPIError as err:
@@ -390,13 +387,10 @@ def get_db_map(self, url, logger, window=False, codename=None, create=False, for
@busy_effect
def _do_get_db_map(self, url, **kwargs):
"""Returns a memorized DatabaseMapping instance from url.
- Called by `get_db_map`.
Args:
url (str, URL)
- codename (str, NoneType)
- upgrade (bool)
- create (bool)
+ **kwargs: arguments passed to worker's get_db_map()
Returns:
DatabaseMapping
@@ -1648,7 +1642,7 @@ def export_to_sqlite(self, file_path, data_for_export, caller):
try:
db_map.commit_session("Export data from Spine Toolbox.")
except SpineDBAPIError as err:
- error_msg = f"[SpineDBAPIError] Unable to export file {db_map.codename}: {err.msg}"
+ error_msg = f"[SpineDBAPIError] Unable to export file {file_path}: {err.msg}"
caller.msg_error.emit(error_msg)
else:
caller.file_exported.emit(file_path, 1.0, True)
@@ -1700,63 +1694,6 @@ def get_items_for_commit(self, db_map, commit_id):
return {}
return worker.commit_cache.get(commit_id.db_id, {})
- @staticmethod
- def get_all_multi_spine_db_editors():
- """Yields all instances of MultiSpineDBEditor currently open.
-
- Yields:
- MultiSpineDBEditor
- """
- for window in qApp.topLevelWindows(): # pylint: disable=undefined-variable
- if isinstance(window, QWindow):
- widget = QWidget.find(window.winId())
- if isinstance(widget, MultiSpineDBEditor) and widget.accepting_new_tabs:
- yield widget
-
- def get_all_spine_db_editors(self):
- """Yields all instances of SpineDBEditor currently open.
-
- Yields:
- SpineDBEditor
- """
- for w in self.get_all_multi_spine_db_editors():
- for k in range(w.tab_widget.count()):
- yield w.tab_widget.widget(k)
-
- def _get_existing_spine_db_editor(self, db_url_codenames):
- db_url_codenames = {str(url): codename for url, codename in db_url_codenames.items()}
- for multi_db_editor in self.get_all_multi_spine_db_editors():
- for k in range(multi_db_editor.tab_widget.count()):
- db_editor = multi_db_editor.tab_widget.widget(k)
- if db_editor.db_url_codenames == db_url_codenames:
- return multi_db_editor, db_editor
- return None
-
- def open_db_editor(self, db_url_codenames, reuse_existing_editor):
- """Opens a SpineDBEditor with given urls. Uses an existing MultiSpineDBEditor if any.
- Also, if the same urls are open in an existing SpineDBEditor, just raises that one
- instead of creating another.
-
- Args:
- db_url_codenames (dict): mapping url to codename
- reuse_existing_editor (bool): if True and the same URL is already open, just raise the existing window
- """
- multi_db_editor = next(self.get_all_multi_spine_db_editors(), None) if reuse_existing_editor else None
- if multi_db_editor is None:
- multi_db_editor = MultiSpineDBEditor(self, db_url_codenames)
- if multi_db_editor.tab_load_success: # don't open an editor if tabs were not loaded successfully
- multi_db_editor.show()
- return
- existing = self._get_existing_spine_db_editor(db_url_codenames)
- if existing is None:
- multi_db_editor.add_new_tab(db_url_codenames)
- else:
- multi_db_editor, db_editor = existing
- multi_db_editor.set_current_tab(db_editor)
- if multi_db_editor.isMinimized():
- multi_db_editor.showNormal()
- multi_db_editor.activateWindow()
-
@Slot(ValidationKey, bool)
def _parameter_value_validated(self, key, is_valid):
with suppress(KeyError):
diff --git a/spinetoolbox/ui_main.py b/spinetoolbox/ui_main.py
index a89261653..09fc15329 100644
--- a/spinetoolbox/ui_main.py
+++ b/spinetoolbox/ui_main.py
@@ -1329,7 +1329,7 @@ def open_specification_file(self, index):
@Slot(bool)
def new_db_editor(self):
- editor = MultiSpineDBEditor(self.db_mngr, {})
+ editor = MultiSpineDBEditor(self.db_mngr, [])
editor.show()
@Slot()
diff --git a/spinetoolbox/widgets/multi_tab_window.py b/spinetoolbox/widgets/multi_tab_window.py
index dd4baa7eb..9c0d332b3 100644
--- a/spinetoolbox/widgets/multi_tab_window.py
+++ b/spinetoolbox/widgets/multi_tab_window.py
@@ -70,7 +70,7 @@ def _make_new_tab(self, *args, **kwargs):
"""Creates a new tab.
Args:
- *args: positional arguments neede to make a new tab
+ *args: positional arguments needed to make a new tab
**kwargs: keyword arguments needed to make a new tab
"""
raise NotImplementedError()
diff --git a/spinetoolbox/widgets/settings_widget.py b/spinetoolbox/widgets/settings_widget.py
index 384471734..c6ff014a7 100644
--- a/spinetoolbox/widgets/settings_widget.py
+++ b/spinetoolbox/widgets/settings_widget.py
@@ -39,6 +39,7 @@
from ..kernel_fetcher import KernelFetcher
from ..link import JumpLink, Link
from ..project_item_icon import ProjectItemIcon
+from ..spine_db_editor.editors import db_editor_registry
from ..widgets.kernel_editor import MiniJuliaKernelEditor, MiniPythonKernelEditor
from .add_up_spine_opt_wizard import AddUpSpineOptWizard
from .install_julia_wizard import InstallJuliaWizard
@@ -234,7 +235,7 @@ def update_ui(self):
@Slot(bool)
def set_hide_empty_classes(self, checked=False):
- for db_editor in self.db_mngr.get_all_spine_db_editors():
+ for db_editor in db_editor_registry.tabs():
db_editor.entity_tree_model.hide_empty_classes = checked
@Slot(bool)
@@ -266,7 +267,7 @@ def set_neg_weight_exp(self, value=None):
self._set_graph_property("neg_weight_exp", value)
def _set_graph_property(self, name, value):
- for db_editor in self.db_mngr.get_all_spine_db_editors():
+ for db_editor in db_editor_registry.tabs():
db_editor.ui.graphicsView.set_property(name, value)
diff --git a/tests/project_item/test_logging_connection.py b/tests/project_item/test_logging_connection.py
index 0afa3c13e..69c67a83e 100644
--- a/tests/project_item/test_logging_connection.py
+++ b/tests/project_item/test_logging_connection.py
@@ -103,9 +103,7 @@ def setUp(self):
project.add_item(store_2)
self._db_mngr_logger = MagicMock()
self._url = "sqlite:///" + str(Path(self._temp_dir.name, "test_database.sqlite"))
- self._db_map = self._toolbox.db_mngr.get_db_map(
- self._url, self._db_mngr_logger, codename="database", create=True
- )
+ self._db_map = self._toolbox.db_mngr.get_db_map(self._url, self._db_mngr_logger, create=True)
def tearDown(self):
clean_up_toolbox(self._toolbox)
diff --git a/tests/spine_db_editor/helpers.py b/tests/spine_db_editor/helpers.py
index 658e77105..564885316 100644
--- a/tests/spine_db_editor/helpers.py
+++ b/tests/spine_db_editor/helpers.py
@@ -40,8 +40,9 @@ def _common_setup(self, url, create):
mock_settings.value.side_effect = lambda *args, **kwargs: 0
self._db_mngr = TestSpineDBManager(mock_settings, None)
logger = mock.MagicMock()
- self._db_map = self._db_mngr.get_db_map(url, logger, codename=self.db_codename, create=create)
- self._db_editor = SpineDBEditor(self._db_mngr, {url: self.db_codename})
+ self._db_map = self._db_mngr.get_db_map(url, logger, create=create)
+ self._db_mngr.name_registry.register(url, self.db_codename)
+ self._db_editor = SpineDBEditor(self._db_mngr, [url])
QApplication.processEvents()
def _common_tear_down(self):
diff --git a/tests/spine_db_editor/mvcmodels/test_alternative_model.py b/tests/spine_db_editor/mvcmodels/test_alternative_model.py
index 2471f0cc3..7f3360f85 100644
--- a/tests/spine_db_editor/mvcmodels/test_alternative_model.py
+++ b/tests/spine_db_editor/mvcmodels/test_alternative_model.py
@@ -30,9 +30,10 @@ def setUp(self):
app_settings = MagicMock()
logger = MagicMock()
self._db_mngr = TestSpineDBManager(app_settings, None)
- self._db_map = self._db_mngr.get_db_map("sqlite://", logger, codename=self.db_codename, create=True)
+ self._db_map = self._db_mngr.get_db_map("sqlite://", logger, create=True)
+ self._db_mngr.name_registry.register(self._db_map.sa_url, self.db_codename)
with patch("spinetoolbox.spine_db_editor.widgets.spine_db_editor.SpineDBEditor.restore_ui"):
- self._db_editor = SpineDBEditor(self._db_mngr, {"sqlite://": self.db_codename})
+ self._db_editor = SpineDBEditor(self._db_mngr)
def tearDown(self):
with (
@@ -129,11 +130,13 @@ def setUp(self):
app_settings = MagicMock()
logger = MagicMock()
self._db_mngr = TestSpineDBManager(app_settings, None)
- self._db_map1 = self._db_mngr.get_db_map("sqlite://", logger, codename="test_db_1", create=True)
+ self._db_map1 = self._db_mngr.get_db_map("sqlite://", logger, create=True)
url2 = "sqlite:///" + str(Path(self._temp_dir.name, "db2.sqlite"))
- self._db_map2 = self._db_mngr.get_db_map(url2, logger, codename=self.db_codename, create=True)
+ self._db_map2 = self._db_mngr.get_db_map(url2, logger, create=True)
+ self._db_mngr.name_registry.register(self._db_map1.sa_url, "test_db_1")
+ self._db_mngr.name_registry.register(self._db_map2.sa_url, self.db_codename)
with patch("spinetoolbox.spine_db_editor.widgets.spine_db_editor.SpineDBEditor.restore_ui"):
- self._db_editor = SpineDBEditor(self._db_mngr, {"sqlite://": "test_db_1", url2: self.db_codename})
+ self._db_editor = SpineDBEditor(self._db_mngr)
def tearDown(self):
with (
diff --git a/tests/spine_db_editor/mvcmodels/test_emptyParameterModels.py b/tests/spine_db_editor/mvcmodels/test_emptyParameterModels.py
index 8ac64dc3e..9c9577cfd 100644
--- a/tests/spine_db_editor/mvcmodels/test_emptyParameterModels.py
+++ b/tests/spine_db_editor/mvcmodels/test_emptyParameterModels.py
@@ -50,7 +50,8 @@ def setUp(self):
app_settings = mock.MagicMock()
logger = mock.MagicMock()
self._db_mngr = TestSpineDBManager(app_settings, None)
- self._db_map = self._db_mngr.get_db_map("sqlite://", logger, codename="mock_db", create=True)
+ self._db_map = self._db_mngr.get_db_map("sqlite://", logger, create=True)
+ self._db_mngr.name_registry.register(self._db_map.sa_url, "mock_db")
import_object_classes(self._db_map, ("dog", "fish"))
import_object_parameters(self._db_map, (("dog", "breed"),))
import_objects(self._db_map, (("dog", "pluto"), ("fish", "nemo")))
diff --git a/tests/spine_db_editor/mvcmodels/test_frozen_table_model.py b/tests/spine_db_editor/mvcmodels/test_frozen_table_model.py
index 3da5f0d33..45ddf56b0 100644
--- a/tests/spine_db_editor/mvcmodels/test_frozen_table_model.py
+++ b/tests/spine_db_editor/mvcmodels/test_frozen_table_model.py
@@ -26,7 +26,8 @@ def setUp(self):
app_settings = MagicMock()
logger = MagicMock()
self._db_mngr = TestSpineDBManager(app_settings, None)
- self._db_map = self._db_mngr.get_db_map("sqlite://", logger, codename=self.db_codename, create=True)
+ self._db_map = self._db_mngr.get_db_map("sqlite://", logger, create=True)
+ self._db_mngr.name_registry.register(self._db_map.sa_url, self.db_codename)
self._parent = QObject()
self._model = FrozenTableModel(self._db_mngr, self._parent)
diff --git a/tests/spine_db_editor/mvcmodels/test_item_metadata_table_model.py b/tests/spine_db_editor/mvcmodels/test_item_metadata_table_model.py
index 7e8ebeae3..7db6ea771 100644
--- a/tests/spine_db_editor/mvcmodels/test_item_metadata_table_model.py
+++ b/tests/spine_db_editor/mvcmodels/test_item_metadata_table_model.py
@@ -78,7 +78,8 @@ def setUp(self):
mock_settings.value.side_effect = lambda *args, **kwargs: 0
self._db_mngr = TestSpineDBManager(mock_settings, None)
logger = mock.MagicMock()
- self._db_map = self._db_mngr.get_db_map(self._url, logger, codename="database")
+ self._db_map = self._db_mngr.get_db_map(self._url, logger)
+ self._db_mngr.name_registry.register(self._db_map.sa_url, "database")
QApplication.processEvents()
self._db_map.fetch_all()
self._model = ItemMetadataTableModel(self._db_mngr, [self._db_map], None)
diff --git a/tests/spine_db_editor/mvcmodels/test_metadata_table_model.py b/tests/spine_db_editor/mvcmodels/test_metadata_table_model.py
index e49447bf6..8869c76d5 100644
--- a/tests/spine_db_editor/mvcmodels/test_metadata_table_model.py
+++ b/tests/spine_db_editor/mvcmodels/test_metadata_table_model.py
@@ -29,7 +29,8 @@ def setUp(self):
mock_settings.value.side_effect = lambda *args, **kwargs: 0
self._db_mngr = TestSpineDBManager(mock_settings, None)
logger = mock.MagicMock()
- self._db_map = self._db_mngr.get_db_map("sqlite://", logger, codename="database", create=True)
+ self._db_map = self._db_mngr.get_db_map("sqlite://", logger, create=True)
+ self._db_mngr.name_registry.register(self._db_map.sa_url, "database")
QApplication.processEvents()
self._model = MetadataTableModel(self._db_mngr, [self._db_map], None)
fetch_model(self._model)
@@ -94,7 +95,8 @@ def test_adding_data_to_another_database(self):
database_path = Path(temp_dir, "db.sqlite")
url = "sqlite:///" + str(database_path)
try:
- db_map_2 = self._db_mngr.get_db_map(url, logger, codename="2nd database", create=True)
+ db_map_2 = self._db_mngr.get_db_map(url, logger, create=True)
+ self._db_mngr.name_registry.register(url, "2nd database")
self._model.set_db_maps([self._db_map, db_map_2])
fetch_model(self._model)
index = self._model.index(1, Column.DB_MAP)
diff --git a/tests/spine_db_editor/mvcmodels/test_scenario_model.py b/tests/spine_db_editor/mvcmodels/test_scenario_model.py
index f789ffea3..ee61a6f95 100644
--- a/tests/spine_db_editor/mvcmodels/test_scenario_model.py
+++ b/tests/spine_db_editor/mvcmodels/test_scenario_model.py
@@ -42,7 +42,8 @@ def setUp(self):
app_settings = MagicMock()
logger = MagicMock()
self._db_mngr = TestSpineDBManager(app_settings, None)
- self._db_map = self._db_mngr.get_db_map("sqlite://", logger, codename=self.db_codename, create=True)
+ self._db_map = self._db_mngr.get_db_map("sqlite://", logger, create=True)
+ self._db_mngr.name_registry.register(self._db_map.db_url, self.db_codename)
with patch("spinetoolbox.spine_db_editor.widgets.spine_db_editor.SpineDBEditor.restore_ui"):
self._db_editor = SpineDBEditor(self._db_mngr, {"sqlite://": self.db_codename})
@@ -433,9 +434,11 @@ def setUp(self):
app_settings = MagicMock()
logger = MagicMock()
self._db_mngr = TestSpineDBManager(app_settings, None)
- self._db_map1 = self._db_mngr.get_db_map("sqlite://", logger, codename="test_db_1", create=True)
+ self._db_map1 = self._db_mngr.get_db_map("sqlite://", logger, create=True)
+ self._db_mngr.name_registry.register(self._db_map1.sa_url, "test_db_1")
url2 = "sqlite:///" + str(Path(self._temp_dir.name, "db_2.sqlite"))
- self._db_map2 = self._db_mngr.get_db_map(url2, logger, codename="test_db_2", create=True)
+ self._db_map2 = self._db_mngr.get_db_map(url2, logger, create=True)
+ self._db_mngr.name_registry.register(self._db_map2.sa_url, "test_db_2")
with patch("spinetoolbox.spine_db_editor.widgets.spine_db_editor.SpineDBEditor.restore_ui"):
self._db_editor = SpineDBEditor(self._db_mngr, {"sqlite://": "test_db_1", url2: "test_db_2"})
diff --git a/tests/spine_db_editor/mvcmodels/test_single_parameter_models.py b/tests/spine_db_editor/mvcmodels/test_single_parameter_models.py
index 152d88a8c..acc4d692a 100644
--- a/tests/spine_db_editor/mvcmodels/test_single_parameter_models.py
+++ b/tests/spine_db_editor/mvcmodels/test_single_parameter_models.py
@@ -78,7 +78,8 @@ class TestSingleObjectParameterValueModel(TestCaseWithQApplication):
def setUp(self):
self._db_mngr = TestSpineDBManager(None, None)
self._logger = MagicMock()
- self._db_map = self._db_mngr.get_db_map("sqlite:///", self._logger, codename="Test database", create=True)
+ self._db_map = self._db_mngr.get_db_map("sqlite:///", self._logger, create=True)
+ self._db_mngr.name_registry.register(self._db_map.db_url, "Test database")
def tearDown(self):
self._db_mngr.close_all_sessions()
diff --git a/tests/spine_db_editor/test_graphics_items.py b/tests/spine_db_editor/test_graphics_items.py
index fdc3a3c1e..7c67c2978 100644
--- a/tests/spine_db_editor/test_graphics_items.py
+++ b/tests/spine_db_editor/test_graphics_items.py
@@ -32,7 +32,8 @@ def setUp(self):
mock_settings.value.side_effect = lambda *args, **kwargs: 0
self._db_mngr = TestSpineDBManager(mock_settings, None)
logger = mock.MagicMock()
- self._db_map = self._db_mngr.get_db_map("sqlite://", logger, codename="database", create=True)
+ self._db_map = self._db_mngr.get_db_map("sqlite://", logger, create=True)
+ self._db_mngr.name_registry.register(self._db_map.sa_url, "database")
self._spine_db_editor = SpineDBEditor(self._db_mngr, {"sqlite://": "database"})
self._spine_db_editor.pivot_table_model = mock.MagicMock()
self._db_mngr.add_entity_classes({self._db_map: [{"name": "oc", "id": 1}]})
diff --git a/tests/spine_db_editor/widgets/spine_db_editor_test_base.py b/tests/spine_db_editor/widgets/spine_db_editor_test_base.py
index 7e04af412..7b8de19d3 100644
--- a/tests/spine_db_editor/widgets/spine_db_editor_test_base.py
+++ b/tests/spine_db_editor/widgets/spine_db_editor_test_base.py
@@ -31,7 +31,8 @@ def setUp(self):
mock_settings.value.side_effect = lambda *args, **kwargs: 0
self.db_mngr = TestSpineDBManager(mock_settings, None)
logger = mock.MagicMock()
- self.mock_db_map = self.db_mngr.get_db_map("sqlite://", logger, codename=self.db_codename, create=True)
+ self.mock_db_map = self.db_mngr.get_db_map("sqlite://", logger, create=True)
+ self.db_mngr.name_registry.register("sqlite://", self.db_codename)
self.spine_db_editor = SpineDBEditor(self.db_mngr, {"sqlite://": self.db_codename})
self.spine_db_editor.pivot_table_model = mock.MagicMock()
self.spine_db_editor.entity_tree_model.hide_empty_classes = False
diff --git a/tests/spine_db_editor/widgets/test_SpineDBEditor.py b/tests/spine_db_editor/widgets/test_SpineDBEditor.py
index 3de9699f5..b142e51e7 100644
--- a/tests/spine_db_editor/widgets/test_SpineDBEditor.py
+++ b/tests/spine_db_editor/widgets/test_SpineDBEditor.py
@@ -263,7 +263,7 @@ def setUp(self):
mock_settings.value.side_effect = lambda *args, **kwargs: 0
self._db_mngr = TestSpineDBManager(mock_settings, None)
logger = mock.MagicMock()
- self._db_map = self._db_mngr.get_db_map("sqlite://", logger, codename="database", create=True)
+ self._db_map = self._db_mngr.get_db_map("sqlite://", logger, create=True)
def tearDown(self):
self._db_mngr.close_all_sessions()
diff --git a/tests/spine_db_editor/widgets/test_SpineDBEditorBase.py b/tests/spine_db_editor/widgets/test_SpineDBEditorBase.py
index c7dc5ceff..585bef02b 100644
--- a/tests/spine_db_editor/widgets/test_SpineDBEditorBase.py
+++ b/tests/spine_db_editor/widgets/test_SpineDBEditorBase.py
@@ -13,6 +13,7 @@
"""Contains unit tests for the SpineDBEditorBase class."""
import unittest
from unittest import mock
+from sqlalchemy.engine.url import make_url
from spinetoolbox.spine_db_editor.widgets.spine_db_editor import SpineDBEditorBase
from tests.mock_helpers import TestCaseWithQApplication, TestSpineDBManager
@@ -21,7 +22,7 @@ class TestSpineDBEditorBase(TestCaseWithQApplication):
def setUp(self):
"""Builds a SpineDBEditorBase object."""
with (
- mock.patch("spinetoolbox.spine_db_worker.DatabaseMapping") as mock_DiffDBMapping,
+ mock.patch("spinetoolbox.spine_db_worker.DatabaseMapping") as mock_DBMapping,
mock.patch("spinetoolbox.spine_db_editor.widgets.spine_db_editor.SpineDBEditor.restore_ui"),
mock.patch("spinetoolbox.spine_db_editor.widgets.spine_db_editor.SpineDBEditor.show"),
):
@@ -29,14 +30,15 @@ def setUp(self):
mock_settings.value.side_effect = lambda *args, **kwards: 0
self.db_mngr = TestSpineDBManager(mock_settings, None)
- def DiffDBMapping_side_effect(url, codename=None, upgrade=False, create=False):
+ def DBMapping_side_effect(url, upgrade=False, create=False):
mock_db_map = mock.MagicMock()
- mock_db_map.codename = codename
mock_db_map.db_url = url
+ mock_db_map.sa_url = make_url(url)
return mock_db_map
- mock_DiffDBMapping.side_effect = DiffDBMapping_side_effect
+ mock_DBMapping.side_effect = DBMapping_side_effect
self.db_editor = SpineDBEditorBase(self.db_mngr)
+ self.db_editor.connect_signals()
def tearDown(self):
"""Frees resources after each test."""
diff --git a/tests/spine_db_editor/widgets/test_SpineDBEditorWithDBMapping.py b/tests/spine_db_editor/widgets/test_SpineDBEditorWithDBMapping.py
index 0a3ef06e2..52bf1457b 100644
--- a/tests/spine_db_editor/widgets/test_SpineDBEditorWithDBMapping.py
+++ b/tests/spine_db_editor/widgets/test_SpineDBEditorWithDBMapping.py
@@ -34,8 +34,9 @@ def setUp(self):
mock_settings.value.side_effect = lambda *args, **kwards: 0
self.db_mngr = TestSpineDBManager(mock_settings, None)
logger = mock.MagicMock()
- self.db_map = self.db_mngr.get_db_map(url, logger, codename="db", create=True)
- self.spine_db_editor = SpineDBEditor(self.db_mngr, {url: "db"})
+ self.db_map = self.db_mngr.get_db_map(url, logger, create=True)
+ self.spine_db_editor = SpineDBEditor(self.db_mngr, [url])
+ self.db_mngr.name_registry.register(self.db_map.sa_url, "db")
self.spine_db_editor.pivot_table_model = mock.MagicMock()
def tearDown(self):
diff --git a/tests/spine_db_editor/widgets/test_add_items_dialog.py b/tests/spine_db_editor/widgets/test_add_items_dialog.py
index 5d86717b7..7b7afdf96 100644
--- a/tests/spine_db_editor/widgets/test_add_items_dialog.py
+++ b/tests/spine_db_editor/widgets/test_add_items_dialog.py
@@ -37,7 +37,8 @@ def setUp(self):
logger = mock.MagicMock()
self._temp_dir = TemporaryDirectory()
url = "sqlite:///" + self._temp_dir.name + "/db.sqlite"
- self._db_map = self._db_mngr.get_db_map(url, logger, codename="mock_db", create=True)
+ self._db_map = self._db_mngr.get_db_map(url, logger, create=True)
+ self._db_mngr.name_registry.register(url, "mock_db")
self._db_editor = SpineDBEditor(self._db_mngr, {url: "mock_db"})
def tearDown(self):
diff --git a/tests/spine_db_editor/widgets/test_commit_viewer.py b/tests/spine_db_editor/widgets/test_commit_viewer.py
index 51a4b4f2f..61d096589 100644
--- a/tests/spine_db_editor/widgets/test_commit_viewer.py
+++ b/tests/spine_db_editor/widgets/test_commit_viewer.py
@@ -30,7 +30,7 @@ def setUp(self):
self._db_mngr = SpineDBManager(mock_settings, None, synchronous=True)
logger = mock.MagicMock()
url = "sqlite://"
- self._db_map = self._db_mngr.get_db_map(url, logger, codename="mock_db", create=True)
+ self._db_map = self._db_mngr.get_db_map(url, logger, create=True)
with mock.patch.object(QSplitter, "restoreState"):
self._commit_viewer = CommitViewer(mock_settings, self._db_mngr, self._db_map)
diff --git a/tests/spine_db_editor/widgets/test_custom_menus.py b/tests/spine_db_editor/widgets/test_custom_menus.py
index dc9b0d2cd..7463d814f 100644
--- a/tests/spine_db_editor/widgets/test_custom_menus.py
+++ b/tests/spine_db_editor/widgets/test_custom_menus.py
@@ -14,8 +14,9 @@
import unittest
from unittest import mock
from PySide6.QtWidgets import QWidget
+from spinetoolbox.database_display_names import NameRegistry
from spinetoolbox.helpers import signal_waiter
-from spinetoolbox.spine_db_editor.widgets.custom_menus import TabularViewCodenameFilterMenu
+from spinetoolbox.spine_db_editor.widgets.custom_menus import TabularViewDatabaseNameFilterMenu
from tests.mock_helpers import TestCaseWithQApplication
@@ -28,11 +29,14 @@ def tearDown(self):
def test_init_fills_filter_list_with_database_codenames(self):
db_map1 = mock.MagicMock()
- db_map1.codename = "db map 1"
+ db_map1.sa_url = "sqlite://a"
db_map2 = mock.MagicMock()
- db_map2.codename = "db map 2"
+ db_map2.sa_url = "sqlite://b"
db_maps = [db_map1, db_map2]
- menu = TabularViewCodenameFilterMenu(self._parent, db_maps, "database")
+ name_registry = NameRegistry()
+ name_registry.register(db_map1.sa_url, "db map 1")
+ name_registry.register(db_map2.sa_url, "db map 2")
+ menu = TabularViewDatabaseNameFilterMenu(self._parent, db_maps, "database", name_registry)
self.assertIs(menu.anchor, self._parent)
filter_list_model = menu._filter._filter_model
filter_rows = []
@@ -42,11 +46,14 @@ def test_init_fills_filter_list_with_database_codenames(self):
def test_filter_changed_signal_is_emitted_correctly(self):
db_map1 = mock.MagicMock()
- db_map1.codename = "db map 1"
+ db_map1.sa_url = "sqlite://a"
db_map2 = mock.MagicMock()
- db_map2.codename = "db map 2"
+ db_map2.sa_url = "sqlite://b"
db_maps = [db_map1, db_map2]
- menu = TabularViewCodenameFilterMenu(self._parent, db_maps, "database")
+ name_registry = NameRegistry()
+ name_registry.register(db_map1.sa_url, "db map 1")
+ name_registry.register(db_map2.sa_url, "db map 2")
+ menu = TabularViewDatabaseNameFilterMenu(self._parent, db_maps, "database", name_registry)
with signal_waiter(menu.filterChanged, timeout=0.1) as waiter:
menu._clear_filter()
waiter.wait()
diff --git a/tests/spine_db_editor/widgets/test_mass_select_items_dialogs.py b/tests/spine_db_editor/widgets/test_mass_select_items_dialogs.py
index af08dd5f8..b66b2def2 100644
--- a/tests/spine_db_editor/widgets/test_mass_select_items_dialogs.py
+++ b/tests/spine_db_editor/widgets/test_mass_select_items_dialogs.py
@@ -30,7 +30,7 @@ def setUp(self):
mock_settings.value.side_effect = lambda *args, **kwargs: 0
self._db_mngr = SpineDBManager(mock_settings, None)
logger = mock.MagicMock()
- self._db_map = self._db_mngr.get_db_map(url, logger, codename="database", create=True)
+ self._db_map = self._db_mngr.get_db_map(url, logger, create=True)
QApplication.processEvents()
def tearDown(self):
diff --git a/tests/spine_db_editor/widgets/test_multi_spine_db_editor.py b/tests/spine_db_editor/widgets/test_multi_spine_db_editor.py
index f920bbfb3..2a13bf2ed 100644
--- a/tests/spine_db_editor/widgets/test_multi_spine_db_editor.py
+++ b/tests/spine_db_editor/widgets/test_multi_spine_db_editor.py
@@ -11,10 +11,15 @@
######################################################################################################################
"""Unit tests for SpineDBEditor classes."""
+from pathlib import Path
from tempfile import TemporaryDirectory
-from PySide6.QtCore import QPoint
-from spinetoolbox.spine_db_editor.widgets.multi_spine_db_editor import MultiSpineDBEditor
-from tests.mock_helpers import FakeDataStore, clean_up_toolbox, create_toolboxui_with_project
+from unittest.mock import MagicMock, patch
+from PySide6.QtCore import QPoint, QSettings
+from PySide6.QtWidgets import QApplication
+from spinetoolbox.multi_tab_windows import MultiTabWindowRegistry
+from spinetoolbox.spine_db_editor.widgets.multi_spine_db_editor import MultiSpineDBEditor, open_db_editor
+from spinetoolbox.spine_db_manager import SpineDBManager
+from tests.mock_helpers import FakeDataStore, TestCaseWithQApplication, clean_up_toolbox, create_toolboxui_with_project
from .spine_db_editor_test_base import DBEditorTestBase
@@ -32,7 +37,7 @@ def tearDown(self):
def test_multi_spine_db_editor(self):
self.db_mngr.setParent(self._toolbox)
multieditor = MultiSpineDBEditor(self.db_mngr)
- multieditor.add_new_tab()
+ multieditor.add_new_tab([])
self.assertEqual(1, multieditor.tab_widget.count())
multieditor.make_context_menu(0)
multieditor.show_plus_button_context_menu(QPoint(0, 0))
@@ -40,3 +45,70 @@ def test_multi_spine_db_editor(self):
self._toolbox.project()._project_items = {"a": FakeDataStore("a")}
multieditor.show_plus_button_context_menu(QPoint(0, 0))
multieditor._take_tab(0)
+
+
+class TestOpenDBEditor(TestCaseWithQApplication):
+ def setUp(self):
+ self._temp_dir = TemporaryDirectory()
+ db_path = Path(self._temp_dir.name, "db.sqlite")
+ self._db_url = "sqlite:///" + str(db_path)
+ self._db_mngr = SpineDBManager(QSettings(), None)
+ self._logger = MagicMock()
+ self._db_editor_registry = MultiTabWindowRegistry()
+
+ def tearDown(self):
+ self._db_mngr.close_all_sessions()
+ self._db_mngr.clean_up()
+ # Database connection may still be open. Retry cleanup until it succeeds.
+ running = True
+ while running:
+ QApplication.processEvents()
+ try:
+ self._temp_dir.cleanup()
+ except NotADirectoryError:
+ pass
+ else:
+ running = False
+
+ def _close_windows(self):
+ for editor in self._db_editor_registry.windows():
+ QApplication.processEvents()
+ editor.close()
+ self.assertFalse(self._db_editor_registry.has_windows())
+
+ def test_open_db_editor(self):
+ with (
+ patch(
+ "spinetoolbox.spine_db_editor.widgets.multi_spine_db_editor.db_editor_registry",
+ self._db_editor_registry,
+ ),
+ patch("spinetoolbox.spine_db_editor.widgets.multi_spine_db_editor.MultiSpineDBEditor.show") as mock_show,
+ ):
+ self.assertFalse(self._db_editor_registry.has_windows())
+ open_db_editor([self._db_url], self._db_mngr, reuse_existing_editor=True)
+ mock_show.assert_called_once()
+ self.assertEqual(len(self._db_editor_registry.windows()), 1)
+ open_db_editor([self._db_url], self._db_mngr, reuse_existing_editor=True)
+ self.assertEqual(len(self._db_editor_registry.windows()), 1)
+ editor = self._db_editor_registry.windows()[0]
+ self.assertEqual(editor.tab_widget.count(), 1)
+ self._close_windows()
+
+ def test_open_db_in_tab_when_editor_has_an_empty_tab(self):
+ with (
+ patch(
+ "spinetoolbox.spine_db_editor.widgets.multi_spine_db_editor.db_editor_registry",
+ self._db_editor_registry,
+ ),
+ patch("spinetoolbox.spine_db_editor.widgets.multi_spine_db_editor.MultiSpineDBEditor.show") as mock_show,
+ ):
+ self.assertFalse(self._db_editor_registry.has_windows())
+ window = MultiSpineDBEditor(self._db_mngr, [])
+ self.assertEqual(window.tab_widget.count(), 1)
+ tab = window.tab_widget.widget(0)
+ self.assertEqual(tab.db_urls, [])
+ open_db_editor([self._db_url], self._db_mngr, reuse_existing_editor=True)
+ self.assertEqual(window.tab_widget.count(), 2)
+ tab = window.tab_widget.widget(1)
+ self.assertEqual(tab.db_urls, [self._db_url])
+ self._close_windows()
diff --git a/tests/test_SpineDBManager.py b/tests/test_SpineDBManager.py
index fc908e693..3f13f7401 100644
--- a/tests/test_SpineDBManager.py
+++ b/tests/test_SpineDBManager.py
@@ -318,7 +318,8 @@ def setUp(self):
self.editor = MagicMock()
self._temp_dir = TemporaryDirectory()
url = "sqlite:///" + self._temp_dir.name + "/db.sqlite"
- self._db_map = self._db_mngr.get_db_map(url, logger, codename="database", create=True)
+ self._db_map = self._db_mngr.get_db_map(url, logger, create=True)
+ self._db_mngr.name_registry.register(url, "test_import_export_data_db")
def tearDown(self):
self._db_mngr.close_all_sessions()
@@ -402,52 +403,6 @@ def test_import_parameter_value_lists(self):
)
-class TestOpenDBEditor(TestCaseWithQApplication):
- def setUp(self):
- self._temp_dir = TemporaryDirectory()
- db_path = Path(self._temp_dir.name, "db.sqlite")
- self._db_url = "sqlite:///" + str(db_path)
- self._db_mngr = SpineDBManager(QSettings(), None)
- self._logger = MagicMock()
-
- @unittest.skip("FIXME")
- def test_open_db_editor(self):
- editors = list(self._db_mngr.get_all_multi_spine_db_editors())
- self.assertFalse(editors)
- self._db_mngr.open_db_editor({self._db_url: "test"}, reuse_existing_editor=True)
- editors = list(self._db_mngr.get_all_multi_spine_db_editors())
- self.assertEqual(len(editors), 1)
- self._db_mngr.open_db_editor({self._db_url: "test"}, reuse_existing_editor=True)
- editors = list(self._db_mngr.get_all_multi_spine_db_editors())
- self.assertEqual(len(editors), 1)
- self._db_mngr.open_db_editor({self._db_url: "not_the_same"}, reuse_existing_editor=True)
- self.assertEqual(len(editors), 1)
- editor = editors[0]
- self.assertEqual(editor.tab_widget.count(), 1)
- # Finally try to open the first tab again
- self._db_mngr.open_db_editor({self._db_url: "test"}, reuse_existing_editor=True)
- editors = list(self._db_mngr.get_all_multi_spine_db_editors())
- editor = editors[0]
- self.assertEqual(editor.tab_widget.count(), 1)
- for editor in self._db_mngr.get_all_multi_spine_db_editors():
- QApplication.processEvents()
- editor.close()
-
- def tearDown(self):
- self._db_mngr.close_all_sessions()
- self._db_mngr.clean_up()
- # Database connection may still be open. Retry cleanup until it succeeds.
- running = True
- while running:
- QApplication.processEvents()
- try:
- self._temp_dir.cleanup()
- except NotADirectoryError:
- pass
- else:
- running = False
-
-
class TestDuplicateEntity(TestCaseWithQApplication):
@classmethod
def setUpClass(cls):
@@ -457,7 +412,8 @@ def setUpClass(cls):
def setUp(self):
self._db_mngr = SpineDBManager(QSettings(), None)
logger = MagicMock()
- self._db_map = self._db_mngr.get_db_map("sqlite://", logger, codename=self.db_codename, create=True)
+ self._db_map = self._db_mngr.get_db_map("sqlite://", logger, create=True)
+ self._db_mngr.name_registry.register(self._db_map.sa_url, self.db_codename)
def tearDown(self):
self._db_mngr.close_all_sessions()
@@ -526,7 +482,8 @@ def setUp(self):
mock_settings.value.side_effect = lambda *args, **kwargs: 0
self._db_mngr = SpineDBManager(mock_settings, None)
self._logger = MagicMock()
- self._db_map = self._db_mngr.get_db_map("sqlite://", self._logger, codename="database", create=True)
+ self._db_map = self._db_mngr.get_db_map("sqlite://", self._logger, create=True)
+ self._db_mngr.name_registry.register(self._db_map.sa_url, "test_update_expanded_parameter_values_db")
def tearDown(self):
self._db_mngr.close_all_sessions()
@@ -571,7 +528,8 @@ def setUp(self):
mock_settings.value.side_effect = lambda *args, **kwargs: 0
self._db_mngr = SpineDBManager(mock_settings, None)
self._logger = MagicMock()
- self._db_map = self._db_mngr.get_db_map("sqlite://", self._logger, codename="database", create=True)
+ self._db_map = self._db_mngr.get_db_map("sqlite://", self._logger, create=True)
+ self._db_mngr.name_registry.register(self._db_map.sa_url, "test_remove_scenario_alternative_db")
def tearDown(self):
self._db_mngr.close_all_sessions()
diff --git a/tests/test_database_display_names.py b/tests/test_database_display_names.py
new file mode 100644
index 000000000..0697dc219
--- /dev/null
+++ b/tests/test_database_display_names.py
@@ -0,0 +1,102 @@
+######################################################################################################################
+# Copyright (C) 2017-2022 Spine project consortium
+# Copyright Spine Toolbox contributors
+# This file is part of Spine Toolbox.
+# Spine Toolbox is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General
+# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option)
+# any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General
+# Public License for more details. You should have received a copy of the GNU Lesser General Public License along with
+# this program. If not, see .
+######################################################################################################################
+import sys
+import unittest
+from unittest import mock
+from sqlalchemy.engine.url import make_url
+from spinetoolbox.database_display_names import NameRegistry, suggest_display_name
+from spinetoolbox.helpers import signal_waiter
+from tests.mock_helpers import TestCaseWithQApplication
+
+
+class TestNameRegistry(TestCaseWithQApplication):
+ def test_display_name_for_unregistered_url(self):
+ registry = NameRegistry()
+ self.assertEqual(registry.display_name("mysql://db.example.com/best_database"), "best_database")
+ sa_url = make_url("mysql://db.example.com/even_better_database")
+ self.assertEqual(registry.display_name(sa_url), "even_better_database")
+
+ def test_display_name_for_registered_url(self):
+ registry = NameRegistry()
+ url = "mysql://db.example.com/best_database"
+ registry.register(url, "Best database")
+ self.assertEqual(registry.display_name(url), "Best database")
+ sa_url = make_url("mysql://db.example.com/even_better_database")
+ registry.register(sa_url, "Even better database")
+ self.assertEqual(registry.display_name(sa_url), "Even better database")
+
+ def test_multiple_registered_names_gives_simple_database_name(self):
+ registry = NameRegistry()
+ url = "mysql://db.example.com/best_database"
+ with signal_waiter(registry.display_name_changed, timeout=0.1) as waiter:
+ registry.register(url, "Best database")
+ self.assertEqual(waiter.args, (url, "Best database"))
+ with signal_waiter(registry.display_name_changed, timeout=0.1) as waiter:
+ registry.register(url, "Even better database")
+ self.assertEqual(waiter.args, (url, "best_database"))
+ self.assertEqual(registry.display_name(url), "best_database")
+
+ def test_unregister(self):
+ registry = NameRegistry()
+ url = "mysql://db.example.com/best_database"
+ with signal_waiter(registry.display_name_changed, timeout=0.1) as waiter:
+ registry.register(url, "Best database")
+ self.assertEqual(waiter.args, (url, "Best database"))
+ self.assertEqual(registry.display_name(url), "Best database")
+ with signal_waiter(registry.display_name_changed, timeout=0.1) as waiter:
+ registry.unregister(url, "Best database")
+ self.assertEqual(waiter.args, (url, "best_database"))
+ self.assertEqual(registry.display_name(url), "best_database")
+
+ def test_unregister_one_of_two_names(self):
+ registry = NameRegistry()
+ url = "mysql://db.example.com/best_database"
+ registry.register(url, "Database 1")
+ registry.register(url, "Database 2")
+ self.assertEqual(registry.display_name(url), "best_database")
+ with signal_waiter(registry.display_name_changed, timeout=0.1) as waiter:
+ registry.unregister(url, "Database 1")
+ self.assertEqual(waiter.args, (url, "Database 2"))
+ self.assertEqual(registry.display_name(url), "Database 2")
+
+ def test_unregister_one_of_three_names(self):
+ registry = NameRegistry()
+ url = "mysql://db.example.com/best_database"
+ registry.register(url, "Database 1")
+ registry.register(url, "Database 2")
+ registry.register(url, "Database 3")
+ self.assertEqual(registry.display_name(url), "best_database")
+ with mock.patch.object(registry, "display_name_changed") as name_changed_signal:
+ registry.unregister(url, "Database 3")
+ name_changed_signal.emit.assert_not_called()
+ self.assertEqual(registry.display_name(url), "best_database")
+
+
+class TestSuggestDisplayName(unittest.TestCase):
+ def test_mysql_url_returns_database_name(self):
+ sa_url = make_url("mysql://db.example.com/my_lovely_db")
+ self.assertEqual(suggest_display_name(sa_url), "my_lovely_db")
+
+ def test_sqlite_url_returns_file_name_without_extension(self):
+ path = "c:\path\to\my_lovely_db.sqlite" if sys.platform == "win32" else "/path/to/my_lovely_db.sqlite"
+ sa_url = make_url(r"sqlite:///" + path)
+ self.assertEqual(suggest_display_name(sa_url), "my_lovely_db")
+
+ def test_in_memory_sqlite_url_returns_random_hash(self):
+ sa_url = make_url(r"sqlite://")
+ name = suggest_display_name(sa_url)
+ self.assertTrue(isinstance(name, str))
+ self.assertTrue(bool(name))
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/tests/test_multi_tab_windows.py b/tests/test_multi_tab_windows.py
new file mode 100644
index 000000000..702002259
--- /dev/null
+++ b/tests/test_multi_tab_windows.py
@@ -0,0 +1,56 @@
+######################################################################################################################
+# Copyright (C) 2017-2022 Spine project consortium
+# Copyright Spine Toolbox contributors
+# This file is part of Spine Toolbox.
+# Spine Toolbox is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General
+# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option)
+# any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General
+# Public License for more details. You should have received a copy of the GNU Lesser General Public License along with
+# this program. If not, see .
+######################################################################################################################
+import unittest
+from unittest import mock
+from spinetoolbox.multi_tab_windows import MultiTabWindowRegistry
+
+
+class TestMultiTabWindowRegistry(unittest.TestCase):
+ def test_initialization(self):
+ registry = MultiTabWindowRegistry()
+ self.assertFalse(registry.has_windows())
+ self.assertEqual(registry.windows(), [])
+ self.assertEqual(registry.tabs(), [])
+ self.assertIsNone(registry.get_some_window())
+
+ def test_register_window(self):
+ registry = MultiTabWindowRegistry()
+ window = mock.MagicMock()
+ registry.register_window(window)
+ self.assertEqual(registry.windows(), [window])
+
+ def test_unregister_window(self):
+ registry = MultiTabWindowRegistry()
+ window = mock.MagicMock()
+ registry.register_window(window)
+ self.assertTrue(registry.has_windows())
+ registry.unregister_window(window)
+ self.assertEqual(registry.windows(), [])
+
+ def test_get_some_window(self):
+ registry = MultiTabWindowRegistry()
+ window = mock.MagicMock()
+ registry.register_window(window)
+ self.assertIs(registry.get_some_window(), window)
+
+ def test_tabs(self):
+ registry = MultiTabWindowRegistry()
+ window = mock.MagicMock()
+ window.tab_widget.count.return_value = 1
+ tab = mock.MagicMock()
+ window.tab_widget.widget.return_value = tab
+ registry.register_window(window)
+ self.assertEqual(registry.tabs(), [tab])
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/tests/test_parameter_type_validation.py b/tests/test_parameter_type_validation.py
index 5aca06561..8f4c20db9 100644
--- a/tests/test_parameter_type_validation.py
+++ b/tests/test_parameter_type_validation.py
@@ -29,7 +29,8 @@ def setUp(self):
mock_settings.value.side_effect = lambda *args, **kwargs: 0
self._db_mngr = TestSpineDBManager(mock_settings, None)
logger = mock.MagicMock()
- self._db_map = self._db_mngr.get_db_map("sqlite://", logger, codename=self.db_codename, create=True)
+ self._db_map = self._db_mngr.get_db_map("sqlite://", logger, create=True)
+ self._db_mngr.name_registry.register(self._db_map.sa_url, self.db_codename)
self._db_mngr.parameter_type_validator.set_interval(0)
def tearDown(self):
diff --git a/tests/test_spine_db_fetcher.py b/tests/test_spine_db_fetcher.py
index 4990c6980..99d8748c3 100644
--- a/tests/test_spine_db_fetcher.py
+++ b/tests/test_spine_db_fetcher.py
@@ -31,7 +31,8 @@ def setUp(self):
app_settings = MagicMock()
self._logger = MagicMock() # Collects error messages therefore handy for debugging.
self._db_mngr = TestSpineDBManager(app_settings, None)
- self._db_map = self._db_mngr.get_db_map("sqlite://", self._logger, codename="db_fetcher_test_db", create=True)
+ self._db_map = self._db_mngr.get_db_map("sqlite://", self._logger, create=True)
+ self._db_mngr.name_registry.register(self._db_map.sa_url, "db_fetcher_test_db")
def tearDown(self):
self._db_mngr.close_all_sessions()