Skip to content

Commit

Permalink
Add support for active_by_default column in entity_class table.
Browse files Browse the repository at this point in the history
This commit adds support for the new active_by_default column in
entity_class tables. It is now possible to change the value of the flag
while creating or editing entity classes in Database editor.

Re spine-tools/Spine-Database-API#316
  • Loading branch information
soininen committed Jan 15, 2024
1 parent a2cd888 commit c2a2e7e
Show file tree
Hide file tree
Showing 17 changed files with 252 additions and 64 deletions.
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,22 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
- New context menu action (Select superclass) for entity class items in the entity tree.

### Changed

#### Spine data structure

Many parts of the Spine data structure have been redesigned.

- *Entities* have replaced objects and relationships.
Zero-dimensional entities correspond to objects while multidimensional entities replace the former relationships.
Unlike relationships, the *elements* of multidimensional entities can now be other multidimensional entities.
- Simple inheritance is now supported by *superclasses*.
- Tools, features and methods have been removed.
The functionality that was previously implemented using the is_active parameter
has been replaced by *entity alternatives*.
Entity classes have a default setting for the entity alternative called *active by default*.

#### Miscellaneous changes

- Julia execution settings are now Tool specification settings instead of global application settings. You can
select a different Julia executable & project or Julia kernel for each Tool spec.
- Headless mode now supports remote execution (see 'python -m spinetoolbox --help')
Expand Down
3 changes: 3 additions & 0 deletions execution_tests/active_by_default/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.spinetoolbox/local/
.spinetoolbox/items/
*.bak*
79 changes: 79 additions & 0 deletions execution_tests/active_by_default/.spinetoolbox/project.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
{
"project": {
"version": 12,
"description": "",
"settings": {
"enable_execute_all": true
},
"specifications": {
"Exporter": [
{
"type": "path",
"relative": true,
"path": ".spinetoolbox/specifications/Exporter/export_entities.json"
}
]
},
"connections": [
{
"name": "from Test data to Exporter",
"from": [
"Test data",
"right"
],
"to": [
"Exporter",
"left"
],
"options": {
"require_scenario_filter": true
},
"filter_settings": {
"known_filters": {
"db_url@Test data": {
"scenario_filter": {
"base_scenario": true
}
}
},
"auto_online": true
}
}
],
"jumps": []
},
"items": {
"Test data": {
"type": "Data Store",
"description": "",
"x": -115.31149147072384,
"y": 22.059589672660213,
"url": {
"dialect": "sqlite",
"host": "",
"port": "",
"database": {
"type": "path",
"relative": true,
"path": "Test data.sqlite"
},
"schema": ""
}
},
"Exporter": {
"type": "Exporter",
"description": "",
"x": 17.046046565237432,
"y": 22.05958967266021,
"output_time_stamps": false,
"cancel_on_error": true,
"output_labels": [
{
"in_label": "db_url@Test data",
"out_label": "data.csv"
}
],
"specification": "Export entities"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"item_type": "Exporter",
"output_format": "csv",
"name": "Export entities",
"description": "",
"mappings": {
"Mapping (1)": {
"type": "entities",
"mapping": [
{
"map_type": "EntityClass",
"position": 0
},
{
"map_type": "Entity",
"position": 1
}
],
"enabled": true,
"always_export_header": true,
"group_fn": "no_group",
"use_fixed_table_name": false
}
}
}
Empty file.
49 changes: 49 additions & 0 deletions execution_tests/active_by_default/execution_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
from pathlib import Path
import shutil
import subprocess
import sys
import unittest
from spinedb_api import DatabaseMapping

class ActiveByDefault(unittest.TestCase):
_root_path = Path(__file__).parent
_database_path = _root_path / "Test data.sqlite"
_exporter_output_path = _root_path / ".spinetoolbox" / "items" / "exporter" / "output"

def _check_addition(self, result):
error = result[1]
self.assertIsNone(error)

def setUp(self):
if self._exporter_output_path.exists():
shutil.rmtree(self._exporter_output_path)
self._database_path.parent.mkdir(parents=True, exist_ok=True)
if self._database_path.exists():
self._database_path.unlink()
url = "sqlite:///" + str(self._database_path)
with DatabaseMapping(url, create=True) as db_map:
self._check_addition(db_map.add_entity_class_item(name="HiddenByDefault", active_by_default=False))
self._check_addition(db_map.add_entity_item(name="hidden", entity_class_name="HiddenByDefault"))
self._check_addition(db_map.add_entity_class_item(name="VisibleByDefault", active_by_default=True))
self._check_addition(db_map.add_entity_item(name="visible", entity_class_name="VisibleByDefault"))
self._check_addition(db_map.add_scenario_item(name="base_scenario"))
self._check_addition(db_map.add_scenario_alternative_item(scenario_name="base_scenario", alternative_name="Base", rank=0))
db_map.commit_session("Add test data.")

def test_execution(self):
this_file = Path(__file__)
completed = subprocess.run((sys.executable, "-m", "spinetoolbox", "--execute-only", str(this_file.parent)))
self.assertEqual(completed.returncode, 0)
self.assertTrue(self._exporter_output_path.exists())
self.assertEqual(len(list(self._exporter_output_path.iterdir())), 1)
output_dir = next(iter(self._exporter_output_path.iterdir()))
filter_id = (output_dir / ".filter_id").read_text(encoding="utf-8").splitlines()
self.assertEqual(len(filter_id), 1)
self.assertEqual(filter_id, ["base_scenario - Test data"])
entities = (output_dir / "data.csv").read_text(encoding="utf-8").splitlines()
expected = ["VisibleByDefault,visible"]
self.assertCountEqual(entities, expected)


if __name__ == '__main__':
unittest.main()
5 changes: 1 addition & 4 deletions spinetoolbox/mvcmodels/empty_row_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,7 @@
# 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 <http://www.gnu.org/licenses/>.
######################################################################################################################

"""
Contains a table model with an empty last row.
"""
""" Contains a table model with an empty last row. """

from PySide6.QtCore import Qt, Slot, QModelIndex
from .minimal_table_model import MinimalTableModel
Expand Down
5 changes: 1 addition & 4 deletions spinetoolbox/mvcmodels/minimal_table_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,7 @@
# 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 <http://www.gnu.org/licenses/>.
######################################################################################################################

"""
Contains a minimal table model.
"""
""" Contains a minimal table model. """

from PySide6.QtCore import Qt, QModelIndex, QAbstractTableModel

Expand Down
6 changes: 1 addition & 5 deletions spinetoolbox/spine_db_editor/mvcmodels/compound_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,7 @@
# 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 <http://www.gnu.org/licenses/>.
######################################################################################################################

"""
Compound models.
These models concatenate several 'single' models and one 'empty' model.
"""
""" Compound models. These models concatenate several 'single' models and one 'empty' model. """
from PySide6.QtCore import Qt, Slot, QTimer, QModelIndex
from PySide6.QtGui import QFont
from spinedb_api.parameter_value import join_value_and_type
Expand Down
36 changes: 24 additions & 12 deletions spinetoolbox/spine_db_editor/widgets/add_items_dialogs.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,7 @@
# 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 <http://www.gnu.org/licenses/>.
######################################################################################################################

"""
Classes for custom QDialogs to add items to databases.
"""
""" Classes for custom QDialogs to add items to databases. """

from itertools import product
from PySide6.QtWidgets import (
Expand Down Expand Up @@ -210,14 +206,15 @@ def __init__(self, parent, item, db_mngr, *db_maps, force_default=False):
self.spin_box.setValue(self.number_of_dimensions)
self.connect_signals()
labels = ['dimension name (1)'] if dimension_one_name is not None else []
labels += ['entity class name', 'description', 'display icon', 'databases']
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)
self.default_display_icon = None
self.model.set_default_row(
**{
'dimension name (1)': dimension_one_name,
'display_icon': self.default_display_icon,
'display icon': self.default_display_icon,
"active by default": self.number_of_dimensions != 0,
'databases': db_names,
}
)
Expand Down Expand Up @@ -245,6 +242,13 @@ def _handle_spin_box_value_changed(self, i):
self.insert_column()
elif i < self.number_of_dimensions:
self.remove_column()
default_value = self.number_of_dimensions != 0
self.model.default_row["active by default"] = default_value
column = self.model.horizontal_header_labels().index("active by default")
last_row = self.model.rowCount() - 1
index = self.model.index(last_row, column)
if index.data() != default_value:
self.model.setData(index, default_value)
self.spin_box.setEnabled(True)
self.resize_window_to_columns()

Expand Down Expand Up @@ -284,10 +288,12 @@ def _handle_model_data_changed(self, top_left, bottom_right, roles):
def accept(self):
"""Collect info from dialog and try to add items."""
db_map_data = dict()
name_column = self.model.horizontal_header_labels().index("entity class name")
description_column = self.model.horizontal_header_labels().index("description")
display_icon_column = self.model.horizontal_header_labels().index("display icon")
db_column = self.model.horizontal_header_labels().index("databases")
header_labels = self.model.horizontal_header_labels()
name_column = header_labels.index("entity class name")
description_column = header_labels.index("description")
display_icon_column = header_labels.index("display icon")
active_by_default_column = header_labels.index("active by default")
db_column = header_labels.index("databases")
for i in range(self.model.rowCount() - 1): # last row will always be empty
row_data = self.model.row_data(i)
entity_class_name = row_data[name_column]
Expand All @@ -298,7 +304,13 @@ def accept(self):
display_icon = row_data[display_icon_column]
if not display_icon:
display_icon = self.default_display_icon
pre_item = {'name': entity_class_name, 'description': description, 'display_icon': display_icon}
active_by_default = row_data[active_by_default_column]
pre_item = {
'name': entity_class_name,
'description': description,
'display_icon': display_icon,
"active_by_default": active_by_default,
}
db_names = row_data[db_column]
if db_names is None:
db_names = ""
Expand Down
32 changes: 17 additions & 15 deletions spinetoolbox/spine_db_editor/widgets/custom_delegates.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,15 @@
# 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 <http://www.gnu.org/licenses/>.
######################################################################################################################

"""
Custom item delegates.
"""
""" Custom item delegates. """

from numbers import Number
from PySide6.QtCore import QModelIndex, Qt, Signal
from PySide6.QtWidgets import QStyledItemDelegate, QComboBox
from spinedb_api import to_database
from spinedb_api.parameter_value import join_value_and_type
from ...widgets.custom_editors import (
BooleanSearchBarEditor,
CustomLineEditor,
PivotHeaderTableLineEditor,
SearchBarEditor,
Expand Down Expand Up @@ -474,14 +472,10 @@ def createEditor(self, parent, option, index):


class BooleanValueDelegate(TableDelegate):
TRUE = "true"
FALSE = "false"

def setModelData(self, editor, model, index):
"""Sends signal."""
try:
value = {self.TRUE: True, self.FALSE: False}[editor.data()]
except KeyError:
value = editor.data()
if not isinstance(value, bool):
return
self.data_committed.emit(index, value)

Expand All @@ -490,11 +484,16 @@ def createEditor(self, parent, option, index):
db_map = self._get_db_map(index)
if not db_map:
return None
editor = SearchBarEditor(self.parent(), parent)
editor.set_data(str(index.data(Qt.ItemDataRole.EditRole)), [self.TRUE, self.FALSE])
editor = self.make_editor(self.parent(), parent, index)
editor.data_committed.connect(lambda *_: self._close_editor(editor, index))
return editor

@classmethod
def make_editor(cls, parent, tutor, index):
editor = BooleanSearchBarEditor(parent, tutor)
editor.set_data(index.data(Qt.ItemDataRole.EditRole), None)
return editor


class AlternativeDelegate(QStyledItemDelegate):
"""A delegate for the alternative tree."""
Expand Down Expand Up @@ -742,14 +741,17 @@ def paint(self, painter, option, index):
def createEditor(self, parent, option, index):
"""Return editor."""
header = index.model().horizontal_header_labels()
if header[index.column()] == 'display icon':
label = header[index.column()]
if label == 'display icon':
self.icon_color_editor_requested.emit(index)
editor = None
elif header[index.column()] in ('entity class name', 'description'):
elif label in ('entity class name', 'description'):
editor = CustomLineEditor(parent)
editor.set_data(index.data(Qt.ItemDataRole.EditRole))
elif header[index.column()] == 'databases':
elif label == 'databases':
editor = self._create_database_editor(parent, index)
elif label == "active by default":
editor = BooleanValueDelegate.make_editor(self.parent(), parent, index)
else:
editor = SearchBarEditor(parent)
entity_class_name_list = self.parent().entity_class_name_list(index.row())
Expand Down
Loading

0 comments on commit c2a2e7e

Please sign in to comment.