mirror of
https://github.com/IRBorisov/ConceptPortal.git
synced 2025-06-26 21:10:38 +03:00
F: Propagate cst_delete
This commit is contained in:
parent
71ba81ea0e
commit
5d6c911583
|
@ -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:
|
||||||
|
|
42
rsconcept/backend/apps/oss/models/PropagationFacade.py
Normal file
42
rsconcept/backend/apps/oss/models/PropagationFacade.py
Normal 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)
|
|
@ -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
|
||||||
|
|
|
@ -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')
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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')
|
||||||
|
|
|
@ -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']}
|
||||||
|
schema = m.RSForm(model)
|
||||||
|
with transaction.atomic():
|
||||||
cst.alias = serializer.validated_data['alias']
|
cst.alias = serializer.validated_data['alias']
|
||||||
cst.cst_type = serializer.validated_data['cst_type']
|
cst.cst_type = serializer.validated_data['cst_type']
|
||||||
schema = m.RSForm(model)
|
|
||||||
|
|
||||||
with transaction.atomic():
|
|
||||||
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)
|
||||||
with transaction.atomic():
|
|
||||||
substitutions: list[tuple[m.Constituenta, m.Constituenta]] = []
|
substitutions: list[tuple[m.Constituenta, m.Constituenta]] = []
|
||||||
|
with transaction.atomic():
|
||||||
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(
|
||||||
|
|
Loading…
Reference in New Issue
Block a user