Skip to content

Commit

Permalink
Improve add entities/classes dialog name field
Browse files Browse the repository at this point in the history
The class name/entity name field will no longer be overwritten by
an automatically generated name when the dimensions/elements are
changed. If the user defined entry is deleted or empty, the name
will be automatically generated.

Re #2874
  • Loading branch information
Henrik Koski committed Jul 5, 2024
1 parent 6548a7b commit 6269d13
Show file tree
Hide file tree
Showing 3 changed files with 136 additions and 10 deletions.
49 changes: 48 additions & 1 deletion spinetoolbox/spine_db_editor/mvcmodels/empty_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
# this program. If not, see <http://www.gnu.org/licenses/>.
######################################################################################################################

"""Empty models for parameter definitions and values."""
"""Empty models for dialogs as well as parameter definitions and values."""
from PySide6.QtCore import Qt
from ...mvcmodels.empty_row_model import EmptyRowModel
from .single_and_empty_model_mixins import SplitValueAndTypeMixin, MakeEntityOnTheFlyMixin
Expand Down Expand Up @@ -326,3 +326,50 @@ def _do_add_items_to_db(self, db_map_items):

def _entity_class_name_candidates(self, db_map, item):
return self._entity_class_name_candidates_by_entity(db_map, item)


class EmptyAddEntityOrClassRowModel(EmptyRowModel):
"""A table model with a last empty row."""
def __init__(self, parent=None, header=None):
super().__init__(parent, header=header)
self._entity_name_user_defined = False

def batch_set_data(self, indexes, data):
"""Reimplemented to fill the entity class name automatically if its data is removed via pressing del."""
if not indexes or not data:
return False

Check warning on line 340 in spinetoolbox/spine_db_editor/mvcmodels/empty_models.py

View check run for this annotation

Codecov / codecov/patch

spinetoolbox/spine_db_editor/mvcmodels/empty_models.py#L340

Added line #L340 was not covered by tests
rows = []
columns = []
for index, value in zip(indexes, data):
if not index.isValid():
continue

Check warning on line 345 in spinetoolbox/spine_db_editor/mvcmodels/empty_models.py

View check run for this annotation

Codecov / codecov/patch

spinetoolbox/spine_db_editor/mvcmodels/empty_models.py#L345

Added line #L345 was not covered by tests
row = index.row()
column = index.column()
if column == self.header.index(self._parent.dialog_item_name()) and not value:
self._entity_name_user_defined = False
self._main_data[row][column] = self._parent.construct_composite_class_name(index.row())
else:
self._main_data[row][column] = value
rows.append(row)
columns.append(column)
# Find square envelope of indexes to emit dataChanged
top = min(rows)
bottom = max(rows)
left = min(columns)
right = max(columns)
self.dataChanged.emit(
self.index(top, left), self.index(bottom, right), [Qt.ItemDataRole.EditRole, Qt.ItemDataRole.DisplayRole]
)
return True

def setData(self, index, value, role=Qt.ItemDataRole.EditRole):
"""Reimplemented to not overwrite user defined entity/class names with automatic composite names."""
if index.column() != self.header.index(self._parent.dialog_item_name()):
return super().setData(index, value, role)
if role == Qt.ItemDataRole.UserRole:
if self._entity_name_user_defined:
return False
role = Qt.ItemDataRole.EditRole
else:
self._entity_name_user_defined = True if value else False
return super().setData(index, value, role)
36 changes: 27 additions & 9 deletions spinetoolbox/spine_db_editor/widgets/add_items_dialogs.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
######################################################################################################################

