From 5d6c9115835b962fae8e224277d640836a1d4ce2 Mon Sep 17 00:00:00 2001 From: Ivan <8611739+IRBorisov@users.noreply.github.com> Date: Sun, 11 Aug 2024 12:38:08 +0300 Subject: [PATCH] F: Propagate cst_delete --- .../backend/apps/oss/models/ChangeManager.py | 42 ++++++++++++++- .../apps/oss/models/PropagationFacade.py | 42 +++++++++++++++ rsconcept/backend/apps/oss/models/__init__.py | 1 + .../tests/s_views/t_change_constituents.py | 12 ++++- .../backend/apps/rsform/models/RSForm.py | 5 +- .../backend/apps/rsform/models/__init__.py | 2 +- .../apps/rsform/tests/s_models/t_RSForm.py | 20 ++++++++ .../backend/apps/rsform/views/rsforms.py | 51 ++++++++----------- 8 files changed, 140 insertions(+), 35 deletions(-) create mode 100644 rsconcept/backend/apps/oss/models/PropagationFacade.py diff --git a/rsconcept/backend/apps/oss/models/ChangeManager.py b/rsconcept/backend/apps/oss/models/ChangeManager.py index c006c163..bd7d1d22 100644 --- a/rsconcept/backend/apps/oss/models/ChangeManager.py +++ b/rsconcept/backend/apps/oss/models/ChangeManager.py @@ -26,7 +26,7 @@ CstMapping = dict[str, Constituenta] class ChangeManager: - ''' Change propagation API. ''' + ''' Change propagation wrapper for OSS. ''' class Cache: ''' Cache for RSForm constituents. ''' @@ -102,6 +102,15 @@ class ChangeManager: ''' Insert new inheritance. ''' 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: self._schemas.append(schema) self._schema_by_id[schema.model.pk] = schema @@ -142,6 +151,37 @@ class ChangeManager: alias_mapping[alias] = cst 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: children = self.cache.graph.outputs[operation.pk] if len(children) == 0: diff --git a/rsconcept/backend/apps/oss/models/PropagationFacade.py b/rsconcept/backend/apps/oss/models/PropagationFacade.py new file mode 100644 index 00000000..abeef19f --- /dev/null +++ b/rsconcept/backend/apps/oss/models/PropagationFacade.py @@ -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) diff --git a/rsconcept/backend/apps/oss/models/__init__.py b/rsconcept/backend/apps/oss/models/__init__.py index fa3be516..0de32d63 100644 --- a/rsconcept/backend/apps/oss/models/__init__.py +++ b/rsconcept/backend/apps/oss/models/__init__.py @@ -6,3 +6,4 @@ from .Inheritance import Inheritance from .Operation import Operation, OperationType from .OperationSchema import OperationSchema from .Substitution import Substitution +from .PropagationFacade import PropagationFacade diff --git a/rsconcept/backend/apps/oss/tests/s_views/t_change_constituents.py b/rsconcept/backend/apps/oss/tests/s_views/t_change_constituents.py index b2b76fb3..a8b5f09a 100644 --- a/rsconcept/backend/apps/oss/tests/s_views/t_change_constituents.py +++ b/rsconcept/backend/apps/oss/tests/s_views/t_change_constituents.py @@ -57,7 +57,6 @@ class TestChangeConstituents(EndpointTester): self.ks3 = RSForm(self.operation3.result) self.assertEqual(self.ks3.constituents().count(), 4) - @decl_endpoint('/api/rsforms/{schema}/create-cst', method='post') def test_create_constituenta(self): data = { @@ -107,3 +106,14 @@ class TestChangeConstituents(EndpointTester): 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_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') diff --git a/rsconcept/backend/apps/rsform/models/RSForm.py b/rsconcept/backend/apps/rsform/models/RSForm.py index 817c27e1..fbbc70b8 100644 --- a/rsconcept/backend/apps/rsform/models/RSForm.py +++ b/rsconcept/backend/apps/rsform/models/RSForm.py @@ -23,6 +23,7 @@ from .api_RSLanguage import ( from .Constituenta import Constituenta, CstType, extract_globals INSERT_LAST: int = -1 +DELETED_ALIAS = 'DEL' class RSForm: @@ -348,10 +349,12 @@ class RSForm: def delete_cst(self, target: Iterable[Constituenta]) -> None: ''' 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.apply_mapping(mapping) Constituenta.objects.filter(pk__in=[cst.pk for cst in target]).delete() self._reset_order() - self.resolve_all_text() self.save() def substitute(self, substitutions: list[tuple[Constituenta, Constituenta]]) -> None: diff --git a/rsconcept/backend/apps/rsform/models/__init__.py b/rsconcept/backend/apps/rsform/models/__init__.py index f091858f..01056a6d 100644 --- a/rsconcept/backend/apps/rsform/models/__init__.py +++ b/rsconcept/backend/apps/rsform/models/__init__.py @@ -1,4 +1,4 @@ ''' Django: Models. ''' 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 diff --git a/rsconcept/backend/apps/rsform/tests/s_models/t_RSForm.py b/rsconcept/backend/apps/rsform/tests/s_models/t_RSForm.py index a71854cd..bae230fe 100644 --- a/rsconcept/backend/apps/rsform/tests/s_models/t_RSForm.py +++ b/rsconcept/backend/apps/rsform/tests/s_models/t_RSForm.py @@ -174,6 +174,26 @@ class TestRSForm(DBTester): 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): x1 = self.schema.insert_new('X1') x2 = self.schema.insert_new('X11') diff --git a/rsconcept/backend/apps/rsform/views/rsforms.py b/rsconcept/backend/apps/rsform/views/rsforms.py index e1844729..eeb61034 100644 --- a/rsconcept/backend/apps/rsform/views/rsforms.py +++ b/rsconcept/backend/apps/rsform/views/rsforms.py @@ -16,7 +16,7 @@ from rest_framework.serializers import ValidationError from apps.library.models import AccessPolicy, LibraryItem, LibraryItemType, LocationHead 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 shared import messages as msg from shared import permissions, utility @@ -84,15 +84,10 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr insert_after = None else: insert_after = data['insert_after'] - schema = m.RSForm(self._get_item()) with transaction.atomic(): new_cst = schema.create_cst(data, insert_after) - hosts = LibraryItem.objects.filter(operations__result=schema.model) - for host in hosts: - ChangeManager(host).on_create_cst(new_cst, schema) - - + PropagationFacade.on_create_cst(new_cst, schema) return Response( status=c.HTTP_201_CREATED, data={ @@ -118,16 +113,12 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr model = self._get_item() serializer = s.CstUpdateSerializer(data=request.data, partial=True, context={'schema': model}) serializer.is_valid(raise_exception=True) - cst = cast(m.Constituenta, serializer.validated_data['target']) schema = m.RSForm(model) data = serializer.validated_data['item_data'] with transaction.atomic(): - hosts = LibraryItem.objects.filter(operations__result=model) old_data = schema.update_cst(cst, data) - for host in hosts: - ChangeManager(host).on_update_cst(cst, data, old_data, schema) - + PropagationFacade.on_update_cst(cst, data, old_data, schema) return Response( status=c.HTTP_200_OK, 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, data={f'{cst.pk}': msg.constituentaNoStructure()} ) + schema = m.RSForm(model) + with transaction.atomic(): - result = m.RSForm(model).produce_structure(cst, cst_parse) + result = schema.produce_structure(cst, cst_parse) return Response( status=c.HTTP_200_OK, data={ '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() serializer = s.CstRenameSerializer(data=request.data, context={'schema': model}) serializer.is_valid(raise_exception=True) - cst = cast(m.Constituenta, serializer.validated_data['target']) changed_type = cst.cst_type != serializer.validated_data['cst_type'] 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) - with transaction.atomic(): + cst.alias = serializer.validated_data['alias'] + cst.cst_type = serializer.validated_data['cst_type'] cst.save() schema.apply_mapping(mapping=mapping, change_aliases=False) cst.refresh_from_db() if changed_type: - hosts = LibraryItem.objects.filter(operations__result=model) - for host in hosts: - ChangeManager(host).on_change_cst_type(cst, schema) - + PropagationFacade.on_change_cst_type(cst, schema) return Response( status=c.HTTP_200_OK, 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} ) serializer.is_valid(raise_exception=True) - + schema = m.RSForm(model) + substitutions: list[tuple[m.Constituenta, m.Constituenta]] = [] with transaction.atomic(): - substitutions: list[tuple[m.Constituenta, m.Constituenta]] = [] for substitution in serializer.validated_data['substitutions']: original = cast(m.Constituenta, substitution['original']) replacement = cast(m.Constituenta, substitution['substitution']) substitutions.append((original, replacement)) - m.RSForm(model).substitute(substitutions) - - model.refresh_from_db() + schema.substitute(substitutions) return Response( status=c.HTTP_200_OK, - data=s.RSFormParseSerializer(model).data + data=s.RSFormParseSerializer(schema.model).data ) @extend_schema( @@ -271,11 +257,14 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr context={'schema': model} ) serializer.is_valid(raise_exception=True) + cst_list: list[m.Constituenta] = serializer.validated_data['items'] + schema = m.RSForm(model) 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( status=c.HTTP_200_OK, - data=s.RSFormParseSerializer(model).data + data=s.RSFormParseSerializer(schema.model).data ) @extend_schema(