diff --git a/rsconcept/backend/apps/oss/models/OperationSchema.py b/rsconcept/backend/apps/oss/models/OperationSchema.py index 39eeae94..f10219e7 100644 --- a/rsconcept/backend/apps/oss/models/OperationSchema.py +++ b/rsconcept/backend/apps/oss/models/OperationSchema.py @@ -95,14 +95,15 @@ class OperationSchema: self.save(update_fields=['time_update']) return result - def delete_operation(self, target: Operation, keep_constituents: bool = False): + def delete_operation(self, target: int, keep_constituents: bool = False): ''' Delete operation. ''' + operation = self.cache.operation_by_id[target] if not keep_constituents: - schema = self.cache.get_schema(target) + schema = self.cache.get_schema(operation) if schema is not None: - self.before_delete_cst(schema.cache.constituents, schema) - self.cache.remove_operation(target.pk) - target.delete() + self.before_delete_cst(schema, schema.cache.constituents) + self.cache.remove_operation(target) + operation.delete() self.save(update_fields=['time_update']) def set_input(self, target: int, schema: Optional[LibraryItem]) -> None: @@ -115,7 +116,7 @@ class OperationSchema: if old_schema is not None: if has_children: - self.before_delete_cst(old_schema.cache.constituents, old_schema) + self.before_delete_cst(old_schema, old_schema.cache.constituents) self.cache.remove_schema(old_schema) operation.result = schema @@ -128,59 +129,76 @@ class OperationSchema: if schema is not None and has_children: rsform = RSForm(schema) - self.after_create_cst(list(rsform.constituents()), rsform) + self.after_create_cst(rsform, list(rsform.constituents())) self.save(update_fields=['time_update']) - def set_arguments(self, operation: Operation, arguments: list[Operation]) -> None: + def set_arguments(self, target: int, arguments: list[Operation]) -> None: ''' Set arguments to operation. ''' + self.cache.ensure_loaded() + operation = self.cache.operation_by_id[target] processed: list[Operation] = [] - changed = False + deleted: list[Argument] = [] for current in operation.getArguments(): if current.argument not in arguments: - changed = True - current.delete() + deleted.append(current) else: processed.append(current.argument) + if len(deleted) > 0: + self.before_delete_arguments(operation, [x.argument for x in deleted]) + for deleted_arg in deleted: + self.cache.remove_argument(deleted_arg) + Argument.objects.filter(pk__in=[x.pk for x in deleted]).delete() + + added: list[Operation] = [] for arg in arguments: if arg not in processed: - changed = True processed.append(arg) - Argument.objects.create(operation=operation, argument=arg) - if not changed: - return - # TODO: trigger on_change effects - self.save(update_fields=['time_update']) + new_arg = Argument.objects.create(operation=operation, argument=arg) + self.cache.insert_argument(new_arg) + added.append(arg) + if len(added) > 0: + self.after_create_arguments(operation, added) + if len(added) > 0 or len(deleted) > 0: + self.save(update_fields=['time_update']) - def set_substitutions(self, target: Operation, substitutes: list[dict]) -> None: + def set_substitutions(self, target: int, substitutes: list[dict]) -> None: ''' Clear all arguments for operation. ''' + self.cache.ensure_loaded() + operation = self.cache.operation_by_id[target] + schema = self.cache.get_schema(operation) processed: list[dict] = [] - changed = False - - for current in target.getSubstitutions(): + deleted: list[Substitution] = [] + for current in operation.getSubstitutions(): subs = [ x for x in substitutes if x['original'] == current.original and x['substitution'] == current.substitution ] if len(subs) == 0: - changed = True - current.delete() + deleted.append(current) else: processed.append(subs[0]) + if len(deleted) > 0: + if schema is not None: + for sub in deleted: + self._undo_substitution(schema, sub) + else: + for sub in deleted: + self.cache.remove_substitution(sub) + Substitution.objects.filter(pk__in=[x.pk for x in deleted]).delete() - for sub in substitutes: - if sub not in processed: - changed = True - Substitution.objects.create( - operation=target, - original=sub['original'], - substitution=sub['substitution'] + added: list[Substitution] = [] + for sub_item in substitutes: + if sub_item not in processed: + new_sub = Substitution.objects.create( + operation=operation, + original=sub_item['original'], + substitution=sub_item['substitution'] ) + added.append(new_sub) + self._process_added_substitutions(schema, added) - if not changed: - return - # TODO: trigger on_change effects - - self.save(update_fields=['time_update']) + if len(added) > 0 or len(deleted) > 0: + self.save(update_fields=['time_update']) def create_input(self, operation: Operation) -> RSForm: ''' Create input RSForm. ''' @@ -240,9 +258,9 @@ class OperationSchema: self.save(update_fields=['time_update']) return True - def after_create_cst(self, cst_list: list[Constituenta], source: RSForm) -> None: + def after_create_cst(self, source: RSForm, cst_list: list[Constituenta]) -> None: ''' Trigger cascade resolutions when new constituent is created. ''' - self.cache.insert(source) + self.cache.insert_schema(source) inserted_aliases = [cst.alias for cst in cst_list] depend_aliases: set[str] = set() for new_cst in cst_list: @@ -254,17 +272,17 @@ class OperationSchema: if cst is not None: alias_mapping[alias] = cst operation = self.cache.get_operation(source.model.pk) - self._cascade_create_cst(cst_list, operation, alias_mapping) + self._cascade_inherit_cst(operation.pk, source, cst_list, alias_mapping) - def after_change_cst_type(self, target: Constituenta, source: RSForm) -> None: + def after_change_cst_type(self, source: RSForm, target: Constituenta) -> None: ''' Trigger cascade resolutions when constituenta type is changed. ''' - self.cache.insert(source) + self.cache.insert_schema(source) operation = self.cache.get_operation(source.model.pk) - self._cascade_change_cst_type(target.pk, target.cst_type, operation.pk) + self._cascade_change_cst_type(operation.pk, target.pk, target.cst_type) - def after_update_cst(self, target: Constituenta, data: dict, old_data: dict, source: RSForm) -> None: + def after_update_cst(self, source: RSForm, target: Constituenta, data: dict, old_data: dict) -> None: ''' Trigger cascade resolutions when constituenta data is changed. ''' - self.cache.insert(source) + self.cache.insert_schema(source) operation = self.cache.get_operation(source.model.pk) depend_aliases = self._extract_data_references(data, old_data) alias_mapping: CstMapping = {} @@ -273,56 +291,94 @@ class OperationSchema: if cst is not None: alias_mapping[alias] = cst self._cascade_update_cst( - cst_id=target.pk, operation=operation.pk, + cst_id=target.pk, data=data, old_data=old_data, mapping=alias_mapping ) - def before_delete_cst(self, target: list[Constituenta], source: RSForm) -> None: + def before_delete_cst(self, source: RSForm, target: list[Constituenta]) -> None: ''' Trigger cascade resolutions before constituents are deleted. ''' - self.cache.insert(source) + self.cache.insert_schema(source) operation = self.cache.get_operation(source.model.pk) - self._cascade_before_delete(target, operation.pk) + self._cascade_delete_inherited(operation.pk, target) - def before_substitute(self, substitutions: CstSubstitution, source: RSForm) -> None: + def before_substitute(self, source: RSForm, substitutions: CstSubstitution) -> None: ''' Trigger cascade resolutions before constituents are substituted. ''' - self.cache.insert(source) + self.cache.insert_schema(source) operation = self.cache.get_operation(source.model.pk) self._cascade_before_substitute(substitutions, operation) - def _cascade_create_cst(self, cst_list: list[Constituenta], operation: Operation, mapping: CstMapping) -> None: - children = self.cache.graph.outputs[operation.pk] + def before_delete_arguments(self, target: Operation, arguments: list[Operation]) -> None: + ''' Trigger cascade resolutions before arguments are deleted. ''' + if target.result_id is None: + return + for argument in arguments: + parent_schema = self.cache.get_schema(argument) + if parent_schema is not None: + self._execute_delete_inherited(target.pk, parent_schema.cache.constituents) + + def after_create_arguments(self, target: Operation, arguments: list[Operation]) -> None: + ''' Trigger cascade resolutions after arguments are created. ''' + schema = self.cache.get_schema(target) + if schema is None: + return + for argument in arguments: + parent_schema = self.cache.get_schema(argument) + if parent_schema is None: + continue + self._execute_inherit_cst( + target_operation=target.pk, + source=parent_schema, + items=list(parent_schema.constituents()), + mapping={} + ) + + def _cascade_inherit_cst( + self, + target_operation: int, + source: RSForm, + items: list[Constituenta], + mapping: CstMapping + ) -> None: + children = self.cache.graph.outputs[target_operation] if len(children) == 0: return - source_schema = self.cache.get_schema(operation) - assert source_schema is not None for child_id in children: - child_operation = self.cache.operation_by_id[child_id] - child_schema = self.cache.get_schema(child_operation) - if child_schema is None: - continue + self._execute_inherit_cst(child_id, source, items, mapping) - # TODO: update substitutions for diamond synthesis (if needed) + def _execute_inherit_cst( + self, + target_operation: int, + source: RSForm, + items: list[Constituenta], + mapping: CstMapping + ) -> None: + operation = self.cache.operation_by_id[target_operation] + destination = self.cache.get_schema(operation) + if destination is None: + return - self.cache.ensure_loaded() - new_mapping = self._transform_mapping(mapping, child_operation, child_schema) - alias_mapping = OperationSchema._produce_alias_mapping(new_mapping) - insert_where = self._determine_insert_position(cst_list[0], child_operation, source_schema, child_schema) - new_cst_list = child_schema.insert_copy(cst_list, insert_where, alias_mapping) - for index, cst in enumerate(new_cst_list): - new_inheritance = Inheritance.objects.create( - operation=child_operation, - child=cst, - parent=cst_list[index] - ) - self.cache.insert_inheritance(new_inheritance) - new_mapping = {alias_mapping[alias]: cst for alias, cst in new_mapping.items()} - self._cascade_create_cst(new_cst_list, child_operation, new_mapping) + # TODO: update substitutions for diamond synthesis (if needed) - def _cascade_change_cst_type(self, cst_id: int, ctype: CstType, operation: int) -> None: - children = self.cache.graph.outputs[operation] + self.cache.ensure_loaded() + new_mapping = self._transform_mapping(mapping, operation, destination) + alias_mapping = OperationSchema._produce_alias_mapping(new_mapping) + insert_where = self._determine_insert_position(items[0], operation, source, destination) + new_cst_list = destination.insert_copy(items, insert_where, alias_mapping) + for index, cst in enumerate(new_cst_list): + new_inheritance = Inheritance.objects.create( + operation=operation, + child=cst, + parent=items[index] + ) + self.cache.insert_inheritance(new_inheritance) + new_mapping = {alias_mapping[alias]: cst for alias, cst in new_mapping.items()} + self._cascade_inherit_cst(operation.pk, destination, new_cst_list, new_mapping) + + def _cascade_change_cst_type(self, operation_id: int, cst_id: int, ctype: CstType) -> None: + children = self.cache.graph.outputs[operation_id] if len(children) == 0: return self.cache.ensure_loaded() @@ -332,13 +388,16 @@ class OperationSchema: if successor_id is None: continue child_schema = self.cache.get_schema(child_operation) - if child_schema is not None and child_schema.change_cst_type(successor_id, ctype): - self._cascade_change_cst_type(successor_id, ctype, child_id) + if child_schema is None: + continue + if child_schema.change_cst_type(successor_id, ctype): + self._cascade_change_cst_type(child_id, successor_id, ctype) # pylint: disable=too-many-arguments def _cascade_update_cst( self, - cst_id: int, operation: int, + operation: int, + cst_id: int, data: dict, old_data: dict, mapping: CstMapping ) -> None: @@ -366,30 +425,33 @@ class OperationSchema: continue new_mapping = {alias_mapping[alias]: cst for alias, cst in new_mapping.items()} self._cascade_update_cst( - cst_id=successor_id, operation=child_id, + cst_id=successor_id, data=new_data, old_data=new_old_data, mapping=new_mapping ) - def _cascade_before_delete(self, target: list[Constituenta], operation: int) -> None: + def _cascade_delete_inherited(self, operation: int, target: list[Constituenta]) -> None: children = self.cache.graph.outputs[operation] if len(children) == 0: return self.cache.ensure_loaded() for child_id in children: - child_operation = self.cache.operation_by_id[child_id] - child_schema = self.cache.get_schema(child_operation) - if child_schema is None: - continue - self._undo_substitutions_cst(target, child_operation, child_schema) - child_target_ids = self.cache.get_inheritors_list([cst.pk for cst in target], child_id) - child_target_cst = [child_schema.cache.by_id[cst_id] for cst_id in child_target_ids] - self._cascade_before_delete(child_target_cst, child_id) - if len(child_target_cst) > 0: - self.cache.remove_cst(child_target_ids, child_id) - child_schema.delete_cst(child_target_cst) + self._execute_delete_inherited(child_id, target) + + def _execute_delete_inherited(self, operation_id: int, parent_cst: list[Constituenta]) -> None: + operation = self.cache.operation_by_id[operation_id] + schema = self.cache.get_schema(operation) + if schema is None: + return + self._undo_substitutions_cst(parent_cst, operation, schema) + target_ids = self.cache.get_inheritors_list([cst.pk for cst in parent_cst], operation_id) + target_cst = [schema.cache.by_id[cst_id] for cst_id in target_ids] + self._cascade_delete_inherited(operation_id, target_cst) + if len(target_cst) > 0: + self.cache.remove_cst(operation_id, target_ids) + schema.delete_cst(target_cst) def _cascade_before_substitute(self, substitutions: CstSubstitution, operation: Operation) -> None: children = self.cache.graph.outputs[operation.pk] @@ -411,12 +473,12 @@ class OperationSchema: self, mapping: CstMapping, target: list[int], - operation: Operation, + operation: int, schema: RSForm ) -> None: alias_mapping = OperationSchema._produce_alias_mapping(mapping) schema.apply_partial_mapping(alias_mapping, target) - children = self.cache.graph.outputs[operation.pk] + children = self.cache.graph.outputs[operation] if len(children) == 0: return self.cache.ensure_loaded() @@ -431,7 +493,7 @@ class OperationSchema: new_target = self.cache.get_inheritors_list(target, child_id) if len(new_target) == 0: continue - self._cascade_partial_mapping(new_mapping, new_target, child_operation, child_schema) + self._cascade_partial_mapping(new_mapping, new_target, child_id, child_schema) @staticmethod def _produce_alias_mapping(mapping: CstMapping) -> dict[str, str]: @@ -555,37 +617,66 @@ class OperationSchema: if sub.original_id in target_ids or sub.substitution_id in target_ids: to_process.append(sub) for sub in to_process: - self._undo_substitution(sub, schema, target_ids) + self._undo_substitution(schema, sub, target_ids) - def _undo_substitution(self, target: Substitution, schema: RSForm, ignore_parents: list[int]) -> None: - operation = self.cache.operation_by_id[target.operation_id] + def _undo_substitution( + self, + schema: RSForm, + target: Substitution, + ignore_parents: Optional[list[int]] = None + ) -> None: + if ignore_parents is None: + ignore_parents = [] + operation_id = target.operation_id original_schema, _, original_cst, substitution_cst = self.cache.unfold_sub(target) dependant = [] for cst_id in original_schema.get_dependant([original_cst.pk]): if cst_id not in ignore_parents: - inheritor_id = self.cache.get_inheritor(cst_id, operation.pk) + inheritor_id = self.cache.get_inheritor(cst_id, operation_id) if inheritor_id is not None: dependant.append(inheritor_id) - self.cache.substitutions[operation.pk].remove(target) + self.cache.substitutions[operation_id].remove(target) target.delete() new_original: Optional[Constituenta] = None if original_cst.pk not in ignore_parents: full_cst = Constituenta.objects.get(pk=original_cst.pk) - self.after_create_cst([full_cst], original_schema) - new_original_id = self.cache.get_inheritor(original_cst.pk, operation.pk) + self.after_create_cst(original_schema, [full_cst]) + new_original_id = self.cache.get_inheritor(original_cst.pk, operation_id) assert new_original_id is not None new_original = schema.cache.by_id[new_original_id] if len(dependant) == 0: return - substitution_id = self.cache.get_inheritor(substitution_cst.pk, operation.pk) + substitution_id = self.cache.get_inheritor(substitution_cst.pk, operation_id) assert substitution_id is not None substitution_inheritor = schema.cache.by_id[substitution_id] mapping = {cast(str, substitution_inheritor.alias): new_original} - self._cascade_partial_mapping(mapping, dependant, operation, schema) + self._cascade_partial_mapping(mapping, dependant, operation_id, schema) + + def _process_added_substitutions(self, schema: Optional[RSForm], added: list[Substitution]) -> None: + if len(added) == 0: + return + if schema is None: + for sub in added: + self.cache.insert_substitution(sub) + return + + cst_mapping: CstSubstitution = [] + for sub in added: + original_id = self.cache.get_inheritor(sub.original_id, sub.operation_id) + substitution_id = self.cache.get_inheritor(sub.substitution_id, sub.operation_id) + if original_id is None or substitution_id is None: + raise ValueError('Substitutions not found.') + original_cst = schema.cache.by_id[original_id] + substitution_cst = schema.cache.by_id[substitution_id] + cst_mapping.append((original_cst, substitution_cst)) + self.before_substitute(schema, cst_mapping) + schema.substitute(cst_mapping) + for sub in added: + self.cache.insert_substitution(sub) class OssCache: @@ -608,11 +699,18 @@ class OssCache: self.substitutions: dict[int, list[Substitution]] = {} self.inheritance: dict[int, list[Inheritance]] = {} - def insert(self, schema: RSForm) -> None: - ''' Insert new schema. ''' - if not self._schema_by_id.get(schema.model.pk): - schema.cache.ensure_loaded() - self._insert_new(schema) + def ensure_loaded(self) -> None: + ''' Ensure cache is fully loaded. ''' + if self.is_loaded: + return + self.is_loaded = True + for operation in self.operations: + self.inheritance[operation.pk] = [] + self.substitutions[operation.pk] = [] + for sub in self._oss.substitutions().only('operation_id', 'original_id', 'substitution_id'): + self.substitutions[sub.operation_id].append(sub) + for item in self._oss.inheritance().only('operation_id', 'parent_id', 'child_id'): + self.inheritance[item.operation_id].append(item) def get_schema(self, operation: Operation) -> Optional[RSForm]: ''' Get schema by Operation. ''' @@ -633,19 +731,6 @@ class OssCache: return operation raise ValueError(f'Operation for schema {schema} not found') - def ensure_loaded(self) -> None: - ''' Ensure cache is fully loaded. ''' - if self.is_loaded: - return - self.is_loaded = True - for operation in self.operations: - self.inheritance[operation.pk] = [] - self.substitutions[operation.pk] = [] - for sub in self._oss.substitutions().only('operation_id', 'original_id', 'substitution_id'): - self.substitutions[sub.operation_id].append(sub) - for item in self._oss.inheritance().only('operation_id', 'parent_id', 'child_id'): - self.inheritance[item.operation_id].append(item) - def get_inheritor(self, parent_cst: int, operation: int) -> Optional[int]: ''' Get child for parent inside target RSFrom. ''' for item in self.inheritance[operation]: @@ -668,6 +753,12 @@ class OssCache: return self.get_inheritor(sub.substitution_id, operation) return self.get_inheritor(parent_cst, operation) + def insert_schema(self, schema: RSForm) -> None: + ''' Insert new schema. ''' + if not self._schema_by_id.get(schema.model.pk): + schema.cache.ensure_loaded() + self._insert_new(schema) + def insert_operation(self, operation: Operation) -> None: ''' Insert new operation. ''' self.operations.append(operation) @@ -677,6 +768,10 @@ class OssCache: self.substitutions[operation.pk] = [] self.inheritance[operation.pk] = [] + def insert_argument(self, argument: Argument) -> None: + ''' Insert new argument. ''' + self.graph.add_edge(argument.operation_id, argument.argument_id) + def insert_inheritance(self, inheritance: Inheritance) -> None: ''' Insert new inheritance. ''' self.inheritance[inheritance.operation_id].append(inheritance) @@ -685,7 +780,7 @@ class OssCache: ''' Insert new substitution. ''' self.substitutions[sub.operation_id].append(sub) - def remove_cst(self, target: list[int], operation: int) -> None: + def remove_cst(self, operation: int, target: list[int]) -> None: ''' Remove constituents from operation. ''' subs_to_delete = [ sub for sub in self.substitutions[operation] @@ -697,9 +792,15 @@ class OssCache: for item in inherit_to_delete: self.inheritance[operation].remove(item) + def remove_schema(self, schema: RSForm) -> None: + ''' Remove schema from cache. ''' + self._schemas.remove(schema) + del self._schema_by_id[schema.model.pk] + def remove_operation(self, operation: int) -> None: ''' Remove operation from cache. ''' target = self.operation_by_id[operation] + self.graph.remove_node(operation) if target.result_id in self._schema_by_id: self._schemas.remove(self._schema_by_id[target.result_id]) del self._schema_by_id[target.result_id] @@ -709,10 +810,13 @@ class OssCache: del self.substitutions[operation] del self.inheritance[operation] - def remove_schema(self, schema: RSForm) -> None: - ''' Remove schema from cache. ''' - self._schemas.remove(schema) - del self._schema_by_id[schema.model.pk] + def remove_argument(self, argument: Argument) -> None: + ''' Remove argument from cache. ''' + self.graph.remove_edge(argument.operation_id, argument.argument_id) + + def remove_substitution(self, target: Substitution) -> None: + ''' Remove substitution from cache. ''' + self.substitutions[target.operation_id].remove(target) def unfold_sub(self, sub: Substitution) -> tuple[RSForm, RSForm, Constituenta, Constituenta]: operation = self.operation_by_id[sub.operation_id] diff --git a/rsconcept/backend/apps/oss/models/PropagationFacade.py b/rsconcept/backend/apps/oss/models/PropagationFacade.py index df3b9550..e0e5d230 100644 --- a/rsconcept/backend/apps/oss/models/PropagationFacade.py +++ b/rsconcept/backend/apps/oss/models/PropagationFacade.py @@ -14,39 +14,39 @@ class PropagationFacade: ''' Change propagation API. ''' @staticmethod - def after_create_cst(new_cst: list[Constituenta], source: RSForm) -> None: + def after_create_cst(source: RSForm, new_cst: list[Constituenta]) -> None: ''' Trigger cascade resolutions when new constituent is created. ''' hosts = _get_oss_hosts(source.model) for host in hosts: - OperationSchema(host).after_create_cst(new_cst, source) + OperationSchema(host).after_create_cst(source, new_cst) @staticmethod - def after_change_cst_type(target: Constituenta, source: RSForm) -> None: + def after_change_cst_type(source: RSForm, target: Constituenta) -> None: ''' Trigger cascade resolutions when constituenta type is changed. ''' hosts = _get_oss_hosts(source.model) for host in hosts: - OperationSchema(host).after_change_cst_type(target, source) + OperationSchema(host).after_change_cst_type(source, target) @staticmethod - def after_update_cst(target: Constituenta, data: dict, old_data: dict, source: RSForm) -> None: + def after_update_cst(source: RSForm, target: Constituenta, data: dict, old_data: dict) -> None: ''' Trigger cascade resolutions when constituenta data is changed. ''' hosts = _get_oss_hosts(source.model) for host in hosts: - OperationSchema(host).after_update_cst(target, data, old_data, source) + OperationSchema(host).after_update_cst(source, target, data, old_data) @staticmethod - def before_delete_cst(target: list[Constituenta], source: RSForm) -> None: + def before_delete_cst(source: RSForm, target: list[Constituenta]) -> None: ''' Trigger cascade resolutions before constituents are deleted. ''' hosts = _get_oss_hosts(source.model) for host in hosts: - OperationSchema(host).before_delete_cst(target, source) + OperationSchema(host).before_delete_cst(source, target) @staticmethod - def before_substitute(substitutions: CstSubstitution, source: RSForm) -> None: + def before_substitute(source: RSForm, substitutions: CstSubstitution) -> None: ''' Trigger cascade resolutions before constituents are substituted. ''' hosts = _get_oss_hosts(source.model) for host in hosts: - OperationSchema(host).before_substitute(substitutions, source) + OperationSchema(host).before_substitute(source, substitutions) @staticmethod def before_delete_schema(item: LibraryItem) -> None: @@ -58,4 +58,4 @@ class PropagationFacade: return schema = RSForm(item) - PropagationFacade.before_delete_cst(list(schema.constituents()), schema) + PropagationFacade.before_delete_cst(schema, list(schema.constituents())) diff --git a/rsconcept/backend/apps/oss/tests/s_propagation/t_constituents.py b/rsconcept/backend/apps/oss/tests/s_propagation/t_constituents.py index 0d9ec899..a7b9f559 100644 --- a/rsconcept/backend/apps/oss/tests/s_propagation/t_constituents.py +++ b/rsconcept/backend/apps/oss/tests/s_propagation/t_constituents.py @@ -51,7 +51,7 @@ class TestChangeConstituents(EndpointTester): alias='3', operation_type=OperationType.SYNTHESIS ) - self.owned.set_arguments(self.operation3, [self.operation1, self.operation2]) + self.owned.set_arguments(self.operation3.pk, [self.operation1, self.operation2]) self.owned.execute_operation(self.operation3) self.operation3.refresh_from_db() self.ks3 = RSForm(self.operation3.result) diff --git a/rsconcept/backend/apps/oss/tests/s_propagation/t_operations.py b/rsconcept/backend/apps/oss/tests/s_propagation/t_operations.py index 987030b3..eaff883e 100644 --- a/rsconcept/backend/apps/oss/tests/s_propagation/t_operations.py +++ b/rsconcept/backend/apps/oss/tests/s_propagation/t_operations.py @@ -71,8 +71,8 @@ class TestChangeOperations(EndpointTester): alias='4', operation_type=OperationType.SYNTHESIS ) - self.owned.set_arguments(self.operation4, [self.operation1, self.operation2]) - self.owned.set_substitutions(self.operation4, [{ + self.owned.set_arguments(self.operation4.pk, [self.operation1, self.operation2]) + self.owned.set_substitutions(self.operation4.pk, [{ 'original': self.ks1X1, 'substitution': self.ks2S1 }]) @@ -92,8 +92,8 @@ class TestChangeOperations(EndpointTester): alias='5', operation_type=OperationType.SYNTHESIS ) - self.owned.set_arguments(self.operation5, [self.operation4, self.operation3]) - self.owned.set_substitutions(self.operation5, [{ + self.owned.set_arguments(self.operation5.pk, [self.operation4, self.operation3]) + self.owned.set_substitutions(self.operation5.pk, [{ 'original': self.ks4X1, 'substitution': self.ks3X1 }]) @@ -249,3 +249,75 @@ class TestChangeOperations(EndpointTester): self.assertEqual(self.ks5.constituents().count(), 8) self.assertEqual(self.ks4D2.definition_formal, r'X1 X2 X3 S1 D1') self.assertEqual(self.ks5D4.definition_formal, r'X1 X2 X3 S1 D1 D2 D3') + + @decl_endpoint('/api/oss/{item}/update-operation', method='patch') + def test_change_substitutions(self): + data = { + 'target': self.operation4.pk, + 'item_data': { + 'alias': 'Test4 mod', + 'title': 'Test title mod', + 'comment': 'Comment mod' + }, + 'positions': [], + 'substitutions': [ + { + 'original': self.ks1X1.pk, + 'substitution': self.ks2X2.pk + }, + { + 'original': self.ks2X1.pk, + 'substitution': self.ks1D1.pk + } + ] + } + + self.executeOK(data=data, item=self.owned_id) + self.ks4D2.refresh_from_db() + self.ks5D4.refresh_from_db() + subs1_2 = self.operation4.getSubstitutions() + self.assertEqual(subs1_2.count(), 2) + subs3_4 = self.operation5.getSubstitutions() + self.assertEqual(subs3_4.count(), 1) + self.assertEqual(self.ks4.constituents().count(), 5) + self.assertEqual(self.ks5.constituents().count(), 7) + self.assertEqual(self.ks4D2.definition_formal, r'X1 D1 X3 S1 D1') + self.assertEqual(self.ks5D4.definition_formal, r'X1 D2 X3 S1 D1 D2 D3') + + @decl_endpoint('/api/oss/{item}/update-operation', method='patch') + def test_change_arguments(self): + data = { + 'target': self.operation4.pk, + 'item_data': { + 'alias': 'Test4 mod', + 'title': 'Test title mod', + 'comment': 'Comment mod' + }, + 'positions': [], + 'arguments': [self.operation1.pk], + } + + self.executeOK(data=data, item=self.owned_id) + self.ks4D2.refresh_from_db() + self.ks5D4.refresh_from_db() + subs1_2 = self.operation4.getSubstitutions() + self.assertEqual(subs1_2.count(), 0) + subs3_4 = self.operation5.getSubstitutions() + self.assertEqual(subs3_4.count(), 1) + self.assertEqual(self.ks4.constituents().count(), 4) + self.assertEqual(self.ks5.constituents().count(), 6) + self.assertEqual(self.ks4D2.definition_formal, r'X1 DEL DEL DEL D1') + self.assertEqual(self.ks5D4.definition_formal, r'X1 DEL DEL DEL D1 D2 D3') + + data['arguments'] = [self.operation1.pk, self.operation2.pk] + self.executeOK(data=data, item=self.owned_id) + self.ks4D2.refresh_from_db() + self.ks5D4.refresh_from_db() + subs1_2 = self.operation4.getSubstitutions() + self.assertEqual(subs1_2.count(), 0) + subs3_4 = self.operation5.getSubstitutions() + self.assertEqual(subs3_4.count(), 1) + self.assertEqual(self.ks4.constituents().count(), 7) + self.assertEqual(self.ks5.constituents().count(), 9) + self.assertEqual(self.ks4D2.definition_formal, r'X1 DEL DEL DEL D1') + self.assertEqual(self.ks5D4.definition_formal, r'X1 DEL DEL DEL D1 D2 D3') diff --git a/rsconcept/backend/apps/oss/tests/s_propagation/t_substitutions.py b/rsconcept/backend/apps/oss/tests/s_propagation/t_substitutions.py index 7cc5ae26..1a383a42 100644 --- a/rsconcept/backend/apps/oss/tests/s_propagation/t_substitutions.py +++ b/rsconcept/backend/apps/oss/tests/s_propagation/t_substitutions.py @@ -71,8 +71,8 @@ class TestChangeSubstitutions(EndpointTester): alias='4', operation_type=OperationType.SYNTHESIS ) - self.owned.set_arguments(self.operation4, [self.operation1, self.operation2]) - self.owned.set_substitutions(self.operation4, [{ + self.owned.set_arguments(self.operation4.pk, [self.operation1, self.operation2]) + self.owned.set_substitutions(self.operation4.pk, [{ 'original': self.ks1X1, 'substitution': self.ks2S1 }]) @@ -92,8 +92,8 @@ class TestChangeSubstitutions(EndpointTester): alias='5', operation_type=OperationType.SYNTHESIS ) - self.owned.set_arguments(self.operation5, [self.operation4, self.operation3]) - self.owned.set_substitutions(self.operation5, [{ + self.owned.set_arguments(self.operation5.pk, [self.operation4, self.operation3]) + self.owned.set_substitutions(self.operation5.pk, [{ 'original': self.ks4X1, 'substitution': self.ks3X1 }]) diff --git a/rsconcept/backend/apps/oss/tests/s_views/t_oss.py b/rsconcept/backend/apps/oss/tests/s_views/t_oss.py index 6937d9bb..ba52d035 100644 --- a/rsconcept/backend/apps/oss/tests/s_views/t_oss.py +++ b/rsconcept/backend/apps/oss/tests/s_views/t_oss.py @@ -55,8 +55,8 @@ class TestOssViewset(EndpointTester): alias='3', operation_type=OperationType.SYNTHESIS ) - self.owned.set_arguments(self.operation3, [self.operation1, self.operation2]) - self.owned.set_substitutions(self.operation3, [{ + self.owned.set_arguments(self.operation3.pk, [self.operation1, self.operation2]) + self.owned.set_substitutions(self.operation3.pk, [{ 'original': self.ks1X1, 'substitution': self.ks2X1 }]) diff --git a/rsconcept/backend/apps/oss/views/oss.py b/rsconcept/backend/apps/oss/views/oss.py index 6a4a4dc5..adc70da8 100644 --- a/rsconcept/backend/apps/oss/views/oss.py +++ b/rsconcept/backend/apps/oss/views/oss.py @@ -129,7 +129,7 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev oss.create_input(new_operation) if new_operation.operation_type != m.OperationType.INPUT and 'arguments' in serializer.validated_data: oss.set_arguments( - operation=new_operation, + target=new_operation.pk, arguments=serializer.validated_data['arguments'] ) return Response( @@ -165,7 +165,7 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev old_schema: Optional[LibraryItem] = operation.result with transaction.atomic(): oss.update_positions(serializer.validated_data['positions']) - oss.delete_operation(operation, serializer.validated_data['keep_constituents']) + oss.delete_operation(operation.pk, serializer.validated_data['keep_constituents']) if old_schema is not None: if serializer.validated_data['delete_schema']: m.PropagationFacade.before_delete_schema(old_schema) @@ -305,9 +305,9 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev operation.result.comment = operation.comment operation.result.save() if 'arguments' in serializer.validated_data: - oss.set_arguments(operation, serializer.validated_data['arguments']) + oss.set_arguments(operation.pk, serializer.validated_data['arguments']) if 'substitutions' in serializer.validated_data: - oss.set_substitutions(operation, serializer.validated_data['substitutions']) + oss.set_substitutions(operation.pk, serializer.validated_data['substitutions']) return Response( status=c.HTTP_200_OK, data=s.OperationSchemaSerializer(oss.model).data diff --git a/rsconcept/backend/apps/rsform/graph.py b/rsconcept/backend/apps/rsform/graph.py index b6a749ad..bdb6ce6e 100644 --- a/rsconcept/backend/apps/rsform/graph.py +++ b/rsconcept/backend/apps/rsform/graph.py @@ -42,6 +42,28 @@ class Graph(Generic[ItemType]): if src not in self.inputs[dest]: self.inputs[dest].append(src) + def remove_edge(self, src: ItemType, dest: ItemType): + ''' Remove edge from graph. ''' + if not self.contains(src) or not self.contains(dest): + return + if dest in self.outputs[src]: + self.outputs[src].remove(dest) + if src in self.inputs[dest]: + self.inputs[dest].remove(src) + + def remove_node(self, target: ItemType): + ''' Remove node from graph. ''' + if not self.contains(target): + return + del self.outputs[target] + del self.inputs[target] + for list_out in self.outputs.values(): + if target in list_out: + list_out.remove(target) + for list_in in self.inputs.values(): + if target in list_in: + list_in.remove(target) + def expand_inputs(self, origin: Iterable[ItemType]) -> list[ItemType]: ''' Expand origin nodes forward through graph edges. ''' result: list[ItemType] = [] diff --git a/rsconcept/backend/apps/rsform/tests/t_graph.py b/rsconcept/backend/apps/rsform/tests/t_graph.py index 63713f95..3c99462a 100644 --- a/rsconcept/backend/apps/rsform/tests/t_graph.py +++ b/rsconcept/backend/apps/rsform/tests/t_graph.py @@ -26,6 +26,32 @@ class TestGraph(unittest.TestCase): self.assertTrue(graph.has_edge(1, 3)) self.assertTrue(graph.has_edge(2, 1)) + def test_remove_node(self): + graph = Graph({ + 1: [2], + 2: [3, 5], + 3: [], + 5: [] + }) + self.assertEqual(len(graph.outputs), 4) + graph.remove_node(0) + graph.remove_node(2) + self.assertEqual(graph.outputs[1], []) + self.assertEqual(len(graph.outputs), 3) + + def test_remove_edge(self): + graph = Graph({ + 1: [2], + 2: [3, 5], + 3: [], + 5: [] + }) + graph.remove_edge(0, 1) + graph.remove_edge(2, 1) + self.assertEqual(graph.outputs[1], [2]) + graph.remove_edge(1, 2) + self.assertEqual(graph.outputs[1], []) + graph.remove_edge(1, 2) def test_expand_outputs(self): graph = Graph({ diff --git a/rsconcept/backend/apps/rsform/views/rsforms.py b/rsconcept/backend/apps/rsform/views/rsforms.py index 4499e836..1565da34 100644 --- a/rsconcept/backend/apps/rsform/views/rsforms.py +++ b/rsconcept/backend/apps/rsform/views/rsforms.py @@ -87,7 +87,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr schema = m.RSForm(self._get_item()) with transaction.atomic(): new_cst = schema.create_cst(data, insert_after) - PropagationFacade.after_create_cst([new_cst], schema) + PropagationFacade.after_create_cst(schema, [new_cst]) return Response( status=c.HTTP_201_CREATED, data={ @@ -118,7 +118,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr data = serializer.validated_data['item_data'] with transaction.atomic(): old_data = schema.update_cst(cst, data) - PropagationFacade.after_update_cst(cst, data, old_data, schema) + PropagationFacade.after_update_cst(schema, cst, data, old_data) return Response( status=c.HTTP_200_OK, data=s.CstSerializer(m.Constituenta.objects.get(pk=request.data['target'])).data @@ -159,7 +159,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr with transaction.atomic(): new_cst = schema.produce_structure(cst, cst_parse) - PropagationFacade.after_create_cst(new_cst, schema) + PropagationFacade.after_create_cst(schema, new_cst) return Response( status=c.HTTP_200_OK, data={ @@ -197,7 +197,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr schema.save() cst.refresh_from_db() if changed_type: - PropagationFacade.after_change_cst_type(cst, schema) + PropagationFacade.after_change_cst_type(schema, cst) return Response( status=c.HTTP_200_OK, data={ @@ -233,7 +233,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr original = cast(m.Constituenta, substitution['original']) replacement = cast(m.Constituenta, substitution['substitution']) substitutions.append((original, replacement)) - PropagationFacade.before_substitute(substitutions, schema) + PropagationFacade.before_substitute(schema, substitutions) schema.substitute(substitutions) return Response( status=c.HTTP_200_OK, @@ -263,7 +263,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr cst_list: list[m.Constituenta] = serializer.validated_data['items'] schema = m.RSForm(model) with transaction.atomic(): - PropagationFacade.before_delete_cst(cst_list, schema) + PropagationFacade.before_delete_cst(schema, cst_list) schema.delete_cst(cst_list) return Response( status=c.HTTP_200_OK, @@ -581,7 +581,7 @@ def inline_synthesis(request: Request) -> HttpResponse: with transaction.atomic(): new_items = receiver.insert_copy(items) - PropagationFacade.after_create_cst(new_items, receiver) + PropagationFacade.after_create_cst(receiver, new_items) substitutions: list[tuple[m.Constituenta, m.Constituenta]] = [] for substitution in serializer.validated_data['substitutions']: @@ -595,7 +595,7 @@ def inline_synthesis(request: Request) -> HttpResponse: replacement = new_items[index] substitutions.append((original, replacement)) - PropagationFacade.before_substitute(substitutions, receiver) + PropagationFacade.before_substitute(receiver, substitutions) receiver.substitute(substitutions) receiver.restore_order() diff --git a/rsconcept/frontend/src/dialogs/DlgEditOperation/DlgEditOperation.tsx b/rsconcept/frontend/src/dialogs/DlgEditOperation/DlgEditOperation.tsx index 81967b3a..876bd3cc 100644 --- a/rsconcept/frontend/src/dialogs/DlgEditOperation/DlgEditOperation.tsx +++ b/rsconcept/frontend/src/dialogs/DlgEditOperation/DlgEditOperation.tsx @@ -67,8 +67,8 @@ function DlgEditOperation({ hideWindow, oss, target, onSubmit }: DlgEditOperatio if (cache.loading || schemas.length !== schemasIDs.length) { return; } - setSubstitutions(prev => - prev.filter(sub => { + setSubstitutions(() => + target.substitutions.filter(sub => { const original = cache.getSchemaByCst(sub.original); if (!original || !schemasIDs.includes(original.id)) { return false; @@ -80,7 +80,7 @@ function DlgEditOperation({ hideWindow, oss, target, onSubmit }: DlgEditOperatio return true; }) ); - }, [schemasIDs, schemas, cache.loading]); + }, [schemasIDs, schemas, cache.loading, target.substitutions]); const handleSubmit = () => { const data: IOperationUpdateData = { diff --git a/rsconcept/frontend/src/pages/RSFormPage/MenuRSTabs.tsx b/rsconcept/frontend/src/pages/RSFormPage/MenuRSTabs.tsx index 3fbce140..5a12c196 100644 --- a/rsconcept/frontend/src/pages/RSFormPage/MenuRSTabs.tsx +++ b/rsconcept/frontend/src/pages/RSFormPage/MenuRSTabs.tsx @@ -30,7 +30,7 @@ import Dropdown from '@/components/ui/Dropdown'; import DropdownButton from '@/components/ui/DropdownButton'; import { useAccessMode } from '@/context/AccessModeContext'; import { useAuth } from '@/context/AuthContext'; -import { useLibrary } from '@/context/LibraryContext'; +import { useGlobalOss } from '@/context/GlobalOssContext'; import { useConceptNavigation } from '@/context/NavigationContext'; import { useRSForm } from '@/context/RSFormContext'; import useDropdown from '@/hooks/useDropdown'; @@ -50,7 +50,7 @@ function MenuRSTabs({ onDestroy }: MenuRSTabsProps) { const router = useConceptNavigation(); const { user } = useAuth(); const model = useRSForm(); - const library = useLibrary(); + const oss = useGlobalOss(); const { accessLevel, setAccessLevel } = useAccessMode(); @@ -185,11 +185,11 @@ function MenuRSTabs({ onDestroy }: MenuRSTabsProps) { onClick={handleCreateNew} /> ) : null} - {library.globalOSS ? ( + {oss.schema ? ( } - onClick={() => router.push(urls.oss(library.globalOSS!.id, OssTabID.GRAPH))} + onClick={() => router.push(urls.oss(oss.schema!.id, OssTabID.GRAPH))} /> ) : null}