F: Implementing references pt2
This commit is contained in:
parent
13e56f51ea
commit
266fdf0c30
|
@ -39,34 +39,29 @@ class OperationSchemaCached:
|
|||
|
||||
def delete_reference(self, target: int, keep_connections: bool = False, keep_constituents: bool = False):
|
||||
''' Delete Reference Operation. '''
|
||||
if not keep_connections:
|
||||
self.delete_operation(target, keep_constituents)
|
||||
return
|
||||
self.cache.ensure_loaded_subs()
|
||||
operation = self.cache.operation_by_id[target]
|
||||
if keep_connections:
|
||||
referred_operations = operation.getQ_reference_target()
|
||||
if len(referred_operations) == 1:
|
||||
referred_operation = referred_operations[0]
|
||||
for arg in operation.getQ_as_argument():
|
||||
arg.pk = None
|
||||
arg.argument = referred_operation
|
||||
arg.save()
|
||||
else:
|
||||
pass
|
||||
# if target.result_id is not None:
|
||||
# self.before_delete_cst(schema, schema.cache.constituents) # TODO: use operation instead of schema
|
||||
|
||||
reference_target = self.cache.reference_target.get(target)
|
||||
if reference_target:
|
||||
for arg in operation.getQ_as_argument():
|
||||
arg.argument_id = reference_target
|
||||
arg.save()
|
||||
self.cache.remove_operation(target)
|
||||
operation.delete()
|
||||
|
||||
|
||||
def delete_operation(self, target: int, keep_constituents: bool = False):
|
||||
''' Delete Operation. '''
|
||||
self.cache.ensure_loaded_subs()
|
||||
operation = self.cache.operation_by_id[target]
|
||||
schema = self.cache.get_schema(operation)
|
||||
children = self.cache.graph.outputs[target]
|
||||
if schema is not None and len(children) > 0:
|
||||
ids = [cst.pk for cst in schema.cache.constituents]
|
||||
if operation.result is not None and len(children) > 0:
|
||||
ids = list(Constituenta.objects.filter(schema=operation.result).values_list('pk', flat=True))
|
||||
if not keep_constituents:
|
||||
self.before_delete_cst(schema.model.pk, ids)
|
||||
self._cascade_delete_inherited(operation.pk, ids)
|
||||
else:
|
||||
inheritance_to_delete: list[Inheritance] = []
|
||||
for child_id in children:
|
||||
|
@ -837,6 +832,8 @@ class OssCache:
|
|||
del self._schema_by_id[target.result_id]
|
||||
self.operations.remove(self.operation_by_id[operation])
|
||||
del self.operation_by_id[operation]
|
||||
if operation in self.reference_target:
|
||||
del self.reference_target[operation]
|
||||
if self.is_loaded_subs:
|
||||
del self.substitutions[operation]
|
||||
del self.inheritance[operation]
|
||||
|
|
|
@ -80,23 +80,42 @@ class ReferencePropagationTestCase(EndpointTester):
|
|||
self.owned.set_arguments(self.operation5.pk, [self.operation4, self.operation3])
|
||||
self.owned.set_substitutions(self.operation5.pk, [{
|
||||
'original': self.ks4X1,
|
||||
'substitution': self.ks1X1
|
||||
'substitution': self.ks1X2
|
||||
}])
|
||||
self.owned.execute_operation(self.operation5)
|
||||
self.operation5.refresh_from_db()
|
||||
self.ks5 = RSForm(self.operation5.result)
|
||||
self.ks5D4 = self.ks5.insert_last(
|
||||
alias='D4',
|
||||
definition_formal=r'X1 X2 X3 S1 D1 D2 D3',
|
||||
definition_formal=r'X1 X2 X3 X4 S1 D1 D2 D3',
|
||||
convention='KS5D4'
|
||||
)
|
||||
|
||||
self.operation6 = self.owned.create_operation(
|
||||
alias='6',
|
||||
operation_type=OperationType.SYNTHESIS
|
||||
)
|
||||
self.owned.set_arguments(self.operation6.pk, [self.operation2, self.operation3])
|
||||
self.owned.set_substitutions(self.operation6.pk, [{
|
||||
'original': self.ks2X1,
|
||||
'substitution': self.ks1X1
|
||||
}])
|
||||
self.owned.execute_operation(self.operation6)
|
||||
self.operation6.refresh_from_db()
|
||||
self.ks6 = RSForm(self.operation6.result)
|
||||
self.ks6D2 = self.ks6.insert_last(
|
||||
alias='D2',
|
||||
definition_formal=r'X1 X2 X3 S1 D1',
|
||||
convention='KS6D2'
|
||||
)
|
||||
|
||||
self.layout_data = [
|
||||
{'nodeID': 'o' + str(self.operation1.pk), 'x': 0, 'y': 0, 'width': 150, 'height': 40},
|
||||
{'nodeID': 'o' + str(self.operation2.pk), 'x': 0, 'y': 0, 'width': 150, 'height': 40},
|
||||
{'nodeID': 'o' + str(self.operation3.pk), 'x': 0, 'y': 0, 'width': 150, 'height': 40},
|
||||
{'nodeID': 'o' + str(self.operation4.pk), 'x': 0, 'y': 0, 'width': 150, 'height': 40},
|
||||
{'nodeID': 'o' + str(self.operation5.pk), 'x': 0, 'y': 0, 'width': 150, 'height': 40}
|
||||
{'nodeID': 'o' + str(self.operation5.pk), 'x': 0, 'y': 0, 'width': 150, 'height': 40},
|
||||
{'nodeID': 'o' + str(self.operation6.pk), 'x': 0, 'y': 0, 'width': 150, 'height': 40},
|
||||
]
|
||||
layout = OperationSchema.layoutQ(self.owned_id)
|
||||
layout.data = self.layout_data
|
||||
|
@ -106,94 +125,22 @@ class ReferencePropagationTestCase(EndpointTester):
|
|||
def test_reference_creation(self):
|
||||
''' Test reference creation. '''
|
||||
self.assertEqual(self.operation1.result, self.operation3.result)
|
||||
self.assertEqual(self.ks1.constituentsQ().count(), 3)
|
||||
self.assertEqual(self.ks2.constituentsQ().count(), 3)
|
||||
self.assertEqual(self.ks4.constituentsQ().count(), 6)
|
||||
self.assertEqual(self.ks5.constituentsQ().count(), 9)
|
||||
self.assertEqual(self.ks6.constituentsQ().count(), 6)
|
||||
|
||||
|
||||
@decl_endpoint('/api/oss/{item}/delete-operation', method='patch')
|
||||
def test_delete_target_propagation(self):
|
||||
''' Test propagation when deleting a target operation. '''
|
||||
data = {
|
||||
'layout': self.layout_data,
|
||||
'target': self.operation1.pk
|
||||
}
|
||||
self.executeOK(data=data, item=self.owned_id)
|
||||
self.assertEqual(self.ks6.constituentsQ().count(), 4)
|
||||
# self.assertEqual(self.ks5.constituentsQ().count(), 5)
|
||||
|
||||
# @decl_endpoint('/api/oss/{item}/delete-reference', method='patch')
|
||||
# def test_delete_reference_propagation(self):
|
||||
# ''' Test propagation when deleting a reference operation. '''
|
||||
# schema_cached = OperationSchemaCached(self.schema2)
|
||||
# # Ensure reference exists
|
||||
# self.assertIn(self.reference.pk, schema_cached.cache.operation_by_id)
|
||||
# # Delete the reference
|
||||
# schema_cached.delete_reference(self.reference.pk)
|
||||
# # Reference should be deleted
|
||||
# with self.assertRaises(Reference.DoesNotExist):
|
||||
# Reference.objects.get(pk=self.reference.pk)
|
||||
# # Operation2 should still exist
|
||||
# self.assertTrue(Operation.objects.filter(pk=self.op2.pk).exists())
|
||||
|
||||
|
||||
# @decl_endpoint('/api/oss/{item}/set-arguments', method='patch')
|
||||
# def test_set_arguments_propagation(self):
|
||||
# ''' Test propagation when setting arguments for a reference. '''
|
||||
# schema_cached = OperationSchemaCached(self.schema2)
|
||||
# # Add op1 as argument to op2
|
||||
# schema_cached.set_arguments(self.op2.pk, [self.op1])
|
||||
# op2 = Operation.objects.get(pk=self.op2.pk)
|
||||
# args = list(op2.getQ_arguments())
|
||||
# self.assertEqual(len(args), 1)
|
||||
# self.assertEqual(args[0].argument, self.op1)
|
||||
|
||||
|
||||
# @decl_endpoint('/api/oss/{item}/delete-operation', method='patch')
|
||||
# def test_delete_operation_with_reference(self):
|
||||
# ''' Test propagation when deleting an operation that is referenced. '''
|
||||
# schema_cached = OperationSchemaCached(self.schema1)
|
||||
# # op1 is referenced by reference
|
||||
# self.assertEqual(self.reference.getQ_reference_target(), self.op1)
|
||||
# # Delete op1
|
||||
# schema_cached.delete_operation(self.op1.pk)
|
||||
# # op1 should be deleted
|
||||
# with self.assertRaises(Operation.DoesNotExist):
|
||||
# Operation.objects.get(pk=self.op1.pk)
|
||||
# # Reference should be deleted as well
|
||||
# self.assertFalse(Reference.objects.filter(pk=self.reference.pk).exists())
|
||||
|
||||
|
||||
# @decl_endpoint('/api/oss/{item}/add-constituent', method='patch')
|
||||
# def test_add_constituent_propagation(self):
|
||||
# ''' Test propagation when adding a constituent to a referenced schema. '''
|
||||
# # Add a new constituent to schema1 (referenced by op1, which is referenced by reference)
|
||||
# new_cst = Constituenta.objects.create(
|
||||
# schema=self.schema1, alias='cst_new', title='New Constituenta', cst_type=CstType.ATTRIBUTE
|
||||
# )
|
||||
# # Simulate propagation: after adding, the reference should still be valid and schema1 should have the new cst
|
||||
# self.assertTrue(Constituenta.objects.filter(pk=new_cst.pk, schema=self.schema1).exists())
|
||||
# # The reference's target operation's result should include the new constituent
|
||||
# op1 = Operation.objects.get(pk=self.op1.pk)
|
||||
# self.assertEqual(op1.result, self.schema1)
|
||||
# self.assertIn(new_cst, Constituenta.objects.filter(schema=op1.result))
|
||||
|
||||
|
||||
# @decl_endpoint('/api/oss/{item}/remove-constituent', method='patch')
|
||||
# def test_remove_constituent_propagation(self):
|
||||
# ''' Test propagation when removing a constituent from a referenced schema. '''
|
||||
# # Remove cst2 from schema1
|
||||
# self.cst2.delete()
|
||||
# # The reference's target operation's result should not include cst2
|
||||
# op1 = Operation.objects.get(pk=self.op1.pk)
|
||||
# self.assertEqual(op1.result, self.schema1)
|
||||
# self.assertNotIn(self.cst2, Constituenta.objects.filter(schema=op1.result))
|
||||
# # Reference should still be valid
|
||||
# self.assertEqual(self.reference.getQ_reference_target(), self.op1)
|
||||
|
||||
|
||||
# @decl_endpoint('/api/oss/{item}/add-constituent-to-referenced-schema', method='patch')
|
||||
# def test_propagation_to_multiple_references(self):
|
||||
# ''' Test propagation when a schema is referenced by multiple references and constituents are added. '''
|
||||
# # Create another reference to op1
|
||||
# reference2 = Reference.objects.create(
|
||||
# alias='ref2', title='Reference 2', type=OperationType.REFERENCE, result=self.schema2
|
||||
# )
|
||||
# reference2.setQ_reference_target(self.op1)
|
||||
# reference2.save()
|
||||
# # Add a new constituent to schema1
|
||||
# new_cst = Constituenta.objects.create(
|
||||
# schema=self.schema1, alias='cst_multi', title='Multi Constituenta', cst_type=CstType.ATTRIBUTE
|
||||
# )
|
||||
# # Both references should still be valid and op1's result should include the new constituent
|
||||
# op1 = Operation.objects.get(pk=self.op1.pk)
|
||||
# self.assertIn(new_cst, Constituenta.objects.filter(schema=op1.result))
|
||||
# self.assertEqual(self.reference.getQ_reference_target(), self.op1)
|
||||
# self.assertEqual(reference2.getQ_reference_target(), self.op1)
|
||||
# TODO: add more tests
|
||||
|
|
|
@ -27,6 +27,7 @@ export class OssLoader {
|
|||
private itemByNodeID = new Map<string, IOssItem>();
|
||||
private blockByID = new Map<number, IBlock>();
|
||||
private schemaIDs: number[] = [];
|
||||
private extendedGraph = new Graph();
|
||||
|
||||
constructor(input: RO<IOperationSchemaDTO>) {
|
||||
this.oss = structuredClone(input) as unknown as IOperationSchema;
|
||||
|
@ -47,6 +48,7 @@ export class OssLoader {
|
|||
result.hierarchy = this.hierarchy;
|
||||
result.schemas = this.schemaIDs;
|
||||
result.stats = this.calculateStats();
|
||||
result.extendedGraph = this.extendedGraph;
|
||||
return result;
|
||||
}
|
||||
|
||||
|
@ -57,6 +59,7 @@ export class OssLoader {
|
|||
this.itemByNodeID.set(operation.nodeID, operation);
|
||||
this.operationByID.set(operation.id, operation);
|
||||
this.graph.addNode(operation.id);
|
||||
this.extendedGraph.addNode(operation.id);
|
||||
this.hierarchy.addNode(operation.nodeID);
|
||||
if (operation.parent) {
|
||||
this.hierarchy.addEdge(constructNodeID(NodeType.BLOCK, operation.parent), operation.nodeID);
|
||||
|
@ -75,7 +78,13 @@ export class OssLoader {
|
|||
}
|
||||
|
||||
private createGraph() {
|
||||
this.oss.arguments.forEach(argument => this.graph.addEdge(argument.argument, argument.operation));
|
||||
this.oss.arguments.forEach(argument => {
|
||||
this.graph.addEdge(argument.argument, argument.operation);
|
||||
this.extendedGraph.addEdge(argument.argument, argument.operation);
|
||||
});
|
||||
this.oss.references.forEach(reference => {
|
||||
this.extendedGraph.addEdge(reference.target, reference.reference);
|
||||
});
|
||||
}
|
||||
|
||||
private extractSchemas() {
|
||||
|
@ -102,7 +111,7 @@ export class OssLoader {
|
|||
break;
|
||||
case OperationType.REFERENCE:
|
||||
const ref = this.oss.references.find(item => item.reference === operationID);
|
||||
const target = !!ref ? this.oss.operationByID.get(ref.target) : null;
|
||||
const target = !!ref ? this.operationByID.get(ref.target) : null;
|
||||
if (!target || !ref) {
|
||||
throw new Error(`Reference ${operationID} not found`);
|
||||
}
|
||||
|
@ -128,13 +137,13 @@ export class OssLoader {
|
|||
}
|
||||
|
||||
private inferConsolidation(operationID: number): boolean {
|
||||
const inputs = this.graph.expandInputs([operationID]);
|
||||
const inputs = this.extendedGraph.expandInputs([operationID]);
|
||||
if (inputs.length === 0) {
|
||||
return false;
|
||||
}
|
||||
const ancestors = [...inputs];
|
||||
inputs.forEach(input => {
|
||||
ancestors.push(...this.graph.expandAllInputs([input]));
|
||||
ancestors.push(...this.extendedGraph.expandAllInputs([input]));
|
||||
});
|
||||
const unique = new Set(ancestors);
|
||||
return unique.size < ancestors.length;
|
||||
|
|
|
@ -18,6 +18,9 @@ export function TabArguments() {
|
|||
} = useFormContext<ICreateSynthesisDTO>();
|
||||
const inputs = useWatch({ control, name: 'arguments' });
|
||||
|
||||
const references = manager.oss.references.filter(item => inputs.includes(item.target)).map(item => item.reference);
|
||||
const filtered = manager.oss.operations.filter(item => !references.includes(item.id));
|
||||
|
||||
return (
|
||||
<div className='cc-fade-in cc-column'>
|
||||
<TextInput
|
||||
|
@ -64,7 +67,7 @@ export function TabArguments() {
|
|||
name='arguments'
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<PickMultiOperation items={manager.oss.operations} value={field.value} onChange={field.onChange} rows={6} />
|
||||
<PickMultiOperation items={filtered} value={field.value} onChange={field.onChange} rows={6} />
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
'use client';
|
||||
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { Controller, useForm, useWatch } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
|
||||
import { HelpTopic } from '@/features/help';
|
||||
|
@ -32,6 +32,7 @@ export function DlgDeleteReference() {
|
|||
keep_connections: false
|
||||
}
|
||||
});
|
||||
const keep_connections = useWatch({ control, name: 'keep_connections' });
|
||||
|
||||
function onSubmit(data: IDeleteReferenceDTO) {
|
||||
return deleteReference({ itemID: oss.id, data: data });
|
||||
|
@ -47,19 +48,6 @@ export function DlgDeleteReference() {
|
|||
helpTopic={HelpTopic.CC_PROPAGATION}
|
||||
>
|
||||
<TextInput disabled dense noBorder id='operation_alias' label='Операция' value={target.alias} />
|
||||
<Controller
|
||||
control={control}
|
||||
name='keep_constituents'
|
||||
render={({ field }) => (
|
||||
<Checkbox
|
||||
label='Сохранить наследованные конституенты'
|
||||
titleHtml='Наследованные конституенты <br/>превратятся в дописанные'
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
disabled={target.result === null}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name='keep_connections'
|
||||
|
@ -73,6 +61,19 @@ export function DlgDeleteReference() {
|
|||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name='keep_constituents'
|
||||
render={({ field }) => (
|
||||
<Checkbox
|
||||
label='Сохранить наследованные конституенты'
|
||||
titleHtml='Наследованные конституенты <br/>превратятся в дописанные'
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
disabled={target.result === null || keep_connections}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</ModalForm>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
'use client';
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
import { Controller, useFormContext, useWatch } from 'react-hook-form';
|
||||
|
||||
import { Label } from '@/components/input';
|
||||
import { useDialogsStore } from '@/stores/dialogs';
|
||||
|
@ -12,7 +12,12 @@ import { type DlgEditOperationProps } from './dlg-edit-operation';
|
|||
export function TabArguments() {
|
||||
const { control, setValue } = useFormContext<IUpdateOperationDTO>();
|
||||
const { manager, target } = useDialogsStore(state => state.props as DlgEditOperationProps);
|
||||
const potentialCycle = [target.id, ...manager.oss.graph.expandAllOutputs([target.id])];
|
||||
const args = useWatch({ control, name: 'arguments' });
|
||||
|
||||
const references = manager.oss.references
|
||||
.filter(item => args.includes(item.target) || item.target === target.id)
|
||||
.map(item => item.reference);
|
||||
const potentialCycle = [target.id, ...references, ...manager.oss.graph.expandAllOutputs([target.id])];
|
||||
const filtered = manager.oss.operations.filter(item => !potentialCycle.includes(item.id));
|
||||
|
||||
function handleChangeArguments(prev: number[], newValue: number[]) {
|
||||
|
|
|
@ -85,6 +85,7 @@ export interface IOperationSchema extends Omit<IOperationSchemaDTO, 'operations'
|
|||
blocks: IBlock[];
|
||||
|
||||
graph: Graph;
|
||||
extendedGraph: Graph;
|
||||
hierarchy: Graph<string>;
|
||||
schemas: number[];
|
||||
stats: IOperationSchemaStats;
|
||||
|
|
|
@ -206,15 +206,32 @@ export function MenuOperation({ operation, onHide }: MenuOperationProps) {
|
|||
}).then(response => setTimeout(() => setSelected([`o${response.new_operation}`]), PARAMETER.refreshTimeout));
|
||||
}
|
||||
|
||||
function handleSelectTarget() {
|
||||
onHide();
|
||||
if (operation.operation_type !== OperationType.REFERENCE) {
|
||||
return;
|
||||
}
|
||||
setSelected([`o${operation.target}`]);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<DropdownButton
|
||||
text='Редактировать'
|
||||
title='Редактировать операцию'
|
||||
icon={<IconEdit2 size='1rem' className='icon-primary' />}
|
||||
onClick={handleEditOperation}
|
||||
disabled={!isMutable || isProcessing}
|
||||
/>
|
||||
{operation.operation_type !== OperationType.REFERENCE ? (
|
||||
<DropdownButton
|
||||
text='Редактировать'
|
||||
title='Редактировать операцию'
|
||||
icon={<IconEdit2 size='1rem' className='icon-primary' />}
|
||||
onClick={handleEditOperation}
|
||||
disabled={!isMutable || isProcessing}
|
||||
/>
|
||||
) : (
|
||||
<DropdownButton
|
||||
text='Оригинал'
|
||||
title='Выделить оригинал'
|
||||
icon={<IconReference size='1rem' className='icon-primary' />}
|
||||
onClick={handleSelectTarget}
|
||||
/>
|
||||
)}
|
||||
|
||||
{operation.result ? (
|
||||
<DropdownButton
|
||||
|
|
|
@ -109,7 +109,7 @@ export const OssEditState = ({ itemID, children }: React.PropsWithChildren<OssEd
|
|||
}
|
||||
|
||||
function canDeleteOperation(target: IOperation) {
|
||||
if (target.operation_type === OperationType.INPUT) {
|
||||
if (target.operation_type === OperationType.INPUT || target.operation_type === OperationType.REFERENCE) {
|
||||
return true;
|
||||
}
|
||||
return schema.graph.expandOutputs([target.id]).length === 0;
|
||||
|
|
Loading…
Reference in New Issue
Block a user