F: Prepare operation change propagation

This commit is contained in:
Ivan 2024-08-14 21:50:10 +03:00
parent 63991f713c
commit e17056ea10
13 changed files with 278 additions and 52 deletions

View File

@ -100,26 +100,36 @@ class OperationSchema:
if not keep_constituents: if not keep_constituents:
schema = self.cache.get_schema(target) schema = self.cache.get_schema(target)
if schema is not None: 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) self.cache.remove_operation(target.pk)
target.delete() target.delete()
self.save(update_fields=['time_update']) 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. ''' ''' 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 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: if schema is not None:
target.result = schema operation.result = schema
target.alias = schema.alias operation.alias = schema.alias
target.title = schema.title operation.title = schema.title
target.comment = schema.comment operation.comment = schema.comment
target.save() operation.save(update_fields=['result', 'alias', 'title', 'comment'])
# TODO: trigger on_change effects if schema is not None and has_children:
rsform = RSForm(schema)
self.save() self.after_create_cst(list(rsform.constituents()), rsform)
self.save(update_fields=['time_update'])
def set_arguments(self, operation: Operation, arguments: list[Operation]) -> None: def set_arguments(self, operation: Operation, arguments: list[Operation]) -> None:
''' Set arguments to operation. ''' ''' Set arguments to operation. '''
@ -699,6 +709,11 @@ class OssCache:
del self.substitutions[operation] del self.substitutions[operation]
del self.inheritance[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]: def unfold_sub(self, sub: Substitution) -> tuple[RSForm, RSForm, Constituenta, Constituenta]:
operation = self.operation_by_id[sub.operation_id] operation = self.operation_by_id[sub.operation_id]
parents = self.graph.inputs[operation.pk] parents = self.graph.inputs[operation.pk]

View File

@ -1,4 +1,5 @@
''' Tests for REST API OSS propagation. ''' ''' Tests for REST API OSS propagation. '''
from .t_attributes import * from .t_attributes import *
from .t_constituents import * from .t_constituents import *
from .t_operations import *
from .t_substitutions import * from .t_substitutions import *

View File

@ -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')

View File

@ -309,8 +309,6 @@ class TestOssViewset(EndpointTester):
data['target'] = self.operation1.pk data['target'] = self.operation1.pk
data['input'] = None data['input'] = None
data['target'] = self.operation1.pk
self.toggle_admin(True) self.toggle_admin(True)
self.executeBadData(data=data, item=self.unowned_id) self.executeBadData(data=data, item=self.unowned_id)
self.logout() self.logout()

View File

@ -224,7 +224,7 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
oss = m.OperationSchema(self.get_object()) oss = m.OperationSchema(self.get_object())
with transaction.atomic(): with transaction.atomic():
oss.update_positions(serializer.validated_data['positions']) 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( return Response(
status=c.HTTP_200_OK, status=c.HTTP_200_OK,
data=s.OperationSchemaSerializer(oss.model).data data=s.OperationSchemaSerializer(oss.model).data

View File

@ -5,7 +5,7 @@ import { useMemo } from 'react';
import Tooltip from '@/components/ui/Tooltip'; import Tooltip from '@/components/ui/Tooltip';
import { OssNodeInternal } from '@/models/miscellaneous'; import { OssNodeInternal } from '@/models/miscellaneous';
import { ICstSubstituteEx } from '@/models/oss'; import { ICstSubstituteEx, OperationType } from '@/models/oss';
import { labelOperationType } from '@/utils/labels'; import { labelOperationType } from '@/utils/labels';
import { IconPageRight } from '../Icons'; import { IconPageRight } from '../Icons';
@ -79,10 +79,13 @@ function TooltipOperation({ node, anchor }: TooltipOperationProps) {
{node.data.operation.comment} {node.data.operation.comment}
</p> </p>
) : null} ) : null}
<p> {node.data.operation.substitutions.length > 0 ? (
<b>Положение:</b> [{node.xPos}, {node.yPos}] table
</p> ) : node.data.operation.operation_type !== OperationType.INPUT ? (
{node.data.operation.substitutions.length > 0 ? table : null} <p>
<b>Отождествления:</b> Отсутствуют
</p>
) : null}
</Tooltip> </Tooltip>
); );
} }

View File

