Skip to content

Commit

Permalink
Add support for entity alternatives into Importer
Browse files Browse the repository at this point in the history
This adds a checkbox to Importer specification editor which,
while checked, inserts entity activity mapping to entity class mapping.

Re spine-tools/Spine-Toolbox#2748
  • Loading branch information
soininen committed Jul 2, 2024
1 parent 93e1bce commit e8aa5f9
Show file tree
Hide file tree
Showing 8 changed files with 727 additions and 246 deletions.
38 changes: 34 additions & 4 deletions spine_items/importer/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ def undo(self):


class SetMappingPositionType(QUndoCommand):
"""Sets the type of a component mapping."""
"""Sets the type of component mapping."""

def __init__(self, table_row, list_row, row, model, position_type, previous_type, previous_position):
"""
Expand Down Expand Up @@ -504,7 +504,7 @@ def __init__(self, table_row, list_row, model, import_entities):
model (MappingsModel): model
import_entities (bool): new flag value
"""
super().__init__("import objects flag change")
super().__init__(("check" if import_entities else "uncheck") + " import entities")

Check warning on line 507 in spine_items/importer/commands.py

View check run for this annotation

Codecov / codecov/patch

spine_items/importer/commands.py#L507

Added line #L507 was not covered by tests
self._table_row = table_row
self._list_row = list_row
self._model = model
Expand All @@ -519,8 +519,38 @@ def undo(self):
self._model.set_import_entities(self._table_row, self._list_row, not self._import_entities)


class SetImportEntityAlternatives(QUndoCommand):
"""Command to add/remove entity alternative imports to/from mappings."""

def __init__(self, table_row, list_row, model, import_entity_alternatives, previous_mapping):
"""
Args:
table_row (int): source table row index
list_row (int): mapping list row index
model (MappingsModel): model
import_entity_alternatives (bool): new flag value
previous_mapping (ImportMapping): previous mapping root
"""
super().__init__(("check" if import_entity_alternatives else "uncheck") + " import entities")
self._table_row = table_row
self._list_row = list_row
self._model = model
self._import_entity_alternatives = import_entity_alternatives

Check warning on line 538 in spine_items/importer/commands.py

View check run for this annotation

Codecov / codecov/patch

spine_items/importer/commands.py#L534-L538

Added lines #L534 - L538 were not covered by tests
self._previous_mapping_dict = [m.to_dict() for m in previous_mapping.flatten()]

def redo(self):
"""Changes the ."""
self._model.set_import_entity_alternatives(self._table_row, self._list_row, self._import_entity_alternatives)

Check warning on line 543 in spine_items/importer/commands.py

View check run for this annotation

Codecov / codecov/patch

spine_items/importer/commands.py#L543

Added line #L543 was not covered by tests

def undo(self):
"""Restores the mappings to previous state."""
self._model.set_root_mapping(

Check warning on line 547 in spine_items/importer/commands.py

View check run for this annotation

Codecov / codecov/patch

spine_items/importer/commands.py#L547

Added line #L547 was not covered by tests
self._table_row, self._list_row, import_mapping_from_dict(self._previous_mapping_dict)
)


class SetParameterType(QUndoCommand):
"""Command to change the parameter type of an item mapping."""
"""Command to change the parameter type of item mapping."""

def __init__(self, table_row, list_row, model, new_type, previous_mapping):
"""
Expand Down Expand Up @@ -550,7 +580,7 @@ def undo(self):


class SetValueType(QUndoCommand):
"""Command to change the value type of an item mapping."""
"""Command to change the value type of item mapping."""

def __init__(self, table_row, list_row, model, new_type, old_type):
"""
Expand Down
93 changes: 84 additions & 9 deletions spine_items/importer/flattened_mappings.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,19 @@

