F: Implementing backend for synthesis operation

This commit is contained in:
Ivan 2024-07-31 18:09:31 +03:00
parent 727c1b3ab6
commit 16a225a959
15 changed files with 221 additions and 133 deletions

View File

@ -432,7 +432,8 @@ disable=too-many-public-methods,
missing-function-docstring,
attribute-defined-outside-init,
ungrouped-imports,
abstract-method
abstract-method,
fixme
# Enable the message, report, category or checker with the given id(s). You can
# either give multiple identifier separated by comma (,) or put this option

View File

@ -5,10 +5,12 @@ from django.core.exceptions import ValidationError
from django.db import transaction
from django.db.models import QuerySet
from apps.library.models import LibraryItem, LibraryItemType
from apps.library.models import Editor, LibraryItem, LibraryItemType
from apps.rsform.models import RSForm
from shared import messages as msg
from .Argument import Argument
from .Inheritance import Inheritance
from .Operation import Operation
from .Substitution import Substitution
@ -76,8 +78,8 @@ class OperationSchema:
''' Delete operation. '''
operation.delete()
# deal with attached schema
# trigger on_change effects
# TODO: deal with attached schema
# TODO: trigger on_change effects
self.save()
@ -86,16 +88,15 @@ class OperationSchema:
''' Set input schema for operation. '''
if schema == target.result:
return
if schema:
target.result = schema
if schema is not None:
target.result = schema
target.alias = schema.alias
target.title = schema.title
target.comment = schema.comment
else:
target.result = None
target.save()
# trigger on_change effects
# TODO: trigger on_change effects
self.save()
@ -117,7 +118,7 @@ class OperationSchema:
Argument.objects.create(operation=operation, argument=arg)
if not changed:
return
# trigger on_change effects
# TODO: trigger on_change effects
self.save()
@transaction.atomic
@ -148,6 +149,63 @@ class OperationSchema:
if not changed:
return
# trigger on_change effects
# TODO: trigger on_change effects
self.save()
@transaction.atomic
def create_input(self, operation: Operation) -> RSForm:
''' Create input RSForm. '''
schema = RSForm.create(
owner=self.model.owner,
alias=operation.alias,
title=operation.title,
comment=operation.comment,
visible=False,
access_policy=self.model.access_policy,
location=self.model.location
)
Editor.set(schema.model, self.model.editors())
operation.result = schema.model
operation.save()
self.save()
return schema
@transaction.atomic
def execute_operation(self, operation: Operation) -> bool:
''' Execute target operation. '''
schemas: list[LibraryItem] = [arg.argument.result for arg in operation.getArguments()]
if None in schemas:
return False
substitutions = operation.getSubstitutions()
receiver = self.create_input(operation)
parents: dict = {}
children: dict = {}
for operand in schemas:
schema = RSForm(operand)
items = list(schema.constituents())
new_items = receiver.insert_copy(items)
for (i, cst) in enumerate(new_items):
parents[cst.pk] = items[i]
children[items[i].pk] = cst
for sub in substitutions:
original = children[sub.original.pk]
replacement = children[sub.substitution.pk]
receiver.substitute(original, replacement)
# TODO: remove duplicates from diamond
for cst in receiver.constituents():
parent = parents.get(cst.id)
assert parent is not None
Inheritance.objects.create(
child=cst,
parent=parent
)
receiver.restore_order()
receiver.reset_aliases()
self.save()
return True

View File

@ -92,8 +92,10 @@ class OperationUpdateSerializer(serializers.Serializer):
if 'substitutions' not in attrs:
return attrs
schemas = [arg.result.pk for arg in attrs['arguments'] if arg.result is not None]
substitutions = attrs['substitutions']
to_delete = {x['original'].pk for x in substitutions}
deleted = set()
for item in attrs['substitutions']:
for item in substitutions:
original_cst = cast(Constituenta, item['original'])
substitution_cst = cast(Constituenta, item['substitution'])
if original_cst.schema.pk not in schemas:
@ -104,7 +106,7 @@ class OperationUpdateSerializer(serializers.Serializer):
raise serializers.ValidationError({
f'{substitution_cst.id}': msg.constituentaNotFromOperation()
})
if original_cst.pk in deleted:
if original_cst.pk in deleted or substitution_cst.pk in to_delete:
raise serializers.ValidationError({
f'{original_cst.id}': msg.substituteDouble(original_cst.alias)
})