@ -45,15 +45,15 @@ function PickMultiOperation({ rows, items, selected, setSelected }: PickMultiOpe
columnHelper.accessor('alias', { columnHelper.accessor('alias', {
id: 'alias', id: 'alias',
header: 'Шифр', header: 'Шифр',
size: 150, size: 300,
minSize: 80, minSize: 150,
maxSize: 150 maxSize: 300
}), }),
columnHelper.accessor('title', { columnHelper.accessor('title', {
id: 'title', id: 'title',
header: 'Название', header: 'Название',
size: 1200, size: 1200,
minSize: 200, minSize: 300,
maxSize: 1200, maxSize: 1200,
cell: props => <div className='text-ellipsis'>{props.getValue()}</div> cell: props => <div className='text-ellipsis'>{props.getValue()}</div>
}), }),

View File

@ -84,10 +84,10 @@ function PickSubstitutions({
const substitutionData: IMultiSubstitution[] = useMemo( const substitutionData: IMultiSubstitution[] = useMemo(
() => () =>
substitutions.map(item => ({ substitutions.map(item => ({
original_source: getSchemaByCst(item.original), original_source: getSchemaByCst(item.original)!,
original: getConstituenta(item.original), original: getConstituenta(item.original)!,
substitution: getConstituenta(item.substitution), substitution: getConstituenta(item.substitution)!,
substitution_source: getSchemaByCst(item.substitution) substitution_source: getSchemaByCst(item.substitution)!
})), })),
[getConstituenta, getSchemaByCst, substitutions] [getConstituenta, getSchemaByCst, substitutions]
); );
@ -138,37 +138,31 @@ function PickSubstitutions({
const columns = useMemo( const columns = useMemo(
() => [ () => [
columnHelper.accessor(item => item.substitution_source?.alias ?? 'N/A', { columnHelper.accessor(item => item.substitution_source.alias, {
id: 'left_schema', id: 'left_schema',
size: 100, size: 100,
cell: props => <div className='min-w-[10.5rem] text-ellipsis text-left'>{props.getValue()}</div> cell: props => <div className='min-w-[10.5rem] text-ellipsis text-left'>{props.getValue()}</div>
}), }),
columnHelper.accessor(item => item.substitution?.alias ?? 'N/A', { columnHelper.accessor(item => item.substitution.alias, {
id: 'left_alias', id: 'left_alias',
size: 65, size: 65,
cell: props => cell: props => (
props.row.original.substitution ? ( <BadgeConstituenta theme={colors} value={props.row.original.substitution} prefixID={`${prefixID}_1_`} />
<BadgeConstituenta theme={colors} value={props.row.original.substitution} prefixID={`${prefixID}_1_`} /> )
) : (
'N/A'
)
}), }),
columnHelper.display({ columnHelper.display({
id: 'status', id: 'status',
size: 40, size: 40,
cell: () => <IconPageRight size='1.2rem' /> cell: () => <IconPageRight size='1.2rem' />
}), }),
columnHelper.accessor(item => item.original?.alias ?? 'N/A', { columnHelper.accessor(item => item.original.alias, {
id: 'right_alias', id: 'right_alias',
size: 65, size: 65,
cell: props => cell: props => (
props.row.original.original ? ( <BadgeConstituenta theme={colors} value={props.row.original.original} prefixID={`${prefixID}_1_`} />
<BadgeConstituenta theme={colors} value={props.row.original.original} prefixID={`${prefixID}_1_`} /> )
) : (
'N/A'
)
}), }),
columnHelper.accessor(item => item.original_source?.alias ?? 'N/A', { columnHelper.accessor(item => item.original_source.alias, {
id: 'right_schema', id: 'right_schema',
size: 100, size: 100,
cell: props => <div className='min-w-[8rem] text-ellipsis text-right'>{props.getValue()}</div> cell: props => <div className='min-w-[8rem] text-ellipsis text-right'>{props.getValue()}</div>

View File

@ -63,6 +63,25 @@ function DlgEditOperation({ hideWindow, oss, target, onSubmit }: DlgEditOperatio
cache.preload(schemasIDs); cache.preload(schemasIDs);
}, [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 handleSubmit = () => {
const data: IOperationUpdateData = { const data: IOperationUpdateData = {
target: target.id, target: target.id,

View File

@ -2,13 +2,12 @@
import { useMemo } from 'react'; import { useMemo } from 'react';
import PickMultiOperation from '@/components/select/PickMultiOperation';
import FlexColumn from '@/components/ui/FlexColumn'; import FlexColumn from '@/components/ui/FlexColumn';
import Label from '@/components/ui/Label'; import Label from '@/components/ui/Label';
import AnimateFade from '@/components/wrap/AnimateFade'; import AnimateFade from '@/components/wrap/AnimateFade';
import { IOperationSchema, OperationID } from '@/models/oss'; import { IOperationSchema, OperationID } from '@/models/oss';
import PickMultiOperation from '../../components/select/PickMultiOperation';
interface TabArgumentsProps { interface TabArgumentsProps {
oss: IOperationSchema; oss: IOperationSchema;
target: OperationID; target: OperationID;

View File

@ -55,11 +55,11 @@ function useRSFormCache() {
useEffect(() => { useEffect(() => {
const ids = pending.filter(id => !processing.includes(id) && !cache.find(schema => schema.id === id)); const ids = pending.filter(id => !processing.includes(id) && !cache.find(schema => schema.id === id));
setPending([]);
if (ids.length === 0) { if (ids.length === 0) {
return; return;
} }
setProcessing(prev => [...prev, ...ids]); setProcessing(prev => [...prev, ...ids]);
setPending([]);
ids.forEach(id => ids.forEach(id =>
getRSFormDetails(String(id), '', { getRSFormDetails(String(id), '', {
showError: false, showError: false,

View File

@ -119,10 +119,10 @@ export interface ICstSubstituteData {
* Represents substitution for multi synthesis table. * Represents substitution for multi synthesis table.
*/ */
export interface IMultiSubstitution { export interface IMultiSubstitution {
original_source: ILibraryItem | undefined; original_source: ILibraryItem;
original: IConstituenta | undefined; original: IConstituenta;
substitution: IConstituenta | undefined; substitution: IConstituenta;
substitution_source: ILibraryItem | undefined; substitution_source: ILibraryItem;
} }
/** /**

View File

@ -35,7 +35,7 @@ function OssTabs() {
const query = useQueryStrings(); const query = useQueryStrings();
const activeTab = query.get('tab') ? (Number(query.get('tab')) as OssTabID) : OssTabID.GRAPH; 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 { schema, loading, errorLoading } = useOSS();
const { destroyItem } = useLibrary(); const { destroyItem } = useLibrary();
@ -53,6 +53,10 @@ function OssTabs() {
} }
}, [schema, schema?.title]); }, [schema, schema?.title]);
useLayoutEffect(() => {
setNoFooter(activeTab === OssTabID.GRAPH);
}, [activeTab, setNoFooter]);
const navigateTab = useCallback( const navigateTab = useCallback(
(tab: OssTabID) => { (tab: OssTabID) => {
if (!schema) { if (!schema) {