"""Contains a model to handle source tables and import mapping."""
from enum import Enum, unique
from spinedb_api.mapping import Position
from spinedb_api.mapping import Position, unflatten
from spinedb_api.parameter_value import split_value_and_type
from spinedb_api import from_database, ParameterValueFormatError
from spinedb_api.helpers import fix_name_ambiguity, string_to_bool
from spinedb_api.helpers import fix_name_ambiguity
from spinedb_api.import_mapping.import_mapping import (
EntityClassMapping,
EntityMapping,
DimensionMapping,
ElementMapping,
EntityGroupMapping,
EntityAlternativeActivityMapping,
AlternativeMapping,
EntityMetadataMapping,
ScenarioMapping,
ScenarioAlternativeMapping,
ParameterValueListMapping,
Expand All @@ -45,7 +47,6 @@
@unique
class MappingType(Enum):
EntityClass = "Entity class"
Entity = "Entity"
EntityGroup = "Entity group"
Alternative = "Alternative"
Scenario = "Scenario"
Expand All @@ -69,6 +70,7 @@ class MappingType(Enum):
"EntityGroup": "Member names",
"Dimension": "Dimension names",
"Element": "Element names",
"EntityAlternativeActivity": "Entity activities",
"Alternative": "Alternative names",
"Scenario": "Scenario names",
"ScenarioActiveFlag": "Scenario active flags",
Expand Down Expand Up @@ -470,13 +472,13 @@ def _resolve_map_type(flattened):
return None
head_mapping = flattened[0]
if isinstance(head_mapping, EntityClassMapping):
if any(isinstance(m, EntityGroupMapping) for m in flattened):
if any(isinstance(m, EntityGroupMapping) for m in flattened[1:]):
return MappingType.EntityGroup
return MappingType.EntityClass
if isinstance(head_mapping, AlternativeMapping):
return MappingType.Alternative
if isinstance(head_mapping, ScenarioMapping):
if any(isinstance(m, ScenarioAlternativeMapping) for m in flattened):
if any(isinstance(m, ScenarioAlternativeMapping) for m in flattened[1:]):
return MappingType.ScenarioAlternative
return MappingType.Scenario
if isinstance(head_mapping, ParameterValueListMapping):
Expand Down Expand Up @@ -526,7 +528,7 @@ def set_dimension_count(self, dimension_count):
dimension_count (int): new dimension count
"""
if not self.can_have_dimensions():
return None, None
return

Check warning on line 531 in spine_items/importer/flattened_mappings.py

View check run for this annotation

Codecov / codecov/patch

spine_items/importer/flattened_mappings.py#L531

Added line #L531 was not covered by tests
current_dimension_count = self.dimension_count()
last_dim_mapping = next(
m for m in reversed(self._components) if isinstance(m, (DimensionMapping, EntityClassMapping))
Expand All @@ -549,13 +551,74 @@ def set_dimension_count(self, dimension_count):
self.set_root_mapping(self._components[0])
self._ensure_consistent_import_entities()

def may_import_entity_alternatives(self):
"""Checks if the mappings can optionally import entity alternatives.
Returns:
bool: True if mappings can import entity alternatives, False otherwise
"""
return self._map_type == MappingType.EntityClass

def import_entity_alternatives(self):
"""Returns the import entity alternatives flag.
Returns:
bool: True if import entity alternatives is set, False otherwise
"""
return any(isinstance(m, EntityAlternativeActivityMapping) for m in self._components)

def set_import_entity_alternatives(self, import_entity_alternatives):
"""Adds or removes AlternativeMapping and EntityAlternativeActivityMapping.
Legacy mappings may already have AlternativeMapping as part of parameter value mappings;
in this case we move the AlternativeMapping before EntityAlternativeActivityMapping.
Args:
import_entity_alternatives (bool): if True, mappings will be added; if False they will be removed
"""
if not self.may_import_entity_alternatives():
return

Check warning on line 580 in spine_items/importer/flattened_mappings.py

View check run for this annotation

Codecov / codecov/patch

spine_items/importer/flattened_mappings.py#L580

Added line #L580 was not covered by tests
if import_entity_alternatives:
for i in range(len(self._components)):
if isinstance(self._components[i], AlternativeMapping):
alternative_mapping = self._components.pop(i)
break
else:
alternative_mapping = AlternativeMapping(Position.hidden)
for i, m in enumerate(self._components):
if isinstance(m, EntityMetadataMapping):
insertion_point = i
break
else:
raise RuntimeError("Logic error: expected to find EntityMetadataMapping")

Check warning on line 593 in spine_items/importer/flattened_mappings.py

View check run for this annotation

Codecov / codecov/patch

spine_items/importer/flattened_mappings.py#L593

Added line #L593 was not covered by tests
new_mappings = []
for i, m in enumerate(self._components):
new_mappings.append(m)
if i == insertion_point:
new_mappings.append(alternative_mapping)
new_mappings.append(EntityAlternativeActivityMapping(Position.hidden))
else:
new_mappings = []
has_value = any(isinstance(m, (ParameterValueMapping, ParameterValueTypeMapping)) for m in self._components)
for m in self._components:
if isinstance(m, EntityAlternativeActivityMapping):
continue
if isinstance(m, AlternativeMapping) and not has_value:
continue
new_mappings.append(m)
self.set_root_mapping(unflatten(new_mappings))

def may_import_entities(self):
"""Checks if the mappings can optionally import entities.
Returns:
bool: True if mappings can import entities, False otherwise
"""
return self._map_type in (MappingType.EntityClass, MappingType.EntityGroup)
if self._map_type == MappingType.EntityGroup:
return True

Check warning on line 618 in spine_items/importer/flattened_mappings.py

View check run for this annotation

Codecov / codecov/patch

spine_items/importer/flattened_mappings.py#L618

Added line #L618 was not covered by tests
if self._map_type == MappingType.EntityClass:
return bool(self._import_entities_mappings())
return False

Check warning on line 621 in spine_items/importer/flattened_mappings.py

View check run for this annotation

Codecov / codecov/patch

spine_items/importer/flattened_mappings.py#L621

Added line #L621 was not covered by tests

def _import_entities_mappings(self):
"""Collects a list of mapping components that have an import_entities attribute.
Expand All @@ -571,7 +634,8 @@ def import_entities(self):
Returns:
bool: True if imports entities is set, False otherwise
"""
return all(m.import_entities for m in self._import_entities_mappings())
import_enabled_mappings = self._import_entities_mappings()
return bool(import_enabled_mappings) and all(m.import_entities for m in self._import_entities_mappings())

def set_import_entities(self, import_entities):
"""Sets the import entities flag for components that support it.
Expand All @@ -585,7 +649,7 @@ def set_import_entities(self, import_entities):
def _ensure_consistent_import_entities(self):
"""If any mapping has the import entities flag set, sets the flag also for all other mappings."""
mappings = self._import_entities_mappings()
if any(mapping.import_entities for mapping in mappings):
if mappings and any(mapping.import_entities for mapping in mappings):
for m in mappings:
m.import_entities = True

Expand All @@ -612,9 +676,20 @@ def set_parameter_components(self, parameter_definition_component):
parameter_definition_component (ImportMapping, optional): root of parameter mappings;
None removes the mappings
"""
if parameter_definition_component is not None and any(
isinstance(m, AlternativeMapping) for m in self._components
):
parameter_definition_component = unflatten(
filter(lambda m: not isinstance(m, AlternativeMapping), parameter_definition_component.flatten())
)
m = self._parameter_definition_component()
parent = m.parent if m is not None else self._components[-1]
parent.child = parameter_definition_component
if parameter_definition_component is None and not self.import_entity_alternatives():
for c in self._components:
if isinstance(c, AlternativeMapping):
c.parent.child = c.child
break
self.set_root_mapping(self._components[0])

def display_parameter_type(self):
Expand Down
29 changes: 22 additions & 7 deletions spine_items/importer/mvcmodels/mappings_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,6 @@
from spinedb_api import from_database, ParameterValueFormatError
from spinedb_api.import_mapping.import_mapping import (
default_import_mapping,
EntityClassMapping,
EntityMapping,
EntityMetadataMapping,
ScenarioBeforeAlternativeMapping,
)
from spinedb_api.import_mapping.import_mapping_compat import (
Expand Down Expand Up @@ -1199,6 +1196,27 @@ def set_value_type(self, table_row, list_row, new_type):
self.endInsertRows()
self.dataChanged.emit(list_index, list_index, [Role.FLATTENED_MAPPINGS])

def set_import_entity_alternatives(self, table_row, list_row, import_entity_alternatives):
"""Adds or removes entity alternative imports.
Args:
table_row (int): source table row index
list_row (int): mapping list row index
import_entity_alternatives (bool): True to import entity alternatives, False to disable importing
"""
table_index = self.index(table_row, 0)
list_index = self.index(list_row, 0, table_index)
mapping_list = self._mappings[table_row].mapping_list[list_row]
flattened_mappings = mapping_list.flattened_mappings
self.beginRemoveRows(list_index, 0, len(flattened_mappings.display_names) - 1)
mapping_list.flattened_mappings = None
self.endRemoveRows()
flattened_mappings.set_import_entity_alternatives(import_entity_alternatives)
self.beginInsertRows(list_index, 0, len(flattened_mappings.display_names) - 1)
mapping_list.flattened_mappings = flattened_mappings
self.endInsertRows()
self.dataChanged.emit(list_index, list_index, [Role.FLATTENED_MAPPINGS])

Check warning on line 1218 in spine_items/importer/mvcmodels/mappings_model.py

View check run for this annotation

Codecov / codecov/patch

spine_items/importer/mvcmodels/mappings_model.py#L1207-L1218

Added lines #L1207 - L1218 were not covered by tests

def change_component_mapping(self, flattened_mappings, index, new_type, new_ref):
"""
Pushes :class:`SetComponentMappingType` to the undo stack.
Expand Down Expand Up @@ -1611,10 +1629,7 @@ def create_default_mapping():
Returns:
ImportMapping: new mapping root
"""
root_mapping = EntityClassMapping(0)
object_mapping = root_mapping.child = EntityMapping(1)
object_mapping.child = EntityMetadataMapping(Position.hidden)
return root_mapping
return default_import_mapping("EntityClass")

def has_mapping_name(self, table_row, name):
"""Checks if a name exists in mapping list.
Expand Down
Loading

0 comments on commit e8aa5f9

Please sign in to comment.