mirror of
https://github.com/IRBorisov/ConceptPortal.git
synced 2025-11-20 17:21:24 +03:00
F: Improve frontend and backend checks for graph editing
This commit is contained in:
parent
584f62579d
commit
e4b60f47da
|
|
@ -56,6 +56,7 @@ class TestChangeConstituents(EndpointTester):
|
|||
self.operation3.refresh_from_db()
|
||||
self.ks3 = RSForm(self.operation3.result)
|
||||
self.assertEqual(self.ks3.constituentsQ().count(), 4)
|
||||
self.ks3X1 = Constituenta.objects.get(as_child__parent_id=self.ks1X1.pk)
|
||||
|
||||
self.layout_data = [
|
||||
{'nodeID': 'o' + str(self.operation1.pk), 'x': 0, 'y': 0, 'width': 150, 'height': 40},
|
||||
|
|
@ -142,6 +143,29 @@ class TestChangeConstituents(EndpointTester):
|
|||
self.assertEqual(inherited_cst.definition_raw, r'@{X2|sing,datv}')
|
||||
|
||||
|
||||
@decl_endpoint('/api/rsforms/{item}/update-cst', method='patch')
|
||||
def test_update_constituenta_inherited(self):
|
||||
data = {
|
||||
'target': self.ks3X1.pk,
|
||||
'item_data': {
|
||||
'definition_formal': r'123',
|
||||
}
|
||||
}
|
||||
self.executeBadData(data, item=self.ks3.model.pk)
|
||||
|
||||
data = {
|
||||
'target': self.ks3X1.pk,
|
||||
'item_data': {
|
||||
'term_raw': r'42',
|
||||
'convention': r'1337',
|
||||
}
|
||||
}
|
||||
self.executeOK(data, item=self.ks3.model.pk)
|
||||
self.ks3X1.refresh_from_db()
|
||||
self.assertEqual(self.ks3X1.term_raw, data['item_data']['term_raw'])
|
||||
self.assertEqual(self.ks3X1.convention, data['item_data']['convention'])
|
||||
|
||||
|
||||
@decl_endpoint('/api/rsforms/{item}/delete-multiple-cst', method='patch')
|
||||
def test_delete_constituenta(self):
|
||||
data = {'items': [self.ks2X1.pk]}
|
||||
|
|
|
|||
|
|
@ -116,6 +116,11 @@ class CstUpdateSerializer(StrictSerializer):
|
|||
raise serializers.ValidationError({
|
||||
'alias': msg.aliasTaken(new_alias)
|
||||
})
|
||||
if 'definition_formal' in attrs['item_data']:
|
||||
if Inheritance.objects.filter(child=cst).exists():
|
||||
raise serializers.ValidationError({
|
||||
'definition_formal': msg.changeInheritedDefinition()
|
||||
})
|
||||
return attrs
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -148,7 +148,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
|||
)
|
||||
|
||||
@extend_schema(
|
||||
summary='update crucial attributes of a given list of constituents',
|
||||
summary='update crucial attribute of a given list of constituents',
|
||||
tags=['RSForm'],
|
||||
request=s.CrucialUpdateSerializer,
|
||||
responses={
|
||||
|
|
@ -160,7 +160,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
|||
)
|
||||
@action(detail=True, methods=['patch'], url_path='update-crucial')
|
||||
def update_crucial(self, request: Request, pk) -> HttpResponse:
|
||||
''' Update crucial attributes of a given list of constituents. '''
|
||||
''' Update crucial attribute of a given list of constituents. '''
|
||||
item = self._get_item()
|
||||
serializer = s.CrucialUpdateSerializer(data=request.data, partial=True, context={'schema': item})
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
|
|
|||
|
|
@ -22,6 +22,10 @@ def constituentaNotInRSform(title: str):
|
|||
return f'Конституента не принадлежит схеме: {title}'
|
||||
|
||||
|
||||
def changeInheritedDefinition():
|
||||
return 'Нельзя изменить определение наследника'
|
||||
|
||||
|
||||
def constituentaNotFromOperation():
|
||||
return 'Конституента не соответствую аргументам операции'
|
||||
|
||||
|
|
|
|||
|
|
@ -88,7 +88,7 @@ export class RSFormLoader {
|
|||
cst.spawn = [];
|
||||
cst.attributes = [];
|
||||
cst.spawn_alias = [];
|
||||
cst.parent_schema = schemaByCst.get(cst.id);
|
||||
cst.parent_schema = schemaByCst.get(cst.id) ?? null;
|
||||
cst.parent_schema_index = cst.parent_schema ? parents.indexOf(cst.parent_schema) + 1 : 0;
|
||||
cst.is_inherited = inherit_children.has(cst.id);
|
||||
cst.has_inherited_children = inherit_parents.has(cst.id);
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ export function SchemasGuide({ schema }: SchemasGuideProps) {
|
|||
const aliases: string[] = [];
|
||||
const indexes: number[] = [];
|
||||
schema.items.forEach(cst => {
|
||||
if (cst.parent_schema && !processed.has(cst.parent_schema)) {
|
||||
if (cst.parent_schema !== null && !processed.has(cst.parent_schema)) {
|
||||
const item = libraryItems.find(item => item.id === cst.parent_schema);
|
||||
if (item) {
|
||||
aliases.push(item.alias);
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@ export interface IConstituenta {
|
|||
*/
|
||||
parent_schema_index: number;
|
||||
/** {@link LibraryItem} that contains parent of this inherited {@link IConstituenta}. */
|
||||
parent_schema?: number;
|
||||
parent_schema: number | null;
|
||||
/** Indicates if this {@link IConstituenta} is inherited. */
|
||||
is_inherited: boolean;
|
||||
/** Indicates if this {@link IConstituenta} has children that are inherited. */
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import {
|
|||
import clsx from 'clsx';
|
||||
|
||||
import { DiagramFlow, useReactFlow } from '@/components/flow/diagram-flow';
|
||||
import { useContinuousPan } from '@/components/flow/use-continous-panning';
|
||||
import { useContinuousPan } from '@/components/flow/use-continuous-panning';
|
||||
import { useWindowSize } from '@/hooks/use-window-size';
|
||||
import { useFitHeight, useMainHeight } from '@/stores/app-layout';
|
||||
import { PARAMETER } from '@/utils/constants';
|
||||
|
|
@ -255,20 +255,26 @@ export function TGFlow() {
|
|||
|
||||
const sourceID = Number(connection.source);
|
||||
const targetID = Number(connection.target);
|
||||
if (
|
||||
(connectionType === TGEdgeType.attribution && schema.attribution_graph.hasEdge(sourceID, targetID)) ||
|
||||
(connectionType === TGEdgeType.definition && schema.graph.hasEdge(sourceID, targetID))
|
||||
) {
|
||||
toast.info(errorMsg.connectionExists);
|
||||
return;
|
||||
const sourceCst = schema.cstByID.get(sourceID);
|
||||
const targetCst = schema.cstByID.get(targetID);
|
||||
if (!targetCst || !sourceCst) {
|
||||
throw new Error('Constituents not found');
|
||||
}
|
||||
|
||||
if (connectionType === TGEdgeType.definition) {
|
||||
const sourceCst = schema.cstByID.get(sourceID);
|
||||
const targetCst = schema.cstByID.get(targetID);
|
||||
if (!targetCst || !sourceCst) {
|
||||
throw new Error('Constituents not found');
|
||||
if (targetCst.is_inherited) {
|
||||
toast.error(errorMsg.changeInheritedDefinition);
|
||||
return;
|
||||
}
|
||||
if (schema.graph.hasEdge(sourceID, targetID)) {
|
||||
toast.error(errorMsg.connectionExists);
|
||||
return;
|
||||
}
|
||||
if (schema.graph.isReachable(targetID, sourceID)) {
|
||||
toast.error(errorMsg.cyclingEdge);
|
||||
return;
|
||||
}
|
||||
|
||||
const newExpressions = addAliasReference(targetCst.definition_formal, sourceCst.alias);
|
||||
void updateConstituenta({
|
||||
itemID: schema.id,
|
||||
|
|
@ -279,8 +285,20 @@ export function TGFlow() {
|
|||
}
|
||||
}
|
||||
});
|
||||
return;
|
||||
} else {
|
||||
if (schema.attribution_graph.hasEdge(sourceID, targetID)) {
|
||||
toast.error(errorMsg.connectionExists);
|
||||
return;
|
||||
}
|
||||
if (schema.attribution_graph.isReachable(targetID, sourceID)) {
|
||||
toast.error(errorMsg.cyclingEdge);
|
||||
return;
|
||||
}
|
||||
if (targetCst.parent_schema !== null && targetCst.parent_schema === sourceCst.parent_schema) {
|
||||
toast.error(errorMsg.addInheritedEdge);
|
||||
return;
|
||||
}
|
||||
|
||||
void createAttribution({
|
||||
itemID: schema.id,
|
||||
data: {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
import { urls, useConceptNavigation } from '@/app';
|
||||
import { useAIStore } from '@/features/ai/stores/ai-context';
|
||||
|
|
@ -14,7 +15,7 @@ import { useDialogsStore } from '@/stores/dialogs';
|
|||
import { useModificationStore } from '@/stores/modification';
|
||||
import { usePreferencesStore } from '@/stores/preferences';
|
||||
import { PARAMETER, prefixes } from '@/utils/constants';
|
||||
import { promptText } from '@/utils/labels';
|
||||
import { errorMsg, promptText } from '@/utils/labels';
|
||||
import { type RO } from '@/utils/meta';
|
||||
import { promptUnsaved } from '@/utils/utils';
|
||||
|
||||
|
|
@ -302,7 +303,16 @@ export const RSEditState = ({
|
|||
const ids = selectedEdges[0].split('-');
|
||||
const sourceID = Number(ids[0]);
|
||||
const targetID = Number(ids[1]);
|
||||
const sourceCst = schema.cstByID.get(sourceID);
|
||||
const targetCst = schema.cstByID.get(targetID);
|
||||
if (!targetCst || !sourceCst) {
|
||||
throw new Error('Constituents not found');
|
||||
}
|
||||
if (schema.attribution_graph.hasEdge(sourceID, targetID)) {
|
||||
if (targetCst.parent_schema !== null && targetCst.parent_schema === sourceCst.parent_schema) {
|
||||
toast.error(errorMsg.deleteInheritedEdge);
|
||||
return;
|
||||
}
|
||||
void deleteAttribution({
|
||||
itemID: schema.id,
|
||||
data: {
|
||||
|
|
@ -311,10 +321,9 @@ export const RSEditState = ({
|
|||
}
|
||||
});
|
||||
} else if (schema.graph.hasEdge(sourceID, targetID)) {
|
||||
const sourceCst = schema.cstByID.get(sourceID);
|
||||
const targetCst = schema.cstByID.get(targetID);
|
||||
if (!targetCst || !sourceCst) {
|
||||
throw new Error('Constituents not found');
|
||||
if (targetCst.is_inherited) {
|
||||
toast.error(errorMsg.changeInheritedDefinition);
|
||||
return;
|
||||
}
|
||||
const newExpressions = removeAliasReference(targetCst.definition_formal, sourceCst.alias);
|
||||
void updateConstituenta({
|
||||
|
|
|
|||
|
|
@ -149,6 +149,10 @@ export class Graph<NodeID = number> {
|
|||
return !!sourceNode.outputs.find(id => id === destination);
|
||||
}
|
||||
|
||||
isReachable(source: NodeID, destination: NodeID): boolean {
|
||||
return this.expandAllOutputs([source]).includes(destination);
|
||||
}
|
||||
|
||||
rootNodes(): NodeID[] {
|
||||
return [...this.nodes.keys()].filter(id => !this.nodes.get(id)?.inputs.length);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -70,7 +70,11 @@ export const errorMsg = {
|
|||
invalidLocation: 'Некорректный формат пути',
|
||||
emptySubstitutions: 'Выберите хотя бы одно отождествление',
|
||||
invalidResponse: 'Некорректный ответ сервера',
|
||||
connectionExists: 'Связь уже существует'
|
||||
connectionExists: 'Связь уже существует',
|
||||
cyclingEdge: 'Связь образует цикл',
|
||||
changeInheritedDefinition: 'Нельзя изменить определение наследника',
|
||||
addInheritedEdge: 'Новая связь между наследниками из одной схемы не допускается',
|
||||
deleteInheritedEdge: 'Нельзя удалить связь между наследниками из одной схемы'
|
||||
} as const;
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user