R: Restructuring layout data pt1

This commit is contained in:
Ivan 2025-04-06 13:28:00 +03:00
parent f1faffd063
commit 3271d9244c
26 changed files with 808 additions and 479 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'])
@ -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,6 +192,7 @@ 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['layout'] = oss.layout().data
result['items'] = [] result['items'] = []
for operation in oss.operations().order_by('pk'): for operation in oss.operations().order_by('pk'):
result['items'].append(OperationSerializer(operation).data) result['items'].append(OperationSerializer(operation).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 = {
'original': self.ks2S1.pk, 'substitutions': [{
'substitution': self.ks2X1.pk 'original': self.ks2S1.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']['items']), 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']
items = [item for item in layout['operations'] if item['id'] == data['target']]
self.assertEqual(len(response.data['items']), 2)
self.assertEqual(len(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,
@ -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,30 @@ 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 = {'operations': [
{'id': self.operation1.pk, 'position_x': 42.1, 'position_y': 1337}, {'id': self.operation1.pk, 'x': 42.1, 'y': 1337},
{'id': self.operation2.pk, 'position_x': 36.1, 'position_y': 1437}, {'id': self.operation2.pk, 'x': 36.1, 'y': 1437},
{'id': self.invalid_id, 'position_x': 31, 'position_y': 12}, {'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

@ -39,7 +39,7 @@ export const ossApi = {
}); });
}, },
updatePositions: ({ updateLayout: ({
itemID, itemID,
positions, positions,
isSilent isSilent
@ -49,7 +49,7 @@ export const ossApi = {
isSilent?: boolean; isSilent?: boolean;
}) => }) =>
axiosPatch({ axiosPatch({
endpoint: `/api/oss/${itemID}/update-positions`, endpoint: `/api/oss/${itemID}/update-layout`,
request: { request: {
data: { positions: positions }, data: { positions: positions },
successMessage: isSilent ? undefined : infoMsg.changesSaved successMessage: isSilent ? undefined : infoMsg.changesSaved

View File

@ -7,17 +7,17 @@ import { KEYS } from '@/backend/configuration';
import { ossApi } from './api'; import { ossApi } from './api';
import { type IOperationPosition } from './types'; import { type IOperationPosition } 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),
onError: () => client.invalidateQueries() onError: () => client.invalidateQueries()
}); });
return { return {
updatePositions: (data: { updateLayout: (data: {
itemID: number; // itemID: number; //
positions: IOperationPosition[]; positions: IOperationPosition[];
isSilent?: boolean; isSilent?: boolean;

View File

@ -18,7 +18,7 @@ import { useDialogsStore } from '@/stores/dialogs';
import { type ICstRelocateDTO, type IOperationPosition, schemaCstRelocate } from '../backend/types'; import { type ICstRelocateDTO, type IOperationPosition, 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';
@ -32,7 +32,7 @@ export interface DlgRelocateConstituentsProps {
export function DlgRelocateConstituents() { export function DlgRelocateConstituents() {
const { oss, initialTarget, positions } = useDialogsStore(state => state.props as DlgRelocateConstituentsProps); const { oss, initialTarget, positions } = 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 {

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';
@ -53,7 +53,7 @@ export function OssFlow() {
const edgeStraight = useOSSGraphStore(state => state.edgeStraight); const edgeStraight = useOSSGraphStore(state => state.edgeStraight);
const getPositions = useGetPositions(); const getPositions = useGetPositions();
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([]);

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 {
@ -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);