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,
LocationHead
)
from apps.oss.models import OperationSchema
from apps.rsform.models import RSForm
from shared.EndpointTester import EndpointTester, decl_endpoint
from shared.testing_utils import response_contains
@ -58,6 +59,8 @@ class TestLibraryViewset(EndpointTester):
'read_only': True
}
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['item_type'], data['item_type'])
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['visible'], data['visible'])
self.assertEqual(response.data['read_only'], data['read_only'])
self.assertEqual(oss.layout().data['operations'], [])
self.assertEqual(oss.layout().data['blocks'], [])
self.logout()
data = {'title': 'Title2'}

View File

@ -13,7 +13,7 @@ from rest_framework.decorators import action
from rest_framework.request import Request
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.serializers import RSFormParseSerializer
from apps.users.models import User
@ -40,6 +40,8 @@ class LibraryViewSet(viewsets.ModelViewSet):
serializer.save(owner=self.request.user)
else:
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:
instance = serializer.save()

View File

@ -15,11 +15,24 @@ class OperationAdmin(admin.ModelAdmin):
'alias',
'title',
'description',
'position_x',
'position_y']
'parent']
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):
''' Admin model: Operation arguments. '''
ordering = ['operation']
@ -42,6 +55,8 @@ class InheritanceAdmin(admin.ModelAdmin):
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.Substitution, SynthesisSubstitutionAdmin)
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. '''
# pylint: disable=duplicate-code
from django.db.models import (
CASCADE,
SET_NULL,
CharField,
FloatField,
ForeignKey,
Model,
QuerySet,
@ -44,6 +44,15 @@ class Operation(Model):
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(
verbose_name='Шифр',
max_length=255,
@ -58,15 +67,6 @@ class Operation(Model):
blank=True
)
position_x = FloatField(
verbose_name='Положение по горизонтали',
default=0
)
position_y = FloatField(
verbose_name='Положение по вертикали',
default=0
)
class Meta:
''' Model metadata. '''
verbose_name = 'Операция'

View File

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

View File

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

View File

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

View File

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

View File

@ -25,9 +25,8 @@ class TestOperation(TestCase):
def test_create_default(self):
self.assertEqual(self.operation.oss, self.oss.model)
self.assertEqual(self.operation.operation_type, OperationType.INPUT)
self.assertEqual(self.operation.parent, None)
self.assertEqual(self.operation.result, None)
self.assertEqual(self.operation.alias, 'KS1')
self.assertEqual(self.operation.title, '')
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.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')
def test_set_owner(self):
data = {'user': self.user3.pk}
@ -142,7 +154,7 @@ class TestChangeAttributes(EndpointTester):
'title': 'Test title mod',
'description': 'Comment mod'
},
'positions': [],
'layout': self.layout_data
}
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.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')
def test_retrieve_inheritance(self):
response = self.executeOK(item=self.ks3.model.pk)

View File

@ -106,6 +106,20 @@ class TestChangeOperations(EndpointTester):
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):
self.assertEqual(self.ks1.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')
def test_delete_input_operation(self):
data = {
'positions': [],
'layout': self.layout_data,
'target': self.operation2.pk
}
self.executeOK(data=data, item=self.owned_id)
@ -137,7 +151,7 @@ class TestChangeOperations(EndpointTester):
@decl_endpoint('/api/oss/{item}/set-input', method='patch')
def test_set_input_null(self):
data = {
'positions': [],
'layout': self.layout_data,
'target': self.operation2.pk,
'input': None
}
@ -169,7 +183,7 @@ class TestChangeOperations(EndpointTester):
ks6D1 = ks6.insert_new('D1', definition_formal='X1 X2', convention='KS6D1')
data = {
'positions': [],
'layout': self.layout_data,
'target': self.operation2.pk,
'input': ks6.model.pk
}
@ -211,7 +225,7 @@ class TestChangeOperations(EndpointTester):
@decl_endpoint('/api/oss/{item}/delete-operation', method='patch')
def test_delete_operation_and_constituents(self):
data = {
'positions': [],
'layout': self.layout_data,
'target': self.operation1.pk,
'keep_constituents': False,
'delete_schema': True
@ -232,7 +246,7 @@ class TestChangeOperations(EndpointTester):
@decl_endpoint('/api/oss/{item}/delete-operation', method='patch')
def test_delete_operation_keep_constituents(self):
data = {
'positions': [],
'layout': self.layout_data,
'target': self.operation1.pk,
'keep_constituents': True,
'delete_schema': True
@ -253,7 +267,7 @@ class TestChangeOperations(EndpointTester):
@decl_endpoint('/api/oss/{item}/delete-operation', method='patch')
def test_delete_operation_keep_schema(self):
data = {
'positions': [],
'layout': self.layout_data,
'target': self.operation1.pk,
'keep_constituents': True,
'delete_schema': False
@ -283,7 +297,7 @@ class TestChangeOperations(EndpointTester):
'title': 'Test title mod',
'description': 'Comment mod'
},
'positions': [],
'layout': self.layout_data,
'substitutions': [
{
'original': self.ks1X1.pk,
@ -317,7 +331,7 @@ class TestChangeOperations(EndpointTester):
'title': 'Test title mod',
'description': 'Comment mod'
},
'positions': [],
'layout': self.layout_data,
'arguments': [self.operation1.pk],
}
@ -356,7 +370,7 @@ class TestChangeOperations(EndpointTester):
data = {
'target': self.operation4.pk,
'positions': []
'layout': self.layout_data
}
self.executeOK(data=data, item=self.owned_id)
self.operation4.refresh_from_db()

View File

@ -106,6 +106,20 @@ class TestChangeSubstitutions(EndpointTester):
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):
self.assertEqual(self.ks1.constituents().count(), 3)
@ -139,10 +153,12 @@ class TestChangeSubstitutions(EndpointTester):
@decl_endpoint('/api/rsforms/{schema}/substitute', method='patch')
def test_substitute_substitution(self):
data = {'substitutions': [{
'original': self.ks2S1.pk,
'substitution': self.ks2X1.pk
}]}
data = {
'substitutions': [{
'original': self.ks2S1.pk,
'substitution': self.ks2X1.pk
}]
}
self.executeOK(data=data, schema=self.ks2.model.pk)
self.ks4D1.refresh_from_db()
self.ks4D2.refresh_from_db()

View File

@ -1,2 +1,3 @@
''' Tests for REST API. '''
from .t_operations 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. '''
from apps.library.models import AccessPolicy, Editor, LibraryItem, LibraryItemType
from apps.oss.models import Operation, OperationSchema, OperationType
from apps.library.models import AccessPolicy, LibraryItemType
from apps.oss.models import OperationSchema, OperationType
from apps.rsform.models import Constituenta, RSForm
from shared.EndpointTester import EndpointTester, decl_endpoint
@ -54,6 +54,14 @@ class TestOssViewset(EndpointTester):
alias='3',
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_substitutions(self.operation3.pk, [{
'original': self.ks1X1,
@ -74,9 +82,9 @@ class TestOssViewset(EndpointTester):
self.assertEqual(response.data['item_type'], LibraryItemType.OPERATION_SCHEMA)
self.assertEqual(len(response.data['items']), 3)
self.assertEqual(response.data['items'][0]['id'], self.operation1.pk)
self.assertEqual(response.data['items'][0]['operation_type'], self.operation1.operation_type)
self.assertEqual(len(response.data['operations']), 3)
self.assertEqual(response.data['operations'][0]['id'], self.operation1.pk)
self.assertEqual(response.data['operations'][0]['operation_type'], self.operation1.operation_type)
self.assertEqual(len(response.data['substitutions']), 1)
sub = response.data['substitutions'][0]
@ -95,6 +103,12 @@ class TestOssViewset(EndpointTester):
self.assertEqual(arguments[1]['operation'], self.operation3.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.executeForbidden(item=self.private_id)
@ -103,401 +117,32 @@ class TestOssViewset(EndpointTester):
self.executeOK(item=self.unowned_id)
self.executeForbidden(item=self.private_id)
@decl_endpoint('/api/oss/{item}/update-positions', method='patch')
def test_update_positions(self):
@decl_endpoint('/api/oss/{item}/update-layout', method='patch')
def test_update_layout(self):
self.populateData()
self.executeBadData(item=self.owned_id)
data = {'positions': []}
data = {'operations': [], 'blocks': []}
self.executeOK(data=data)
data = {'positions': [
{'id': self.operation1.pk, 'position_x': 42.1, 'position_y': 1337},
{'id': self.operation2.pk, 'position_x': 36.1, 'position_y': 1437},
{'id': self.invalid_id, 'position_x': 31, 'position_y': 12},
]}
data = {
'operations': [
{'id': self.operation1.pk, 'x': 42.1, 'y': 1337},
{'id': self.operation2.pk, 'x': 36.1, 'y': 1437},
{'id': self.operation3.pk, 'x': 36.1, 'y': 1435}
], 'blocks': []
}
self.toggle_admin(True)
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.executeOK(data=data, item=self.owned_id)
self.operation1.refresh_from_db()
self.operation2.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.assertEqual(self.operation2.position_x, data['positions'][1]['position_x'])
self.assertEqual(self.operation2.position_y, data['positions'][1]['position_y'])
self.owned.refresh_from_db()
self.assertEqual(self.owned.layout().data, data)
self.executeForbidden(data=data, item=self.unowned_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')
def test_get_predecessor(self):
self.populateData()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -41,7 +41,7 @@ export class OssLoader {
}
private prepareLookups() {
this.oss.items.forEach(operation => {
this.oss.operations.forEach(operation => {
this.operationByID.set(operation.id, operation);
this.graph.addNode(operation.id);
});
@ -52,13 +52,16 @@ export class OssLoader {
}
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() {
this.graph.topologicalOrder().forEach(operationID => {
const operation = this.operationByID.get(operationID)!;
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_owned = !schema || (schema.owner === this.oss.owner && schema.location === this.oss.location);
operation.substitutions = this.oss.substitutions.filter(item => item.operation === operationID);
@ -82,7 +85,7 @@ export class OssLoader {
}
private calculateStats(): IOperationSchemaStats {
const items = this.oss.items;
const items = this.oss.operations;
return {
count_operations: items.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}. */
export type IOperationSchemaDTO = z.infer<typeof schemaOperationSchema>;
/** Represents {@link IOperation} position. */
export type IOperationPosition = z.infer<typeof schemaOperationPosition>;
/** Represents {@link schemaOperation} layout. */
export type IOssLayout = z.infer<typeof schemaOssLayout>;
/** Represents {@link IOperation} data, used in creation process. */
export type IOperationCreateDTO = z.infer<typeof schemaOperationCreate>;
@ -35,7 +35,7 @@ export type IOperationCreatedResponse = z.infer<typeof schemaOperationCreatedRes
* Represents target {@link IOperation}.
*/
export interface ITargetOperation {
positions: IOperationPosition[];
layout: IOssLayout;
target: number;
}
@ -69,9 +69,7 @@ export const schemaOperation = z.strictObject({
title: z.string(),
description: z.string(),
position_x: z.number(),
position_y: z.number(),
parent: z.number().nullable(),
result: z.number().nullable()
});
@ -83,9 +81,21 @@ export const schemaCstSubstituteInfo = schemaCstSubstitute.extend({
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({
editors: z.number().array(),
items: z.array(schemaOperation),
operations: z.array(schemaOperation),
layout: schemaOssLayout,
arguments: z
.object({
operation: z.number(),
@ -95,23 +105,18 @@ export const schemaOperationSchema = schemaLibraryItem.extend({
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({
positions: z.array(schemaOperationPosition),
layout: schemaOssLayout,
item_data: z.strictObject({
alias: z.string().nonempty(),
operation_type: schemaOperationType,
title: z.string(),
description: z.string(),
position_x: z.number(),
position_y: z.number(),
parent: z.number().nullable(),
result: z.number().nullable()
}),
position_x: z.number(),
position_y: z.number(),
arguments: z.array(z.number()),
create_schema: z.boolean()
});
@ -123,14 +128,14 @@ export const schemaOperationCreatedResponse = z.strictObject({
export const schemaOperationDelete = z.strictObject({
target: z.number(),
positions: z.array(schemaOperationPosition),
layout: schemaOssLayout,
keep_constituents: z.boolean(),
delete_schema: z.boolean()
});
export const schemaInputUpdate = z.strictObject({
target: z.number(),
positions: z.array(schemaOperationPosition),
layout: schemaOssLayout,
input: z.number().nullable()
});
@ -141,7 +146,7 @@ export const schemaInputCreatedResponse = z.strictObject({
export const schemaOperationUpdate = z.strictObject({
target: z.number(),
positions: z.array(schemaOperationPosition),
layout: schemaOssLayout,
item_data: z.strictObject({
alias: z.string().nonempty(errorMsg.requiredField),
title: z.string(),

View File

@ -14,7 +14,7 @@ export const useOperationUpdate = () => {
mutationFn: ossApi.operationUpdate,
onSuccess: (data, variables) => {
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) {
return;
}

View File

@ -5,21 +5,33 @@ import { useUpdateTimestamp } from '@/features/library/backend/use-update-timest
import { KEYS } from '@/backend/configuration';
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 { updateTimestamp } = useUpdateTimestamp();
const mutation = useMutation({
mutationKey: [KEYS.global_mutation, ossApi.baseKey, 'update-positions'],
mutationFn: ossApi.updatePositions,
onSuccess: (_, variables) => updateTimestamp(variables.itemID),
mutationKey: [KEYS.global_mutation, ossApi.baseKey, 'update-layout'],
mutationFn: ossApi.updateLayout,
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()
});
return {
updatePositions: (data: {
updateLayout: (data: {
itemID: number; //
positions: IOperationPosition[];
data: IOssLayout;
isSilent?: boolean;
}) => mutation.mutateAsync(data)
};

View File

@ -13,7 +13,7 @@ import { Label } from '@/components/input';
import { ModalForm } from '@/components/modal';
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 { type IOperation, type IOperationSchema } from '../models/oss';
import { sortItemsForOSS } from '../models/oss-api';
@ -21,18 +21,18 @@ import { sortItemsForOSS } from '../models/oss-api';
export interface DlgChangeInputSchemaProps {
oss: IOperationSchema;
target: IOperation;
positions: IOperationPosition[];
layout: IOssLayout;
}
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 { setValue, handleSubmit, control } = useForm<IInputUpdateDTO>({
resolver: zodResolver(schemaInputUpdate),
defaultValues: {
target: target.id,
positions: positions,
layout: layout,
input: target.result
}
});

View File

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

View File

@ -9,25 +9,25 @@ import { Checkbox, TextInput } from '@/components/input';
import { ModalForm } from '@/components/modal';
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 { type IOperation, type IOperationSchema } from '../models/oss';
export interface DlgDeleteOperationProps {
oss: IOperationSchema;
target: IOperation;
positions: IOperationPosition[];
layout: IOssLayout;
}
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 { handleSubmit, control } = useForm<IOperationDeleteDTO>({
resolver: zodResolver(schemaOperationDelete),
defaultValues: {
target: target.id,
positions: positions,
layout: layout,
keep_constituents: false,
delete_schema: false
}

View File

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

View File

@ -13,7 +13,7 @@ export function TabArguments() {
const { control, setValue } = useFormContext<IOperationUpdateDTO>();
const { oss, target } = useDialogsStore(state => state.props as DlgEditOperationProps);
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[]) {
setValue('arguments', newValue, { shouldValidate: true });

View File

@ -16,9 +16,9 @@ import { Loader } from '@/components/loader';
import { ModalForm } from '@/components/modal';
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 { useUpdatePositions } from '../backend/use-update-positions';
import { useUpdateLayout } from '../backend/use-update-layout';
import { IconRelocationUp } from '../components/icon-relocation-up';
import { type IOperation, type IOperationSchema } from '../models/oss';
import { getRelocateCandidates } from '../models/oss-api';
@ -26,13 +26,13 @@ import { getRelocateCandidates } from '../models/oss-api';
export interface DlgRelocateConstituentsProps {
oss: IOperationSchema;
initialTarget?: IOperation;
positions: IOperationPosition[];
layout?: IOssLayout;
}
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 { updatePositions } = useUpdatePositions();
const { updateLayout: updatePositions } = useUpdateLayout();
const { relocateConstituents } = useRelocateConstituents();
const {
@ -55,7 +55,7 @@ export function DlgRelocateConstituents() {
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 destinationSchemas = (() => {
if (!operation) {
@ -73,7 +73,7 @@ export function DlgRelocateConstituents() {
if (!sourceData.schema || !destinationItem || !operation) {
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);
})();
@ -98,17 +98,13 @@ export function DlgRelocateConstituents() {
}
function onSubmit(data: ICstRelocateDTO) {
const positionsUnchanged = positions.every(item => {
const operation = oss.operationByID.get(item.id)!;
return operation.position_x === item.position_x && operation.position_y === item.position_y;
});
if (positionsUnchanged) {
if (!layout || JSON.stringify(layout) === JSON.stringify(oss.layout)) {
return relocateConstituents(data);
} else {
return updatePositions({
isSilent: true,
itemID: oss.id,
positions: positions
data: layout
}).then(() => relocateConstituents(data));
}
}

View File

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

View File

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

View File

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

View File

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

View File

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