diff --git a/rsconcept/backend/apps/oss/models/OperationSchema.py b/rsconcept/backend/apps/oss/models/OperationSchema.py index 9572b7c8..9d7f4012 100644 --- a/rsconcept/backend/apps/oss/models/OperationSchema.py +++ b/rsconcept/backend/apps/oss/models/OperationSchema.py @@ -100,26 +100,36 @@ class OperationSchema: if not keep_constituents: schema = self.cache.get_schema(target) if schema is not None: - self._cascade_before_delete(schema.cache.constituents, target.pk) + self.before_delete(schema.cache.constituents, schema) self.cache.remove_operation(target.pk) target.delete() self.save(update_fields=['time_update']) - def set_input(self, target: Operation, schema: Optional[LibraryItem]) -> None: + def set_input(self, target: int, schema: Optional[LibraryItem]) -> None: ''' Set input schema for operation. ''' - if schema == target.result: + operation = self.cache.operation_by_id[target] + has_children = len(self.cache.graph.outputs[target]) > 0 + old_schema = self.cache.get_schema(operation) + if schema == old_schema: return - target.result = schema + + if old_schema is not None: + if has_children: + self.before_delete(old_schema.cache.constituents, old_schema) + self.cache.remove_schema(old_schema) + + operation.result = schema if schema is not None: - target.result = schema - target.alias = schema.alias - target.title = schema.title - target.comment = schema.comment - target.save() + operation.result = schema + operation.alias = schema.alias + operation.title = schema.title + operation.comment = schema.comment + operation.save(update_fields=['result', 'alias', 'title', 'comment']) - # TODO: trigger on_change effects - - self.save() + if schema is not None and has_children: + rsform = RSForm(schema) + self.after_create_cst(list(rsform.constituents()), rsform) + self.save(update_fields=['time_update']) def set_arguments(self, operation: Operation, arguments: list[Operation]) -> None: ''' Set arguments to operation. ''' @@ -699,6 +709,11 @@ 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 unfold_sub(self, sub: Substitution) -> tuple[RSForm, RSForm, Constituenta, Constituenta]: operation = self.operation_by_id[sub.operation_id] parents = self.graph.inputs[operation.pk] diff --git a/rsconcept/backend/apps/oss/tests/s_propagation/__init__.py b/rsconcept/backend/apps/oss/tests/s_propagation/__init__.py index 3e4ff908..fd9d7fbb 100644 --- a/rsconcept/backend/apps/oss/tests/s_propagation/__init__.py +++ b/rsconcept/backend/apps/oss/tests/s_propagation/__init__.py @@ -1,4 +1,5 @@ ''' Tests for REST API OSS propagation. ''' from .t_attributes import * from .t_constituents import * +from .t_operations import * from .t_substitutions import * diff --git a/rsconcept/backend/apps/oss/tests/s_propagation/t_operations.py b/rsconcept/backend/apps/oss/tests/s_propagation/t_operations.py new file mode 100644 index 00000000..76dec21a --- /dev/null +++ b/rsconcept/backend/apps/oss/tests/s_propagation/t_operations.py @@ -0,0 +1,193 @@ +''' Testing API: Change substitutions in OSS. ''' + +from apps.oss.models import OperationSchema, OperationType +from apps.rsform.models import Constituenta, CstType, RSForm +from shared.EndpointTester import EndpointTester, decl_endpoint + + +class TestChangeOperations(EndpointTester): + ''' Testing Operations change propagation in OSS. ''' + + def setUp(self): + super().setUp() + self.owned = OperationSchema.create( + title='Test', + alias='T1', + owner=self.user + ) + self.owned_id = self.owned.model.pk + + self.ks1 = RSForm.create( + alias='KS1', + title='Test1', + owner=self.user + ) + self.ks1X1 = self.ks1.insert_new('X1', convention='KS1X1') + self.ks1X2 = self.ks1.insert_new('X2', convention='KS1X2') + self.ks1D1 = self.ks1.insert_new('D1', definition_formal='X1 X2', convention='KS1D1') + + self.ks2 = RSForm.create( + alias='KS2', + title='Test2', + owner=self.user + ) + self.ks2X1 = self.ks2.insert_new('X1', convention='KS2X1') + self.ks2X2 = self.ks2.insert_new('X2', convention='KS2X2') + self.ks2S1 = self.ks2.insert_new( + alias='S1', + definition_formal=r'X1', + convention='KS2S1' + ) + + self.ks3 = RSForm.create( + alias='KS3', + title='Test3', + owner=self.user + ) + self.ks3X1 = self.ks3.insert_new('X1', convention='KS3X1') + self.ks3D1 = self.ks3.insert_new( + alias='D1', + definition_formal='X1 X1', + convention='KS3D1' + ) + + self.operation1 = self.owned.create_operation( + alias='1', + operation_type=OperationType.INPUT, + result=self.ks1.model + ) + self.operation2 = self.owned.create_operation( + alias='2', + operation_type=OperationType.INPUT, + result=self.ks2.model + ) + self.operation3 = self.owned.create_operation( + alias='3', + operation_type=OperationType.INPUT, + result=self.ks3.model + ) + + self.operation4 = self.owned.create_operation( + alias='4', + operation_type=OperationType.SYNTHESIS + ) + self.owned.set_arguments(self.operation4, [self.operation1, self.operation2]) + self.owned.set_substitutions(self.operation4, [{ + 'original': self.ks1X1, + 'substitution': self.ks2S1 + }]) + self.owned.execute_operation(self.operation4) + self.operation4.refresh_from_db() + self.ks4 = RSForm(self.operation4.result) + self.ks4X1 = Constituenta.objects.get(as_child__parent_id=self.ks1X2.pk) + self.ks4S1 = Constituenta.objects.get(as_child__parent_id=self.ks2S1.pk) + self.ks4D1 = Constituenta.objects.get(as_child__parent_id=self.ks1D1.pk) + self.ks4D2 = self.ks4.insert_new( + alias='D2', + definition_formal=r'X1 X2 X3 S1 D1', + convention='KS4D2' + ) + + self.operation5 = self.owned.create_operation( + alias='5', + operation_type=OperationType.SYNTHESIS + ) + self.owned.set_arguments(self.operation5, [self.operation4, self.operation3]) + self.owned.set_substitutions(self.operation5, [{ + 'original': self.ks4X1, + 'substitution': self.ks3X1 + }]) + self.owned.execute_operation(self.operation5) + self.operation5.refresh_from_db() + self.ks5 = RSForm(self.operation5.result) + self.ks5D4 = self.ks5.insert_new( + alias='D4', + definition_formal=r'X1 X2 X3 S1 D1 D2 D3', + convention='KS5D4' + ) + + def test_oss_setup(self): + self.assertEqual(self.ks1.constituents().count(), 3) + self.assertEqual(self.ks2.constituents().count(), 3) + self.assertEqual(self.ks3.constituents().count(), 2) + self.assertEqual(self.ks4.constituents().count(), 6) + self.assertEqual(self.ks5.constituents().count(), 8) + self.assertEqual(self.ks4D1.definition_formal, 'S1 X1') + + @decl_endpoint('/api/oss/{item}/delete-operation', method='patch') + def test_delete_input_operation(self): + data = { + 'positions': [], + 'target': self.operation2.pk + } + self.executeOK(data=data, item=self.owned_id) + self.ks4D1.refresh_from_db() + 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.ks4D1.definition_formal, r'X4 X1') + 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') + + @decl_endpoint('/api/oss/{item}/set-input', method='patch') + def test_set_input_null(self): + data = { + 'positions': [], + 'target': self.operation2.pk, + 'input': None + } + self.executeOK(data=data, item=self.owned_id) + self.ks4D1.refresh_from_db() + self.ks4D2.refresh_from_db() + self.ks5D4.refresh_from_db() + self.operation2.refresh_from_db() + self.assertEqual(self.operation2.result, None) + 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.ks4D1.definition_formal, r'X4 X1') + 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') + + @decl_endpoint('/api/oss/{item}/set-input', method='patch') + def test_set_input_change_schema(self): + ks6 = RSForm.create( + alias='KS6', + title='Test6', + owner=self.user + ) + ks6X1 = ks6.insert_new('X1', convention='KS6X1') + ks6X2 = ks6.insert_new('X2', convention='KS6X2') + ks6D1 = ks6.insert_new('D1', definition_formal='X1 X2', convention='KS6D1') + + data = { + 'positions': [], + 'target': self.operation2.pk, + 'input': ks6.model.pk + } + self.executeOK(data=data, item=self.owned_id) + ks4Dks6 = Constituenta.objects.get(as_child__parent_id=ks6D1.pk) + self.ks4D1.refresh_from_db() + self.ks4D2.refresh_from_db() + self.ks5D4.refresh_from_db() + self.operation2.refresh_from_db() + self.assertEqual(self.operation2.result, ks6.model) + self.assertEqual(self.operation2.alias, ks6.model.alias) + 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(ks4Dks6.definition_formal, r'X5 X6') + self.assertEqual(self.ks4D1.definition_formal, r'X4 X1') + 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_views/t_oss.py b/rsconcept/backend/apps/oss/tests/s_views/t_oss.py index 74df29da..5684f59d 100644 --- a/rsconcept/backend/apps/oss/tests/s_views/t_oss.py +++ b/rsconcept/backend/apps/oss/tests/s_views/t_oss.py @@ -309,8 +309,6 @@ class TestOssViewset(EndpointTester): data['target'] = self.operation1.pk data['input'] = None - - data['target'] = self.operation1.pk self.toggle_admin(True) self.executeBadData(data=data, item=self.unowned_id) self.logout() diff --git a/rsconcept/backend/apps/oss/views/oss.py b/rsconcept/backend/apps/oss/views/oss.py index 3994ae04..a536d9aa 100644 --- a/rsconcept/backend/apps/oss/views/oss.py +++ b/rsconcept/backend/apps/oss/views/oss.py @@ -224,7 +224,7 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev oss = m.OperationSchema(self.get_object()) with transaction.atomic(): oss.update_positions(serializer.validated_data['positions']) - oss.set_input(operation, serializer.validated_data['input']) + oss.set_input(operation.pk, serializer.validated_data['input']) return Response( status=c.HTTP_200_OK, data=s.OperationSchemaSerializer(oss.model).data diff --git a/rsconcept/frontend/src/components/info/TooltipOperation.tsx b/rsconcept/frontend/src/components/info/TooltipOperation.tsx index babfe7aa..e53a6ffc 100644 --- a/rsconcept/frontend/src/components/info/TooltipOperation.tsx +++ b/rsconcept/frontend/src/components/info/TooltipOperation.tsx @@ -5,7 +5,7 @@ import { useMemo } from 'react'; import Tooltip from '@/components/ui/Tooltip'; import { OssNodeInternal } from '@/models/miscellaneous'; -import { ICstSubstituteEx } from '@/models/oss'; +import { ICstSubstituteEx, OperationType } from '@/models/oss'; import { labelOperationType } from '@/utils/labels'; import { IconPageRight } from '../Icons'; @@ -79,10 +79,13 @@ function TooltipOperation({ node, anchor }: TooltipOperationProps) { {node.data.operation.comment}
) : null} -- Положение: [{node.xPos}, {node.yPos}] -
- {node.data.operation.substitutions.length > 0 ? table : null} + {node.data.operation.substitutions.length > 0 ? ( + table + ) : node.data.operation.operation_type !== OperationType.INPUT ? ( ++ Отождествления: Отсутствуют +
+ ) : null} ); } diff --git a/rsconcept/frontend/src/components/select/PickMultiOperation.tsx b/rsconcept/frontend/src/components/select/PickMultiOperation.tsx index 13d89655..75365a48 100644 --- a/rsconcept/frontend/src/components/select/PickMultiOperation.tsx +++ b/rsconcept/frontend/src/components/select/PickMultiOperation.tsx @@ -45,15 +45,15 @@ function PickMultiOperation({ rows, items, selected, setSelected }: PickMultiOpe columnHelper.accessor('alias', { id: 'alias', header: 'Шифр', - size: 150, - minSize: 80, - maxSize: 150 + size: 300, + minSize: 150, + maxSize: 300 }), columnHelper.accessor('title', { id: 'title', header: 'Название', size: 1200, - minSize: 200, + minSize: 300, maxSize: 1200, cell: props =>