F: Implement create-block API
Some checks failed
Backend CI / build (3.12) (push) Waiting to run
Backend CI / notify-failure (push) Blocked by required conditions
Frontend CI / build (22.x) (push) Has been cancelled
Frontend CI / notify-failure (push) Has been cancelled

This commit is contained in:
Ivan 2025-04-14 23:02:35 +03:00
parent 1f15a6a53c
commit 0bdfe67fb1
25 changed files with 489 additions and 26 deletions

View File

@ -19,6 +19,7 @@ from apps.rsform.models import (
)
from .Argument import Argument
from .Block import Block
from .Inheritance import Inheritance
from .Layout import Layout
from .Operation import Operation
@ -60,6 +61,10 @@ class OperationSchema:
''' Get QuerySet containing all operations of current OSS. '''
return Operation.objects.filter(oss=self.model)
def blocks(self) -> QuerySet[Block]:
''' Get QuerySet containing all blocks of current OSS. '''
return Block.objects.filter(oss=self.model)
def arguments(self) -> QuerySet[Argument]:
''' Operation arguments. '''
return Argument.objects.filter(operation__oss=self.model)
@ -99,6 +104,12 @@ class OperationSchema:
self.save(update_fields=['time_update'])
return result
def create_block(self, **kwargs) -> Block:
''' Insert new block. '''
result = Block.objects.create(oss=self.model, **kwargs)
self.save(update_fields=['time_update'])
return result
def delete_operation(self, target: int, keep_constituents: bool = False):
''' Delete operation. '''
self.cache.ensure_loaded()

View File

