R: Restructuring layout data

This commit is contained in:
Ivan 2025-04-06 15:49:43 +03:00
parent 5584fa9fc8
commit 8aedd4c209
45 changed files with 959 additions and 643 deletions

View File

@ -9,6 +9,7 @@ from apps.library.models import (
LibraryTemplate, LibraryTemplate,
LocationHead LocationHead
) )
from apps.oss.models import OperationSchema
from apps.rsform.models import RSForm from apps.rsform.models import RSForm
from shared.EndpointTester import EndpointTester, decl_endpoint from shared.EndpointTester import EndpointTester, decl_endpoint
from shared.testing_utils import response_contains from shared.testing_utils import response_contains
@ -58,6 +59,8 @@ class TestLibraryViewset(EndpointTester):
'read_only': True 'read_only': True
} }
response = self.executeCreated(data=data) response = self.executeCreated(data=data)
oss = OperationSchema(LibraryItem.objects.get(pk=response.data['id']))
self.assertEqual(oss.model.owner, self.user)
self.assertEqual(response.data['owner'], self.user.pk) self.assertEqual(response.data['owner'], self.user.pk)
self.assertEqual(response.data['item_type'], data['item_type']) self.assertEqual(response.data['item_type'], data['item_type'])
self.assertEqual(response.data['title'], data['title']) self.assertEqual(response.data['title'], data['title'])
@ -65,6 +68,8 @@ class TestLibraryViewset(EndpointTester):
self.assertEqual(response.data['access_policy'], data['access_policy']) self.assertEqual(response.data['access_policy'], data['access_policy'])
self.assertEqual(response.data['visible'], data['visible']) self.assertEqual(response.data['visible'], data['visible'])
self.assertEqual(response.data['read_only'], data['read_only']) self.assertEqual(response.data['read_only'], data['read_only'])
self.assertEqual(oss.layout().data['operations'], [])
self.assertEqual(oss.layout().data['blocks'], [])
self.logout() self.logout()
data = {'title': 'Title2'} data = {'title': 'Title2'}

View File

@ -13,7 +13,7 @@ from rest_framework.decorators import action
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.response import Response from rest_framework.response import Response
from apps.oss.models import Operation, OperationSchema, PropagationFacade from apps.oss.models import Layout, Operation, OperationSchema, PropagationFacade
from apps.rsform.models import RSForm from apps.rsform.models import RSForm
from apps.rsform.serializers import RSFormParseSerializer from apps.rsform.serializers import RSFormParseSerializer
from apps.users.models import User from apps.users.models import User
@ -40,6 +40,8 @@ class LibraryViewSet(viewsets.ModelViewSet):
serializer.save(owner=self.request.user) serializer.save(owner=self.request.user)
else: else:
serializer.save() serializer.save()
if serializer.data.get('item_type') == m.LibraryItemType.OPERATION_SCHEMA:
Layout.objects.create(oss=serializer.instance, data={'operations': [], 'blocks': []})
def perform_update(self, serializer) -> None: def perform_update(self, serializer) -> None:
instance = serializer.save() instance = serializer.save()

View File

@ -15,11 +15,24 @@ class OperationAdmin(admin.ModelAdmin):
'alias', 'alias',
'title', 'title',
'description', 'description',
'position_x', 'parent']
'position_y']
search_fields = ['id', 'operation_type', 'title', 'alias'] search_fields = ['id', 'operation_type', 'title', 'alias']
class BlockAdmin(admin.ModelAdmin):
''' Admin model: Block. '''
ordering = ['oss']
list_display = ['id', 'oss', 'title', 'description', 'parent']
search_fields = ['oss']
class LayoutAdmin(admin.ModelAdmin):
''' Admin model: Layout. '''
ordering = ['oss']
list_display = ['id', 'oss', 'data']
search_fields = ['oss']
class ArgumentAdmin(admin.ModelAdmin): class ArgumentAdmin(admin.ModelAdmin):
''' Admin model: Operation arguments. ''' ''' Admin model: Operation arguments. '''
ordering = ['operation'] ordering = ['operation']
@ -42,6 +55,8 @@ class InheritanceAdmin(admin.ModelAdmin):
admin.site.register(models.Operation, OperationAdmin) admin.site.register(models.Operation, OperationAdmin)
admin.site.register(models.Block, BlockAdmin)
admin.site.register(models.Layout, LayoutAdmin)
admin.site.register(models.Argument, ArgumentAdmin) admin.site.register(models.Argument, ArgumentAdmin)
admin.site.register(models.Substitution, SynthesisSubstitutionAdmin) admin.site.register(models.Substitution, SynthesisSubstitutionAdmin)
admin.site.register(models.Inheritance, InheritanceAdmin) admin.site.register(models.Inheritance, InheritanceAdmin)

View File

@ -0,0 +1,84 @@
# Generated by Django 5.1.7 on 2025-03-26 16:04
import django.db.models.deletion
from django.db import migrations, models
def migrate_layout(apps, schema_editor):
LibraryItem = apps.get_model('library', 'LibraryItem')
Operation = apps.get_model('oss', 'Operation')
Layout = apps.get_model('oss', 'Layout')
for library_item in LibraryItem.objects.filter(item_type='oss'):
layout_data = {'operations': [], 'blocks': []}
operations = Operation.objects.filter(oss=library_item)
for operation in operations:
layout_data['operations'].append({
'id': operation.id,
'x': operation.position_x,
'y': operation.position_y
})
Layout.objects.create(oss=library_item, data=layout_data)
class Migration(migrations.Migration):
dependencies = [
('library', '0007_rename_libraryitem_comment_libraryitem_description'),
('oss', '0010_rename_comment_operation_description'),
]
operations = [
migrations.CreateModel(
name='Layout',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('data', models.JSONField(default=dict, verbose_name='Расположение')),
('oss', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE,
related_name='layout', to='library.libraryitem', verbose_name='Схема синтеза')),
],
options={
'verbose_name': 'Схема расположения',
'verbose_name_plural': 'Схемы расположения',
},
),
migrations.RunPython(migrate_layout),
migrations.RemoveField(
model_name='operation',
name='position_x',
),
migrations.RemoveField(
model_name='operation',
name='position_y',
),
migrations.CreateModel(
name='Block',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.TextField(blank=True, verbose_name='Название')),
('description', models.TextField(blank=True, verbose_name='Описание')),
('oss', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE,
related_name='blocks', to='library.libraryitem', verbose_name='Схема синтеза')),
('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL,
related_name='as_child_block', to='oss.block', verbose_name='Содержащий блок')),
],
options={
'verbose_name': 'Блок',
'verbose_name_plural': 'Блоки',
},
),
migrations.AddField(
model_name='operation',
name='parent',
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name='as_child_operation',
to='oss.block',
verbose_name='Содержащий блок'),
),
]

View File

@ -0,0 +1,39 @@
''' Models: Content Block in OSS. '''
# pylint: disable=duplicate-code
from django.db.models import CASCADE, SET_NULL, ForeignKey, Model, TextField
class Block(Model):
''' Block of content in OSS.'''
oss = ForeignKey(
verbose_name='Схема синтеза',
to='library.LibraryItem',
on_delete=CASCADE,
related_name='blocks'
)
title = TextField(
verbose_name='Название',
blank=True
)
description = TextField(
verbose_name='Описание',
blank=True
)
parent = ForeignKey(
verbose_name='Содержащий блок',
to='oss.Block',
blank=True,
null=True,
on_delete=SET_NULL,
related_name='as_child_block'
)
class Meta:
''' Model metadata. '''
verbose_name = 'Блок'
verbose_name_plural = 'Блоки'
def __str__(self) -> str:
return f'Блок {self.title}'

View File

@ -0,0 +1,25 @@
''' Models: Content Block in OSS. '''
from django.db.models import CASCADE, ForeignKey, JSONField, Model
class Layout(Model):
''' Node layout in OSS.'''
oss = ForeignKey(
verbose_name='Схема синтеза',
to='library.LibraryItem',
on_delete=CASCADE,
related_name='layout'
)
data = JSONField(
verbose_name='Расположение',
default=dict
)
class Meta:
''' Model metadata. '''
verbose_name = 'Схема расположения'
verbose_name_plural = 'Схемы расположения'
def __str__(self) -> str:
return f'Схема расположения {self.oss.alias}'

View File

@ -1,9 +1,9 @@
''' Models: Operation in OSS. ''' ''' Models: Operation in OSS. '''
# pylint: disable=duplicate-code
from django.db.models import ( from django.db.models import (
CASCADE, CASCADE,
SET_NULL, SET_NULL,
CharField, CharField,
FloatField,
ForeignKey, ForeignKey,
Model, Model,
QuerySet, QuerySet,
@ -44,6 +44,15 @@ class Operation(Model):
related_name='producer' related_name='producer'
) )
parent = ForeignKey(
verbose_name='Содержащий блок',
to='oss.Block',
blank=True,
null=True,
on_delete=SET_NULL,
related_name='as_child_operation'
)
alias = CharField( alias = CharField(
verbose_name='Шифр', verbose_name='Шифр',
max_length=255, max_length=255,
@ -58,15 +67,6 @@ class Operation(Model):
blank=True blank=True
) )
position_x = FloatField(
verbose_name='Положение по горизонтали',
default=0
)
position_y = FloatField(
verbose_name='Положение по вертикали',
default=0
)
class Meta: class Meta:
''' Model metadata. ''' ''' Model metadata. '''
verbose_name = 'Операция' verbose_name = 'Операция'

View File

@ -20,6 +20,7 @@ from apps.rsform.models import (
from .Argument import Argument from .Argument import Argument
from .Inheritance import Inheritance from .Inheritance import Inheritance
from .Layout import Layout
from .Operation import Operation from .Operation import Operation
from .Substitution import Substitution from .Substitution import Substitution
@ -38,6 +39,7 @@ class OperationSchema:
def create(**kwargs) -> 'OperationSchema': def create(**kwargs) -> 'OperationSchema':
''' Create LibraryItem via OperationSchema. ''' ''' Create LibraryItem via OperationSchema. '''
model = LibraryItem.objects.create(item_type=LibraryItemType.OPERATION_SCHEMA, **kwargs) model = LibraryItem.objects.create(item_type=LibraryItemType.OPERATION_SCHEMA, **kwargs)
Layout.objects.create(oss=model, data={'operations': [], 'blocks': []})
return OperationSchema(model) return OperationSchema(model)
@staticmethod @staticmethod
@ -62,6 +64,12 @@ class OperationSchema:
''' Operation arguments. ''' ''' Operation arguments. '''
return Argument.objects.filter(operation__oss=self.model) return Argument.objects.filter(operation__oss=self.model)
def layout(self) -> Layout:
''' OSS layout. '''
result = Layout.objects.filter(oss=self.model).first()
assert result is not None
return result
def substitutions(self) -> QuerySet[Substitution]: def substitutions(self) -> QuerySet[Substitution]:
''' Operation substitutions. ''' ''' Operation substitutions. '''
return Substitution.objects.filter(operation__oss=self.model) return Substitution.objects.filter(operation__oss=self.model)
@ -78,15 +86,11 @@ class OperationSchema:
location=self.model.location location=self.model.location
) )
def update_positions(self, data: list[dict]) -> None: def update_layout(self, data: dict) -> None:
''' Update positions. ''' ''' Update positions. '''
lookup = {x['id']: x for x in data} layout = self.layout()
operations = self.operations() layout.data = data
for item in operations: layout.save()
if item.pk in lookup:
item.position_x = lookup[item.pk]['position_x']
item.position_y = lookup[item.pk]['position_y']
Operation.objects.bulk_update(operations, ['position_x', 'position_y'])
def create_operation(self, **kwargs) -> Operation: def create_operation(self, **kwargs) -> Operation:
''' Insert new operation. ''' ''' Insert new operation. '''

View File

@ -1,7 +1,9 @@
''' Django: Models. ''' ''' Django: Models. '''
from .Argument import Argument from .Argument import Argument
from .Block import Block
from .Inheritance import Inheritance from .Inheritance import Inheritance
from .Layout import Layout
from .Operation import Operation, OperationType from .Operation import Operation, OperationType
from .OperationSchema import OperationSchema from .OperationSchema import OperationSchema
from .PropagationFacade import PropagationFacade from .PropagationFacade import PropagationFacade

View File