View File

@ -23,10 +23,27 @@ class TestOssViewset(EndpointTester):
def populateData(self):
self.ks1 = RSForm.create(alias='KS1', title='Test1', owner=self.user)
self.ks1x1 = self.ks1.insert_new('X1', term_resolved='X1_1')
self.ks2 = RSForm.create(alias='KS2', title='Test2', owner=self.user)
self.ks2x1 = self.ks2.insert_new('X2', term_resolved='X1_2')
self.ks1 = RSForm.create(
alias='KS1',
title='Test1',
owner=self.user
)
self.ks1x1 = self.ks1.insert_new(
'X1',
term_raw='X1_1',
term_resolved='X1_1'
)
self.ks2 = RSForm.create(
alias='KS2',
title='Test2',
owner=self.user
)
self.ks2x1 = self.ks2.insert_new(
'X2',
term_raw='X1_2',
term_resolved='X1_2'
)
self.operation1 = self.owned.create_operation(
alias='1',
operation_type=OperationType.INPUT,
@ -399,3 +416,61 @@ class TestOssViewset(EndpointTester):
self.assertEqual(self.operation1.result.alias, data['item_data']['alias'])
self.assertEqual(self.operation1.result.title, data['item_data']['title'])
self.assertEqual(self.operation1.result.comment, data['item_data']['comment'])
@decl_endpoint('/api/oss/{item}/update-operation', method='patch')
def test_update_operation_invalid_substitution(self):
self.populateData()
self.ks1x2 = self.ks1.insert_new('X2')
data = {
'target': self.operation3.pk,
'item_data': {
'alias': 'Test3 mod',
'title': 'Test title mod',
'comment': 'Comment mod'
},
'positions': [],
'arguments': [self.operation1.pk, self.operation2.pk],
'substitutions': [
{
'original': self.ks1x1.pk,
'substitution': self.ks2x1.pk
},
{
'original': self.ks2x1.pk,
'substitution': self.ks1x2.pk
}
]
}
self.executeBadData(data=data, item=self.owned_id)
@decl_endpoint('/api/oss/{item}/execute-operation', method='post')
def test_execute_operation(self):
self.populateData()
self.executeBadData(item=self.owned_id)
data = {
'positions': [],
'target': self.operation1.pk
}
self.executeBadData(data=data)
data['target'] = self.operation3.pk
self.toggle_admin(True)
self.executeBadData(data=data, item=self.unowned_id)
self.logout()
self.executeForbidden(data=data, item=self.owned_id)
self.login()
self.executeOK(data=data)
self.operation3.refresh_from_db()
schema = self.operation3.result
self.assertEqual(schema.alias, self.operation3.alias)
self.assertEqual(schema.comment, self.operation3.comment)
self.assertEqual(schema.title, self.operation3.title)
self.assertEqual(schema.visible, False)
items = list(RSForm(schema).constituents())
self.assertEqual(len(items), 1)
self.assertEqual(items[0].alias, 'X1')
self.assertEqual(items[0].term_resolved, self.ks2x1.term_resolved)

View File

@ -10,7 +10,7 @@ from rest_framework.decorators import action
from rest_framework.request import Request
from rest_framework.response import Response
from apps.library.models import Editor, LibraryItem, LibraryItemType
from apps.library.models import LibraryItem, LibraryItemType
from apps.library.serializers import LibraryItemSerializer
from shared import messages as msg
from shared import permissions
@ -38,7 +38,7 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
'create_input',
'set_input',
'update_operation',
'execute_operation',
'execute_operation'
]:
permission_list = [permissions.ItemEditor]
elif self.action in ['details']:
@ -103,28 +103,14 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
oss = m.OperationSchema(self.get_object())
with transaction.atomic():
oss.update_positions(serializer.validated_data['positions'])
data: dict = serializer.validated_data['item_data']
if data['operation_type'] == m.OperationType.INPUT and serializer.validated_data['create_schema']:
schema = LibraryItem.objects.create(
item_type=LibraryItemType.RSFORM,
owner=oss.model.owner,
alias=data['alias'],
title=data['title'],
comment=data['comment'],
visible=False,
access_policy=oss.model.access_policy,
location=oss.model.location
)
Editor.set(schema, oss.model.editors())
data['result'] = schema
new_operation = oss.create_operation(**data)
new_operation = oss.create_operation(**serializer.validated_data['item_data'])
if new_operation.operation_type == m.OperationType.INPUT and serializer.validated_data['create_schema']:
oss.create_input(new_operation)
if new_operation.operation_type != m.OperationType.INPUT and 'arguments' in serializer.validated_data:
oss.set_arguments(
operation=new_operation,
arguments=serializer.validated_data['arguments']
)
oss.refresh_from_db()
return Response(
status=c.HTTP_201_CREATED,
data={
@ -158,7 +144,6 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
oss.update_positions(serializer.validated_data['positions'])
oss.delete_operation(serializer.validated_data['target'])
oss.refresh_from_db()
return Response(
status=c.HTTP_200_OK,
data=s.OperationSchemaSerializer(oss.model).data
@ -197,25 +182,12 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
oss = m.OperationSchema(self.get_object())
with transaction.atomic():
oss.update_positions(serializer.validated_data['positions'])
schema = LibraryItem.objects.create(
item_type=LibraryItemType.RSFORM,
owner=oss.model.owner,
alias=operation.alias,
title=operation.title,
comment=operation.comment,
visible=False,
access_policy=oss.model.access_policy,
location=oss.model.location
)
Editor.set(schema, oss.model.editors())
operation.result = schema
operation.save()
schema = oss.create_input(operation)
oss.refresh_from_db()
return Response(
status=c.HTTP_200_OK,
data={
'new_schema': LibraryItemSerializer(schema).data,
'new_schema': LibraryItemSerializer(schema.model).data,
'oss': s.OperationSchemaSerializer(oss.model).data
}
)
@ -241,20 +213,10 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
serializer.is_valid(raise_exception=True)
operation: m.Operation = cast(m.Operation, serializer.validated_data['target'])
result = serializer.validated_data['input']
oss = m.OperationSchema(self.get_object())
with transaction.atomic():
oss.update_positions(serializer.validated_data['positions'])
operation.result = result
if result is not None:
operation.title = result.title
operation.comment = result.comment
operation.alias = result.alias
operation.save()
# update arguments
oss.refresh_from_db()
oss.set_input(operation, serializer.validated_data['input'])
return Response(
status=c.HTTP_200_OK,
data=s.OperationSchemaSerializer(oss.model).data
@ -336,17 +298,10 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
})
oss = m.OperationSchema(self.get_object())
# with transaction.atomic():
# oss.update_positions(serializer.validated_data['positions'])
# operation.result.refresh_from_db()
# operation.result.title = operation.title
# operation.result.comment = operation.comment
# operation.result.alias = operation.alias
# operation.result.save()
with transaction.atomic():
oss.update_positions(serializer.validated_data['positions'])
oss.execute_operation(operation)
# update arguments
oss.refresh_from_db()
return Response(
status=c.HTTP_200_OK,
data=s.OperationSchemaSerializer(oss.model).data

View File

@ -12,7 +12,6 @@ from django.db.models import (
TextChoices,
TextField
)
from django.urls import reverse
from ..utils import apply_pattern

View File

@ -119,7 +119,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
cst = m.Constituenta.objects.get(pk=request.data['id'])
if cst.schema != schema:
raise ValidationError({
f'schema': msg.constituentaNotInRSform(schema.title)
'schema': msg.constituentaNotInRSform(schema.title)
})
serializer.update(instance=cst, validated_data=serializer.validated_data)

View File

@ -14,6 +14,10 @@ def operationNotInOSS(title: str):
return f'Операция не принадлежит ОСС: {title}'
def previousResultMissing():
return 'Отсутствует результат предыдущей операции'
def substitutionNotInList():
return 'Отождествляемая конституента отсутствует в списке'

View File

@ -73,10 +73,3 @@ export function postExecuteOperation(oss: string, request: FrontExchange<ITarget
request: request
});
}
export function postExecuteAll(oss: string, request: FrontExchange<IPositionsData, IOperationSchemaData>) {
AxiosPost({
endpoint: `/api/oss/${oss}/execute-all`,
request: request
});
}

View File

@ -1,6 +1,7 @@
'use client';
import { useCallback, useMemo, useState } from 'react';
import { toast } from 'react-toastify';
import BadgeConstituenta from '@/components/info/BadgeConstituenta';
import SelectConstituenta from '@/components/select/SelectConstituenta';
@ -10,6 +11,7 @@ import { useConceptOptions } from '@/context/ConceptOptionsContext';
import { ILibraryItem } from '@/models/library';
import { ICstSubstitute, IMultiSubstitution } from '@/models/oss';
import { ConstituentaID, IConstituenta, IRSForm } from '@/models/rsform';
import { errors } from '@/utils/labels';
import { IconPageLeft, IconPageRight, IconRemove, IconReplace } from '../Icons';
import NoData from '../ui/NoData';
@ -98,6 +100,18 @@ function PickSubstitutions({
original: deleteRight ? rightCst.id : leftCst.id,
substitution: deleteRight ? leftCst.id : rightCst.id
};
const toDelete = substitutions.map(item => item.original);
const replacements = substitutions.map(item => item.substitution);
console.log(toDelete, replacements);
console.log(newSubstitution);
if (
toDelete.includes(newSubstitution.original) ||
toDelete.includes(newSubstitution.substitution) ||
replacements.includes(newSubstitution.original)
) {
toast.error(errors.reuseOriginal);
return;
}
setSubstitutions(prev => [...prev, newSubstitution]);
setLeftCst(undefined);
setRightCst(undefined);

View File

@ -19,7 +19,6 @@ import {
patchUpdateOperation,
patchUpdatePositions,
postCreateOperation,
postExecuteAll,
postExecuteOperation
} from '@/backend/oss';
import { type ErrorData } from '@/components/info/InfoError';
@ -69,7 +68,6 @@ interface IOssContext {
setInput: (data: IOperationSetInputData, callback?: () => void) => void;
updateOperation: (data: IOperationUpdateData, callback?: () => void) => void;
executeOperation: (data: ITargetOperation, callback?: () => void) => void;
executeAll: (data: IPositionsData, callback?: () => void) => void;
}
const OssContext = createContext<IOssContext | null>(null);
@ -413,28 +411,6 @@ export const OssState = ({ itemID, children }: OssStateProps) => {
[itemID, schema, library]
);
const executeAll = useCallback(
(data: IPositionsData, callback?: () => void) => {
if (!schema) {
return;
}
setProcessingError(undefined);
postExecuteAll(itemID, {
data: data,
showError: true,
setLoading: setProcessing,
onError: setProcessingError,
onSuccess: newData => {
library.setGlobalOSS(newData);
library.reloadItems(() => {
if (callback) callback();
});
}
});
},
[itemID, schema, library]
);
return (
<OssContext.Provider
value={{
@ -461,8 +437,7 @@ export const OssState = ({ itemID, children }: OssStateProps) => {
createInput,
setInput,
updateOperation,
executeOperation,
executeAll
executeOperation
}}
>
{children}

View File

@ -170,12 +170,11 @@ function OssFlow({ isModified, setIsModified }: OssFlowProps) {
);
const handleExecuteSelected = useCallback(() => {
if (controller.selected.length === 1) {
handleExecuteOperation(controller.selected[0]);
} else {
controller.executeAll(getPositions());
if (controller.selected.length !== 1) {
return;
}
}, [controller, handleExecuteOperation, getPositions]);
handleExecuteOperation(controller.selected[0]);
}, [controller, handleExecuteOperation]);
const handleFitView = useCallback(() => {
flow.fitView({ duration: PARAMETER.zoomDuration });

View File

@ -1,4 +1,7 @@
'use client';
import clsx from 'clsx';
import { useMemo } from 'react';
import {
IconAnimation,
@ -18,6 +21,7 @@ import {
import BadgeHelp from '@/components/info/BadgeHelp';
import MiniButton from '@/components/ui/MiniButton';
import { HelpTopic } from '@/models/miscellaneous';
import { OperationType } from '@/models/oss';
import { PARAMETER } from '@/utils/constants';
import { prepareTooltip } from '@/utils/labels';
@ -59,6 +63,30 @@ function ToolbarOssGraph({
toggleEdgeStraight
}: ToolbarOssGraphProps) {
const controller = useOssEdit();
const selectedOperation = useMemo(
() => controller.schema?.operationByID.get(controller.selected[0]),
[controller.selected, controller.schema]
);
const readyForSynthesis = useMemo(() => {
if (!selectedOperation || selectedOperation.operation_type !== OperationType.SYNTHESIS) {
return false;
}
if (!controller.schema || selectedOperation.result) {
return false;
}
const argumentIDs = controller.schema.graph.expandInputs([selectedOperation.id]);
if (!argumentIDs || argumentIDs.length < 2) {
return false;
}
const argumentOperations = argumentIDs.map(id => controller.schema!.operationByID.get(id)!);
if (argumentOperations.some(item => item.result === null)) {
return false;
}
return true;
}, [selectedOperation, controller.schema]);
return (
<div className='flex flex-col items-center'>
@ -120,7 +148,6 @@ function ToolbarOssGraph({
</div>
{controller.isMutable ? (
<div className='cc-icons'>
{' '}
<MiniButton
titleHtml={prepareTooltip('Сохранить изменения', 'Ctrl + S')}
icon={<IconSave size='1.25rem' className='icon-primary' />}
@ -134,14 +161,9 @@ function ToolbarOssGraph({
onClick={onCreate}
/>
<MiniButton
title='Выполнить выбранную / все операции'
icon={
<IconExecute
size='1.25rem'
className={controller.selected.length === 1 ? 'icon-primary' : 'icon-green'}
/>
}
disabled={controller.isProcessing}
title='Выполнить операцию'
icon={<IconExecute size='1.25rem' className='icon-green' />}
disabled={controller.isProcessing || controller.selected.length !== 1 || !readyForSynthesis}
onClick={onExecute}
/>
<MiniButton

View File

@ -57,7 +57,6 @@ export interface IOssEditContext {
promptEditInput: (target: OperationID, positions: IOperationPosition[]) => void;
promptEditOperation: (target: OperationID, positions: IOperationPosition[]) => void;
executeOperation: (target: OperationID, positions: IOperationPosition[]) => void;
executeAll: (positions: IOperationPosition[]) => void;
}
const OssEditContext = createContext<IOssEditContext | null>(null);
@ -290,14 +289,6 @@ export const OssEditState = ({ selected, setSelected, children }: OssEditStatePr
[model]
);
const executeAll = useCallback(
(positions: IOperationPosition[]) => {
const data = { positions: positions };
model.executeAll(data, () => toast.success(information.allOperationExecuted));
},
[model]
);
return (
<OssEditContext.Provider
value={{
@ -326,8 +317,7 @@ export const OssEditState = ({ selected, setSelected, children }: OssEditStatePr
createInput,
promptEditInput,
promptEditOperation,
executeOperation,
executeAll
executeOperation
}}
>
{model.schema ? (

View File

@ -951,7 +951,8 @@ export const information = {
export const errors = {
astFailed: 'Невозможно построить дерево разбора',
passwordsMismatch: 'Пароли не совпадают',
imageFailed: 'Ошибка при создании изображения'
imageFailed: 'Ошибка при создании изображения',
reuseOriginal: 'Повторное использование удаляемой конституенты при отождествлении'
};
/**