F: Constituenta relocation pt2
This commit is contained in:
parent
2e775463a9
commit
f0af0db3f9
|
@ -267,7 +267,51 @@ class OperationSchema:
|
||||||
self.save(update_fields=['time_update'])
|
self.save(update_fields=['time_update'])
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def after_create_cst(self, source: RSForm, cst_list: list[Constituenta]) -> None:
|
def relocate_down(self, source: RSForm, destination: RSForm, items: list[Constituenta]):
|
||||||
|
''' Move list of constituents to specific schema inheritor. '''
|
||||||
|
self.cache.ensure_loaded()
|
||||||
|
self.cache.insert_schema(source)
|
||||||
|
self.cache.insert_schema(destination)
|
||||||
|
operation = self.cache.get_operation(destination.model.pk)
|
||||||
|
|
||||||
|
self._undo_substitutions_cst(items, operation, destination)
|
||||||
|
|
||||||
|
inheritance_to_delete = [item for item in self.cache.inheritance[operation.pk] if item.parent_id in items]
|
||||||
|
for item in inheritance_to_delete:
|
||||||
|
self.cache.remove_inheritance(item)
|
||||||
|
Inheritance.objects.filter(operation_id=operation.pk, parent__in=items).delete()
|
||||||
|
|
||||||
|
def relocate_up(self, source: RSForm, destination: RSForm, items: list[Constituenta]) -> list[Constituenta]:
|
||||||
|
''' Move list of constituents to specific schema upstream. '''
|
||||||
|
self.cache.ensure_loaded()
|
||||||
|
self.cache.insert_schema(source)
|
||||||
|
self.cache.insert_schema(destination)
|
||||||
|
|
||||||
|
operation = self.cache.get_operation(source.model.pk)
|
||||||
|
alias_mapping: dict[str, str] = {}
|
||||||
|
for item in self.cache.inheritance[operation.pk]:
|
||||||
|
if item.parent_id in destination.cache.by_id:
|
||||||
|
source_cst = source.cache.by_id[item.child_id]
|
||||||
|
destination_cst = destination.cache.by_id[item.parent_id]
|
||||||
|
alias_mapping[source_cst.alias] = destination_cst.alias
|
||||||
|
|
||||||
|
new_items = destination.insert_copy(items, initial_mapping=alias_mapping)
|
||||||
|
for index, cst in enumerate(new_items):
|
||||||
|
new_inheritance = Inheritance.objects.create(
|
||||||
|
operation=operation,
|
||||||
|
child=items[index],
|
||||||
|
parent=cst
|
||||||
|
)
|
||||||
|
self.cache.insert_inheritance(new_inheritance)
|
||||||
|
self.after_create_cst(destination, new_items, exclude=[operation.pk])
|
||||||
|
|
||||||
|
return new_items
|
||||||
|
|
||||||
|
def after_create_cst(
|
||||||
|
self, source: RSForm,
|
||||||
|
cst_list: list[Constituenta],
|
||||||
|
exclude: Optional[list[int]] = None
|
||||||
|
) -> None:
|
||||||
''' Trigger cascade resolutions when new constituent is created. '''
|
''' Trigger cascade resolutions when new constituent is created. '''
|
||||||
self.cache.insert_schema(source)
|
self.cache.insert_schema(source)
|
||||||
inserted_aliases = [cst.alias for cst in cst_list]
|
inserted_aliases = [cst.alias for cst in cst_list]
|
||||||
|
@ -281,7 +325,7 @@ class OperationSchema:
|
||||||
if cst is not None:
|
if cst is not None:
|
||||||
alias_mapping[alias] = cst
|
alias_mapping[alias] = cst
|
||||||
operation = self.cache.get_operation(source.model.pk)
|
operation = self.cache.get_operation(source.model.pk)
|
||||||
self._cascade_inherit_cst(operation.pk, source, cst_list, alias_mapping)
|
self._cascade_inherit_cst(operation.pk, source, cst_list, alias_mapping, exclude)
|
||||||
|
|
||||||
def after_change_cst_type(self, source: RSForm, target: Constituenta) -> None:
|
def after_change_cst_type(self, source: RSForm, target: Constituenta) -> None:
|
||||||
''' Trigger cascade resolutions when constituenta type is changed. '''
|
''' Trigger cascade resolutions when constituenta type is changed. '''
|
||||||
|
@ -344,18 +388,20 @@ class OperationSchema:
|
||||||
mapping={}
|
mapping={}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# pylint: disable=too-many-arguments, too-many-positional-arguments
|
||||||
def _cascade_inherit_cst(
|
def _cascade_inherit_cst(
|
||||||
self,
|
self, target_operation: int,
|
||||||
target_operation: int,
|
|
||||||
source: RSForm,
|
source: RSForm,
|
||||||
items: list[Constituenta],
|
items: list[Constituenta],
|
||||||
mapping: CstMapping
|
mapping: CstMapping,
|
||||||
|
exclude: Optional[list[int]] = None
|
||||||
) -> None:
|
) -> None:
|
||||||
children = self.cache.graph.outputs[target_operation]
|
children = self.cache.graph.outputs[target_operation]
|
||||||
if len(children) == 0:
|
if len(children) == 0:
|
||||||
return
|
return
|
||||||
for child_id in children:
|
for child_id in children:
|
||||||
self._execute_inherit_cst(child_id, source, items, mapping)
|
if not exclude or child_id not in exclude:
|
||||||
|
self._execute_inherit_cst(child_id, source, items, mapping)
|
||||||
|
|
||||||
def _execute_inherit_cst(
|
def _execute_inherit_cst(
|
||||||
self,
|
self,
|
||||||
|
@ -827,6 +873,10 @@ class OssCache:
|
||||||
''' Remove substitution from cache. '''
|
''' Remove substitution from cache. '''
|
||||||
self.substitutions[target.operation_id].remove(target)
|
self.substitutions[target.operation_id].remove(target)
|
||||||
|
|
||||||
|
def remove_inheritance(self, target: Inheritance) -> None:
|
||||||
|
''' Remove inheritance from cache. '''
|
||||||
|
self.inheritance[target.operation_id].remove(target)
|
||||||
|
|
||||||
def unfold_sub(self, sub: Substitution) -> tuple[RSForm, RSForm, Constituenta, Constituenta]:
|
def unfold_sub(self, sub: Substitution) -> tuple[RSForm, RSForm, Constituenta, Constituenta]:
|
||||||
operation = self.operation_by_id[sub.operation_id]
|
operation = self.operation_by_id[sub.operation_id]
|
||||||
parents = self.graph.inputs[operation.pk]
|
parents = self.graph.inputs[operation.pk]
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
''' Models: Change propagation facade - managing all changes in OSS. '''
|
''' Models: Change propagation facade - managing all changes in OSS. '''
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
from apps.library.models import LibraryItem, LibraryItemType
|
from apps.library.models import LibraryItem, LibraryItemType
|
||||||
from apps.rsform.models import Constituenta, RSForm
|
from apps.rsform.models import Constituenta, RSForm
|
||||||
|
|
||||||
|
@ -14,42 +16,53 @@ class PropagationFacade:
|
||||||
''' Change propagation API. '''
|
''' Change propagation API. '''
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def after_create_cst(source: RSForm, new_cst: list[Constituenta]) -> None:
|
def after_create_cst(source: RSForm, new_cst: list[Constituenta], exclude: Optional[list[int]] = None) -> None:
|
||||||
''' Trigger cascade resolutions when new constituent is created. '''
|
''' Trigger cascade resolutions when new constituent is created. '''
|
||||||
hosts = _get_oss_hosts(source.model)
|
hosts = _get_oss_hosts(source.model)
|
||||||
for host in hosts:
|
for host in hosts:
|
||||||
OperationSchema(host).after_create_cst(source, new_cst)
|
if exclude is None or host.pk not in exclude:
|
||||||
|
OperationSchema(host).after_create_cst(source, new_cst)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def after_change_cst_type(source: RSForm, target: Constituenta) -> None:
|
def after_change_cst_type(source: RSForm, target: Constituenta, exclude: Optional[list[int]] = None) -> None:
|
||||||
''' Trigger cascade resolutions when constituenta type is changed. '''
|
''' Trigger cascade resolutions when constituenta type is changed. '''
|
||||||
hosts = _get_oss_hosts(source.model)
|
hosts = _get_oss_hosts(source.model)
|
||||||
for host in hosts:
|
for host in hosts:
|
||||||
OperationSchema(host).after_change_cst_type(source, target)
|
if exclude is None or host.pk not in exclude:
|
||||||
|
OperationSchema(host).after_change_cst_type(source, target)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def after_update_cst(source: RSForm, target: Constituenta, data: dict, old_data: dict) -> None:
|
def after_update_cst(
|
||||||
|
source: RSForm,
|
||||||
|
target: Constituenta,
|
||||||
|
data: dict,
|
||||||
|
old_data: dict,
|
||||||
|
exclude: Optional[list[int]] = None
|
||||||
|
) -> None:
|
||||||
''' Trigger cascade resolutions when constituenta data is changed. '''
|
''' Trigger cascade resolutions when constituenta data is changed. '''
|
||||||
hosts = _get_oss_hosts(source.model)
|
hosts = _get_oss_hosts(source.model)
|
||||||
for host in hosts:
|
for host in hosts:
|
||||||
OperationSchema(host).after_update_cst(source, target, data, old_data)
|
if exclude is None or host.pk not in exclude:
|
||||||
|
OperationSchema(host).after_update_cst(source, target, data, old_data)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def before_delete_cst(source: RSForm, target: list[Constituenta]) -> None:
|
def before_delete_cst(source: RSForm, target: list[Constituenta], exclude: Optional[list[int]] = None) -> None:
|
||||||
''' Trigger cascade resolutions before constituents are deleted. '''
|
''' Trigger cascade resolutions before constituents are deleted. '''
|
||||||
hosts = _get_oss_hosts(source.model)
|
hosts = _get_oss_hosts(source.model)
|
||||||
for host in hosts:
|
for host in hosts:
|
||||||
OperationSchema(host).before_delete_cst(source, target)
|
if exclude is None or host.pk not in exclude:
|
||||||
|
OperationSchema(host).before_delete_cst(source, target)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def before_substitute(source: RSForm, substitutions: CstSubstitution) -> None:
|
def before_substitute(source: RSForm, substitutions: CstSubstitution, exclude: Optional[list[int]] = None) -> None:
|
||||||
''' Trigger cascade resolutions before constituents are substituted. '''
|
''' Trigger cascade resolutions before constituents are substituted. '''
|
||||||
hosts = _get_oss_hosts(source.model)
|
hosts = _get_oss_hosts(source.model)
|
||||||
for host in hosts:
|
for host in hosts:
|
||||||
OperationSchema(host).before_substitute(source, substitutions)
|
if exclude is None or host.pk not in exclude:
|
||||||
|
OperationSchema(host).before_substitute(source, substitutions)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def before_delete_schema(item: LibraryItem) -> None:
|
def before_delete_schema(item: LibraryItem, exclude: Optional[list[int]] = None) -> None:
|
||||||
''' Trigger cascade resolutions before schema is deleted. '''
|
''' Trigger cascade resolutions before schema is deleted. '''
|
||||||
if item.item_type != LibraryItemType.RSFORM:
|
if item.item_type != LibraryItemType.RSFORM:
|
||||||
return
|
return
|
||||||
|
@ -58,4 +71,4 @@ class PropagationFacade:
|
||||||
return
|
return
|
||||||
|
|
||||||
schema = RSForm(item)
|
schema = RSForm(item)
|
||||||
PropagationFacade.before_delete_cst(schema, list(schema.constituents().order_by('order')))
|
PropagationFacade.before_delete_cst(schema, list(schema.constituents().order_by('order')), exclude)
|
||||||
|
|
|
@ -9,6 +9,7 @@ from .data_access import (
|
||||||
OperationSerializer,
|
OperationSerializer,
|
||||||
OperationTargetSerializer,
|
OperationTargetSerializer,
|
||||||
OperationUpdateSerializer,
|
OperationUpdateSerializer,
|
||||||
|
RelocateConstituentsSerializer,
|
||||||
SetOperationInputSerializer
|
SetOperationInputSerializer
|
||||||
)
|
)
|
||||||
from .responses import ConstituentaReferenceResponse, NewOperationResponse, NewSchemaResponse
|
from .responses import ConstituentaReferenceResponse, NewOperationResponse, NewSchemaResponse
|
||||||
|
|
|
@ -11,7 +11,7 @@ from apps.rsform.models import Constituenta
|
||||||
from apps.rsform.serializers import SubstitutionSerializerBase
|
from apps.rsform.serializers import SubstitutionSerializerBase
|
||||||
from shared import messages as msg
|
from shared import messages as msg
|
||||||
|
|
||||||
from ..models import Argument, Operation, OperationSchema, OperationType
|
from ..models import Argument, Inheritance, Operation, OperationSchema, OperationType
|
||||||
from .basics import OperationPositionSerializer, SubstitutionExSerializer
|
from .basics import OperationPositionSerializer, SubstitutionExSerializer
|
||||||
|
|
||||||
|
|
||||||
|
@ -118,8 +118,6 @@ class OperationUpdateSerializer(serializers.Serializer):
|
||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class OperationTargetSerializer(serializers.Serializer):
|
class OperationTargetSerializer(serializers.Serializer):
|
||||||
''' Serializer: Target single operation. '''
|
''' Serializer: Target single operation. '''
|
||||||
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'))
|
||||||
|
@ -224,3 +222,62 @@ class OperationSchemaSerializer(serializers.ModelSerializer):
|
||||||
).order_by('pk'):
|
).order_by('pk'):
|
||||||
result['substitutions'].append(substitution)
|
result['substitutions'].append(substitution)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
class RelocateConstituentsSerializer(serializers.Serializer):
|
||||||
|
''' Serializer: Relocate constituents. '''
|
||||||
|
destination = PKField(
|
||||||
|
many=False,
|
||||||
|
queryset=LibraryItem.objects.all().only('id')
|
||||||
|
)
|
||||||
|
items = PKField(
|
||||||
|
many=True,
|
||||||
|
allow_empty=False,
|
||||||
|
queryset=Constituenta.objects.all()
|
||||||
|
)
|
||||||
|
|
||||||
|
def validate(self, attrs):
|
||||||
|
attrs['destination'] = attrs['destination'].id
|
||||||
|
attrs['source'] = attrs['items'][0].schema_id
|
||||||
|
|
||||||
|
# TODO: check permissions for editing source and destination
|
||||||
|
|
||||||
|
if attrs['source'] == attrs['destination']:
|
||||||
|
raise serializers.ValidationError({
|
||||||
|
'destination': msg.sourceEqualDestination()
|
||||||
|
})
|
||||||
|
for cst in attrs['items']:
|
||||||
|
if cst.schema_id != attrs['source']:
|
||||||
|
raise serializers.ValidationError({
|
||||||
|
f'{cst.pk}': msg.constituentaNotInRSform(attrs['items'][0].schema.title)
|
||||||
|
})
|
||||||
|
if Inheritance.objects.filter(child__in=attrs['items']).exists():
|
||||||
|
raise serializers.ValidationError({
|
||||||
|
'items': msg.RelocatingInherited()
|
||||||
|
})
|
||||||
|
|
||||||
|
oss = LibraryItem.objects \
|
||||||
|
.filter(operations__result_id=attrs['destination']) \
|
||||||
|
.filter(operations__result_id=attrs['source']).only('id')
|
||||||
|
if not oss.exists():
|
||||||
|
raise serializers.ValidationError({
|
||||||
|
'destination': msg.schemasNotConnected()
|
||||||
|
})
|
||||||
|
attrs['oss'] = oss[0].pk
|
||||||
|
|
||||||
|
if Argument.objects.filter(
|
||||||
|
operation__result_id=attrs['destination'],
|
||||||
|
argument__result_id=attrs['source']
|
||||||
|
).exists():
|
||||||
|
attrs['move_down'] = True
|
||||||
|
elif Argument.objects.filter(
|
||||||
|
operation__result_id=attrs['source'],
|
||||||
|
argument__result_id=attrs['destination']
|
||||||
|
).exists():
|
||||||
|
attrs['move_down'] = False
|
||||||
|
else:
|
||||||
|
raise serializers.ValidationError({
|
||||||
|
'destination': msg.schemasNotConnected()
|
||||||
|
})
|
||||||
|
|
||||||
|
return attrs
|
||||||
|
|
|
@ -339,3 +339,65 @@ class TestChangeOperations(EndpointTester):
|
||||||
self.ks5.refresh_from_db()
|
self.ks5.refresh_from_db()
|
||||||
self.assertNotEqual(self.operation4.result, None)
|
self.assertNotEqual(self.operation4.result, None)
|
||||||
self.assertEqual(self.ks5.constituents().count(), 8)
|
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()
|
||||||
|
ks4_old_count = self.ks4.constituents().count()
|
||||||
|
operation6 = self.owned.create_operation(
|
||||||
|
alias='6',
|
||||||
|
operation_type=OperationType.SYNTHESIS
|
||||||
|
)
|
||||||
|
self.owned.set_arguments(operation6.pk, [self.operation1, self.operation2])
|
||||||
|
self.owned.execute_operation(operation6)
|
||||||
|
operation6.refresh_from_db()
|
||||||
|
ks6 = RSForm(operation6.result)
|
||||||
|
ks6A1 = ks6.insert_new('A1')
|
||||||
|
ks6_old_count = ks6.constituents().count()
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'destination': self.ks1.model.pk,
|
||||||
|
'items': [ks6A1.pk]
|
||||||
|
}
|
||||||
|
|
||||||
|
self.executeOK(data=data)
|
||||||
|
ks6.refresh_from_db()
|
||||||
|
self.ks1.refresh_from_db()
|
||||||
|
self.ks4.refresh_from_db()
|
||||||
|
|
||||||
|
self.assertEqual(ks6.constituents().count(), ks6_old_count)
|
||||||
|
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()
|
||||||
|
ks4_old_count = self.ks4.constituents().count()
|
||||||
|
|
||||||
|
operation6 = self.owned.create_operation(
|
||||||
|
alias='6',
|
||||||
|
operation_type=OperationType.SYNTHESIS
|
||||||
|
)
|
||||||
|
self.owned.set_arguments(operation6.pk, [self.operation1, self.operation2])
|
||||||
|
self.owned.execute_operation(operation6)
|
||||||
|
operation6.refresh_from_db()
|
||||||
|
ks6 = RSForm(operation6.result)
|
||||||
|
ks6_old_count = ks6.constituents().count()
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'destination': ks6.model.pk,
|
||||||
|
'items': [self.ks1X2.pk]
|
||||||
|
}
|
||||||
|
|
||||||
|
self.executeOK(data=data)
|
||||||
|
ks6.refresh_from_db()
|
||||||
|
self.ks1.refresh_from_db()
|
||||||
|
self.ks4.refresh_from_db()
|
||||||
|
self.ks4D2.refresh_from_db()
|
||||||
|
self.ks5D4.refresh_from_db()
|
||||||
|
|
||||||
|
self.assertEqual(ks6.constituents().count(), ks6_old_count)
|
||||||
|
self.assertEqual(self.ks1.constituents().count(), ks1_old_count - 1)
|
||||||
|
self.assertEqual(self.ks4.constituents().count(), ks4_old_count - 1)
|
||||||
|
self.assertEqual(self.ks4D2.definition_formal, r'DEL X2 X3 S1 D1')
|
||||||
|
self.assertEqual(self.ks5D4.definition_formal, r'X1 X2 X3 S1 D1 D2 D3')
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
''' Testing API: Operation Schema. '''
|
''' Testing API: Operation Schema. '''
|
||||||
from apps.library.models import AccessPolicy, Editor, LibraryItem, LibraryItemType
|
from apps.library.models import AccessPolicy, Editor, LibraryItem, LibraryItemType
|
||||||
from apps.oss.models import Operation, OperationSchema, OperationType
|
from apps.oss.models import Operation, OperationSchema, OperationType
|
||||||
from apps.rsform.models import RSForm
|
from apps.rsform.models import Constituenta, RSForm
|
||||||
from shared.EndpointTester import EndpointTester, decl_endpoint
|
from shared.EndpointTester import EndpointTester, decl_endpoint
|
||||||
|
|
||||||
|
|
||||||
|
@ -18,7 +18,6 @@ class TestOssViewset(EndpointTester):
|
||||||
self.private_id = self.private.model.pk
|
self.private_id = self.private.model.pk
|
||||||
self.invalid_id = self.private.model.pk + 1337
|
self.invalid_id = self.private.model.pk + 1337
|
||||||
|
|
||||||
|
|
||||||
def populateData(self):
|
def populateData(self):
|
||||||
self.ks1 = RSForm.create(
|
self.ks1 = RSForm.create(
|
||||||
alias='KS1',
|
alias='KS1',
|
||||||
|
@ -135,7 +134,6 @@ class TestOssViewset(EndpointTester):
|
||||||
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')
|
@decl_endpoint('/api/oss/{item}/create-operation', method='post')
|
||||||
def test_create_operation(self):
|
def test_create_operation(self):
|
||||||
self.populateData()
|
self.populateData()
|
||||||
|
@ -499,3 +497,87 @@ class TestOssViewset(EndpointTester):
|
||||||
self.assertEqual(len(items), 1)
|
self.assertEqual(len(items), 1)
|
||||||
self.assertEqual(items[0].alias, 'X1')
|
self.assertEqual(items[0].alias, 'X1')
|
||||||
self.assertEqual(items[0].term_resolved, self.ks2X1.term_resolved)
|
self.assertEqual(items[0].term_resolved, self.ks2X1.term_resolved)
|
||||||
|
|
||||||
|
@decl_endpoint('/api/oss/get-predecessor', method='post')
|
||||||
|
def test_get_predecessor(self):
|
||||||
|
self.populateData()
|
||||||
|
self.ks1X2 = self.ks1.insert_new('X2')
|
||||||
|
|
||||||
|
self.owned.execute_operation(self.operation3)
|
||||||
|
self.operation3.refresh_from_db()
|
||||||
|
self.ks3 = RSForm(self.operation3.result)
|
||||||
|
self.ks3X2 = Constituenta.objects.get(as_child__parent_id=self.ks1X2.pk)
|
||||||
|
|
||||||
|
self.executeBadData(data={'target': self.invalid_id})
|
||||||
|
|
||||||
|
response = self.executeOK(data={'target': self.ks1X1.pk})
|
||||||
|
self.assertEqual(response.data['id'], self.ks1X1.pk)
|
||||||
|
self.assertEqual(response.data['schema'], self.ks1.model.pk)
|
||||||
|
|
||||||
|
response = self.executeOK(data={'target': self.ks3X2.pk})
|
||||||
|
self.assertEqual(response.data['id'], self.ks1X2.pk)
|
||||||
|
self.assertEqual(response.data['schema'], self.ks1.model.pk)
|
||||||
|
|
||||||
|
@decl_endpoint('/api/oss/relocate-constituents', method='post')
|
||||||
|
def test_relocate_constituents(self):
|
||||||
|
self.populateData()
|
||||||
|
self.ks1X2 = self.ks1.insert_new('X2', convention='test')
|
||||||
|
|
||||||
|
self.owned.execute_operation(self.operation3)
|
||||||
|
self.operation3.refresh_from_db()
|
||||||
|
self.ks3 = RSForm(self.operation3.result)
|
||||||
|
self.ks3X2 = Constituenta.objects.get(as_child__parent_id=self.ks1X2.pk)
|
||||||
|
self.ks3X10 = self.ks3.insert_new('X10', convention='test2')
|
||||||
|
|
||||||
|
# invalid destination
|
||||||
|
data = {
|
||||||
|
'destination': self.invalid_id,
|
||||||
|
'items': []
|
||||||
|
}
|
||||||
|
self.executeBadData(data=data)
|
||||||
|
|
||||||
|
# empty items
|
||||||
|
data = {
|
||||||
|
'destination': self.ks1.model.pk,
|
||||||
|
'items': []
|
||||||
|
}
|
||||||
|
self.executeBadData(data=data)
|
||||||
|
|
||||||
|
# source == destination
|
||||||
|
data = {
|
||||||
|
'destination': self.ks1.model.pk,
|
||||||
|
'items': [self.ks1X1.pk]
|
||||||
|
}
|
||||||
|
self.executeBadData(data=data)
|
||||||
|
|
||||||
|
# moving inherited
|
||||||
|
data = {
|
||||||
|
'destination': self.ks1.model.pk,
|
||||||
|
'items': [self.ks3X2.pk]
|
||||||
|
}
|
||||||
|
self.executeBadData(data=data)
|
||||||
|
|
||||||
|
# source and destination are not connected
|
||||||
|
data = {
|
||||||
|
'destination': self.ks2.model.pk,
|
||||||
|
'items': [self.ks1X1.pk]
|
||||||
|
}
|
||||||
|
self.executeBadData(data=data)
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'destination': self.ks3.model.pk,
|
||||||
|
'items': [self.ks1X2.pk]
|
||||||
|
}
|
||||||
|
self.ks3X2.refresh_from_db()
|
||||||
|
self.assertEqual(self.ks3X2.convention, 'test')
|
||||||
|
self.executeOK(data=data)
|
||||||
|
self.assertFalse(Constituenta.objects.filter(as_child__parent_id=self.ks1X2.pk).exists())
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'destination': self.ks1.model.pk,
|
||||||
|
'items': [self.ks3X10.pk]
|
||||||
|
}
|
||||||
|
self.executeOK(data=data)
|
||||||
|
self.assertTrue(Constituenta.objects.filter(as_parent__child_id=self.ks3X10.pk).exists())
|
||||||
|
self.ks1X3 = Constituenta.objects.get(as_parent__child_id=self.ks3X10.pk)
|
||||||
|
self.assertEqual(self.ks1X3.convention, 'test2')
|
||||||
|
|
|
@ -14,7 +14,7 @@ from rest_framework.response import Response
|
||||||
|
|
||||||
from apps.library.models import LibraryItem, LibraryItemType
|
from apps.library.models import LibraryItem, LibraryItemType
|
||||||
from apps.library.serializers import LibraryItemSerializer
|
from apps.library.serializers import LibraryItemSerializer
|
||||||
from apps.rsform.models import Constituenta
|
from apps.rsform.models import Constituenta, RSForm
|
||||||
from apps.rsform.serializers import CstTargetSerializer
|
from apps.rsform.serializers import CstTargetSerializer
|
||||||
from shared import messages as msg
|
from shared import messages as msg
|
||||||
from shared import permissions
|
from shared import permissions
|
||||||
|
@ -42,7 +42,8 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
|
||||||
'create_input',
|
'create_input',
|
||||||
'set_input',
|
'set_input',
|
||||||
'update_operation',
|
'update_operation',
|
||||||
'execute_operation'
|
'execute_operation',
|
||||||
|
'relocate_constituents'
|
||||||
]:
|
]:
|
||||||
permission_list = [permissions.ItemEditor]
|
permission_list = [permissions.ItemEditor]
|
||||||
elif self.action in ['details']:
|
elif self.action in ['details']:
|
||||||
|
@ -385,3 +386,36 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
|
||||||
'schema': cst.schema_id
|
'schema': cst.schema_id
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@extend_schema(
|
||||||
|
summary='relocate constituents from one schema to another',
|
||||||
|
tags=['OSS'],
|
||||||
|
request=s.RelocateConstituentsSerializer(),
|
||||||
|
responses={
|
||||||
|
c.HTTP_200_OK: s.OperationSchemaSerializer,
|
||||||
|
c.HTTP_400_BAD_REQUEST: None,
|
||||||
|
c.HTTP_403_FORBIDDEN: None,
|
||||||
|
c.HTTP_404_NOT_FOUND: None
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@action(detail=False, methods=['post'], url_path='relocate-constituents')
|
||||||
|
def relocate_constituents(self, request: Request) -> Response:
|
||||||
|
''' Relocate constituents from one schema to another. '''
|
||||||
|
serializer = s.RelocateConstituentsSerializer(data=request.data)
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
|
||||||
|
data = serializer.validated_data
|
||||||
|
oss = m.OperationSchema(LibraryItem.objects.get(pk=data['oss']))
|
||||||
|
source = RSForm(LibraryItem.objects.get(pk=data['source']))
|
||||||
|
destination = RSForm(LibraryItem.objects.get(pk=data['destination']))
|
||||||
|
|
||||||
|
with transaction.atomic():
|
||||||
|
if data['move_down']:
|
||||||
|
oss.relocate_down(source, destination, data['items'])
|
||||||
|
m.PropagationFacade.before_delete_cst(source, data['items'])
|
||||||
|
source.delete_cst(data['items'])
|
||||||
|
else:
|
||||||
|
new_items = oss.relocate_up(source, destination, data['items'])
|
||||||
|
m.PropagationFacade.after_create_cst(destination, new_items, exclude=[oss.model.pk])
|
||||||
|
|
||||||
|
return Response(status=c.HTTP_200_OK)
|
||||||
|
|
|
@ -64,6 +64,7 @@ class RSForm:
|
||||||
def refresh_from_db(self) -> None:
|
def refresh_from_db(self) -> None:
|
||||||
''' Model wrapper. '''
|
''' Model wrapper. '''
|
||||||
self.model.refresh_from_db()
|
self.model.refresh_from_db()
|
||||||
|
self.cache = RSFormCache(self)
|
||||||
|
|
||||||
def constituents(self) -> QuerySet[Constituenta]:
|
def constituents(self) -> QuerySet[Constituenta]:
|
||||||
''' Get QuerySet containing all constituents of current RSForm. '''
|
''' Get QuerySet containing all constituents of current RSForm. '''
|
||||||
|
|
|
@ -38,6 +38,18 @@ def operationResultFromAnotherOSS():
|
||||||
return 'Схема является результатом другой ОСС'
|
return 'Схема является результатом другой ОСС'
|
||||||
|
|
||||||
|
|
||||||
|
def schemasNotConnected():
|
||||||
|
return 'Концептуальные схемы не связаны через ОСС'
|
||||||
|
|
||||||
|
|
||||||
|
def sourceEqualDestination():
|
||||||
|
return 'Схема-источник и схема-получатель не могут быть одинаковыми'
|
||||||
|
|
||||||
|
|
||||||
|
def RelocatingInherited():
|
||||||
|
return 'Невозможно переместить наследуемые конституенты'
|
||||||
|
|
||||||
|
|
||||||
def operationInputAlreadyConnected():
|
def operationInputAlreadyConnected():
|
||||||
return 'Схема уже подключена к другой операции'
|
return 'Схема уже подключена к другой операции'
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
ICstRelocateData,
|
||||||
IInputCreatedResponse,
|
IInputCreatedResponse,
|
||||||
IOperationCreateData,
|
IOperationCreateData,
|
||||||
IOperationCreatedResponse,
|
IOperationCreatedResponse,
|
||||||
|
@ -76,6 +77,13 @@ export function postExecuteOperation(oss: string, request: FrontExchange<ITarget
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function postRelocateConstituents(request: FrontPush<ICstRelocateData>) {
|
||||||
|
AxiosPost({
|
||||||
|
endpoint: `/api/oss/relocate-constituents`,
|
||||||
|
request: request
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function postFindPredecessor(request: FrontExchange<ITargetCst, IConstituentaReference>) {
|
export function postFindPredecessor(request: FrontExchange<ITargetCst, IConstituentaReference>) {
|
||||||
AxiosPost({
|
AxiosPost({
|
||||||
endpoint: `/api/oss/get-predecessor`,
|
endpoint: `/api/oss/get-predecessor`,
|
||||||
|
|
|
@ -17,12 +17,14 @@ import {
|
||||||
patchUpdateOperation,
|
patchUpdateOperation,
|
||||||
patchUpdatePositions,
|
patchUpdatePositions,
|
||||||
postCreateOperation,
|
postCreateOperation,
|
||||||
postExecuteOperation
|
postExecuteOperation,
|
||||||
|
postRelocateConstituents
|
||||||
} from '@/backend/oss';
|
} from '@/backend/oss';
|
||||||
import { type ErrorData } from '@/components/info/InfoError';
|
import { type ErrorData } from '@/components/info/InfoError';
|
||||||
import { AccessPolicy, ILibraryItem } from '@/models/library';
|
import { AccessPolicy, ILibraryItem } from '@/models/library';
|
||||||
import { ILibraryUpdateData } from '@/models/library';
|
import { ILibraryUpdateData } from '@/models/library';
|
||||||
import {
|
import {
|
||||||
|
ICstRelocateData,
|
||||||
IOperationCreateData,
|
IOperationCreateData,
|
||||||
IOperationData,
|
IOperationData,
|
||||||
IOperationDeleteData,
|
IOperationDeleteData,
|
||||||
|
@ -65,6 +67,7 @@ interface IOssContext {
|
||||||
setInput: (data: IOperationSetInputData, callback?: () => void) => void;
|
setInput: (data: IOperationSetInputData, callback?: () => void) => void;
|
||||||
updateOperation: (data: IOperationUpdateData, callback?: () => void) => void;
|
updateOperation: (data: IOperationUpdateData, callback?: () => void) => void;
|
||||||
executeOperation: (data: ITargetOperation, callback?: () => void) => void;
|
executeOperation: (data: ITargetOperation, callback?: () => void) => void;
|
||||||
|
relocateConstituents: (data: ICstRelocateData, callback?: () => void) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const OssContext = createContext<IOssContext | null>(null);
|
const OssContext = createContext<IOssContext | null>(null);
|
||||||
|
@ -353,6 +356,28 @@ export const OssState = ({ itemID, children }: React.PropsWithChildren<OssStateP
|
||||||
[itemID, model, library, oss]
|
[itemID, model, library, oss]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const relocateConstituents = useCallback(
|
||||||
|
(data: ICstRelocateData, callback?: () => void) => {
|
||||||
|
if (!model) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setProcessingError(undefined);
|
||||||
|
postRelocateConstituents({
|
||||||
|
data: data,
|
||||||
|
showError: true,
|
||||||
|
setLoading: setProcessing,
|
||||||
|
onError: setProcessingError,
|
||||||
|
onSuccess: () => {
|
||||||
|
oss.reload();
|
||||||
|
library.reloadItems(() => {
|
||||||
|
if (callback) callback();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[model, library, oss]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<OssContext.Provider
|
<OssContext.Provider
|
||||||
value={{
|
value={{
|
||||||
|
@ -376,7 +401,8 @@ export const OssState = ({ itemID, children }: React.PropsWithChildren<OssStateP
|
||||||
createInput,
|
createInput,
|
||||||
setInput,
|
setInput,
|
||||||
updateOperation,
|
updateOperation,
|
||||||
executeOperation
|
executeOperation,
|
||||||
|
relocateConstituents
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|
|
@ -55,12 +55,15 @@ function DlgRelocateConstituents({ oss, hideWindow, target, onSubmit }: DlgReloc
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleSubmit = useCallback(() => {
|
const handleSubmit = useCallback(() => {
|
||||||
|
if (!destination) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const data: ICstRelocateData = {
|
const data: ICstRelocateData = {
|
||||||
destination: target.result ?? 0,
|
destination: destination.id,
|
||||||
items: []
|
items: selected
|
||||||
};
|
};
|
||||||
onSubmit(data);
|
onSubmit(data);
|
||||||
}, [target, onSubmit]);
|
}, [destination, onSubmit, selected]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
|
|
|
@ -199,9 +199,9 @@ function OssFlow({ isModified, setIsModified }: OssFlowProps) {
|
||||||
|
|
||||||
const handleRelocateConstituents = useCallback(
|
const handleRelocateConstituents = useCallback(
|
||||||
(target: OperationID) => {
|
(target: OperationID) => {
|
||||||
controller.promptRelocateConstituents(target);
|
controller.promptRelocateConstituents(target, getPositions());
|
||||||
},
|
},
|
||||||
[controller]
|
[controller, getPositions]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleFitView = useCallback(() => {
|
const handleFitView = useCallback(() => {
|
||||||
|
|
|
@ -76,7 +76,7 @@ export interface IOssEditContext extends ILibraryItemEditor {
|
||||||
promptEditInput: (target: OperationID, positions: IOperationPosition[]) => void;
|
promptEditInput: (target: OperationID, positions: IOperationPosition[]) => void;
|
||||||
promptEditOperation: (target: OperationID, positions: IOperationPosition[]) => void;
|
promptEditOperation: (target: OperationID, positions: IOperationPosition[]) => void;
|
||||||
executeOperation: (target: OperationID, positions: IOperationPosition[]) => void;
|
executeOperation: (target: OperationID, positions: IOperationPosition[]) => void;
|
||||||
promptRelocateConstituents: (target: OperationID) => void;
|
promptRelocateConstituents: (target: OperationID, positions: IOperationPosition[]) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const OssEditContext = createContext<IOssEditContext | null>(null);
|
const OssEditContext = createContext<IOssEditContext | null>(null);
|
||||||
|
@ -360,16 +360,20 @@ export const OssEditState = ({ selected, setSelected, children }: React.PropsWit
|
||||||
[model]
|
[model]
|
||||||
);
|
);
|
||||||
|
|
||||||
const promptRelocateConstituents = useCallback((target: OperationID) => {
|
const promptRelocateConstituents = useCallback((target: OperationID, positions: IOperationPosition[]) => {
|
||||||
|
setPositions(positions);
|
||||||
setTargetOperationID(target);
|
setTargetOperationID(target);
|
||||||
setShowRelocateConstituents(true);
|
setShowRelocateConstituents(true);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleRelocateConstituents = useCallback((data: ICstRelocateData) => {
|
const handleRelocateConstituents = useCallback(
|
||||||
// TODO: implement backed call
|
(data: ICstRelocateData) => {
|
||||||
console.log(data);
|
model.savePositions({ positions: positions }, () =>
|
||||||
toast.success('В разработке');
|
model.relocateConstituents(data, () => toast.success(information.changesSaved))
|
||||||
}, []);
|
);
|
||||||
|
},
|
||||||
|
[model, positions]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<OssEditContext.Provider
|
<OssEditContext.Provider
|
||||||
|
|
Loading…
Reference in New Issue
Block a user