@ -1,6 +1,6 @@
''' REST API: Serializers. ''' ''' REST API: Serializers. '''
from .basics import OperationPositionSerializer, PositionsSerializer, SubstitutionExSerializer from .basics import LayoutSerializer, SubstitutionExSerializer
from .data_access import ( from .data_access import (
ArgumentSerializer, ArgumentSerializer,
OperationCreateSerializer, OperationCreateSerializer,

View File

@ -2,17 +2,29 @@
from rest_framework import serializers from rest_framework import serializers
class OperationPositionSerializer(serializers.Serializer): class OperationNodeSerializer(serializers.Serializer):
''' Operation position. ''' ''' Operation position. '''
id = serializers.IntegerField() id = serializers.IntegerField()
position_x = serializers.FloatField() x = serializers.FloatField()
position_y = serializers.FloatField() y = serializers.FloatField()
class PositionsSerializer(serializers.Serializer): class BlockNodeSerializer(serializers.Serializer):
''' Operations position for OperationSchema. ''' ''' Block position. '''
positions = serializers.ListField( id = serializers.IntegerField()
child=OperationPositionSerializer() x = serializers.FloatField()
y = serializers.FloatField()
width = serializers.FloatField()
height = serializers.FloatField()
class LayoutSerializer(serializers.Serializer):
''' Layout for OperationSchema. '''
blocks = serializers.ListField(
child=BlockNodeSerializer()
)
operations = serializers.ListField(
child=OperationNodeSerializer()
) )

View File

@ -12,7 +12,7 @@ from apps.rsform.serializers import SubstitutionSerializerBase
from shared import messages as msg from shared import messages as msg
from ..models import Argument, Inheritance, Operation, OperationSchema, OperationType from ..models import Argument, Inheritance, Operation, OperationSchema, OperationType
from .basics import OperationPositionSerializer, SubstitutionExSerializer from .basics import LayoutSerializer, SubstitutionExSerializer
class OperationSerializer(serializers.ModelSerializer): class OperationSerializer(serializers.ModelSerializer):
@ -44,17 +44,16 @@ class OperationCreateSerializer(serializers.Serializer):
model = Operation model = Operation
fields = \ fields = \
'alias', 'operation_type', 'title', \ 'alias', 'operation_type', 'title', \
'description', 'result', 'position_x', 'position_y' 'description', 'result', 'parent'
layout = LayoutSerializer()
position_x = serializers.FloatField()
position_y = serializers.FloatField()
create_schema = serializers.BooleanField(default=False, required=False)
item_data = OperationCreateData() item_data = OperationCreateData()
create_schema = serializers.BooleanField(default=False, required=False)
arguments = PKField(many=True, queryset=Operation.objects.all().only('pk'), required=False) arguments = PKField(many=True, queryset=Operation.objects.all().only('pk'), required=False)
positions = serializers.ListField(
child=OperationPositionSerializer(),
default=[]
)
class OperationUpdateSerializer(serializers.Serializer): class OperationUpdateSerializer(serializers.Serializer):
''' Serializer: Operation update. ''' ''' Serializer: Operation update. '''
@ -65,6 +64,7 @@ class OperationUpdateSerializer(serializers.Serializer):
model = Operation model = Operation
fields = 'alias', 'title', 'description' fields = 'alias', 'title', 'description'
layout = LayoutSerializer()
target = PKField(many=False, queryset=Operation.objects.all()) target = PKField(many=False, queryset=Operation.objects.all())
item_data = OperationUpdateData() item_data = OperationUpdateData()
arguments = PKField(many=True, queryset=Operation.objects.all().only('oss_id', 'result_id'), required=False) arguments = PKField(many=True, queryset=Operation.objects.all().only('oss_id', 'result_id'), required=False)
@ -73,11 +73,6 @@ class OperationUpdateSerializer(serializers.Serializer):
required=False required=False
) )
positions = serializers.ListField(
child=OperationPositionSerializer(),
default=[]
)
def validate(self, attrs): def validate(self, attrs):
if 'arguments' not in attrs: if 'arguments' not in attrs:
return attrs return attrs
@ -120,11 +115,8 @@ class OperationUpdateSerializer(serializers.Serializer):
class OperationTargetSerializer(serializers.Serializer): class OperationTargetSerializer(serializers.Serializer):
''' Serializer: Target single operation. ''' ''' Serializer: Target single operation. '''
layout = LayoutSerializer()
target = PKField(many=False, queryset=Operation.objects.all().only('oss_id', 'result_id')) target = PKField(many=False, queryset=Operation.objects.all().only('oss_id', 'result_id'))
positions = serializers.ListField(
child=OperationPositionSerializer(),
default=[]
)
def validate(self, attrs): def validate(self, attrs):
oss = cast(LibraryItem, self.context['oss']) oss = cast(LibraryItem, self.context['oss'])
@ -138,11 +130,8 @@ class OperationTargetSerializer(serializers.Serializer):
class OperationDeleteSerializer(serializers.Serializer): class OperationDeleteSerializer(serializers.Serializer):
''' Serializer: Delete operation. ''' ''' Serializer: Delete operation. '''
layout = LayoutSerializer()
target = PKField(many=False, queryset=Operation.objects.all().only('oss_id', 'result')) target = PKField(many=False, queryset=Operation.objects.all().only('oss_id', 'result'))
positions = serializers.ListField(
child=OperationPositionSerializer(),
default=[]
)
keep_constituents = serializers.BooleanField(default=False, required=False) keep_constituents = serializers.BooleanField(default=False, required=False)
delete_schema = serializers.BooleanField(default=False, required=False) delete_schema = serializers.BooleanField(default=False, required=False)
@ -158,6 +147,7 @@ class OperationDeleteSerializer(serializers.Serializer):
class SetOperationInputSerializer(serializers.Serializer): class SetOperationInputSerializer(serializers.Serializer):
''' Serializer: Set input schema for operation. ''' ''' Serializer: Set input schema for operation. '''
layout = LayoutSerializer()
target = PKField(many=False, queryset=Operation.objects.all()) target = PKField(many=False, queryset=Operation.objects.all())
input = PKField( input = PKField(
many=False, many=False,
@ -165,10 +155,6 @@ class SetOperationInputSerializer(serializers.Serializer):
allow_null=True, allow_null=True,
default=None default=None
) )
positions = serializers.ListField(
child=OperationPositionSerializer(),
default=[]
)
def validate(self, attrs): def validate(self, attrs):
oss = cast(LibraryItem, self.context['oss']) oss = cast(LibraryItem, self.context['oss'])
@ -186,7 +172,7 @@ class SetOperationInputSerializer(serializers.Serializer):
class OperationSchemaSerializer(serializers.ModelSerializer): class OperationSchemaSerializer(serializers.ModelSerializer):
''' Serializer: Detailed data for OSS. ''' ''' Serializer: Detailed data for OSS. '''
items = serializers.ListField( operations = serializers.ListField(
child=OperationSerializer() child=OperationSerializer()
) )
arguments = serializers.ListField( arguments = serializers.ListField(
@ -195,6 +181,7 @@ class OperationSchemaSerializer(serializers.ModelSerializer):
substitutions = serializers.ListField( substitutions = serializers.ListField(
child=SubstitutionExSerializer() child=SubstitutionExSerializer()
) )
layout = LayoutSerializer()
class Meta: class Meta:
''' serializer metadata. ''' ''' serializer metadata. '''
@ -205,9 +192,10 @@ class OperationSchemaSerializer(serializers.ModelSerializer):
result = LibraryItemDetailsSerializer(instance).data result = LibraryItemDetailsSerializer(instance).data
del result['versions'] del result['versions']
oss = OperationSchema(instance) oss = OperationSchema(instance)
result['items'] = [] result['layout'] = oss.layout().data
result['operations'] = []
for operation in oss.operations().order_by('pk'): for operation in oss.operations().order_by('pk'):
result['items'].append(OperationSerializer(operation).data) result['operations'].append(OperationSerializer(operation).data)
result['arguments'] = [] result['arguments'] = []
for argument in oss.arguments().order_by('order'): for argument in oss.arguments().order_by('order'):
result['arguments'].append(ArgumentSerializer(argument).data) result['arguments'].append(ArgumentSerializer(argument).data)

View File

@ -25,9 +25,8 @@ class TestOperation(TestCase):
def test_create_default(self): def test_create_default(self):
self.assertEqual(self.operation.oss, self.oss.model) self.assertEqual(self.operation.oss, self.oss.model)
self.assertEqual(self.operation.operation_type, OperationType.INPUT) self.assertEqual(self.operation.operation_type, OperationType.INPUT)
self.assertEqual(self.operation.parent, None)
self.assertEqual(self.operation.result, None) self.assertEqual(self.operation.result, None)
self.assertEqual(self.operation.alias, 'KS1') self.assertEqual(self.operation.alias, 'KS1')
self.assertEqual(self.operation.title, '') self.assertEqual(self.operation.title, '')
self.assertEqual(self.operation.description, '') self.assertEqual(self.operation.description, '')
self.assertEqual(self.operation.position_x, 0)
self.assertEqual(self.operation.position_y, 0)

View File

@ -58,6 +58,18 @@ class TestChangeAttributes(EndpointTester):
self.operation3.refresh_from_db() self.operation3.refresh_from_db()
self.ks3 = RSForm(self.operation3.result) self.ks3 = RSForm(self.operation3.result)
self.layout_data = {
'operations': [
{'id': self.operation1.pk, 'x': 0, 'y': 0},
{'id': self.operation2.pk, 'x': 0, 'y': 0},
{'id': self.operation3.pk, 'x': 0, 'y': 0},
],
'blocks': []
}
layout = self.owned.layout()
layout.data = self.layout_data
layout.save()
@decl_endpoint('/api/library/{item}/set-owner', method='patch') @decl_endpoint('/api/library/{item}/set-owner', method='patch')
def test_set_owner(self): def test_set_owner(self):
data = {'user': self.user3.pk} data = {'user': self.user3.pk}
@ -142,7 +154,7 @@ class TestChangeAttributes(EndpointTester):
'title': 'Test title mod', 'title': 'Test title mod',
'description': 'Comment mod' 'description': 'Comment mod'
}, },
'positions': [], 'layout': self.layout_data
} }
response = self.executeOK(data=data, item=self.owned_id) response = self.executeOK(data=data, item=self.owned_id)

View File

@ -57,6 +57,18 @@ class TestChangeConstituents(EndpointTester):
self.ks3 = RSForm(self.operation3.result) self.ks3 = RSForm(self.operation3.result)
self.assertEqual(self.ks3.constituents().count(), 4) self.assertEqual(self.ks3.constituents().count(), 4)
self.layout_data = {
'operations': [
{'id': self.operation1.pk, 'x': 0, 'y': 0},
{'id': self.operation2.pk, 'x': 0, 'y': 0},
{'id': self.operation3.pk, 'x': 0, 'y': 0},
],
'blocks': []
}
layout = self.owned.layout()
layout.data = self.layout_data
layout.save()
@decl_endpoint('/api/rsforms/{item}/details', method='get') @decl_endpoint('/api/rsforms/{item}/details', method='get')
def test_retrieve_inheritance(self): def test_retrieve_inheritance(self):
response = self.executeOK(item=self.ks3.model.pk) response = self.executeOK(item=self.ks3.model.pk)

View File