@ -3,6 +3,8 @@
from .basics import LayoutSerializer, SubstitutionExSerializer
from .data_access import (
ArgumentSerializer,
BlockCreateSerializer,
BlockSerializer,
OperationCreateSerializer,
OperationDeleteSerializer,
OperationSchemaSerializer,
@ -12,4 +14,9 @@ from .data_access import (
RelocateConstituentsSerializer,
SetOperationInputSerializer
)
from .responses import ConstituentaReferenceResponse, NewOperationResponse, NewSchemaResponse
from .responses import (
ConstituentaReferenceResponse,
NewBlockResponse,
NewOperationResponse,
NewSchemaResponse
)

View File

@ -11,7 +11,7 @@ from apps.rsform.models import Constituenta
from apps.rsform.serializers import SubstitutionSerializerBase
from shared import messages as msg
from ..models import Argument, Inheritance, Operation, OperationSchema, OperationType
from ..models import Argument, Block, Inheritance, Operation, OperationSchema, OperationType
from .basics import LayoutSerializer, SubstitutionExSerializer
@ -24,6 +24,15 @@ class OperationSerializer(serializers.ModelSerializer):
read_only_fields = ('id', 'oss')
class BlockSerializer(serializers.ModelSerializer):
''' Serializer: Block data. '''
class Meta:
''' serializer metadata. '''
model = Block
fields = '__all__'
read_only_fields = ('id', 'oss')
class ArgumentSerializer(serializers.ModelSerializer):
''' Serializer: Operation data. '''
class Meta:
@ -32,6 +41,49 @@ class ArgumentSerializer(serializers.ModelSerializer):
fields = ('operation', 'argument')
class BlockCreateSerializer(serializers.Serializer):
''' Serializer: Block creation. '''
class BlockCreateData(serializers.ModelSerializer):
''' Serializer: Block creation data. '''
class Meta:
''' serializer metadata. '''
model = Block
fields = 'title', 'description', 'parent'
layout = LayoutSerializer()
item_data = BlockCreateData()
width = serializers.FloatField()
height = serializers.FloatField()
position_x = serializers.FloatField()
position_y = serializers.FloatField()
children_operations = PKField(many=True, queryset=Operation.objects.all().only('oss_id'))
children_blocks = PKField(many=True, queryset=Block.objects.all().only('oss_id'))
def validate(self, attrs):
oss = cast(LibraryItem, self.context['oss'])
if 'parent' in attrs['item_data'] and \
attrs['item_data']['parent'] is not None and \
attrs['item_data']['parent'].oss_id != oss.pk:
raise serializers.ValidationError({
'parent': msg.parentNotInOSS()
})
for operation in attrs['children_operations']:
if operation.oss_id != oss.pk:
raise serializers.ValidationError({
'children_operations': msg.childNotInOSS()
})
for block in attrs['children_blocks']:
if block.oss_id != oss.pk:
raise serializers.ValidationError({
'children_blocks': msg.childNotInOSS()
})
return attrs
class OperationCreateSerializer(serializers.Serializer):
''' Serializer: Operation creation. '''
class OperationCreateData(serializers.ModelSerializer):
@ -47,13 +99,31 @@ class OperationCreateSerializer(serializers.Serializer):
'description', 'result', 'parent'
layout = LayoutSerializer()
position_x = serializers.FloatField()
position_y = serializers.FloatField()
item_data = OperationCreateData()
position_x = serializers.FloatField()
position_y = serializers.FloatField()
create_schema = serializers.BooleanField(default=False, required=False)
arguments = PKField(many=True, queryset=Operation.objects.all().only('pk'), required=False)
def validate(self, attrs):
oss = cast(LibraryItem, self.context['oss'])
if 'parent' in attrs['item_data'] and \
attrs['item_data']['parent'] is not None and \
attrs['item_data']['parent'].oss_id != oss.pk:
raise serializers.ValidationError({
'parent': msg.parentNotInOSS()
})
if 'arguments' not in attrs:
return attrs
for operation in attrs['arguments']:
if operation.oss_id != oss.pk:
raise serializers.ValidationError({
'arguments': msg.operationNotInOSS(oss.title)
})
return attrs
class OperationUpdateSerializer(serializers.Serializer):
''' Serializer: Operation update. '''
@ -74,10 +144,19 @@ class OperationUpdateSerializer(serializers.Serializer):
)
def validate(self, attrs):
oss = cast(LibraryItem, self.context['oss'])
if 'parent' in attrs['item_data'] and attrs['item_data']['parent'].oss_id != oss.pk:
raise serializers.ValidationError({
'parent': msg.parentNotInOSS()
})
if 'arguments' not in attrs:
if 'substitutions' in attrs:
raise serializers.ValidationError({
'arguments': msg.missingArguments()
})
return attrs
oss = cast(LibraryItem, self.context['oss'])
for operation in attrs['arguments']:
if operation.oss_id != oss.pk:
raise serializers.ValidationError({
@ -175,6 +254,9 @@ class OperationSchemaSerializer(serializers.ModelSerializer):
operations = serializers.ListField(
child=OperationSerializer()
)
blocks = serializers.ListField(
child=BlockSerializer()
)
arguments = serializers.ListField(
child=ArgumentSerializer()
)
@ -194,12 +276,15 @@ class OperationSchemaSerializer(serializers.ModelSerializer):
oss = OperationSchema(instance)
result['layout'] = oss.layout().data
result['operations'] = []
result['blocks'] = []
result['arguments'] = []
result['substitutions'] = []
for operation in oss.operations().order_by('pk'):
result['operations'].append(OperationSerializer(operation).data)
result['arguments'] = []
for block in oss.blocks().order_by('pk'):
result['blocks'].append(BlockSerializer(block).data)
for argument in oss.arguments().order_by('order'):
result['arguments'].append(ArgumentSerializer(argument).data)
result['substitutions'] = []
for substitution in oss.substitutions().values(
'operation',
'original',

View File

@ -3,7 +3,7 @@ from rest_framework import serializers
from apps.library.serializers import LibraryItemSerializer
from .data_access import OperationSchemaSerializer, OperationSerializer
from .data_access import BlockSerializer, OperationSchemaSerializer, OperationSerializer
class NewOperationResponse(serializers.Serializer):
@ -12,6 +12,12 @@ class NewOperationResponse(serializers.Serializer):
oss = OperationSchemaSerializer()
class NewBlockResponse(serializers.Serializer):
''' Serializer: Create block response. '''
new_block = BlockSerializer()
oss = OperationSchemaSerializer()
class NewSchemaResponse(serializers.Serializer):
''' Serializer: Create RSForm for input operation response. '''
new_schema = LibraryItemSerializer()

View File

@ -8,6 +8,7 @@ from apps.oss.models import Argument, Operation, OperationSchema, OperationType
class TestArgument(TestCase):
''' Testing Argument model. '''
def setUp(self):
self.oss = OperationSchema.create(alias='T1')

View File

@ -8,6 +8,7 @@ from apps.rsform.models import Constituenta, RSForm
class TestInheritance(TestCase):
''' Testing Inheritance model. '''
def setUp(self):
self.oss = OperationSchema.create(alias='T1')

View File

@ -9,6 +9,7 @@ from apps.rsform.models import RSForm
class TestOperation(TestCase):
''' Testing Operation model. '''
def setUp(self):
self.oss = OperationSchema.create(alias='T1')
self.operation = Operation.objects.create(

View File

@ -10,6 +10,7 @@ from apps.rsform.models import RSForm
class TestSynthesisSubstitution(TestCase):
''' Testing Synthesis Substitution model. '''
def setUp(self):
self.oss = OperationSchema.create(alias='T1')

View File

@ -9,6 +9,7 @@ from shared.EndpointTester import EndpointTester, decl_endpoint
class TestChangeAttributes(EndpointTester):
''' Testing LibraryItem view when OSS is associated with RSForms. '''
def setUp(self):
super().setUp()
self.user3 = User.objects.create(
@ -70,6 +71,7 @@ class TestChangeAttributes(EndpointTester):
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}
@ -85,6 +87,7 @@ class TestChangeAttributes(EndpointTester):
self.assertEqual(self.ks2.model.owner, self.user2)
self.assertEqual(self.ks3.model.owner, self.user3)
@decl_endpoint('/api/library/{item}/set-location', method='patch')
def test_set_location(self):
data = {'location': '/U/temp'}
@ -100,6 +103,7 @@ class TestChangeAttributes(EndpointTester):
self.assertNotEqual(self.ks2.model.location, data['location'])
self.assertEqual(self.ks3.model.location, data['location'])
@decl_endpoint('/api/library/{item}/set-access-policy', method='patch')
def test_set_access_policy(self):
data = {'access_policy': AccessPolicy.PROTECTED}
@ -115,6 +119,7 @@ class TestChangeAttributes(EndpointTester):
self.assertNotEqual(self.ks2.model.access_policy, data['access_policy'])
self.assertEqual(self.ks3.model.access_policy, data['access_policy'])
@decl_endpoint('/api/library/{item}/set-editors', method='patch')
def test_set_editors(self):
Editor.set(self.owned.model.pk, [self.user2.pk])
@ -133,6 +138,7 @@ class TestChangeAttributes(EndpointTester):
self.assertEqual(list(self.ks2.model.getQ_editors()), [])
self.assertEqual(set(self.ks3.model.getQ_editors()), set([self.user, self.user3]))
@decl_endpoint('/api/library/{item}', method='patch')
def test_sync_from_result(self):
data = {'alias': 'KS111', 'title': 'New Title', 'description': 'New description'}
@ -145,6 +151,7 @@ class TestChangeAttributes(EndpointTester):
self.assertEqual(self.operation1.title, data['title'])
self.assertEqual(self.operation1.description, data['description'])
@decl_endpoint('/api/oss/{item}/update-operation', method='patch')
def test_sync_from_operation(self):
data = {
@ -163,6 +170,7 @@ class TestChangeAttributes(EndpointTester):
self.assertEqual(self.ks3.model.title, data['item_data']['title'])
self.assertEqual(self.ks3.model.description, data['item_data']['description'])
@decl_endpoint('/api/library/{item}', method='delete')
def test_destroy_oss_consequence(self):
response = self.executeNoContent(item=self.owned_id)

View File

@ -69,6 +69,7 @@ class TestChangeConstituents(EndpointTester):
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)
@ -96,6 +97,7 @@ class TestChangeConstituents(EndpointTester):
},
])
@decl_endpoint('/api/rsforms/{schema}/create-cst', method='post')
def test_create_constituenta(self):
data = {
@ -112,6 +114,7 @@ class TestChangeConstituents(EndpointTester):
self.assertEqual(inherited_cst.order, 2)
self.assertEqual(inherited_cst.definition_formal, 'X1 = X2')
@decl_endpoint('/api/rsforms/{schema}/rename-cst', method='patch')
def test_rename_constituenta(self):
data = {'target': self.ks1X1.pk, 'alias': 'D21', 'cst_type': CstType.TERM}
@ -123,6 +126,7 @@ class TestChangeConstituents(EndpointTester):
self.assertEqual(inherited_cst.alias, 'D2')
self.assertEqual(inherited_cst.cst_type, data['cst_type'])
@decl_endpoint('/api/rsforms/{schema}/update-cst', method='patch')
def test_update_constituenta(self):
d2 = self.ks3.insert_new('D2', cst_type=CstType.TERM, definition_raw='@{X1|sing,nomn}')
@ -149,6 +153,7 @@ class TestChangeConstituents(EndpointTester):
self.assertEqual(inherited_cst.definition_formal, r'X1\X1')
self.assertEqual(inherited_cst.definition_raw, r'@{X2|sing,datv}')
@decl_endpoint('/api/rsforms/{schema}/delete-multiple-cst', method='patch')
def test_delete_constituenta(self):
data = {'items': [self.ks2X1.pk]}
@ -160,6 +165,7 @@ class TestChangeConstituents(EndpointTester):
self.assertEqual(self.ks2D1.definition_formal, r'DEL\DEL')
self.assertEqual(inherited_cst.definition_formal, r'DEL\DEL')
@decl_endpoint('/api/rsforms/{schema}/substitute', method='patch')
def test_substitute(self):
d2 = self.ks3.insert_new('D2', cst_type=CstType.TERM, definition_formal=r'X1\X2\X3')

View File

@ -8,6 +8,7 @@ from shared.EndpointTester import EndpointTester, decl_endpoint
class TestChangeOperations(EndpointTester):
''' Testing Operations change propagation in OSS. '''
def setUp(self):
super().setUp()
self.owned = OperationSchema.create(
@ -120,6 +121,7 @@ class TestChangeOperations(EndpointTester):
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)
@ -128,6 +130,7 @@ class TestChangeOperations(EndpointTester):
self.assertEqual(self.ks5.constituents().count(), 8)
self.assertEqual(self.ks4D1.definition_formal, 'S1 X1')
@decl_endpoint('/api/oss/{item}/delete-operation', method='patch')
def test_delete_input_operation(self):
data = {
@ -148,6 +151,7 @@ class TestChangeOperations(EndpointTester):
self.assertEqual(self.ks4D2.definition_formal, r'X1 DEL DEL DEL D1')
self.assertEqual(self.ks5D4.definition_formal, r'DEL DEL X3 DEL D1 D2 D3')
@decl_endpoint('/api/oss/{item}/set-input', method='patch')
def test_set_input_null(self):
data = {
@ -171,6 +175,7 @@ class TestChangeOperations(EndpointTester):
self.assertEqual(self.ks4D2.definition_formal, r'X1 DEL DEL DEL D1')
self.assertEqual(self.ks5D4.definition_formal, r'DEL DEL X3 DEL D1 D2 D3')
@decl_endpoint('/api/oss/{item}/set-input', method='patch')
def test_set_input_change_schema(self):
ks6 = RSForm.create(
@ -206,6 +211,7 @@ class TestChangeOperations(EndpointTester):
self.assertEqual(self.ks4D2.definition_formal, r'X1 DEL DEL DEL D1')
self.assertEqual(self.ks5D4.definition_formal, r'DEL DEL X3 DEL D1 D2 D3')
@decl_endpoint('/api/library/{item}', method='delete')
def test_delete_schema(self):
self.executeNoContent(item=self.ks1.model.pk)
@ -222,6 +228,7 @@ class TestChangeOperations(EndpointTester):
self.assertEqual(self.ks4D2.definition_formal, r'DEL X2 X3 S1 DEL')
self.assertEqual(self.ks5D4.definition_formal, r'X1 X2 X3 S1 DEL D2 D3')
@decl_endpoint('/api/oss/{item}/delete-operation', method='patch')
def test_delete_operation_and_constituents(self):
data = {
@ -243,6 +250,7 @@ class TestChangeOperations(EndpointTester):
self.assertEqual(self.ks4D2.definition_formal, r'DEL X2 X3 S1 DEL')
self.assertEqual(self.ks5D4.definition_formal, r'X1 X2 X3 S1 DEL D2 D3')
@decl_endpoint('/api/oss/{item}/delete-operation', method='patch')
def test_delete_operation_keep_constituents(self):
data = {
@ -264,6 +272,7 @@ class TestChangeOperations(EndpointTester):
self.assertEqual(self.ks4D2.definition_formal, r'X1 X2 X3 S1 D1')
self.assertEqual(self.ks5D4.definition_formal, r'X1 X2 X3 S1 D1 D2 D3')
@decl_endpoint('/api/oss/{item}/delete-operation', method='patch')
def test_delete_operation_keep_schema(self):
data = {
@ -288,6 +297,7 @@ class TestChangeOperations(EndpointTester):
self.assertEqual(self.ks5D4.definition_formal, r'X1 X2 X3 S1 D1 D2 D3')
self.assertFalse(Constituenta.objects.filter(as_child__parent_id=self.ks1D1.pk).exists())
@decl_endpoint('/api/oss/{item}/update-operation', method='patch')
def test_change_substitutions(self):
data = {
@ -298,6 +308,7 @@ class TestChangeOperations(EndpointTester):
'description': 'Comment mod'
},
'layout': self.layout_data,
'arguments': [self.operation1.pk, self.operation2.pk],
'substitutions': [
{
'original': self.ks1X1.pk,
@ -322,6 +333,7 @@ class TestChangeOperations(EndpointTester):
self.assertEqual(self.ks4D2.definition_formal, r'X1 D1 X3 S1 D1')
self.assertEqual(self.ks5D4.definition_formal, r'D1 X2 X3 S1 D1 D2 D3')
@decl_endpoint('/api/oss/{item}/update-operation', method='patch')
def test_change_arguments(self):
data = {
@ -360,6 +372,7 @@ class TestChangeOperations(EndpointTester):
self.assertEqual(self.ks4D2.definition_formal, r'X1 DEL DEL DEL D1')
self.assertEqual(self.ks5D4.definition_formal, r'DEL DEL X3 DEL D1 D2 D3')
@decl_endpoint('/api/oss/{item}/execute-operation', method='post')
def test_execute_middle_operation(self):
self.client.delete(f'/api/library/{self.ks4.model.pk}')
@ -378,6 +391,7 @@ class TestChangeOperations(EndpointTester):
self.assertNotEqual(self.operation4.result, None)
self.assertEqual(self.ks5.constituents().count(), 8)
@decl_endpoint('/api/oss/relocate-constituents', method='post')
def test_relocate_constituents_up(self):
ks1_old_count = self.ks1.constituents().count()
@ -407,6 +421,7 @@ class TestChangeOperations(EndpointTester):
self.assertEqual(self.ks1.constituents().count(), ks1_old_count + 1)
self.assertEqual(self.ks4.constituents().count(), ks4_old_count + 1)
@decl_endpoint('/api/oss/relocate-constituents', method='post')
def test_relocate_constituents_down(self):
ks1_old_count = self.ks1.constituents().count()

View File

@ -8,6 +8,7 @@ from shared.EndpointTester import EndpointTester, decl_endpoint
class TestChangeSubstitutions(EndpointTester):
''' Testing Substitutions change propagation in OSS. '''
def setUp(self):
super().setUp()
self.owned = OperationSchema.create(
@ -129,6 +130,7 @@ class TestChangeSubstitutions(EndpointTester):
self.assertEqual(self.ks5.constituents().count(), 8)
self.assertEqual(self.ks4D1.definition_formal, 'S1 X1')
@decl_endpoint('/api/rsforms/{schema}/substitute', method='patch')
def test_substitute_original(self):
data = {'substitutions': [{
@ -151,6 +153,7 @@ class TestChangeSubstitutions(EndpointTester):
self.assertEqual(self.ks4D2.definition_formal, r'S1 X2 X3 S1 D1')
self.assertEqual(self.ks5D4.definition_formal, r'X1 X2 X3 X3 D1 D2 D3')
@decl_endpoint('/api/rsforms/{schema}/substitute', method='patch')
def test_substitute_substitution(self):
data = {
@ -175,6 +178,7 @@ class TestChangeSubstitutions(EndpointTester):
self.assertEqual(self.ks4D2.definition_formal, r'X1 X2 X3 X2 D1')
self.assertEqual(self.ks5D4.definition_formal, r'X1 X2 X3 X1 D1 D2 D3')
@decl_endpoint('/api/rsforms/{schema}/delete-multiple-cst', method='patch')
def test_delete_original(self):
data = {'items': [self.ks1X1.pk, self.ks1D1.pk]}
@ -189,6 +193,7 @@ class TestChangeSubstitutions(EndpointTester):
self.assertEqual(self.ks4D2.definition_formal, r'X1 X2 X3 S1 DEL')
self.assertEqual(self.ks5D4.definition_formal, r'X1 X2 X3 S1 DEL D2 D3')
@decl_endpoint('/api/rsforms/{schema}/delete-multiple-cst', method='patch')
def test_delete_substitution(self):
data = {'items': [self.ks2S1.pk, self.ks2X2.pk]}

View File

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

View File

@ -0,0 +1,167 @@
''' 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 TestOssBlocks(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.invalid_id = self.unowned_id + 1337
def populateData(self):
self.unowned.create_block()
self.unowned.create_block()
self.unowned.create_block()
self.unowned.create_block()
self.block1 = self.owned.create_block(
title='1',
)
self.operation1 = self.owned.create_operation(
alias='1',
operation_type=OperationType.INPUT,
parent=self.block1,
)
self.operation2 = self.owned.create_operation(
alias='2',
operation_type=OperationType.INPUT,
)
self.operation3 = self.unowned.create_operation(
alias='3',
operation_type=OperationType.INPUT
)
self.block2 = self.owned.create_block(
title='2',
parent=self.block1
)
self.block3 = self.unowned.create_block(
title='3',
parent=self.block1
)
self.layout_data = {
'operations': [
{'id': self.operation1.pk, 'x': 0, 'y': 0},
{'id': self.operation2.pk, 'x': 0, 'y': 0},
],
'blocks': [
{'id': self.block1.pk, 'x': 0, 'y': 0, 'width': 0.5, 'height': 0.5},
{'id': self.block2.pk, 'x': 0, 'y': 0, 'width': 0.5, 'height': 0.5},
]
}
layout = self.owned.layout()
layout.data = self.layout_data
layout.save()
@decl_endpoint('/api/oss/{item}/create-block', method='post')
def test_create_block(self):
self.populateData()
self.executeBadData(item=self.owned_id)
data = {
'item_data': {
'title': 'Test title',
'description': 'Тест кириллицы',
},
'layout': self.layout_data,
'position_x': 1337,
'position_y': 1337,
'width': 0.42,
'height': 0.42,
'children_operations': [],
'children_blocks': []
}
self.executeNotFound(data=data, item=self.invalid_id)
response = self.executeCreated(data=data, item=self.owned_id)
self.assertEqual(len(response.data['oss']['blocks']), 3)
new_block = response.data['new_block']
layout = response.data['oss']['layout']
item = [item for item in layout['blocks'] if item['id'] == new_block['id']][0]
self.assertEqual(new_block['title'], data['item_data']['title'])
self.assertEqual(new_block['description'], data['item_data']['description'])
self.assertEqual(new_block['parent'], None)
self.assertEqual(item['x'], data['position_x'])
self.assertEqual(item['y'], data['position_y'])
self.assertEqual(item['width'], data['width'])
self.assertEqual(item['height'], data['height'])
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-block', method='post')
def test_create_block_parent(self):
self.populateData()
data = {
'item_data': {
'title': 'Test title',
'description': 'Тест кириллицы',
'parent': self.invalid_id
},
'layout': self.layout_data,
'position_x': 1337,
'position_y': 1337,
'width': 0.42,
'height': 0.42,
'children_operations': [],
'children_blocks': []
}
self.executeBadData(data=data, item=self.owned_id)
data['item_data']['parent'] = self.block3.pk
self.executeBadData(data=data)
data['item_data']['parent'] = self.block1.pk
response = self.executeCreated(data=data)
new_block = response.data['new_block']
self.assertEqual(new_block['parent'], self.block1.pk)
@decl_endpoint('/api/oss/{item}/create-block', method='post')
def test_create_block_children(self):
self.populateData()
data = {
'item_data': {
'title': 'Test title',
'description': 'Тест кириллицы',
},
'layout': self.layout_data,
'position_x': 1337,
'position_y': 1337,
'width': 0.42,
'height': 0.42,
'children_operations': [self.invalid_id],
'children_blocks': []
}
self.executeBadData(data=data, item=self.owned_id)
data['children_operations'] = [self.operation3.pk]
self.executeBadData(data=data)
data['children_operations'] = [self.block1.pk]
self.executeBadData(data=data)
data['children_operations'] = [self.operation1.pk]
data['children_blocks'] = [self.operation1.pk]
self.executeBadData(data=data)
data['children_blocks'] = [self.block1.pk]
response = self.executeCreated(data=data)
new_block = response.data['new_block']
self.operation1.refresh_from_db()
self.block1.refresh_from_db()
self.assertEqual(self.operation1.parent.pk, new_block['id'])
self.assertEqual(self.block1.parent.pk, new_block['id'])

View File

@ -8,15 +8,15 @@ 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
self.invalid_id = self.unowned_id + 1337
def populateData(self):
self.ks1 = RSForm.create(
@ -72,6 +72,7 @@ class TestOssOperations(EndpointTester):
'substitution': self.ks2X1
}])
@decl_endpoint('/api/oss/{item}/create-operation', method='post')
def test_create_operation(self):
self.populateData()
@ -116,6 +117,38 @@ class TestOssOperations(EndpointTester):
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_parent(self):
self.populateData()
data = {
'item_data': {
'parent': self.invalid_id,
'alias': 'Test3',
'title': 'Test title',
'description': '',
'operation_type': OperationType.INPUT
},
'layout': self.layout_data,
'position_x': 1,
'position_y': 1
}
self.executeBadData(data=data, item=self.owned_id)
block_unowned = self.unowned.create_block(title='TestBlock1')
data['item_data']['parent'] = block_unowned.id
self.executeBadData(data=data, item=self.owned_id)
block_owned = self.owned.create_block(title='TestBlock2')
data['item_data']['parent'] = block_owned.id
response = self.executeCreated(data=data, item=self.owned_id)
self.assertEqual(len(response.data['oss']['operations']), 4)
new_operation = response.data['new_operation']
self.assertEqual(new_operation['parent'], block_owned.id)
@decl_endpoint('/api/oss/{item}/create-operation', method='post')
def test_create_operation_arguments(self):
self.populateData()
@ -136,6 +169,7 @@ class TestOssOperations(EndpointTester):
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()
@ -154,10 +188,10 @@ class TestOssOperations(EndpointTester):
'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()
@ -189,6 +223,7 @@ class TestOssOperations(EndpointTester):
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)
@ -214,6 +249,7 @@ class TestOssOperations(EndpointTester):
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()
@ -249,6 +285,7 @@ class TestOssOperations(EndpointTester):
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()
@ -283,6 +320,7 @@ class TestOssOperations(EndpointTester):
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()
@ -317,6 +355,7 @@ class TestOssOperations(EndpointTester):
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()
@ -388,6 +427,7 @@ class TestOssOperations(EndpointTester):
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()
@ -416,6 +456,7 @@ class TestOssOperations(EndpointTester):
}
self.executeBadData(data=data, item=self.owned_id)
@decl_endpoint('/api/oss/{item}/execute-operation', method='post')
def test_execute_operation(self):
self.populateData()

View File

@ -16,7 +16,8 @@ class TestOssViewset(EndpointTester):
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
self.invalid_id = self.private_id + 1337
def populateData(self):
self.ks1 = RSForm.create(
@ -68,6 +69,7 @@ class TestOssViewset(EndpointTester):
'substitution': self.ks2X1
}])
@decl_endpoint('/api/oss/{item}/details', method='get')
def test_details(self):
self.populateData()
@ -117,6 +119,7 @@ class TestOssViewset(EndpointTester):
self.executeOK(item=self.unowned_id)
self.executeForbidden(item=self.private_id)
@decl_endpoint('/api/oss/{item}/update-layout', method='patch')
def test_update_layout(self):
self.populateData()
@ -143,6 +146,7 @@ class TestOssViewset(EndpointTester):
self.executeForbidden(data=data, item=self.unowned_id)
self.executeForbidden(data=data, item=self.private_id)
@decl_endpoint('/api/oss/get-predecessor', method='post')
def test_get_predecessor(self):
self.populateData()

View File

@ -38,6 +38,7 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
if self.action in [
'update_layout',
'create_operation',
'create_block',
'delete_operation',
'create_input',
'set_input',
@ -104,7 +105,10 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
@action(detail=True, methods=['post'], url_path='create-operation')
def create_operation(self, request: Request, pk) -> HttpResponse:
''' Create new operation. '''
serializer = s.OperationCreateSerializer(data=request.data)
serializer = s.OperationCreateSerializer(
data=request.data,
context={'oss': self.get_object()}
)
serializer.is_valid(raise_exception=True)
oss = m.OperationSchema(self.get_object())
@ -148,6 +152,57 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
}
)
@extend_schema(
summary='create block',
tags=['OSS'],
request=s.BlockCreateSerializer(),
responses={
c.HTTP_201_CREATED: s.NewBlockResponse,
c.HTTP_400_BAD_REQUEST: None,
c.HTTP_403_FORBIDDEN: None,
c.HTTP_404_NOT_FOUND: None
}
)
@action(detail=True, methods=['post'], url_path='create-block')
def create_block(self, request: Request, pk) -> HttpResponse:
''' Create new block. '''
serializer = s.BlockCreateSerializer(
data=request.data,
context={'oss': self.get_object()}
)
serializer.is_valid(raise_exception=True)
oss = m.OperationSchema(self.get_object())
layout = serializer.validated_data['layout']
children_blocks: list[m.Block] = serializer.validated_data['children_blocks']
children_operations: list[m.Operation] = serializer.validated_data['children_operations']
with transaction.atomic():
new_block = oss.create_block(**serializer.validated_data['item_data'])
layout['blocks'].append({
'id': new_block.pk,
'x': serializer.validated_data['position_x'],
'y': serializer.validated_data['position_y'],
'width': serializer.validated_data['width'],
'height': serializer.validated_data['height'],
})
oss.update_layout(layout)
if len(children_blocks) > 0:
for block in children_blocks:
block.parent = new_block
m.Block.objects.bulk_update(children_blocks, ['parent'])
if len(children_operations) > 0:
for operation in children_operations:
operation.parent = new_block
m.Operation.objects.bulk_update(children_operations, ['parent'])
return Response(
status=c.HTTP_201_CREATED,
data={
'new_block': s.BlockSerializer(new_block).data,
'oss': s.OperationSchemaSerializer(oss.model).data
}
)
@extend_schema(
summary='delete operation',
tags=['OSS'],

View File

@ -9,6 +9,7 @@ from apps.rsform.models import Constituenta, CstType, RSForm
class TestConstituenta(TestCase):
''' Testing Constituenta model. '''
def setUp(self):
self.schema1 = RSForm.create(title='Test1')
self.schema2 = RSForm.create(title='Test2')
@ -47,6 +48,7 @@ class TestConstituenta(TestCase):
self.assertEqual(cst.definition_resolved, '')
self.assertEqual(cst.definition_raw, '')
def test_extract_references(self):
cst = Constituenta.objects.create(
alias='X1',
@ -57,6 +59,7 @@ class TestConstituenta(TestCase):
)
self.assertEqual(cst.extract_references(), set(['X1', 'X2', 'X3', 'X4', 'X5']))
def text_apply_mapping(self):
cst = Constituenta.objects.create(
alias='X1',

View File

@ -9,6 +9,7 @@ from shared.DBTester import DBTester
class TestRSForm(DBTester):
''' Testing RSForm wrapper. '''
def setUp(self):
super().setUp()
self.user1 = User.objects.create(username='User1')
@ -103,6 +104,7 @@ class TestRSForm(DBTester):
self.assertEqual(x2.schema, self.schema.model)
self.assertEqual(x1.order, 0)
def test_create_cst(self):
data = {
'alias': 'X3',
@ -194,6 +196,7 @@ class TestRSForm(DBTester):
self.assertEqual(d1.definition_raw, '@{DEL|sing}')
self.assertEqual(d1.term_raw, '@{X2|plur}')
def test_apply_mapping(self):
x1 = self.schema.insert_new('X1')
x2 = self.schema.insert_new('X11')

View File

@ -138,6 +138,7 @@ class TestRSFormViewset(EndpointTester):
self.assertEqual(response.data['typification'], 'LOGIC')
self.assertEqual(response.data['valueClass'], 'value')
@decl_endpoint('/api/rsforms/{item}/check-constituenta', method='post')
def test_check_constituenta_error(self):
self.owned.insert_new('X1')
@ -145,6 +146,7 @@ class TestRSFormViewset(EndpointTester):
response = self.executeOK(data=data, item=self.owned_id)
self.assertEqual(response.data['parseResult'], False)
@decl_endpoint('/api/rsforms/{item}/resolve', method='post')
def test_resolve(self):
x1 = self.owned.insert_new(

View File

@ -7,6 +7,7 @@ from apps.rsform.graph import Graph
class TestGraph(unittest.TestCase):
''' Test class for graph. '''
def test_construction(self):
graph = Graph()
self.assertFalse(graph.contains(1))
@ -26,6 +27,7 @@ class TestGraph(unittest.TestCase):
self.assertTrue(graph.has_edge(1, 3))
self.assertTrue(graph.has_edge(2, 1))
def test_remove_node(self):
graph = Graph({
1: [2],
@ -39,6 +41,7 @@ class TestGraph(unittest.TestCase):
self.assertEqual(graph.outputs[1], [])
self.assertEqual(len(graph.outputs), 3)
def test_remove_edge(self):
graph = Graph({
1: [2],
@ -53,6 +56,7 @@ class TestGraph(unittest.TestCase):
self.assertEqual(graph.outputs[1], [])
graph.remove_edge(1, 2)
def test_expand_outputs(self):
graph = Graph({
1: [2],
@ -67,6 +71,7 @@ class TestGraph(unittest.TestCase):
self.assertEqual(graph.expand_outputs([7]), [])
self.assertEqual(graph.expand_outputs([2, 5]), [3, 6, 1])
def test_expand_inputs(self):
graph = Graph({
1: [2],
@ -101,6 +106,7 @@ class TestGraph(unittest.TestCase):
7: [6]
})
def test_topological_order(self):
self.assertEqual(Graph().topological_order(), [])
graph = Graph({
@ -122,6 +128,7 @@ class TestGraph(unittest.TestCase):
})
self.assertEqual(graph.topological_order(), [5, 3, 2, 4, 1])
def test_sort_stable(self):
graph = Graph({
1: [2],

View File

@ -8,6 +8,7 @@ from apps.rsform.utils import apply_pattern, fix_old_references
class TestUtils(unittest.TestCase):
''' Test various utility functions. '''
def test_apply_mapping_patter(self):
mapping = {'X101': 'X20'}
pattern = re.compile(r'(X[0-9]+)')

View File

@ -1,9 +1,13 @@
import os
import runpy
import sys
# Build the module path from the test file
filepath = sys.argv[1]
project_root = os.path.join(os.path.dirname(__file__))
project_root = os.path.dirname(__file__)
relpath = os.path.relpath(filepath, project_root)
module_path = relpath.replace('/', '.').replace('\\', '.').rstrip('.py')
module_path = relpath.replace('/', '.').replace('\\', '.').removesuffix('.py')
os.system(f"python manage.py test {module_path}")
# Run manage.py in-process so breakpoints work
sys.argv = ["manage.py", "test", module_path]
runpy.run_path("manage.py", run_name="__main__")

View File

@ -14,6 +14,18 @@ def operationNotInOSS(title: str):
return f'Операция не принадлежит ОСС: {title}'
def parentNotInOSS():
return f'Родительский блок не принадлежит ОСС'
def childNotInOSS():
return f'Дочерний элемент блок не принадлежит ОСС'
def missingArguments():
return 'Операция не содержит аргументов, при этом содержит отождествления'
def exteorFileCorrupted():
return 'Файл Экстеор не соответствует ожидаемому формату. Попробуйте сохранить файл в новой версии'

View File

@ -5,9 +5,7 @@ import { schemaCstSubstitute } from '@/features/rsform/backend/types';
import { errorMsg } from '@/utils/labels';
/**
* Represents {@link IOperation} type.
*/
/** Represents {@link IOperation} type. */
export const OperationType = {
INPUT: 'input',
SYNTHESIS: 'synthesis'
@ -20,10 +18,13 @@ export type ICstSubstituteInfo = z.infer<typeof schemaCstSubstituteInfo>;
/** Represents {@link IOperation} data from server. */
export type IOperationDTO = z.infer<typeof schemaOperation>;
/** Represents {@link IOperation} data from server. */
export type IBlockDTO = z.infer<typeof schemaBlock>;
/** Represents backend data for {@link IOperationSchema}. */
export type IOperationSchemaDTO = z.infer<typeof schemaOperationSchema>;
/** Represents {@link schemaOperation} layout. */
/** Represents {@link IOperationSchema} layout. */
export type IOssLayout = z.infer<typeof schemaOssLayout>;
/** Represents {@link IOperation} data, used in creation process. */
@ -64,15 +65,21 @@ export const schemaOperation = z.strictObject({
id: z.number(),
operation_type: schemaOperationType,
oss: z.number(),
alias: z.string(),
title: z.string(),
description: z.string(),
parent: z.number().nullable(),
result: z.number().nullable()
});
export const schemaBlock = z.strictObject({
id: z.number(),
oss: z.number(),
title: z.string(),
description: z.string(),
parent: z.number().nullable()
});
export const schemaCstSubstituteInfo = schemaCstSubstitute.extend({
operation: z.number(),
original_alias: z.string(),
@ -81,20 +88,29 @@ export const schemaCstSubstituteInfo = schemaCstSubstitute.extend({
substitution_term: z.string()
});
export const schemaPosition = z.strictObject({
export const schemaOperationPosition = z.strictObject({
id: z.number(),
x: z.number(),
y: z.number()
});
export const schemaBlockPosition = z.strictObject({
id: z.number(),
x: z.number(),
y: z.number(),
width: z.number(),
height: z.number()
});
export const schemaOssLayout = z.strictObject({
operations: z.array(schemaPosition),
blocks: z.array(schemaPosition)
operations: z.array(schemaOperationPosition),
blocks: z.array(schemaBlockPosition)
});
export const schemaOperationSchema = schemaLibraryItem.extend({
editors: z.number().array(),
operations: z.array(schemaOperation),
blocks: z.array(schemaBlock),
layout: schemaOssLayout,
arguments: z
.object({