""" Classes for custom QDialogs to add items to databases. """
from contextlib import suppress
from itertools import product
from PySide6.QtWidgets import (
QHBoxLayout,
Expand All @@ -36,7 +35,7 @@
from PySide6.QtGui import QIcon
from spinedb_api.helpers import name_from_elements, name_from_dimensions
from ..helpers import string_to_bool, string_to_display_icon
from ...mvcmodels.empty_row_model import EmptyRowModel
from ..mvcmodels.empty_models import EmptyAddEntityOrClassRowModel
from ...mvcmodels.compound_table_model import CompoundTableModel
from ...mvcmodels.minimal_table_model import MinimalTableModel
from ...helpers import DB_ITEM_SEPARATOR
Expand Down Expand Up @@ -193,7 +192,7 @@ def __init__(self, parent, item, db_mngr, *db_maps, force_default=False):
self.setWindowTitle("Add entity classes")
self.table_view.set_column_converter_for_pasting("display icon", string_to_display_icon)
self.table_view.set_column_converter_for_pasting("active by default", string_to_bool)
self.model = EmptyRowModel(self)
self.model = EmptyAddEntityOrClassRowModel(self)
self.model.force_default = force_default
self.table_view.setModel(self.model)
self.dimension_count_widget = QWidget(self)
Expand Down Expand Up @@ -276,11 +275,10 @@ def _handle_model_data_changed(self, top_left, bottom_right, roles):
top = top_left.row()
bottom = bottom_right.row()
for row in range(top, bottom + 1):
obj_cls_names = [
name for j in range(self.number_of_dimensions) if (name := self.model.index(row, j).data())
]
relationship_class_name = name_from_dimensions(obj_cls_names)
self.model.setData(self.model.index(row, self.number_of_dimensions), relationship_class_name)
relationship_class_name = self.construct_composite_class_name(row)
self.model.setData(
self.model.index(row, self.number_of_dimensions), relationship_class_name, role=Qt.ItemDataRole.UserRole
)

@Slot()
def accept(self):
Expand Down Expand Up @@ -337,6 +335,22 @@ def accept(self):
self.db_mngr.add_entity_classes(db_map_data)
super().accept()

def construct_composite_class_name(self, row):
"""Returns a ND entity class name from all the currently selected dimension names.
Args:
row (int): The index of the row.
Returns:
str: The ND entity class name
"""
class_names = [name for j in range(self.number_of_dimensions) if (name := self.model.index(row, j).data())]
return name_from_dimensions(class_names)

@staticmethod
def dialog_item_name():
return "entity class name"


class AddEntitiesOrManageElementsDialog(GetEntityClassesMixin, GetEntitiesMixin, AddItemsDialog):
"""A dialog to query user's preferences for new entities."""
Expand Down Expand Up @@ -483,7 +497,7 @@ def _accepts_class(self, ent_cls):
return self.entity_class["name"] in set(ent_cls["dimension_name_list"]) | {ent_cls["name"]}

def make_model(self):
return EmptyRowModel(self)
return EmptyAddEntityOrClassRowModel(self)

def _do_reset_model(self):
header = self.dimension_name_list + ("entity name", "alternative", "entity group", "databases")
Expand Down Expand Up @@ -642,6 +656,10 @@ def make_entity_groups(self, entities):
)
return db_map_data

@staticmethod
def dialog_item_name():
return "entity name"


class ManageElementsDialog(AddEntitiesOrManageElementsDialog):
"""A dialog to query user's preferences for managing entity dimensions."""
Expand Down
61 changes: 61 additions & 0 deletions tests/spine_db_editor/widgets/test_add_items_dialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,67 @@ def test_pasting_data_to_display_icon_column(self):
self._paste_to_table_view("GIBBERISH", dialog)
self.assertIsNone(model.index(0, display_icon_column).data())

def test_composite_name_functionality(self):
"""Test that the entity class name column fills automatically and correctly for ND entity classes."""
dialog = AddEntityClassesDialog(
self._db_editor, self._db_editor.entity_tree_model.root_item, self._db_mngr, self._db_map
)
model = dialog.model
header = model.header
model.fetchMore(QModelIndex())
dialog._handle_spin_box_value_changed(1)
dialog._handle_spin_box_value_changed(2)
self.assertEqual(
header,
[
"dimension name (1)",
"dimension name (2)",
"entity class name",
"description",
"display icon",
"active by default",
"databases",
],
)
indexes = [
model.index(0, header.index(field))
for field in ("dimension name (1)", "dimension name (2)", "entity class name", "databases")
]
values = ["Start", None, None, "mock_db"]
model.batch_set_data(indexes, values)
expected = ["Start", None, "Start__", None, None, True, "mock_db"]
result = [model.index(0, column).data() for column in range(model.columnCount())]
self.assertEqual(expected, result)
value = "class_name"
model.setData(indexes[2], value)
expected = ["Start", None, "class_name", None, None, True, "mock_db"]
result = [model.index(0, column).data() for column in range(model.columnCount())]
self.assertEqual(expected, result)
value = "End"
model.setData(indexes[1], value)
expected = ["Start", "End", "class_name", None, None, True, "mock_db"]
result = [model.index(0, column).data() for column in range(model.columnCount())]
self.assertEqual(expected, result)
values = [None, None]
model.batch_set_data(indexes[1:3], values)
expected = ["Start", None, "Start__", None, None, True, "mock_db"]
result = [model.index(0, column).data() for column in range(model.columnCount())]
self.assertEqual(expected, result)
dialog._handle_spin_box_value_changed(1)
indexes = [
model.index(0, header.index(field)) for field in ("dimension name (1)", "entity class name", "databases")
]
value = "one"
model.setData(indexes[1], value)
expected = ["Start", "one", None, None, True, "mock_db"]
result = [model.index(0, column).data() for column in range(model.columnCount())]
self.assertEqual(expected, result)
value = ""
model.setData(indexes[1], value)
expected = ["Start", "Start__", None, None, True, "mock_db"]
result = [model.index(0, column).data() for column in range(model.columnCount())]
self.assertEqual(expected, result)

@staticmethod
def _paste_to_table_view(text, dialog):
mock_clipboard = mock.MagicMock()
Expand Down

0 comments on commit 6269d13

Please sign in to comment.