@ -106,6 +106,20 @@ class TestChangeOperations(EndpointTester):
convention='KS5D4' convention='KS5D4'
) )
self.layout_data = {
'operations': [
{'id': self.operation1.pk, 'x': 0, 'y': 0},
{'id': self.operation2.pk, 'x': 0, 'y': 0},
{'id': self.operation3.pk, 'x': 0, 'y': 0},
{'id': self.operation4.pk, 'x': 0, 'y': 0},
{'id': self.operation5.pk, 'x': 0, 'y': 0},
],
'blocks': []
}
layout = self.owned.layout()
layout.data = self.layout_data
layout.save()
def test_oss_setup(self): def test_oss_setup(self):
self.assertEqual(self.ks1.constituents().count(), 3) self.assertEqual(self.ks1.constituents().count(), 3)
self.assertEqual(self.ks2.constituents().count(), 3) self.assertEqual(self.ks2.constituents().count(), 3)
@ -117,7 +131,7 @@ class TestChangeOperations(EndpointTester):
@decl_endpoint('/api/oss/{item}/delete-operation', method='patch') @decl_endpoint('/api/oss/{item}/delete-operation', method='patch')
def test_delete_input_operation(self): def test_delete_input_operation(self):
data = { data = {
'positions': [], 'layout': self.layout_data,
'target': self.operation2.pk 'target': self.operation2.pk
} }
self.executeOK(data=data, item=self.owned_id) self.executeOK(data=data, item=self.owned_id)
@ -137,7 +151,7 @@ class TestChangeOperations(EndpointTester):
@decl_endpoint('/api/oss/{item}/set-input', method='patch') @decl_endpoint('/api/oss/{item}/set-input', method='patch')
def test_set_input_null(self): def test_set_input_null(self):
data = { data = {
'positions': [], 'layout': self.layout_data,
'target': self.operation2.pk, 'target': self.operation2.pk,
'input': None 'input': None
} }
@ -169,7 +183,7 @@ class TestChangeOperations(EndpointTester):
ks6D1 = ks6.insert_new('D1', definition_formal='X1 X2', convention='KS6D1') ks6D1 = ks6.insert_new('D1', definition_formal='X1 X2', convention='KS6D1')
data = { data = {
'positions': [], 'layout': self.layout_data,
'target': self.operation2.pk, 'target': self.operation2.pk,
'input': ks6.model.pk 'input': ks6.model.pk
} }
@ -211,7 +225,7 @@ class TestChangeOperations(EndpointTester):
@decl_endpoint('/api/oss/{item}/delete-operation', method='patch') @decl_endpoint('/api/oss/{item}/delete-operation', method='patch')
def test_delete_operation_and_constituents(self): def test_delete_operation_and_constituents(self):
data = { data = {
'positions': [], 'layout': self.layout_data,
'target': self.operation1.pk, 'target': self.operation1.pk,
'keep_constituents': False, 'keep_constituents': False,
'delete_schema': True 'delete_schema': True
@ -232,7 +246,7 @@ class TestChangeOperations(EndpointTester):
@decl_endpoint('/api/oss/{item}/delete-operation', method='patch') @decl_endpoint('/api/oss/{item}/delete-operation', method='patch')
def test_delete_operation_keep_constituents(self): def test_delete_operation_keep_constituents(self):
data = { data = {
'positions': [], 'layout': self.layout_data,
'target': self.operation1.pk, 'target': self.operation1.pk,
'keep_constituents': True, 'keep_constituents': True,
'delete_schema': True 'delete_schema': True
@ -253,7 +267,7 @@ class TestChangeOperations(EndpointTester):
@decl_endpoint('/api/oss/{item}/delete-operation', method='patch') @decl_endpoint('/api/oss/{item}/delete-operation', method='patch')
def test_delete_operation_keep_schema(self): def test_delete_operation_keep_schema(self):
data = { data = {
'positions': [], 'layout': self.layout_data,
'target': self.operation1.pk, 'target': self.operation1.pk,
'keep_constituents': True, 'keep_constituents': True,
'delete_schema': False 'delete_schema': False
@ -283,7 +297,7 @@ class TestChangeOperations(EndpointTester):
'title': 'Test title mod', 'title': 'Test title mod',
'description': 'Comment mod' 'description': 'Comment mod'
}, },
'positions': [], 'layout': self.layout_data,
'substitutions': [ 'substitutions': [
{ {
'original': self.ks1X1.pk, 'original': self.ks1X1.pk,
@ -317,7 +331,7 @@ class TestChangeOperations(EndpointTester):
'title': 'Test title mod', 'title': 'Test title mod',
'description': 'Comment mod' 'description': 'Comment mod'
}, },
'positions': [], 'layout': self.layout_data,
'arguments': [self.operation1.pk], 'arguments': [self.operation1.pk],
} }
@ -356,7 +370,7 @@ class TestChangeOperations(EndpointTester):
data = { data = {
'target': self.operation4.pk, 'target': self.operation4.pk,
'positions': [] 'layout': self.layout_data
} }
self.executeOK(data=data, item=self.owned_id) self.executeOK(data=data, item=self.owned_id)
self.operation4.refresh_from_db() self.operation4.refresh_from_db()

View File

@ -106,6 +106,20 @@ class TestChangeSubstitutions(EndpointTester):
convention='KS5D4' convention='KS5D4'
) )
self.layout_data = {
'operations': [
{'id': self.operation1.pk, 'x': 0, 'y': 0},
{'id': self.operation2.pk, 'x': 0, 'y': 0},
{'id': self.operation3.pk, 'x': 0, 'y': 0},
{'id': self.operation4.pk, 'x': 0, 'y': 0},
{'id': self.operation5.pk, 'x': 0, 'y': 0},
],
'blocks': []
}
layout = self.owned.layout()
layout.data = self.layout_data
layout.save()
def test_oss_setup(self): def test_oss_setup(self):
self.assertEqual(self.ks1.constituents().count(), 3) self.assertEqual(self.ks1.constituents().count(), 3)
@ -139,10 +153,12 @@ class TestChangeSubstitutions(EndpointTester):
@decl_endpoint('/api/rsforms/{schema}/substitute', method='patch') @decl_endpoint('/api/rsforms/{schema}/substitute', method='patch')
def test_substitute_substitution(self): def test_substitute_substitution(self):
data = {'substitutions': [{ data = {
'substitutions': [{
'original': self.ks2S1.pk, 'original': self.ks2S1.pk,
'substitution': self.ks2X1.pk 'substitution': self.ks2X1.pk
}]} }]
}
self.executeOK(data=data, schema=self.ks2.model.pk) self.executeOK(data=data, schema=self.ks2.model.pk)
self.ks4D1.refresh_from_db() self.ks4D1.refresh_from_db()
self.ks4D2.refresh_from_db() self.ks4D2.refresh_from_db()

View File

@ -1,2 +1,3 @@
''' Tests for REST API. ''' ''' Tests for REST API. '''
from .t_operations import *
from .t_oss import * from .t_oss import *

View File

@ -0,0 +1,447 @@
''' Testing API: Operation Schema. '''
from apps.library.models import AccessPolicy, Editor, LibraryItem, LibraryItemType
from apps.oss.models import Operation, OperationSchema, OperationType
from apps.rsform.models import Constituenta, RSForm
from shared.EndpointTester import EndpointTester, decl_endpoint
class TestOssOperations(EndpointTester):
''' Testing OSS view - operations. '''
def setUp(self):
super().setUp()
self.owned = OperationSchema.create(title='Test', alias='T1', owner=self.user)
self.owned_id = self.owned.model.pk
self.unowned = OperationSchema.create(title='Test2', alias='T2')
self.unowned_id = self.unowned.model.pk
self.private = OperationSchema.create(title='Test2', alias='T2', access_policy=AccessPolicy.PRIVATE)
self.private_id = self.private.model.pk
self.invalid_id = self.private.model.pk + 1337
def populateData(self):
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,
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.SYNTHESIS
)
self.layout_data = {
'operations': [
{'id': self.operation1.pk, 'x': 0, 'y': 0},
{'id': self.operation2.pk, 'x': 0, 'y': 0},
{'id': self.operation3.pk, 'x': 0, 'y': 0},
],
'blocks': []
}
layout = self.owned.layout()
layout.data = self.layout_data
layout.save()
self.owned.set_arguments(self.operation3.pk, [self.operation1, self.operation2])
self.owned.set_substitutions(self.operation3.pk, [{
'original': self.ks1X1,
'substitution': self.ks2X1
}])
@decl_endpoint('/api/oss/{item}/create-operation', method='post')
def test_create_operation(self):
self.populateData()
self.executeBadData(item=self.owned_id)
data = {
'item_data': {
'alias': 'Test3',
'title': 'Test title',
'description': 'Тест кириллицы',
},
'layout': self.layout_data,
'position_x': 1,
'position_y': 1
}
self.executeBadData(data=data)
data['item_data']['operation_type'] = 'invalid'
self.executeBadData(data=data)
data['item_data']['operation_type'] = OperationType.INPUT
self.executeNotFound(data=data, item=self.invalid_id)
response = self.executeCreated(data=data, item=self.owned_id)
self.assertEqual(len(response.data['oss']['operations']), 4)
new_operation = response.data['new_operation']
layout = response.data['oss']['layout']
item = [item for item in layout['operations'] if item['id'] == new_operation['id']][0]
self.assertEqual(new_operation['alias'], data['item_data']['alias'])
self.assertEqual(new_operation['operation_type'], data['item_data']['operation_type'])
self.assertEqual(new_operation['title'], data['item_data']['title'])
self.assertEqual(new_operation['description'], data['item_data']['description'])
self.assertEqual(new_operation['result'], None)
self.assertEqual(new_operation['parent'], None)
self.assertEqual(item['x'], data['position_x'])
self.assertEqual(item['y'], data['position_y'])
self.operation1.refresh_from_db()
self.executeForbidden(data=data, item=self.unowned_id)
self.toggle_admin(True)
self.executeCreated(data=data, item=self.unowned_id)
@decl_endpoint('/api/oss/{item}/create-operation', method='post')
def test_create_operation_arguments(self):
self.populateData()
data = {
'item_data': {
'alias': 'Test4',
'operation_type': OperationType.SYNTHESIS
},
'layout': self.layout_data,
'position_x': 1,
'position_y': 1,
'arguments': [self.operation1.pk, self.operation3.pk]
}
response = self.executeCreated(data=data, item=self.owned_id)
self.owned.refresh_from_db()
new_operation = response.data['new_operation']
arguments = self.owned.arguments()
self.assertTrue(arguments.filter(operation__id=new_operation['id'], argument=self.operation1))
self.assertTrue(arguments.filter(operation__id=new_operation['id'], argument=self.operation3))
@decl_endpoint('/api/oss/{item}/create-operation', method='post')
def test_create_operation_result(self):
self.populateData()
self.operation1.result = None
self.operation1.save()
data = {
'item_data': {
'alias': 'Test4',
'operation_type': OperationType.INPUT,
'result': self.ks1.model.pk
},
'layout': self.layout_data,
'position_x': 1,
'position_y': 1
}
response = self.executeCreated(data=data, item=self.owned_id)
self.owned.refresh_from_db()
new_operation = response.data['new_operation']
self.assertEqual(new_operation['result'], self.ks1.model.pk)
@decl_endpoint('/api/oss/{item}/create-operation', method='post')
def test_create_operation_schema(self):
self.populateData()
Editor.add(self.owned.model.pk, self.user2.pk)
data = {
'item_data': {
'alias': 'Test4',
'title': 'Test title',
'description': 'Comment',
'operation_type': OperationType.INPUT,
'result': self.ks1.model.pk
},
'create_schema': True,
'layout': self.layout_data,
'position_x': 1,
'position_y': 1
}
self.executeBadData(data=data, item=self.owned_id)
data['item_data']['result'] = None
response = self.executeCreated(data=data, item=self.owned_id)
self.owned.refresh_from_db()
new_operation = response.data['new_operation']
schema = LibraryItem.objects.get(pk=new_operation['result'])
self.assertEqual(schema.alias, data['item_data']['alias'])
self.assertEqual(schema.title, data['item_data']['title'])
self.assertEqual(schema.description, data['item_data']['description'])
self.assertEqual(schema.visible, False)
self.assertEqual(schema.access_policy, self.owned.model.access_policy)
self.assertEqual(schema.location, self.owned.model.location)
self.assertIn(self.user2, schema.getQ_editors())
@decl_endpoint('/api/oss/{item}/delete-operation', method='patch')
def test_delete_operation(self):
self.executeNotFound(item=self.invalid_id)
self.populateData()
self.executeBadData(item=self.owned_id)
data = {
'layout': self.layout_data
}
self.executeBadData(data=data)
data['target'] = self.operation1.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()
response = self.executeOK(data=data)
layout = response.data['layout']
deleted_items = [item for item in layout['operations'] if item['id'] == data['target']]
self.assertEqual(len(response.data['operations']), 2)
self.assertEqual(len(deleted_items), 0)
@decl_endpoint('/api/oss/{item}/create-input', method='patch')
def test_create_input(self):
self.populateData()
self.executeBadData(item=self.owned_id)
data = {
'layout': self.layout_data
}
self.executeBadData(data=data)
data['target'] = self.operation1.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.executeBadData(data=data, item=self.owned_id)
self.operation1.result = None
self.operation1.description = 'TestComment'
self.operation1.title = 'TestTitle'
self.operation1.save()
response = self.executeOK(data=data)
self.operation1.refresh_from_db()
new_schema = response.data['new_schema']
self.assertEqual(new_schema['id'], self.operation1.result.pk)
self.assertEqual(new_schema['alias'], self.operation1.alias)
self.assertEqual(new_schema['title'], self.operation1.title)
self.assertEqual(new_schema['description'], self.operation1.description)
data['target'] = self.operation3.pk
self.executeBadData(data=data)
@decl_endpoint('/api/oss/{item}/set-input', method='patch')
def test_set_input_null(self):
self.populateData()
self.executeBadData(item=self.owned_id)
data = {
'layout': self.layout_data
}
self.executeBadData(data=data)
data['target'] = self.operation1.pk
data['input'] = None
self.toggle_admin(True)
self.executeBadData(data=data, item=self.unowned_id)
self.logout()
self.executeForbidden(data=data, item=self.owned_id)
self.login()
response = self.executeOK(data=data)
self.operation1.refresh_from_db()
self.assertEqual(self.operation1.result, None)
data['input'] = self.ks1.model.pk
self.ks1.model.alias = 'Test42'
self.ks1.model.title = 'Test421'
self.ks1.model.description = 'TestComment42'
self.ks1.save()
response = self.executeOK(data=data)
self.operation1.refresh_from_db()
self.assertEqual(self.operation1.result, self.ks1.model)
self.assertEqual(self.operation1.alias, self.ks1.model.alias)
self.assertEqual(self.operation1.title, self.ks1.model.title)
self.assertEqual(self.operation1.description, self.ks1.model.description)
@decl_endpoint('/api/oss/{item}/set-input', method='patch')
def test_set_input_change_schema(self):
self.populateData()
self.operation2.result = None
data = {
'layout': self.layout_data,
'target': self.operation1.pk,
'input': self.ks2.model.pk
}
self.executeBadData(data=data, item=self.owned_id)
self.ks2.model.visible = False
self.ks2.model.save(update_fields=['visible'])
data = {
'layout': self.layout_data,
'target': self.operation2.pk,
'input': None
}
self.executeOK(data=data, item=self.owned_id)
self.operation2.refresh_from_db()
self.ks2.model.refresh_from_db()
self.assertEqual(self.operation2.result, None)
self.assertEqual(self.ks2.model.visible, True)
data = {
'layout': self.layout_data,
'target': self.operation1.pk,
'input': self.ks2.model.pk
}
self.executeOK(data=data, item=self.owned_id)
self.operation1.refresh_from_db()
self.assertEqual(self.operation1.result, self.ks2.model)
@decl_endpoint('/api/oss/{item}/update-operation', method='patch')
def test_update_operation(self):
self.populateData()
self.executeBadData(item=self.owned_id)
ks3 = RSForm.create(alias='KS3', title='Test3', owner=self.user)
ks3x1 = ks3.insert_new('X1', term_resolved='X1_1')
data = {
'target': self.operation3.pk,
'item_data': {
'alias': 'Test3 mod',
'title': 'Test title mod',
'description': 'Comment mod'
},
'layout': self.layout_data,
'arguments': [self.operation2.pk, self.operation1.pk],
'substitutions': [
{
'original': self.ks1X1.pk,
'substitution': ks3x1.pk
}
]
}
self.executeBadData(data=data)
data['substitutions'][0]['substitution'] = self.ks2X1.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()
response = self.executeOK(data=data)
self.operation3.refresh_from_db()
self.assertEqual(self.operation3.alias, data['item_data']['alias'])
self.assertEqual(self.operation3.title, data['item_data']['title'])
self.assertEqual(self.operation3.description, data['item_data']['description'])
args = self.operation3.getQ_arguments().order_by('order')
self.assertEqual(args[0].argument.pk, data['arguments'][0])
self.assertEqual(args[0].order, 0)
self.assertEqual(args[1].argument.pk, data['arguments'][1])
self.assertEqual(args[1].order, 1)
sub = self.operation3.getQ_substitutions()[0]
self.assertEqual(sub.original.pk, data['substitutions'][0]['original'])
self.assertEqual(sub.substitution.pk, data['substitutions'][0]['substitution'])
@decl_endpoint('/api/oss/{item}/update-operation', method='patch')
def test_update_operation_sync(self):
self.populateData()
self.executeBadData(item=self.owned_id)
data = {
'target': self.operation1.pk,
'item_data': {
'alias': 'Test3 mod',
'title': 'Test title mod',
'description': 'Comment mod'
},
'layout': self.layout_data
}
response = self.executeOK(data=data)
self.operation1.refresh_from_db()
self.assertEqual(self.operation1.alias, data['item_data']['alias'])
self.assertEqual(self.operation1.title, data['item_data']['title'])
self.assertEqual(self.operation1.description, data['item_data']['description'])
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.description, data['item_data']['description'])
@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',
'description': 'Comment mod'
},
'layout': self.layout_data,
'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 = {
'layout': self.layout_data,
'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.description, self.operation3.description)
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

