F: Propagate cst_delete

This commit is contained in:
Ivan 2024-08-11 12:38:08 +03:00
parent 71ba81ea0e
commit 5d6c911583
8 changed files with 140 additions and 35 deletions

View File

@ -26,7 +26,7 @@ CstMapping = dict[str, Constituenta]
class ChangeManager: class ChangeManager:
''' Change propagation API. ''' ''' Change propagation wrapper for OSS. '''
class Cache: class Cache:
''' Cache for RSForm constituents. ''' ''' Cache for RSForm constituents. '''
@ -102,6 +102,15 @@ class ChangeManager:
''' Insert new inheritance. ''' ''' Insert new inheritance. '''
self.inheritance[inheritance.operation_id].append((inheritance.parent_id, inheritance.child_id)) self.inheritance[inheritance.operation_id].append((inheritance.parent_id, inheritance.child_id))
def remove_cst(self, target: list[int], operation: int) -> None:
''' Remove constituents from operation. '''
subs = [sub for sub in self.substitutions if sub.original_id in target or sub.substitution_id in target]
for sub in subs:
self.substitutions.remove(sub)
to_delete = [item for item in self.inheritance[operation] if item[1] in target]
for item in to_delete:
self.inheritance[operation].remove(item)
def _insert_new(self, schema: RSForm) -> None: def _insert_new(self, schema: RSForm) -> None:
self._schemas.append(schema) self._schemas.append(schema)
self._schema_by_id[schema.model.pk] = schema self._schema_by_id[schema.model.pk] = schema
@ -142,6 +151,37 @@ class ChangeManager:
alias_mapping[alias] = cst alias_mapping[alias] = cst
self._cascade_update_cst(target.pk, operation, data, old_data, alias_mapping) self._cascade_update_cst(target.pk, operation, data, old_data, alias_mapping)
def before_delete(self, target: list[Constituenta], source: RSForm) -> None:
''' Trigger cascade resolutions before constituents are deleted. '''
self.cache.insert(source)
operation = self.cache.get_operation(source)
self._cascade_before_delete(target, operation)
def _cascade_before_delete(self, target: list[Constituenta], operation: Operation) -> None:
children = self.cache.graph.outputs[operation.pk]
if len(children) == 0:
return
self.cache.ensure_loaded()
for child_id in children:
child_operation = self.cache.operation_by_id[child_id]
child_schema = self.cache.get_schema(child_operation)
if child_schema is None:
continue
child_schema.cache.ensure_loaded()
# TODO: check if substitutions are affected. Undo substitutions before deletion
child_target_cst = []
child_target_ids = []
for cst in target:
successor_id = self.cache.get_successor_for(cst.pk, child_id, ignore_substitution=True)
if successor_id is not None:
child_target_ids.append(successor_id)
child_target_cst.append(child_schema.cache.by_id[successor_id])
self._cascade_before_delete(child_target_cst, child_operation)
self.cache.remove_cst(child_target_ids, child_id)
child_schema.delete_cst(child_target_cst)
def _cascade_create_cst(self, prototype: Constituenta, operation: Operation, mapping: CstMapping) -> None: def _cascade_create_cst(self, prototype: Constituenta, operation: Operation, mapping: CstMapping) -> None:
children = self.cache.graph.outputs[operation.pk] children = self.cache.graph.outputs[operation.pk]
if len(children) == 0: if len(children) == 0:

View File

