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 =>
{props.getValue()}
}), diff --git a/rsconcept/frontend/src/components/select/PickSubstitutions.tsx b/rsconcept/frontend/src/components/select/PickSubstitutions.tsx index dfb35f9a..0540bca7 100644 --- a/rsconcept/frontend/src/components/select/PickSubstitutions.tsx +++ b/rsconcept/frontend/src/components/select/PickSubstitutions.tsx @@ -84,10 +84,10 @@ function PickSubstitutions({ const substitutionData: IMultiSubstitution[] = useMemo( () => substitutions.map(item => ({ - original_source: getSchemaByCst(item.original), - original: getConstituenta(item.original), - substitution: getConstituenta(item.substitution), - substitution_source: getSchemaByCst(item.substitution) + original_source: getSchemaByCst(item.original)!, + original: getConstituenta(item.original)!, + substitution: getConstituenta(item.substitution)!, + substitution_source: getSchemaByCst(item.substitution)! })), [getConstituenta, getSchemaByCst, substitutions] ); @@ -138,37 +138,31 @@ function PickSubstitutions({ const columns = useMemo( () => [ - columnHelper.accessor(item => item.substitution_source?.alias ?? 'N/A', { + columnHelper.accessor(item => item.substitution_source.alias, { id: 'left_schema', size: 100, cell: props =>
{props.getValue()}
}), - columnHelper.accessor(item => item.substitution?.alias ?? 'N/A', { + columnHelper.accessor(item => item.substitution.alias, { id: 'left_alias', size: 65, - cell: props => - props.row.original.substitution ? ( - - ) : ( - 'N/A' - ) + cell: props => ( + + ) }), columnHelper.display({ id: 'status', size: 40, cell: () => }), - columnHelper.accessor(item => item.original?.alias ?? 'N/A', { + columnHelper.accessor(item => item.original.alias, { id: 'right_alias', size: 65, - cell: props => - props.row.original.original ? ( - - ) : ( - 'N/A' - ) + cell: props => ( + + ) }), - columnHelper.accessor(item => item.original_source?.alias ?? 'N/A', { + columnHelper.accessor(item => item.original_source.alias, { id: 'right_schema', size: 100, cell: props =>
{props.getValue()}
diff --git a/rsconcept/frontend/src/dialogs/DlgEditOperation/DlgEditOperation.tsx b/rsconcept/frontend/src/dialogs/DlgEditOperation/DlgEditOperation.tsx index 2d914b46..81967b3a 100644 --- a/rsconcept/frontend/src/dialogs/DlgEditOperation/DlgEditOperation.tsx +++ b/rsconcept/frontend/src/dialogs/DlgEditOperation/DlgEditOperation.tsx @@ -63,6 +63,25 @@ function DlgEditOperation({ hideWindow, oss, target, onSubmit }: DlgEditOperatio cache.preload(schemasIDs); }, [schemasIDs]); + useEffect(() => { + if (cache.loading || schemas.length !== schemasIDs.length) { + return; + } + setSubstitutions(prev => + prev.filter(sub => { + const original = cache.getSchemaByCst(sub.original); + if (!original || !schemasIDs.includes(original.id)) { + return false; + } + const substitution = cache.getSchemaByCst(sub.substitution); + if (!substitution || !schemasIDs.includes(substitution.id)) { + return false; + } + return true; + }) + ); + }, [schemasIDs, schemas, cache.loading]); + const handleSubmit = () => { const data: IOperationUpdateData = { target: target.id, diff --git a/rsconcept/frontend/src/dialogs/DlgEditOperation/TabArguments.tsx b/rsconcept/frontend/src/dialogs/DlgEditOperation/TabArguments.tsx index b872057c..c1208230 100644 --- a/rsconcept/frontend/src/dialogs/DlgEditOperation/TabArguments.tsx +++ b/rsconcept/frontend/src/dialogs/DlgEditOperation/TabArguments.tsx @@ -2,13 +2,12 @@ import { useMemo } from 'react'; +import PickMultiOperation from '@/components/select/PickMultiOperation'; import FlexColumn from '@/components/ui/FlexColumn'; import Label from '@/components/ui/Label'; import AnimateFade from '@/components/wrap/AnimateFade'; import { IOperationSchema, OperationID } from '@/models/oss'; -import PickMultiOperation from '../../components/select/PickMultiOperation'; - interface TabArgumentsProps { oss: IOperationSchema; target: OperationID; diff --git a/rsconcept/frontend/src/hooks/useRSFormCache.ts b/rsconcept/frontend/src/hooks/useRSFormCache.ts index c69872ce..a62f7890 100644 --- a/rsconcept/frontend/src/hooks/useRSFormCache.ts +++ b/rsconcept/frontend/src/hooks/useRSFormCache.ts @@ -55,11 +55,11 @@ function useRSFormCache() { useEffect(() => { const ids = pending.filter(id => !processing.includes(id) && !cache.find(schema => schema.id === id)); + setPending([]); if (ids.length === 0) { return; } setProcessing(prev => [...prev, ...ids]); - setPending([]); ids.forEach(id => getRSFormDetails(String(id), '', { showError: false, diff --git a/rsconcept/frontend/src/models/oss.ts b/rsconcept/frontend/src/models/oss.ts index c818b413..fb31af81 100644 --- a/rsconcept/frontend/src/models/oss.ts +++ b/rsconcept/frontend/src/models/oss.ts @@ -119,10 +119,10 @@ export interface ICstSubstituteData { * Represents substitution for multi synthesis table. */ export interface IMultiSubstitution { - original_source: ILibraryItem | undefined; - original: IConstituenta | undefined; - substitution: IConstituenta | undefined; - substitution_source: ILibraryItem | undefined; + original_source: ILibraryItem; + original: IConstituenta; + substitution: IConstituenta; + substitution_source: ILibraryItem; } /** diff --git a/rsconcept/frontend/src/pages/OssPage/OssTabs.tsx b/rsconcept/frontend/src/pages/OssPage/OssTabs.tsx index 72e02cb1..2d5dc170 100644 --- a/rsconcept/frontend/src/pages/OssPage/OssTabs.tsx +++ b/rsconcept/frontend/src/pages/OssPage/OssTabs.tsx @@ -35,7 +35,7 @@ function OssTabs() { const query = useQueryStrings(); const activeTab = query.get('tab') ? (Number(query.get('tab')) as OssTabID) : OssTabID.GRAPH; - const { calculateHeight } = useConceptOptions(); + const { calculateHeight, setNoFooter } = useConceptOptions(); const { schema, loading, errorLoading } = useOSS(); const { destroyItem } = useLibrary(); @@ -53,6 +53,10 @@ function OssTabs() { } }, [schema, schema?.title]); + useLayoutEffect(() => { + setNoFooter(activeTab === OssTabID.GRAPH); + }, [activeTab, setNoFooter]); + const navigateTab = useCallback( (tab: OssTabID) => { if (!schema) {