mirror of
https://github.com/IRBorisov/ConceptPortal.git
synced 2025-06-26 04:50:36 +03:00
F: Prepare operation change propagation
This commit is contained in:
parent
ebf8af38a8
commit
263f3f93eb
|
@ -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]
|
||||
|
|
|
@ -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 *
|
||||
|
|
193
rsconcept/backend/apps/oss/tests/s_propagation/t_operations.py
Normal file
193
rsconcept/backend/apps/oss/tests/s_propagation/t_operations.py
Normal 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')
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}
|
||||
</p>
|
||||
) : null}
|
||||
<p>
|
||||
<b>Положение:</b> [{node.xPos}, {node.yPos}]
|
||||
</p>
|
||||
{node.data.operation.substitutions.length > 0 ? table : null}
|
||||
{node.data.operation.substitutions.length > 0 ? (
|
||||
table
|
||||
) : node.data.operation.operation_type !== OperationType.INPUT ? (
|
||||
<p>
|
||||
<b>Отождествления:</b> Отсутствуют
|
||||
</p>
|
||||
) : null}
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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 => <div className='text-ellipsis'>{props.getValue()}</div>
|
||||
}),
|
||||
|
|
|
@ -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 => <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',
|
||||
size: 65,
|
||||
cell: props =>
|
||||
props.row.original.substitution ? (
|
||||
<BadgeConstituenta theme={colors} value={props.row.original.substitution} prefixID={`${prefixID}_1_`} />
|
||||
) : (
|
||||
'N/A'
|
||||
)
|
||||
cell: props => (
|
||||
<BadgeConstituenta theme={colors} value={props.row.original.substitution} prefixID={`${prefixID}_1_`} />
|
||||
)
|
||||
}),
|
||||
columnHelper.display({
|
||||
id: 'status',
|
||||
size: 40,
|
||||
cell: () => <IconPageRight size='1.2rem' />
|
||||
}),
|
||||
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 ? (
|
||||
<BadgeConstituenta theme={colors} value={props.row.original.original} prefixID={`${prefixID}_1_`} />
|
||||
) : (
|
||||
'N/A'
|
||||
)
|
||||
cell: props => (
|
||||
<BadgeConstituenta theme={colors} value={props.row.original.original} prefixID={`${prefixID}_1_`} />
|
||||
)
|
||||
}),
|
||||
columnHelper.accessor(item => item.original_source?.alias ?? 'N/A', {
|
||||
columnHelper.accessor(item => item.original_source.alias, {
|
||||
id: 'right_schema',
|
||||
size: 100,
|
||||
cell: props => <div className='min-w-[8rem] text-ellipsis text-right'>{props.getValue()}</div>
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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) {
|
||||
|
|
Loading…
Reference in New Issue
Block a user