@ -0,0 +1,42 @@
''' Models: Change propagation facade - managing all changes in OSS. '''
from apps.library.models import LibraryItem
from apps.rsform.models import Constituenta, RSForm
from .ChangeManager import ChangeManager
def _get_oss_hosts(item: LibraryItem) -> list[LibraryItem]:
''' Get all hosts for LibraryItem. '''
return list(LibraryItem.objects.filter(operations__result=item).only('pk'))
class PropagationFacade:
''' Change propagation API. '''
@classmethod
def on_create_cst(cls, new_cst: Constituenta, source: RSForm) -> None:
''' Trigger cascade resolutions when new constituent is created. '''
hosts = _get_oss_hosts(source.model)
for host in hosts:
ChangeManager(host).on_create_cst(new_cst, source)
@classmethod
def on_change_cst_type(cls, target: Constituenta, source: RSForm) -> None:
''' Trigger cascade resolutions when constituenta type is changed. '''
hosts = _get_oss_hosts(source.model)
for host in hosts:
ChangeManager(host).on_change_cst_type(target, source)
@classmethod
def on_update_cst(cls, target: Constituenta, data: dict, old_data: dict, source: RSForm) -> None:
''' Trigger cascade resolutions when constituenta data is changed. '''
hosts = _get_oss_hosts(source.model)
for host in hosts:
ChangeManager(host).on_update_cst(target, data, old_data, source)
@classmethod
def before_delete(cls, target: list[Constituenta], source: RSForm) -> None:
''' Trigger cascade resolutions before constituents are deleted. '''
hosts = _get_oss_hosts(source.model)
for host in hosts:
ChangeManager(host).before_delete(target, source)

View File

@ -6,3 +6,4 @@ from .Inheritance import Inheritance
from .Operation import Operation, OperationType from .Operation import Operation, OperationType
from .OperationSchema import OperationSchema from .OperationSchema import OperationSchema
from .Substitution import Substitution from .Substitution import Substitution
from .PropagationFacade import PropagationFacade

View File

@ -57,7 +57,6 @@ 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)
@decl_endpoint('/api/rsforms/{schema}/create-cst', method='post') @decl_endpoint('/api/rsforms/{schema}/create-cst', method='post')
def test_create_constituenta(self): def test_create_constituenta(self):
data = { data = {
@ -107,3 +106,14 @@ class TestChangeConstituents(EndpointTester):
self.assertEqual(inherited_cst.term_raw, data['item_data']['term_raw']) self.assertEqual(inherited_cst.term_raw, data['item_data']['term_raw'])
self.assertEqual(inherited_cst.definition_formal, r'X1\X1') self.assertEqual(inherited_cst.definition_formal, r'X1\X1')
self.assertEqual(inherited_cst.definition_raw, r'@{X2|sing,datv}') 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]}
response = self.executeOK(data=data, schema=self.ks2.model.pk)
inherited_cst = Constituenta.objects.get(as_child__parent_id=self.ks2D1.pk)
self.ks2D1.refresh_from_db()
self.assertEqual(self.ks2.constituents().count(), 1)
self.assertEqual(self.ks3.constituents().count(), 3)
self.assertEqual(self.ks2D1.definition_formal, r'DEL\DEL')
self.assertEqual(inherited_cst.definition_formal, r'DEL\DEL')

View File