@ -1,6 +1,6 @@
''' Testing API: Operation Schema. ''' ''' Testing API: Operation Schema. '''
from apps.library.models import AccessPolicy, Editor, LibraryItem, LibraryItemType from apps.library.models import AccessPolicy, LibraryItemType
from apps.oss.models import Operation, OperationSchema, OperationType from apps.oss.models import OperationSchema, OperationType
from apps.rsform.models import Constituenta, RSForm from apps.rsform.models import Constituenta, RSForm
from shared.EndpointTester import EndpointTester, decl_endpoint from shared.EndpointTester import EndpointTester, decl_endpoint
@ -54,6 +54,14 @@ class TestOssViewset(EndpointTester):
alias='3', alias='3',
operation_type=OperationType.SYNTHESIS operation_type=OperationType.SYNTHESIS
) )
layout = self.owned.layout()
layout.data = {'operations': [
{'id': self.operation1.pk, 'x': 0, 'y': 0},
{'id': self.operation2.pk, 'x': 0, 'y': 0},
{'id': self.operation3.pk, 'x': 0, 'y': 0},
], 'blocks': []}
layout.save()
self.owned.set_arguments(self.operation3.pk, [self.operation1, self.operation2]) self.owned.set_arguments(self.operation3.pk, [self.operation1, self.operation2])
self.owned.set_substitutions(self.operation3.pk, [{ self.owned.set_substitutions(self.operation3.pk, [{
'original': self.ks1X1, 'original': self.ks1X1,
@ -74,9 +82,9 @@ class TestOssViewset(EndpointTester):
self.assertEqual(response.data['item_type'], LibraryItemType.OPERATION_SCHEMA) self.assertEqual(response.data['item_type'], LibraryItemType.OPERATION_SCHEMA)
self.assertEqual(len(response.data['items']), 3) self.assertEqual(len(response.data['operations']), 3)
self.assertEqual(response.data['items'][0]['id'], self.operation1.pk) self.assertEqual(response.data['operations'][0]['id'], self.operation1.pk)
self.assertEqual(response.data['items'][0]['operation_type'], self.operation1.operation_type) self.assertEqual(response.data['operations'][0]['operation_type'], self.operation1.operation_type)
self.assertEqual(len(response.data['substitutions']), 1) self.assertEqual(len(response.data['substitutions']), 1)
sub = response.data['substitutions'][0] sub = response.data['substitutions'][0]
@ -95,6 +103,12 @@ class TestOssViewset(EndpointTester):
self.assertEqual(arguments[1]['operation'], self.operation3.pk) self.assertEqual(arguments[1]['operation'], self.operation3.pk)
self.assertEqual(arguments[1]['argument'], self.operation2.pk) self.assertEqual(arguments[1]['argument'], self.operation2.pk)
layout = response.data['layout']
self.assertEqual(layout['blocks'], [])
self.assertEqual(layout['operations'][0], {'id': self.operation1.pk, 'x': 0, 'y': 0})
self.assertEqual(layout['operations'][1], {'id': self.operation2.pk, 'x': 0, 'y': 0})
self.assertEqual(layout['operations'][2], {'id': self.operation3.pk, 'x': 0, 'y': 0})
self.executeOK(item=self.unowned_id) self.executeOK(item=self.unowned_id)
self.executeForbidden(item=self.private_id) self.executeForbidden(item=self.private_id)
@ -103,401 +117,32 @@ class TestOssViewset(EndpointTester):
self.executeOK(item=self.unowned_id) self.executeOK(item=self.unowned_id)
self.executeForbidden(item=self.private_id) self.executeForbidden(item=self.private_id)
@decl_endpoint('/api/oss/{item}/update-positions', method='patch') @decl_endpoint('/api/oss/{item}/update-layout', method='patch')
def test_update_positions(self): def test_update_layout(self):
self.populateData() self.populateData()
self.executeBadData(item=self.owned_id) self.executeBadData(item=self.owned_id)
data = {'positions': []} data = {'operations': [], 'blocks': []}
self.executeOK(data=data) self.executeOK(data=data)
data = {'positions': [ data = {
{'id': self.operation1.pk, 'position_x': 42.1, 'position_y': 1337}, 'operations': [
{'id': self.operation2.pk, 'position_x': 36.1, 'position_y': 1437}, {'id': self.operation1.pk, 'x': 42.1, 'y': 1337},
{'id': self.invalid_id, 'position_x': 31, 'position_y': 12}, {'id': self.operation2.pk, 'x': 36.1, 'y': 1437},
]} {'id': self.operation3.pk, 'x': 36.1, 'y': 1435}
], 'blocks': []
}
self.toggle_admin(True) self.toggle_admin(True)
self.executeOK(data=data, item=self.unowned_id) self.executeOK(data=data, item=self.unowned_id)
self.operation1.refresh_from_db()
self.assertNotEqual(self.operation1.position_x, data['positions'][0]['position_x'])
self.assertNotEqual(self.operation1.position_y, data['positions'][0]['position_y'])
self.toggle_admin(False) self.toggle_admin(False)
self.executeOK(data=data, item=self.owned_id) self.executeOK(data=data, item=self.owned_id)
self.operation1.refresh_from_db() self.owned.refresh_from_db()
self.operation2.refresh_from_db() self.assertEqual(self.owned.layout().data, data)
self.assertEqual(self.operation1.position_x, data['positions'][0]['position_x'])
self.assertEqual(self.operation1.position_y, data['positions'][0]['position_y'])
self.assertEqual(self.operation2.position_x, data['positions'][1]['position_x'])
self.assertEqual(self.operation2.position_y, data['positions'][1]['position_y'])
self.executeForbidden(data=data, item=self.unowned_id) self.executeForbidden(data=data, item=self.unowned_id)
self.executeForbidden(data=data, item=self.private_id) self.executeForbidden(data=data, item=self.private_id)
@decl_endpoint('/api/oss/{item}/create-operation', method='post')
def test_create_operation(self):
self.populateData()
self.executeBadData(item=self.owned_id)
data = {
'item_data': {
'alias': 'Test3',
'title': 'Test title',
'description': 'Тест кириллицы',
'position_x': 1,
'position_y': 1,
},
'positions': [
{'id': self.operation1.pk, 'position_x': 42.1, 'position_y': 1337}
]
}
self.executeBadData(data=data)
data['item_data']['operation_type'] = 'invalid'
self.executeBadData(data=data)
data['item_data']['operation_type'] = OperationType.INPUT
self.executeNotFound(data=data, item=self.invalid_id)
response = self.executeCreated(data=data, item=self.owned_id)
self.assertEqual(len(response.data['oss']['items']), 4)
new_operation = response.data['new_operation']
self.assertEqual(new_operation['alias'], data['item_data']['alias'])
self.assertEqual(new_operation['operation_type'], data['item_data']['operation_type'])
self.assertEqual(new_operation['title'], data['item_data']['title'])
self.assertEqual(new_operation['description'], data['item_data']['description'])
self.assertEqual(new_operation['position_x'], data['item_data']['position_x'])
self.assertEqual(new_operation['position_y'], data['item_data']['position_y'])
self.assertEqual(new_operation['result'], None)
self.operation1.refresh_from_db()
self.assertEqual(self.operation1.position_x, data['positions'][0]['position_x'])
self.assertEqual(self.operation1.position_y, data['positions'][0]['position_y'])
self.executeForbidden(data=data, item=self.unowned_id)
self.toggle_admin(True)
self.executeCreated(data=data, item=self.unowned_id)
@decl_endpoint('/api/oss/{item}/create-operation', method='post')
def test_create_operation_arguments(self):
self.populateData()
data = {
'item_data': {
'alias': 'Test4',
'operation_type': OperationType.SYNTHESIS
},
'positions': [],
'arguments': [self.operation1.pk, self.operation3.pk]
}
response = self.executeCreated(data=data, item=self.owned_id)
self.owned.refresh_from_db()
new_operation = response.data['new_operation']
arguments = self.owned.arguments()
self.assertTrue(arguments.filter(operation__id=new_operation['id'], argument=self.operation1))
self.assertTrue(arguments.filter(operation__id=new_operation['id'], argument=self.operation3))
@decl_endpoint('/api/oss/{item}/create-operation', method='post')
def test_create_operation_result(self):
self.populateData()
self.operation1.result = None
self.operation1.save()
data = {
'item_data': {
'alias': 'Test4',
'operation_type': OperationType.INPUT,
'result': self.ks1.model.pk
},
'positions': [],
}
response = self.executeCreated(data=data, item=self.owned_id)
self.owned.refresh_from_db()
new_operation = response.data['new_operation']
self.assertEqual(new_operation['result'], self.ks1.model.pk)
@decl_endpoint('/api/oss/{item}/create-operation', method='post')
def test_create_operation_schema(self):
self.populateData()
Editor.add(self.owned.model.pk, self.user2.pk)
data = {
'item_data': {
'alias': 'Test4',
'title': 'Test title',
'description': 'Comment',
'operation_type': OperationType.INPUT,
'result': self.ks1.model.pk
},
'create_schema': True,
'positions': [],
}
self.executeBadData(data=data, item=self.owned_id)
data['item_data']['result'] = None
response = self.executeCreated(data=data, item=self.owned_id)
self.owned.refresh_from_db()
new_operation = response.data['new_operation']
schema = LibraryItem.objects.get(pk=new_operation['result'])
self.assertEqual(schema.alias, data['item_data']['alias'])
self.assertEqual(schema.title, data['item_data']['title'])
self.assertEqual(schema.description, data['item_data']['description'])
self.assertEqual(schema.visible, False)
self.assertEqual(schema.access_policy, self.owned.model.access_policy)
self.assertEqual(schema.location, self.owned.model.location)
self.assertIn(self.user2, schema.getQ_editors())
@decl_endpoint('/api/oss/{item}/delete-operation', method='patch')
def test_delete_operation(self):
self.executeNotFound(item=self.invalid_id)
self.populateData()
self.executeBadData(item=self.owned_id)
data = {
'positions': []
}
self.executeBadData(data=data)
data['target'] = self.operation1.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()
response = self.executeOK(data=data)
self.assertEqual(len(response.data['items']), 2)
@decl_endpoint('/api/oss/{item}/create-input', method='patch')
def test_create_input(self):
self.populateData()
self.executeBadData(item=self.owned_id)
data = {
'positions': []
}
self.executeBadData(data=data)
data['target'] = self.operation1.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.executeBadData(data=data, item=self.owned_id)
self.operation1.result = None
self.operation1.description = 'TestComment'
self.operation1.title = 'TestTitle'
self.operation1.save()
response = self.executeOK(data=data)
self.operation1.refresh_from_db()
new_schema = response.data['new_schema']
self.assertEqual(new_schema['id'], self.operation1.result.pk)
self.assertEqual(new_schema['alias'], self.operation1.alias)
self.assertEqual(new_schema['title'], self.operation1.title)
self.assertEqual(new_schema['description'], self.operation1.description)
data['target'] = self.operation3.pk
self.executeBadData(data=data)
@decl_endpoint('/api/oss/{item}/set-input', method='patch')
def test_set_input_null(self):
self.populateData()
self.executeBadData(item=self.owned_id)
data = {
'positions': []
}
self.executeBadData(data=data)
data['target'] = self.operation1.pk
data['input'] = None
self.toggle_admin(True)
self.executeBadData(data=data, item=self.unowned_id)
self.logout()
self.executeForbidden(data=data, item=self.owned_id)
self.login()
response = self.executeOK(data=data)
self.operation1.refresh_from_db()
self.assertEqual(self.operation1.result, None)
data['input'] = self.ks1.model.pk
self.ks1.model.alias = 'Test42'
self.ks1.model.title = 'Test421'
self.ks1.model.description = 'TestComment42'
self.ks1.save()
response = self.executeOK(data=data)
self.operation1.refresh_from_db()
self.assertEqual(self.operation1.result, self.ks1.model)
self.assertEqual(self.operation1.alias, self.ks1.model.alias)
self.assertEqual(self.operation1.title, self.ks1.model.title)
self.assertEqual(self.operation1.description, self.ks1.model.description)
@decl_endpoint('/api/oss/{item}/set-input', method='patch')
def test_set_input_change_schema(self):
self.populateData()
self.operation2.result = None
data = {
'positions': [],
'target': self.operation1.pk,
'input': self.ks2.model.pk
}
self.executeBadData(data=data, item=self.owned_id)
self.ks2.model.visible = False
self.ks2.model.save(update_fields=['visible'])
data = {
'positions': [],
'target': self.operation2.pk,
'input': None
}
self.executeOK(data=data, item=self.owned_id)
self.operation2.refresh_from_db()
self.ks2.model.refresh_from_db()
self.assertEqual(self.operation2.result, None)
self.assertEqual(self.ks2.model.visible, True)
data = {
'positions': [],
'target': self.operation1.pk,
'input': self.ks2.model.pk
}
self.executeOK(data=data, item=self.owned_id)
self.operation1.refresh_from_db()
self.assertEqual(self.operation1.result, self.ks2.model)
@decl_endpoint('/api/oss/{item}/update-operation', method='patch')
def test_update_operation(self):
self.populateData()
self.executeBadData(item=self.owned_id)
ks3 = RSForm.create(alias='KS3', title='Test3', owner=self.user)
ks3x1 = ks3.insert_new('X1', term_resolved='X1_1')
data = {
'target': self.operation3.pk,
'item_data': {
'alias': 'Test3 mod',
'title': 'Test title mod',
'description': 'Comment mod'
},
'positions': [],
'arguments': [self.operation2.pk, self.operation1.pk],
'substitutions': [
{
'original': self.ks1X1.pk,
'substitution': ks3x1.pk
}
]
}
self.executeBadData(data=data)
data['substitutions'][0]['substitution'] = self.ks2X1.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()
response = self.executeOK(data=data)
self.operation3.refresh_from_db()
self.assertEqual(self.operation3.alias, data['item_data']['alias'])
self.assertEqual(self.operation3.title, data['item_data']['title'])
self.assertEqual(self.operation3.description, data['item_data']['description'])
args = self.operation3.getQ_arguments().order_by('order')
self.assertEqual(args[0].argument.pk, data['arguments'][0])
self.assertEqual(args[0].order, 0)
self.assertEqual(args[1].argument.pk, data['arguments'][1])
self.assertEqual(args[1].order, 1)
sub = self.operation3.getQ_substitutions()[0]
self.assertEqual(sub.original.pk, data['substitutions'][0]['original'])
self.assertEqual(sub.substitution.pk, data['substitutions'][0]['substitution'])
@decl_endpoint('/api/oss/{item}/update-operation', method='patch')
def test_update_operation_sync(self):
self.populateData()
self.executeBadData(item=self.owned_id)
data = {
'target': self.operation1.pk,
'item_data': {
'alias': 'Test3 mod',
'title': 'Test title mod',
'description': 'Comment mod'
},
'positions': [],
}
response = self.executeOK(data=data)
self.operation1.refresh_from_db()
self.assertEqual(self.operation1.alias, data['item_data']['alias'])
self.assertEqual(self.operation1.title, data['item_data']['title'])
self.assertEqual(self.operation1.description, data['item_data']['description'])
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.description, data['item_data']['description'])
@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',
'description': '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.description, self.operation3.description)
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)
@decl_endpoint('/api/oss/get-predecessor', method='post') @decl_endpoint('/api/oss/get-predecessor', method='post')
def test_get_predecessor(self): def test_get_predecessor(self):
self.populateData() self.populateData()

View File

@ -36,9 +36,9 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
def get_permissions(self): def get_permissions(self):
''' Determine permission class. ''' ''' Determine permission class. '''
if self.action in [ if self.action in [
'update_layout',
'create_operation', 'create_operation',
'delete_operation', 'delete_operation',
'update_positions',
'create_input', 'create_input',
'set_input', 'set_input',
'update_operation', 'update_operation',
@ -73,21 +73,21 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
) )
@extend_schema( @extend_schema(
summary='update positions', summary='update layout',
tags=['OSS'], tags=['OSS'],
request=s.PositionsSerializer, request=s.LayoutSerializer,
responses={ responses={
c.HTTP_200_OK: None, c.HTTP_200_OK: None,
c.HTTP_403_FORBIDDEN: None, c.HTTP_403_FORBIDDEN: None,
c.HTTP_404_NOT_FOUND: None c.HTTP_404_NOT_FOUND: None
} }
) )
@action(detail=True, methods=['patch'], url_path='update-positions') @action(detail=True, methods=['patch'], url_path='update-layout')
def update_positions(self, request: Request, pk) -> HttpResponse: def update_layout(self, request: Request, pk) -> HttpResponse:
''' Endpoint: Update operations positions. ''' ''' Endpoint: Update schema layout. '''
serializer = s.PositionsSerializer(data=request.data) serializer = s.LayoutSerializer(data=request.data)
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
m.OperationSchema(self.get_object()).update_positions(serializer.validated_data['positions']) m.OperationSchema(self.get_object()).update_layout(serializer.validated_data)
return Response(status=c.HTTP_200_OK) return Response(status=c.HTTP_200_OK)
@extend_schema( @extend_schema(
@ -108,9 +108,16 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
oss = m.OperationSchema(self.get_object()) oss = m.OperationSchema(self.get_object())
layout = serializer.validated_data['layout']
with transaction.atomic(): with transaction.atomic():
oss.update_positions(serializer.validated_data['positions'])
new_operation = oss.create_operation(**serializer.validated_data['item_data']) new_operation = oss.create_operation(**serializer.validated_data['item_data'])
layout['operations'].append({
'id': new_operation.pk,
'x': serializer.validated_data['position_x'],
'y': serializer.validated_data['position_y']
})
oss.update_layout(layout)
schema = new_operation.result schema = new_operation.result
if schema is not None: if schema is not None:
connected_operations = \ connected_operations = \
@ -164,9 +171,11 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
oss = m.OperationSchema(self.get_object()) oss = m.OperationSchema(self.get_object())
operation = cast(m.Operation, serializer.validated_data['target']) operation = cast(m.Operation, serializer.validated_data['target'])
old_schema = operation.result old_schema = operation.result
layout = serializer.validated_data['layout']
layout['operations'] = [x for x in layout['operations'] if x['id'] != operation.pk]
with transaction.atomic(): with transaction.atomic():
oss.update_positions(serializer.validated_data['positions'])
oss.delete_operation(operation.pk, serializer.validated_data['keep_constituents']) oss.delete_operation(operation.pk, serializer.validated_data['keep_constituents'])
oss.update_layout(layout)
if old_schema is not None: if old_schema is not None:
if serializer.validated_data['delete_schema']: if serializer.validated_data['delete_schema']:
m.PropagationFacade.before_delete_schema(old_schema) m.PropagationFacade.before_delete_schema(old_schema)
@ -211,7 +220,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_layout(serializer.validated_data['layout'])
schema = oss.create_input(operation) schema = oss.create_input(operation)
return Response( return Response(
@ -262,7 +271,7 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
if old_schema.is_synced(oss.model): if old_schema.is_synced(oss.model):
old_schema.visible = True old_schema.visible = True
old_schema.save(update_fields=['visible']) old_schema.save(update_fields=['visible'])
oss.update_positions(serializer.validated_data['positions']) oss.update_layout(serializer.validated_data['layout'])
oss.set_input(target_operation.pk, schema) oss.set_input(target_operation.pk, schema)
return Response( return Response(
status=c.HTTP_200_OK, status=c.HTTP_200_OK,
@ -292,7 +301,7 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
operation: m.Operation = cast(m.Operation, serializer.validated_data['target']) operation: m.Operation = cast(m.Operation, serializer.validated_data['target'])
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_layout(serializer.validated_data['layout'])
operation.alias = serializer.validated_data['item_data']['alias'] operation.alias = serializer.validated_data['item_data']['alias']
operation.title = serializer.validated_data['item_data']['title'] operation.title = serializer.validated_data['item_data']['title']
operation.description = serializer.validated_data['item_data']['description'] operation.description = serializer.validated_data['item_data']['description']
@ -346,7 +355,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_layout(serializer.validated_data['layout'])
oss.execute_operation(operation) oss.execute_operation(operation)
return Response( return Response(

View File

@ -20,7 +20,7 @@ export const useSetAccessPolicy = () => {
client.setQueryData(ossKey, { ...ossData, access_policy: variables.policy }); client.setQueryData(ossKey, { ...ossData, access_policy: variables.policy });
return Promise.allSettled([ return Promise.allSettled([
client.invalidateQueries({ queryKey: KEYS.composite.libraryList }), client.invalidateQueries({ queryKey: KEYS.composite.libraryList }),
...ossData.items ...ossData.operations
.map(item => { .map(item => {
if (!item.result) { if (!item.result) {
return; return;

View File

@ -18,7 +18,7 @@ export const useSetEditors = () => {
if (ossData) { if (ossData) {
client.setQueryData(ossKey, { ...ossData, editors: variables.editors }); client.setQueryData(ossKey, { ...ossData, editors: variables.editors });
return Promise.allSettled( return Promise.allSettled(
ossData.items ossData.operations
.map(item => { .map(item => {
if (!item.result) { if (!item.result) {
return; return;

View File

@ -20,7 +20,7 @@ export const useSetLocation = () => {
client.setQueryData(ossKey, { ...ossData, location: variables.location }); client.setQueryData(ossKey, { ...ossData, location: variables.location });
return Promise.allSettled([ return Promise.allSettled([
client.invalidateQueries({ queryKey: libraryApi.libraryListKey }), client.invalidateQueries({ queryKey: libraryApi.libraryListKey }),
...ossData.items ...ossData.operations
.map(item => { .map(item => {
if (!item.result) { if (!item.result) {
return; return;

View File

@ -20,7 +20,7 @@ export const useSetOwner = () => {
client.setQueryData(ossKey, { ...ossData, owner: variables.owner }); client.setQueryData(ossKey, { ...ossData, owner: variables.owner });
return Promise.allSettled([ return Promise.allSettled([
client.invalidateQueries({ queryKey: libraryApi.libraryListKey }), client.invalidateQueries({ queryKey: libraryApi.libraryListKey }),
...ossData.items ...ossData.operations
.map(item => { .map(item => {
if (!item.result) { if (!item.result) {
return; return;

View File

@ -12,9 +12,9 @@ import {
type IOperationCreatedResponse, type IOperationCreatedResponse,
type IOperationCreateDTO, type IOperationCreateDTO,
type IOperationDeleteDTO, type IOperationDeleteDTO,
type IOperationPosition,
type IOperationSchemaDTO, type IOperationSchemaDTO,
type IOperationUpdateDTO, type IOperationUpdateDTO,
type IOssLayout,
type ITargetOperation, type ITargetOperation,
schemaConstituentaReference, schemaConstituentaReference,
schemaOperationCreatedResponse, schemaOperationCreatedResponse,
@ -39,19 +39,11 @@ export const ossApi = {
}); });
}, },
updatePositions: ({ updateLayout: ({ itemID, data, isSilent }: { itemID: number; data: IOssLayout; isSilent?: boolean }) =>
itemID,
positions,
isSilent
}: {
itemID: number;
positions: IOperationPosition[];
isSilent?: boolean;
}) =>
axiosPatch({ axiosPatch({
endpoint: `/api/oss/${itemID}/update-positions`, endpoint: `/api/oss/${itemID}/update-layout`,
request: { request: {
data: { positions: positions }, data: data,
successMessage: isSilent ? undefined : infoMsg.changesSaved successMessage: isSilent ? undefined : infoMsg.changesSaved
} }
}), }),

View File

@ -41,7 +41,7 @@ export class OssLoader {
} }
private prepareLookups() { private prepareLookups() {
this.oss.items.forEach(operation => { this.oss.operations.forEach(operation => {
this.operationByID.set(operation.id, operation); this.operationByID.set(operation.id, operation);
this.graph.addNode(operation.id); this.graph.addNode(operation.id);
}); });
@ -52,13 +52,16 @@ export class OssLoader {
} }
private extractSchemas() { private extractSchemas() {
this.schemaIDs = this.oss.items.map(operation => operation.result).filter(item => item !== null); this.schemaIDs = this.oss.operations.map(operation => operation.result).filter(item => item !== null);
} }
private inferOperationAttributes() { private inferOperationAttributes() {
this.graph.topologicalOrder().forEach(operationID => { this.graph.topologicalOrder().forEach(operationID => {
const operation = this.operationByID.get(operationID)!; const operation = this.operationByID.get(operationID)!;
const schema = this.items.find(item => item.id === operation.result); const schema = this.items.find(item => item.id === operation.result);
const position = this.oss.layout.operations.find(item => item.id === operationID);
operation.x = position?.x ?? 0;
operation.y = position?.y ?? 0;
operation.is_consolidation = this.inferConsolidation(operationID); operation.is_consolidation = this.inferConsolidation(operationID);
operation.is_owned = !schema || (schema.owner === this.oss.owner && schema.location === this.oss.location); operation.is_owned = !schema || (schema.owner === this.oss.owner && schema.location === this.oss.location);
operation.substitutions = this.oss.substitutions.filter(item => item.operation === operationID); operation.substitutions = this.oss.substitutions.filter(item => item.operation === operationID);
@ -82,7 +85,7 @@ export class OssLoader {
} }
private calculateStats(): IOperationSchemaStats { private calculateStats(): IOperationSchemaStats {
const items = this.oss.items; const items = this.oss.operations;
return { return {
count_operations: items.length, count_operations: items.length,
count_inputs: items.filter(item => item.operation_type === OperationType.INPUT).length, count_inputs: items.filter(item => item.operation_type === OperationType.INPUT).length,

View File

@ -23,8 +23,8 @@ export type IOperationDTO = z.infer<typeof schemaOperation>;
/** Represents backend data for {@link IOperationSchema}. */ /** Represents backend data for {@link IOperationSchema}. */
export type IOperationSchemaDTO = z.infer<typeof schemaOperationSchema>; export type IOperationSchemaDTO = z.infer<typeof schemaOperationSchema>;
/** Represents {@link IOperation} position. */ /** Represents {@link schemaOperation} layout. */
export type IOperationPosition = z.infer<typeof schemaOperationPosition>; export type IOssLayout = z.infer<typeof schemaOssLayout>;
/** Represents {@link IOperation} data, used in creation process. */ /** Represents {@link IOperation} data, used in creation process. */
export type IOperationCreateDTO = z.infer<typeof schemaOperationCreate>; export type IOperationCreateDTO = z.infer<typeof schemaOperationCreate>;
@ -35,7 +35,7 @@ export type IOperationCreatedResponse = z.infer<typeof schemaOperationCreatedRes
* Represents target {@link IOperation}. * Represents target {@link IOperation}.
*/ */
export interface ITargetOperation { export interface ITargetOperation {
positions: IOperationPosition[]; layout: IOssLayout;
target: number; target: number;
} }
@ -69,9 +69,7 @@ export const schemaOperation = z.strictObject({
title: z.string(), title: z.string(),
description: z.string(), description: z.string(),
position_x: z.number(), parent: z.number().nullable(),
position_y: z.number(),
result: z.number().nullable() result: z.number().nullable()
}); });
@ -83,9 +81,21 @@ export const schemaCstSubstituteInfo = schemaCstSubstitute.extend({
substitution_term: z.string() substitution_term: z.string()
}); });
export const schemaPosition = z.strictObject({
id: z.number(),
x: z.number(),
y: z.number()
});
export const schemaOssLayout = z.strictObject({
operations: z.array(schemaPosition),
blocks: z.array(schemaPosition)
});
export const schemaOperationSchema = schemaLibraryItem.extend({ export const schemaOperationSchema = schemaLibraryItem.extend({
editors: z.number().array(), editors: z.number().array(),
items: z.array(schemaOperation), operations: z.array(schemaOperation),
layout: schemaOssLayout,
arguments: z arguments: z
.object({ .object({
operation: z.number(), operation: z.number(),
@ -95,23 +105,18 @@ export const schemaOperationSchema = schemaLibraryItem.extend({
substitutions: z.array(schemaCstSubstituteInfo) substitutions: z.array(schemaCstSubstituteInfo)
}); });
export const schemaOperationPosition = z.strictObject({
id: z.number(),
position_x: z.number(),
position_y: z.number()
});
export const schemaOperationCreate = z.strictObject({ export const schemaOperationCreate = z.strictObject({
positions: z.array(schemaOperationPosition), layout: schemaOssLayout,
item_data: z.strictObject({ item_data: z.strictObject({
alias: z.string().nonempty(), alias: z.string().nonempty(),
operation_type: schemaOperationType, operation_type: schemaOperationType,
title: z.string(), title: z.string(),
description: z.string(), description: z.string(),
position_x: z.number(), parent: z.number().nullable(),
position_y: z.number(),
result: z.number().nullable() result: z.number().nullable()
}), }),
position_x: z.number(),
position_y: z.number(),
arguments: z.array(z.number()), arguments: z.array(z.number()),
create_schema: z.boolean() create_schema: z.boolean()
}); });
@ -123,14 +128,14 @@ export const schemaOperationCreatedResponse = z.strictObject({
export const schemaOperationDelete = z.strictObject({ export const schemaOperationDelete = z.strictObject({
target: z.number(), target: z.number(),
positions: z.array(schemaOperationPosition), layout: schemaOssLayout,
keep_constituents: z.boolean(), keep_constituents: z.boolean(),
delete_schema: z.boolean() delete_schema: z.boolean()
}); });
export const schemaInputUpdate = z.strictObject({ export const schemaInputUpdate = z.strictObject({
target: z.number(), target: z.number(),
positions: z.array(schemaOperationPosition), layout: schemaOssLayout,
input: z.number().nullable() input: z.number().nullable()
}); });
@ -141,7 +146,7 @@ export const schemaInputCreatedResponse = z.strictObject({
export const schemaOperationUpdate = z.strictObject({ export const schemaOperationUpdate = z.strictObject({
target: z.number(), target: z.number(),
positions: z.array(schemaOperationPosition), layout: schemaOssLayout,
item_data: z.strictObject({ item_data: z.strictObject({
alias: z.string().nonempty(errorMsg.requiredField), alias: z.string().nonempty(errorMsg.requiredField),
title: z.string(), title: z.string(),

View File

@ -14,7 +14,7 @@ export const useOperationUpdate = () => {
mutationFn: ossApi.operationUpdate, mutationFn: ossApi.operationUpdate,
onSuccess: (data, variables) => { onSuccess: (data, variables) => {
client.setQueryData(KEYS.composite.ossItem({ itemID: data.id }), data); client.setQueryData(KEYS.composite.ossItem({ itemID: data.id }), data);
const schemaID = data.items.find(item => item.id === variables.data.target)?.result; const schemaID = data.operations.find(item => item.id === variables.data.target)?.result;
if (!schemaID) { if (!schemaID) {
return; return;
} }

View File

@ -5,21 +5,33 @@ import { useUpdateTimestamp } from '@/features/library/backend/use-update-timest
import { KEYS } from '@/backend/configuration'; import { KEYS } from '@/backend/configuration';
import { ossApi } from './api'; import { ossApi } from './api';
import { type IOperationPosition } from './types'; import { type IOperationSchemaDTO, type IOssLayout } from './types';
export const useUpdatePositions = () => { export const useUpdateLayout = () => {
const client = useQueryClient(); const client = useQueryClient();
const { updateTimestamp } = useUpdateTimestamp(); const { updateTimestamp } = useUpdateTimestamp();
const mutation = useMutation({ const mutation = useMutation({
mutationKey: [KEYS.global_mutation, ossApi.baseKey, 'update-positions'], mutationKey: [KEYS.global_mutation, ossApi.baseKey, 'update-layout'],
mutationFn: ossApi.updatePositions, mutationFn: ossApi.updateLayout,
onSuccess: (_, variables) => updateTimestamp(variables.itemID), onSuccess: (_, variables) => {
updateTimestamp(variables.itemID);
client.setQueryData(
ossApi.getOssQueryOptions({ itemID: variables.itemID }).queryKey,
(prev: IOperationSchemaDTO | undefined) =>
!prev
? prev
: {
...prev,
layout: variables.data
}
);
},
onError: () => client.invalidateQueries() onError: () => client.invalidateQueries()
}); });
return { return {
updatePositions: (data: { updateLayout: (data: {
itemID: number; // itemID: number; //
positions: IOperationPosition[]; data: IOssLayout;
isSilent?: boolean; isSilent?: boolean;
}) => mutation.mutateAsync(data) }) => mutation.mutateAsync(data)
}; };

View File

@ -13,7 +13,7 @@ import { Label } from '@/components/input';
import { ModalForm } from '@/components/modal'; import { ModalForm } from '@/components/modal';
import { useDialogsStore } from '@/stores/dialogs'; import { useDialogsStore } from '@/stores/dialogs';
import { type IInputUpdateDTO, type IOperationPosition, schemaInputUpdate } from '../backend/types'; import { type IInputUpdateDTO, type IOssLayout, schemaInputUpdate } from '../backend/types';
import { useInputUpdate } from '../backend/use-input-update'; import { useInputUpdate } from '../backend/use-input-update';
import { type IOperation, type IOperationSchema } from '../models/oss'; import { type IOperation, type IOperationSchema } from '../models/oss';
import { sortItemsForOSS } from '../models/oss-api'; import { sortItemsForOSS } from '../models/oss-api';
@ -21,18 +21,18 @@ import { sortItemsForOSS } from '../models/oss-api';
export interface DlgChangeInputSchemaProps { export interface DlgChangeInputSchemaProps {
oss: IOperationSchema; oss: IOperationSchema;
target: IOperation; target: IOperation;
positions: IOperationPosition[]; layout: IOssLayout;
} }
export function DlgChangeInputSchema() { export function DlgChangeInputSchema() {
const { oss, target, positions } = useDialogsStore(state => state.props as DlgChangeInputSchemaProps); const { oss, target, layout } = useDialogsStore(state => state.props as DlgChangeInputSchemaProps);
const { inputUpdate } = useInputUpdate(); const { inputUpdate } = useInputUpdate();
const { setValue, handleSubmit, control } = useForm<IInputUpdateDTO>({ const { setValue, handleSubmit, control } = useForm<IInputUpdateDTO>({
resolver: zodResolver(schemaInputUpdate), resolver: zodResolver(schemaInputUpdate),
defaultValues: { defaultValues: {
target: target.id, target: target.id,
positions: positions, layout: layout,
input: target.result input: target.result
} }
}); });

View File

@ -10,12 +10,7 @@ import { ModalForm } from '@/components/modal';
import { TabLabel, TabList, TabPanel, Tabs } from '@/components/tabs'; import { TabLabel, TabList, TabPanel, Tabs } from '@/components/tabs';
import { useDialogsStore } from '@/stores/dialogs'; import { useDialogsStore } from '@/stores/dialogs';
import { import { type IOperationCreateDTO, type IOssLayout, OperationType, schemaOperationCreate } from '../../backend/types';
type IOperationCreateDTO,
type IOperationPosition,
OperationType,
schemaOperationCreate
} from '../../backend/types';
import { useOperationCreate } from '../../backend/use-operation-create'; import { useOperationCreate } from '../../backend/use-operation-create';
import { describeOperationType, labelOperationType } from '../../labels'; import { describeOperationType, labelOperationType } from '../../labels';
import { type IOperationSchema } from '../../models/oss'; import { type IOperationSchema } from '../../models/oss';
@ -26,7 +21,7 @@ import { TabSynthesisOperation } from './tab-synthesis-operation';
export interface DlgCreateOperationProps { export interface DlgCreateOperationProps {
oss: IOperationSchema; oss: IOperationSchema;
positions: IOperationPosition[]; layout: IOssLayout;
initialInputs: number[]; initialInputs: number[];
defaultX: number; defaultX: number;
defaultY: number; defaultY: number;
@ -42,7 +37,7 @@ export type TabID = (typeof TabID)[keyof typeof TabID];
export function DlgCreateOperation() { export function DlgCreateOperation() {
const { operationCreate } = useOperationCreate(); const { operationCreate } = useOperationCreate();
const { oss, positions, initialInputs, onCreate, defaultX, defaultY } = useDialogsStore( const { oss, layout, initialInputs, onCreate, defaultX, defaultY } = useDialogsStore(
state => state.props as DlgCreateOperationProps state => state.props as DlgCreateOperationProps
); );
@ -51,30 +46,31 @@ export function DlgCreateOperation() {
defaultValues: { defaultValues: {
item_data: { item_data: {
operation_type: initialInputs.length === 0 ? OperationType.INPUT : OperationType.SYNTHESIS, operation_type: initialInputs.length === 0 ? OperationType.INPUT : OperationType.SYNTHESIS,
result: null,
position_x: defaultX,
position_y: defaultY,
alias: '', alias: '',
title: '', title: '',
description: '' description: '',
result: null,
parent: null
}, },
position_x: defaultX,
position_y: defaultY,
arguments: initialInputs, arguments: initialInputs,
create_schema: false, create_schema: false,
positions: positions layout: layout
}, },
mode: 'onChange' mode: 'onChange'
}); });
const alias = useWatch({ control: methods.control, name: 'item_data.alias' }); const alias = useWatch({ control: methods.control, name: 'item_data.alias' });
const [activeTab, setActiveTab] = useState(initialInputs.length === 0 ? TabID.INPUT : TabID.SYNTHESIS); const [activeTab, setActiveTab] = useState(initialInputs.length === 0 ? TabID.INPUT : TabID.SYNTHESIS);
const isValid = !!alias && !oss.items.some(operation => operation.alias === alias); const isValid = !!alias && !oss.operations.some(operation => operation.alias === alias);
function onSubmit(data: IOperationCreateDTO) { function onSubmit(data: IOperationCreateDTO) {
const target = calculateInsertPosition(oss, data.arguments, positions, { const target = calculateInsertPosition(oss, data.arguments, layout, {
x: defaultX, x: defaultX,
y: defaultY y: defaultY
}); });
data.item_data.position_x = target.x; data.position_x = target.x;
data.item_data.position_y = target.y; data.position_y = target.y;
void operationCreate({ itemID: oss.id, data: data }).then(response => onCreate?.(response.new_operation.id)); void operationCreate({ itemID: oss.id, data: data }).then(response => onCreate?.(response.new_operation.id));
} }

View File

@ -50,7 +50,7 @@ export function TabSynthesisOperation() {
name='arguments' name='arguments'
control={control} control={control}
render={({ field }) => ( render={({ field }) => (
<PickMultiOperation items={oss.items} value={field.value} onChange={field.onChange} rows={6} /> <PickMultiOperation items={oss.operations} value={field.value} onChange={field.onChange} rows={6} />
)} )}
/> />
</div> </div>

View File

@ -9,25 +9,25 @@ import { Checkbox, TextInput } from '@/components/input';
import { ModalForm } from '@/components/modal'; import { ModalForm } from '@/components/modal';
import { useDialogsStore } from '@/stores/dialogs'; import { useDialogsStore } from '@/stores/dialogs';
import { type IOperationDeleteDTO, type IOperationPosition, schemaOperationDelete } from '../backend/types'; import { type IOperationDeleteDTO, type IOssLayout, schemaOperationDelete } from '../backend/types';
import { useOperationDelete } from '../backend/use-operation-delete'; import { useOperationDelete } from '../backend/use-operation-delete';
import { type IOperation, type IOperationSchema } from '../models/oss'; import { type IOperation, type IOperationSchema } from '../models/oss';
export interface DlgDeleteOperationProps { export interface DlgDeleteOperationProps {
oss: IOperationSchema; oss: IOperationSchema;
target: IOperation; target: IOperation;
positions: IOperationPosition[]; layout: IOssLayout;
} }
export function DlgDeleteOperation() { export function DlgDeleteOperation() {
const { oss, target, positions } = useDialogsStore(state => state.props as DlgDeleteOperationProps); const { oss, target, layout } = useDialogsStore(state => state.props as DlgDeleteOperationProps);
const { operationDelete } = useOperationDelete(); const { operationDelete } = useOperationDelete();
const { handleSubmit, control } = useForm<IOperationDeleteDTO>({ const { handleSubmit, control } = useForm<IOperationDeleteDTO>({
resolver: zodResolver(schemaOperationDelete), resolver: zodResolver(schemaOperationDelete),
defaultValues: { defaultValues: {
target: target.id, target: target.id,
positions: positions, layout: layout,
keep_constituents: false, keep_constituents: false,
delete_schema: false delete_schema: false
} }

View File

@ -11,12 +11,7 @@ import { ModalForm } from '@/components/modal';
import { TabLabel, TabList, TabPanel, Tabs } from '@/components/tabs'; import { TabLabel, TabList, TabPanel, Tabs } from '@/components/tabs';
import { useDialogsStore } from '@/stores/dialogs'; import { useDialogsStore } from '@/stores/dialogs';
import { import { type IOperationUpdateDTO, type IOssLayout, OperationType, schemaOperationUpdate } from '../../backend/types';
type IOperationPosition,
type IOperationUpdateDTO,
OperationType,
schemaOperationUpdate
} from '../../backend/types';
import { useOperationUpdate } from '../../backend/use-operation-update'; import { useOperationUpdate } from '../../backend/use-operation-update';
import { type IOperation, type IOperationSchema } from '../../models/oss'; import { type IOperation, type IOperationSchema } from '../../models/oss';
@ -27,7 +22,7 @@ import { TabSynthesis } from './tab-synthesis';
export interface DlgEditOperationProps { export interface DlgEditOperationProps {
oss: IOperationSchema; oss: IOperationSchema;
target: IOperation; target: IOperation;
positions: IOperationPosition[]; layout: IOssLayout;
} }
export const TabID = { export const TabID = {
@ -38,7 +33,7 @@ export const TabID = {
export type TabID = (typeof TabID)[keyof typeof TabID]; export type TabID = (typeof TabID)[keyof typeof TabID];
export function DlgEditOperation() { export function DlgEditOperation() {
const { oss, target, positions } = useDialogsStore(state => state.props as DlgEditOperationProps); const { oss, target, layout } = useDialogsStore(state => state.props as DlgEditOperationProps);
const { operationUpdate } = useOperationUpdate(); const { operationUpdate } = useOperationUpdate();
const methods = useForm<IOperationUpdateDTO>({ const methods = useForm<IOperationUpdateDTO>({
@ -55,7 +50,7 @@ export function DlgEditOperation() {
original: sub.original, original: sub.original,
substitution: sub.substitution substitution: sub.substitution
})), })),
positions: positions layout: layout
}, },
mode: 'onChange' mode: 'onChange'
}); });

View File

@ -13,7 +13,7 @@ export function TabArguments() {
const { control, setValue } = useFormContext<IOperationUpdateDTO>(); const { control, setValue } = useFormContext<IOperationUpdateDTO>();
const { oss, target } = useDialogsStore(state => state.props as DlgEditOperationProps); const { oss, target } = useDialogsStore(state => state.props as DlgEditOperationProps);
const potentialCycle = [target.id, ...oss.graph.expandAllOutputs([target.id])]; const potentialCycle = [target.id, ...oss.graph.expandAllOutputs([target.id])];
const filtered = oss.items.filter(item => !potentialCycle.includes(item.id)); const filtered = oss.operations.filter(item => !potentialCycle.includes(item.id));
function handleChangeArguments(prev: number[], newValue: number[]) { function handleChangeArguments(prev: number[], newValue: number[]) {
setValue('arguments', newValue, { shouldValidate: true }); setValue('arguments', newValue, { shouldValidate: true });

View File

@ -16,9 +16,9 @@ import { Loader } from '@/components/loader';
import { ModalForm } from '@/components/modal'; import { ModalForm } from '@/components/modal';
import { useDialogsStore } from '@/stores/dialogs'; import { useDialogsStore } from '@/stores/dialogs';
import { type ICstRelocateDTO, type IOperationPosition, schemaCstRelocate } from '../backend/types'; import { type ICstRelocateDTO, type IOssLayout, schemaCstRelocate } from '../backend/types';
import { useRelocateConstituents } from '../backend/use-relocate-constituents'; import { useRelocateConstituents } from '../backend/use-relocate-constituents';
import { useUpdatePositions } from '../backend/use-update-positions'; import { useUpdateLayout } from '../backend/use-update-layout';
import { IconRelocationUp } from '../components/icon-relocation-up'; import { IconRelocationUp } from '../components/icon-relocation-up';
import { type IOperation, type IOperationSchema } from '../models/oss'; import { type IOperation, type IOperationSchema } from '../models/oss';
import { getRelocateCandidates } from '../models/oss-api'; import { getRelocateCandidates } from '../models/oss-api';
@ -26,13 +26,13 @@ import { getRelocateCandidates } from '../models/oss-api';
export interface DlgRelocateConstituentsProps { export interface DlgRelocateConstituentsProps {
oss: IOperationSchema; oss: IOperationSchema;
initialTarget?: IOperation; initialTarget?: IOperation;
positions: IOperationPosition[]; layout?: IOssLayout;
} }
export function DlgRelocateConstituents() { export function DlgRelocateConstituents() {
const { oss, initialTarget, positions } = useDialogsStore(state => state.props as DlgRelocateConstituentsProps); const { oss, initialTarget, layout } = useDialogsStore(state => state.props as DlgRelocateConstituentsProps);
const { items: libraryItems } = useLibrary(); const { items: libraryItems } = useLibrary();
const { updatePositions } = useUpdatePositions(); const { updateLayout: updatePositions } = useUpdateLayout();
const { relocateConstituents } = useRelocateConstituents(); const { relocateConstituents } = useRelocateConstituents();
const { const {
@ -55,7 +55,7 @@ export function DlgRelocateConstituents() {
libraryItems.find(item => item.id === initialTarget?.result) ?? null libraryItems.find(item => item.id === initialTarget?.result) ?? null
); );
const operation = oss.items.find(item => item.result === source?.id); const operation = oss.operations.find(item => item.result === source?.id);
const sourceSchemas = libraryItems.filter(item => oss.schemas.includes(item.id)); const sourceSchemas = libraryItems.filter(item => oss.schemas.includes(item.id));
const destinationSchemas = (() => { const destinationSchemas = (() => {
if (!operation) { if (!operation) {
@ -73,7 +73,7 @@ export function DlgRelocateConstituents() {
if (!sourceData.schema || !destinationItem || !operation) { if (!sourceData.schema || !destinationItem || !operation) {
return []; return [];
} }
const destinationOperation = oss.items.find(item => item.result === destination); const destinationOperation = oss.operations.find(item => item.result === destination);
return getRelocateCandidates(operation.id, destinationOperation!.id, sourceData.schema, oss); return getRelocateCandidates(operation.id, destinationOperation!.id, sourceData.schema, oss);
})(); })();
@ -98,17 +98,13 @@ export function DlgRelocateConstituents() {
} }
function onSubmit(data: ICstRelocateDTO) { function onSubmit(data: ICstRelocateDTO) {
const positionsUnchanged = positions.every(item => { if (!layout || JSON.stringify(layout) === JSON.stringify(oss.layout)) {
const operation = oss.operationByID.get(item.id)!;
return operation.position_x === item.position_x && operation.position_y === item.position_y;
});
if (positionsUnchanged) {
return relocateConstituents(data); return relocateConstituents(data);
} else { } else {
return updatePositions({ return updatePositions({
isSilent: true, isSilent: true,
itemID: oss.id, itemID: oss.id,
positions: positions data: layout
}).then(() => relocateConstituents(data)); }).then(() => relocateConstituents(data));
} }
} }

View File

@ -23,7 +23,7 @@ import { infoMsg } from '@/utils/labels';
import { TextMatcher } from '@/utils/utils'; import { TextMatcher } from '@/utils/utils';
import { Graph } from '../../../models/graph'; import { Graph } from '../../../models/graph';
import { type IOperationPosition } from '../backend/types'; import { type IOssLayout } from '../backend/types';
import { describeSubstitutionError } from '../labels'; import { describeSubstitutionError } from '../labels';
import { type IOperation, type IOperationSchema, SubstitutionErrorType } from './oss'; import { type IOperation, type IOperationSchema, SubstitutionErrorType } from './oss';
@ -494,40 +494,39 @@ export function getRelocateCandidates(
export function calculateInsertPosition( export function calculateInsertPosition(
oss: IOperationSchema, oss: IOperationSchema,
argumentsOps: number[], argumentsOps: number[],
positions: IOperationPosition[], layout: IOssLayout,
defaultPosition: Position2D defaultPosition: Position2D
): Position2D { ): Position2D {
const result = defaultPosition; const result = defaultPosition;
if (positions.length === 0) { const operations = layout.operations;
if (operations.length === 0) {
return result; return result;
} }
if (argumentsOps.length === 0) { if (argumentsOps.length === 0) {
let inputsPositions = positions.filter(pos => let inputsPositions = operations.filter(pos =>
oss.items.find(operation => operation.arguments.length === 0 && operation.id === pos.id) oss.operations.find(operation => operation.arguments.length === 0 && operation.id === pos.id)
); );
if (inputsPositions.length === 0) { if (inputsPositions.length === 0) {
inputsPositions = positions; inputsPositions = operations;
} }
const maxX = Math.max(...inputsPositions.map(node => node.position_x)); const maxX = Math.max(...inputsPositions.map(node => node.x));
const minY = Math.min(...inputsPositions.map(node => node.position_y)); const minY = Math.min(...inputsPositions.map(node => node.y));
result.x = maxX + DISTANCE_X; result.x = maxX + DISTANCE_X;
result.y = minY; result.y = minY;
} else { } else {
const argNodes = positions.filter(pos => argumentsOps.includes(pos.id)); const argNodes = operations.filter(pos => argumentsOps.includes(pos.id));
const maxY = Math.max(...argNodes.map(node => node.position_y)); const maxY = Math.max(...argNodes.map(node => node.y));
const minX = Math.min(...argNodes.map(node => node.position_x)); const minX = Math.min(...argNodes.map(node => node.x));
const maxX = Math.max(...argNodes.map(node => node.position_x)); const maxX = Math.max(...argNodes.map(node => node.x));
result.x = Math.ceil((maxX + minX) / 2 / GRID_SIZE) * GRID_SIZE; result.x = Math.ceil((maxX + minX) / 2 / GRID_SIZE) * GRID_SIZE;
result.y = maxY + DISTANCE_Y; result.y = maxY + DISTANCE_Y;
} }
let flagIntersect = false; let flagIntersect = false;
do { do {
flagIntersect = positions.some( flagIntersect = operations.some(
position => position => Math.abs(position.x - result.x) < MIN_DISTANCE && Math.abs(position.y - result.y) < MIN_DISTANCE
Math.abs(position.position_x - result.x) < MIN_DISTANCE &&
Math.abs(position.position_y - result.y) < MIN_DISTANCE
); );
if (flagIntersect) { if (flagIntersect) {
result.x += MIN_DISTANCE; result.x += MIN_DISTANCE;

View File

@ -8,6 +8,8 @@ import { type ICstSubstituteInfo, type IOperationDTO, type IOperationSchemaDTO }
/** Represents Operation. */ /** Represents Operation. */
export interface IOperation extends IOperationDTO { export interface IOperation extends IOperationDTO {
x: number;
y: number;
is_owned: boolean; is_owned: boolean;
is_consolidation: boolean; // aka 'diamond synthesis' is_consolidation: boolean; // aka 'diamond synthesis'
substitutions: ICstSubstituteInfo[]; substitutions: ICstSubstituteInfo[];
@ -25,7 +27,7 @@ export interface IOperationSchemaStats {
/** Represents OperationSchema. */ /** Represents OperationSchema. */
export interface IOperationSchema extends IOperationSchemaDTO { export interface IOperationSchema extends IOperationSchemaDTO {
items: IOperation[]; operations: IOperation[];
graph: Graph; graph: Graph;
schemas: number[]; schemas: number[];

View File

@ -27,7 +27,7 @@ import { useMutatingOss } from '../../../backend/use-mutating-oss';
import { type IOperation } from '../../../models/oss'; import { type IOperation } from '../../../models/oss';
import { useOssEdit } from '../oss-edit-context'; import { useOssEdit } from '../oss-edit-context';
import { useGetPositions } from './use-get-positions'; import { useGetLayout } from './use-get-layout';
// pixels - size of OSS context menu // pixels - size of OSS context menu
const MENU_WIDTH = 200; const MENU_WIDTH = 200;
@ -49,7 +49,7 @@ export function NodeContextMenu({ isOpen, operation, cursorX, cursorY, onHide }:
const { items: libraryItems } = useLibrary(); const { items: libraryItems } = useLibrary();
const { schema, navigateOperationSchema, isMutable, canDeleteOperation: canDelete } = useOssEdit(); const { schema, navigateOperationSchema, isMutable, canDeleteOperation: canDelete } = useOssEdit();
const isProcessing = useMutatingOss(); const isProcessing = useMutatingOss();
const getPositions = useGetPositions(); const getLayout = useGetLayout();
const { inputCreate } = useInputCreate(); const { inputCreate } = useInputCreate();
const { operationExecute } = useOperationExecute(); const { operationExecute } = useOperationExecute();
@ -104,7 +104,7 @@ export function NodeContextMenu({ isOpen, operation, cursorX, cursorY, onHide }:
showEditInput({ showEditInput({
oss: schema, oss: schema,
target: operation, target: operation,
positions: getPositions() layout: getLayout()
}); });
} }
@ -116,7 +116,7 @@ export function NodeContextMenu({ isOpen, operation, cursorX, cursorY, onHide }:
showEditOperation({ showEditOperation({
oss: schema, oss: schema,
target: operation, target: operation,
positions: getPositions() layout: getLayout()
}); });
} }
@ -128,7 +128,7 @@ export function NodeContextMenu({ isOpen, operation, cursorX, cursorY, onHide }:
showDeleteOperation({ showDeleteOperation({
oss: schema, oss: schema,
target: operation, target: operation,
positions: getPositions() layout: getLayout()
}); });
} }
@ -139,7 +139,7 @@ export function NodeContextMenu({ isOpen, operation, cursorX, cursorY, onHide }:
onHide(); onHide();
void operationExecute({ void operationExecute({
itemID: schema.id, // itemID: schema.id, //
data: { target: operation.id, positions: getPositions() } data: { target: operation.id, layout: getLayout() }
}); });
} }
@ -154,7 +154,7 @@ export function NodeContextMenu({ isOpen, operation, cursorX, cursorY, onHide }:
onHide(); onHide();
void inputCreate({ void inputCreate({
itemID: schema.id, itemID: schema.id,
data: { target: operation.id, positions: getPositions() } data: { target: operation.id, layout: getLayout() }
}).then(new_schema => router.push({ path: urls.schema(new_schema.id), force: true })); }).then(new_schema => router.push({ path: urls.schema(new_schema.id), force: true }));
} }
@ -166,7 +166,7 @@ export function NodeContextMenu({ isOpen, operation, cursorX, cursorY, onHide }:
showRelocateConstituents({ showRelocateConstituents({
oss: schema, oss: schema,
initialTarget: operation, initialTarget: operation,
positions: getPositions() layout: getLayout()
}); });
} }

View File

@ -16,7 +16,7 @@ import { useDialogsStore } from '@/stores/dialogs';
import { PARAMETER } from '@/utils/constants'; import { PARAMETER } from '@/utils/constants';
import { useMutatingOss } from '../../../backend/use-mutating-oss'; import { useMutatingOss } from '../../../backend/use-mutating-oss';
import { useUpdatePositions } from '../../../backend/use-update-positions'; import { useUpdateLayout } from '../../../backend/use-update-layout';
import { GRID_SIZE } from '../../../models/oss-api'; import { GRID_SIZE } from '../../../models/oss-api';
import { type OssNode } from '../../../models/oss-layout'; import { type OssNode } from '../../../models/oss-layout';
import { useOperationTooltipStore } from '../../../stores/operation-tooltip'; import { useOperationTooltipStore } from '../../../stores/operation-tooltip';
@ -26,7 +26,7 @@ import { useOssEdit } from '../oss-edit-context';
import { OssNodeTypes } from './graph/oss-node-types'; import { OssNodeTypes } from './graph/oss-node-types';
import { type ContextMenuData, NodeContextMenu } from './node-context-menu'; import { type ContextMenuData, NodeContextMenu } from './node-context-menu';
import { ToolbarOssGraph } from './toolbar-oss-graph'; import { ToolbarOssGraph } from './toolbar-oss-graph';
import { useGetPositions } from './use-get-positions'; import { useGetLayout } from './use-get-layout';
const ZOOM_MAX = 2; const ZOOM_MAX = 2;
const ZOOM_MIN = 0.5; const ZOOM_MIN = 0.5;
@ -52,8 +52,8 @@ export function OssFlow() {
const edgeAnimate = useOSSGraphStore(state => state.edgeAnimate); const edgeAnimate = useOSSGraphStore(state => state.edgeAnimate);
const edgeStraight = useOSSGraphStore(state => state.edgeStraight); const edgeStraight = useOSSGraphStore(state => state.edgeStraight);
const getPositions = useGetPositions(); const getLayout = useGetLayout();
const { updatePositions } = useUpdatePositions(); const { updateLayout: updatePositions } = useUpdateLayout();
const [nodes, setNodes, onNodesChange] = useNodesState([]); const [nodes, setNodes, onNodesChange] = useNodesState([]);
const [edges, setEdges, onEdgesChange] = useEdgesState([]); const [edges, setEdges, onEdgesChange] = useEdgesState([]);
@ -78,10 +78,10 @@ export function OssFlow() {
useEffect(() => { useEffect(() => {
setNodes( setNodes(
schema.items.map(operation => ({ schema.operations.map(operation => ({
id: String(operation.id), id: String(operation.id),
data: { label: operation.alias, operation: operation }, data: { label: operation.alias, operation: operation },
position: { x: operation.position_x, y: operation.position_y }, position: { x: operation.x, y: operation.y },
type: operation.operation_type.toString() type: operation.operation_type.toString()
})) }))
); );
@ -93,8 +93,7 @@ export function OssFlow() {
type: edgeStraight ? 'straight' : 'simplebezier', type: edgeStraight ? 'straight' : 'simplebezier',
animated: edgeAnimate, animated: edgeAnimate,
targetHandle: targetHandle:
schema.operationByID.get(argument.argument)!.position_x > schema.operationByID.get(argument.argument)!.x > schema.operationByID.get(argument.operation)!.x
schema.operationByID.get(argument.operation)!.position_x
? 'right' ? 'right'
: 'left' : 'left'
})) }))
@ -103,16 +102,7 @@ export function OssFlow() {
}, [schema, setNodes, setEdges, toggleReset, edgeStraight, edgeAnimate, fitView]); }, [schema, setNodes, setEdges, toggleReset, edgeStraight, edgeAnimate, fitView]);
function handleSavePositions() { function handleSavePositions() {
const positions = getPositions(); void updatePositions({ itemID: schema.id, data: getLayout() });
void updatePositions({ itemID: schema.id, positions: positions }).then(() => {
positions.forEach(item => {
const operation = schema.operationByID.get(item.id);
if (operation) {
operation.position_x = item.position_x;
operation.position_y = item.position_y;
}
});
});
} }
function handleCreateOperation() { function handleCreateOperation() {
@ -121,7 +111,7 @@ export function OssFlow() {
oss: schema, oss: schema,
defaultX: targetPosition.x, defaultX: targetPosition.x,
defaultY: targetPosition.y, defaultY: targetPosition.y,
positions: getPositions(), layout: getLayout(),
initialInputs: selected, initialInputs: selected,
onCreate: () => onCreate: () =>
setTimeout(() => fitView({ duration: PARAMETER.zoomDuration, padding: VIEW_PADDING }), PARAMETER.refreshTimeout) setTimeout(() => fitView({ duration: PARAMETER.zoomDuration, padding: VIEW_PADDING }), PARAMETER.refreshTimeout)
@ -139,7 +129,7 @@ export function OssFlow() {
showDeleteOperation({ showDeleteOperation({
oss: schema, oss: schema,
target: operation, target: operation,
positions: getPositions() layout: getLayout()
}); });
} }

View File

@ -6,7 +6,7 @@ import clsx from 'clsx';
import { HelpTopic } from '@/features/help'; import { HelpTopic } from '@/features/help';
import { BadgeHelp } from '@/features/help/components'; import { BadgeHelp } from '@/features/help/components';
import { useOperationExecute } from '@/features/oss/backend/use-operation-execute'; import { useOperationExecute } from '@/features/oss/backend/use-operation-execute';
import { useUpdatePositions } from '@/features/oss/backend/use-update-positions'; import { useUpdateLayout } from '@/features/oss/backend/use-update-layout';
import { MiniButton } from '@/components/control'; import { MiniButton } from '@/components/control';
import { import {
@ -34,7 +34,7 @@ import { useOSSGraphStore } from '../../../stores/oss-graph';
import { useOssEdit } from '../oss-edit-context'; import { useOssEdit } from '../oss-edit-context';
import { VIEW_PADDING } from './oss-flow'; import { VIEW_PADDING } from './oss-flow';
import { useGetPositions } from './use-get-positions'; import { useGetLayout } from './use-get-layout';
interface ToolbarOssGraphProps extends Styling { interface ToolbarOssGraphProps extends Styling {
onCreate: () => void; onCreate: () => void;
@ -53,7 +53,7 @@ export function ToolbarOssGraph({
const isProcessing = useMutatingOss(); const isProcessing = useMutatingOss();
const { fitView } = useReactFlow(); const { fitView } = useReactFlow();
const selectedOperation = schema.operationByID.get(selected[0]); const selectedOperation = schema.operationByID.get(selected[0]);
const getPositions = useGetPositions(); const getLayout = useGetLayout();
const showGrid = useOSSGraphStore(state => state.showGrid); const showGrid = useOSSGraphStore(state => state.showGrid);
const edgeAnimate = useOSSGraphStore(state => state.edgeAnimate); const edgeAnimate = useOSSGraphStore(state => state.edgeAnimate);
@ -62,7 +62,7 @@ export function ToolbarOssGraph({
const toggleEdgeAnimate = useOSSGraphStore(state => state.toggleEdgeAnimate); const toggleEdgeAnimate = useOSSGraphStore(state => state.toggleEdgeAnimate);
const toggleEdgeStraight = useOSSGraphStore(state => state.toggleEdgeStraight); const toggleEdgeStraight = useOSSGraphStore(state => state.toggleEdgeStraight);
const { updatePositions } = useUpdatePositions(); const { updateLayout: updatePositions } = useUpdateLayout();
const { operationExecute } = useOperationExecute(); const { operationExecute } = useOperationExecute();
const showEditOperation = useDialogsStore(state => state.showEditOperation); const showEditOperation = useDialogsStore(state => state.showEditOperation);
@ -93,16 +93,7 @@ export function ToolbarOssGraph({
} }
function handleSavePositions() { function handleSavePositions() {
const positions = getPositions(); void updatePositions({ itemID: schema.id, data: getLayout() });
void updatePositions({ itemID: schema.id, positions: positions }).then(() => {
positions.forEach(item => {
const operation = schema.operationByID.get(item.id);
if (operation) {
operation.position_x = item.position_x;
operation.position_y = item.position_y;
}
});
});
} }
function handleOperationExecute() { function handleOperationExecute() {
@ -111,7 +102,7 @@ export function ToolbarOssGraph({
} }
void operationExecute({ void operationExecute({
itemID: schema.id, // itemID: schema.id, //
data: { target: selectedOperation.id, positions: getPositions() } data: { target: selectedOperation.id, layout: getLayout() }
}); });
} }
@ -122,7 +113,7 @@ export function ToolbarOssGraph({
showEditOperation({ showEditOperation({
oss: schema, oss: schema,
target: selectedOperation, target: selectedOperation,
positions: getPositions() layout: getLayout()
}); });
} }

View File

@ -0,0 +1,17 @@
import { useReactFlow } from 'reactflow';
import { type IOssLayout } from '@/features/oss/backend/types';
export function useGetLayout() {
const { getNodes } = useReactFlow();
return function getLayout(): IOssLayout {
return {
operations: getNodes().map(node => ({
id: Number(node.id),
x: node.position.x,
y: node.position.y
})),
blocks: []
};
};
}

View File

@ -1,12 +0,0 @@
import { useReactFlow } from 'reactflow';
export function useGetPositions() {
const { getNodes } = useReactFlow();
return function getPositions() {
return getNodes().map(node => ({
id: Number(node.id),
position_x: node.position.x,
position_y: node.position.y
}));
};
}

View File

@ -21,8 +21,7 @@ export function MenuEditOss() {
menu.hide(); menu.hide();
showRelocateConstituents({ showRelocateConstituents({
oss: schema, oss: schema,
initialTarget: undefined, initialTarget: undefined
positions: []
}); });
} }