@ -23,6 +23,7 @@ from .api_RSLanguage import (
from .Constituenta import Constituenta, CstType, extract_globals from .Constituenta import Constituenta, CstType, extract_globals
INSERT_LAST: int = -1 INSERT_LAST: int = -1
DELETED_ALIAS = 'DEL'
class RSForm: class RSForm:
@ -348,10 +349,12 @@ class RSForm:
def delete_cst(self, target: Iterable[Constituenta]) -> None: def delete_cst(self, target: Iterable[Constituenta]) -> None:
''' Delete multiple constituents. Do not check if listCst are from this schema. ''' ''' Delete multiple constituents. Do not check if listCst are from this schema. '''
mapping = {cst.alias: DELETED_ALIAS for cst in target}
self.cache.ensure_loaded()
self.cache.remove_multi(target) self.cache.remove_multi(target)
self.apply_mapping(mapping)
Constituenta.objects.filter(pk__in=[cst.pk for cst in target]).delete() Constituenta.objects.filter(pk__in=[cst.pk for cst in target]).delete()
self._reset_order() self._reset_order()
self.resolve_all_text()
self.save() self.save()
def substitute(self, substitutions: list[tuple[Constituenta, Constituenta]]) -> None: def substitute(self, substitutions: list[tuple[Constituenta, Constituenta]]) -> None:

View File

@ -1,4 +1,4 @@
''' Django: Models. ''' ''' Django: Models. '''
from .Constituenta import Constituenta, CstType, extract_globals, replace_entities, replace_globals from .Constituenta import Constituenta, CstType, extract_globals, replace_entities, replace_globals
from .RSForm import INSERT_LAST, RSForm from .RSForm import DELETED_ALIAS, INSERT_LAST, RSForm, SemanticInfo

View File

@ -174,6 +174,26 @@ class TestRSForm(DBTester):
self.assertEqual(s2.definition_raw, '@{X11|plur}') self.assertEqual(s2.definition_raw, '@{X11|plur}')
def test_delete_cst(self):
x1 = self.schema.insert_new('X1')
x2 = self.schema.insert_new('X2')
d1 = self.schema.insert_new(
alias='D1',
definition_formal='X1 = X2',
definition_raw='@{X1|sing}',
term_raw='@{X2|plur}'
)
self.schema.delete_cst([x1])
x2.refresh_from_db()
d1.refresh_from_db()
self.assertEqual(self.schema.constituents().count(), 2)
self.assertEqual(x2.order, 1)
self.assertEqual(d1.order, 2)
self.assertEqual(d1.definition_formal, 'DEL = X2')
self.assertEqual(d1.definition_raw, '@{DEL|sing}')
self.assertEqual(d1.term_raw, '@{X2|plur}')
def test_apply_mapping(self): def test_apply_mapping(self):
x1 = self.schema.insert_new('X1') x1 = self.schema.insert_new('X1')
x2 = self.schema.insert_new('X11') x2 = self.schema.insert_new('X11')

View File

@ -16,7 +16,7 @@ from rest_framework.serializers import ValidationError
from apps.library.models import AccessPolicy, LibraryItem, LibraryItemType, LocationHead from apps.library.models import AccessPolicy, LibraryItem, LibraryItemType, LocationHead
from apps.library.serializers import LibraryItemSerializer from apps.library.serializers import LibraryItemSerializer
from apps.oss.models import ChangeManager from apps.oss.models import PropagationFacade
from apps.users.models import User from apps.users.models import User
from shared import messages as msg from shared import messages as msg
from shared import permissions, utility from shared import permissions, utility
@ -84,15 +84,10 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
insert_after = None insert_after = None
else: else:
insert_after = data['insert_after'] insert_after = data['insert_after']
schema = m.RSForm(self._get_item()) schema = m.RSForm(self._get_item())
with transaction.atomic(): with transaction.atomic():
new_cst = schema.create_cst(data, insert_after) new_cst = schema.create_cst(data, insert_after)
hosts = LibraryItem.objects.filter(operations__result=schema.model) PropagationFacade.on_create_cst(new_cst, schema)
for host in hosts:
ChangeManager(host).on_create_cst(new_cst, schema)
return Response( return Response(
status=c.HTTP_201_CREATED, status=c.HTTP_201_CREATED,
data={ data={
@ -118,16 +113,12 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
model = self._get_item() model = self._get_item()
serializer = s.CstUpdateSerializer(data=request.data, partial=True, context={'schema': model}) serializer = s.CstUpdateSerializer(data=request.data, partial=True, context={'schema': model})
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
cst = cast(m.Constituenta, serializer.validated_data['target']) cst = cast(m.Constituenta, serializer.validated_data['target'])
schema = m.RSForm(model) schema = m.RSForm(model)
data = serializer.validated_data['item_data'] data = serializer.validated_data['item_data']
with transaction.atomic(): with transaction.atomic():
hosts = LibraryItem.objects.filter(operations__result=model)
old_data = schema.update_cst(cst, data) old_data = schema.update_cst(cst, data)
for host in hosts: PropagationFacade.on_update_cst(cst, data, old_data, schema)
ChangeManager(host).on_update_cst(cst, data, old_data, schema)
return Response( return Response(
status=c.HTTP_200_OK, status=c.HTTP_200_OK,
data=s.CstSerializer(m.Constituenta.objects.get(pk=request.data['target'])).data data=s.CstSerializer(m.Constituenta.objects.get(pk=request.data['target'])).data
@ -164,13 +155,15 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
status=c.HTTP_400_BAD_REQUEST, status=c.HTTP_400_BAD_REQUEST,
data={f'{cst.pk}': msg.constituentaNoStructure()} data={f'{cst.pk}': msg.constituentaNoStructure()}
) )
schema = m.RSForm(model)
with transaction.atomic(): with transaction.atomic():
result = m.RSForm(model).produce_structure(cst, cst_parse) result = schema.produce_structure(cst, cst_parse)
return Response( return Response(
status=c.HTTP_200_OK, status=c.HTTP_200_OK,
data={ data={
'cst_list': result, 'cst_list': result,
'schema': s.RSFormParseSerializer(model).data 'schema': s.RSFormParseSerializer(schema.model).data
} }
) )
@ -191,28 +184,23 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
model = self._get_item() model = self._get_item()
serializer = s.CstRenameSerializer(data=request.data, context={'schema': model}) serializer = s.CstRenameSerializer(data=request.data, context={'schema': model})
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
cst = cast(m.Constituenta, serializer.validated_data['target']) cst = cast(m.Constituenta, serializer.validated_data['target'])
changed_type = cst.cst_type != serializer.validated_data['cst_type'] changed_type = cst.cst_type != serializer.validated_data['cst_type']
mapping = {cst.alias: serializer.validated_data['alias']} mapping = {cst.alias: serializer.validated_data['alias']}
cst.alias = serializer.validated_data['alias']
cst.cst_type = serializer.validated_data['cst_type']
schema = m.RSForm(model) schema = m.RSForm(model)
with transaction.atomic(): with transaction.atomic():
cst.alias = serializer.validated_data['alias']
cst.cst_type = serializer.validated_data['cst_type']
cst.save() cst.save()
schema.apply_mapping(mapping=mapping, change_aliases=False) schema.apply_mapping(mapping=mapping, change_aliases=False)
cst.refresh_from_db() cst.refresh_from_db()
if changed_type: if changed_type:
hosts = LibraryItem.objects.filter(operations__result=model) PropagationFacade.on_change_cst_type(cst, schema)
for host in hosts:
ChangeManager(host).on_change_cst_type(cst, schema)
return Response( return Response(
status=c.HTTP_200_OK, status=c.HTTP_200_OK,
data={ data={
'new_cst': s.CstSerializer(cst).data, 'new_cst': s.CstSerializer(cst).data,
'schema': s.RSFormParseSerializer(model).data 'schema': s.RSFormParseSerializer(schema.model).data
} }
) )
@ -236,19 +224,17 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
context={'schema': model} context={'schema': model}
) )
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
schema = m.RSForm(model)
substitutions: list[tuple[m.Constituenta, m.Constituenta]] = []
with transaction.atomic(): with transaction.atomic():
substitutions: list[tuple[m.Constituenta, m.Constituenta]] = []
for substitution in serializer.validated_data['substitutions']: for substitution in serializer.validated_data['substitutions']:
original = cast(m.Constituenta, substitution['original']) original = cast(m.Constituenta, substitution['original'])
replacement = cast(m.Constituenta, substitution['substitution']) replacement = cast(m.Constituenta, substitution['substitution'])
substitutions.append((original, replacement)) substitutions.append((original, replacement))
m.RSForm(model).substitute(substitutions) schema.substitute(substitutions)
model.refresh_from_db()
return Response( return Response(
status=c.HTTP_200_OK, status=c.HTTP_200_OK,
data=s.RSFormParseSerializer(model).data data=s.RSFormParseSerializer(schema.model).data
) )
@extend_schema( @extend_schema(
@ -271,11 +257,14 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
context={'schema': model} context={'schema': model}
) )
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
cst_list: list[m.Constituenta] = serializer.validated_data['items']
schema = m.RSForm(model)
with transaction.atomic(): with transaction.atomic():
m.RSForm(model).delete_cst(serializer.validated_data['items']) PropagationFacade.before_delete(cst_list, schema)
schema.delete_cst(cst_list)
return Response( return Response(
status=c.HTTP_200_OK, status=c.HTTP_200_OK,
data=s.RSFormParseSerializer(model).data data=s.RSFormParseSerializer(schema.model).data
) )
@extend_schema( @extend_schema(