diff --git a/rsconcept/backend/apps/oss/models/ChangeManager.py b/rsconcept/backend/apps/oss/models/ChangeManager.py index 524db3e9..95158733 100644 --- a/rsconcept/backend/apps/oss/models/ChangeManager.py +++ b/rsconcept/backend/apps/oss/models/ChangeManager.py @@ -2,6 +2,7 @@ from typing import Optional, cast from cctext import extract_entities +from rest_framework.serializers import ValidationError from apps.library.models import LibraryItem from apps.rsform.graph import Graph @@ -16,108 +17,20 @@ from apps.rsform.models import ( ) from .Inheritance import Inheritance -from .Operation import Operation +from .Operation import Operation, OperationType from .OperationSchema import OperationSchema from .Substitution import Substitution CstMapping = dict[str, Constituenta] +CstSubstitution = list[tuple[Constituenta, Constituenta]] class ChangeManager: ''' Change propagation wrapper for OSS. ''' - class Cache: - ''' Cache for RSForm constituents. ''' - - def __init__(self, oss: OperationSchema): - self._oss = oss - self._schemas: list[RSForm] = [] - self._schema_by_id: dict[int, RSForm] = {} - - self.operations = list(oss.operations().only('result_id')) - self.operation_by_id = {operation.pk: operation for operation in self.operations} - self.graph = Graph[int]() - for operation in self.operations: - self.graph.add_node(operation.pk) - for argument in self._oss.arguments().only('operation_id', 'argument_id'): - self.graph.add_edge(argument.argument_id, argument.operation_id) - - self.is_loaded = False - self.substitutions: list[Substitution] = [] - self.inheritance: dict[int, list[tuple[int, int]]] = {} - - def insert(self, schema: RSForm) -> None: - ''' Insert new schema. ''' - if not self._schema_by_id.get(schema.model.pk): - self._insert_new(schema) - - def get_schema(self, operation: Operation) -> Optional[RSForm]: - ''' Get schema by Operation. ''' - if operation.result_id is None: - return None - if operation.result_id in self._schema_by_id: - return self._schema_by_id[operation.result_id] - else: - schema = RSForm.from_id(operation.result_id) - schema.cache.ensure_loaded() - self._insert_new(schema) - return schema - - def get_operation(self, schema: RSForm) -> Operation: - ''' Get operation by schema. ''' - for operation in self.operations: - if operation.result_id == schema.model.pk: - return operation - raise ValueError(f'Operation for schema {schema.model.pk} not found') - - def ensure_loaded(self) -> None: - ''' Ensure propagation of changes. ''' - if self.is_loaded: - return - self.is_loaded = True - self.substitutions = list(self._oss.substitutions().only('operation_id', 'original_id', 'substitution_id')) - for operation in self.operations: - self.inheritance[operation.pk] = [] - for item in self._oss.inheritance().only('operation_id', 'parent_id', 'child_id'): - self.inheritance[item.operation_id].append((item.parent_id, item.child_id)) - - def get_successor_for( - self, - parent_cst: int, - operation: int, - ignore_substitution: bool = False - ) -> Optional[int]: - ''' Get child for parent inside target RSFrom. ''' - if not ignore_substitution: - for sub in self.substitutions: - if sub.operation_id == operation and sub.original_id == parent_cst: - return sub.substitution_id - for item in self.inheritance[operation]: - if item[0] == parent_cst: - return item[1] - return None - - def insert_inheritance(self, inheritance: Inheritance) -> None: - ''' 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 - def __init__(self, model: LibraryItem): self.oss = OperationSchema(model) - self.cache = ChangeManager.Cache(self.oss) - + self.cache = OssCache(self.oss) def after_create_cst(self, cst_list: list[Constituenta], source: RSForm) -> None: ''' Trigger cascade resolutions when new constituent is created. ''' @@ -159,6 +72,33 @@ class ChangeManager: operation = self.cache.get_operation(source) self._cascade_before_delete(target, operation) + def before_substitute(self, substitutions: CstSubstitution, source: RSForm) -> None: + ''' Trigger cascade resolutions before constituents are substituted. ''' + self.cache.insert(source) + operation = self.cache.get_operation(source) + self._cascade_before_substitute(substitutions, operation) + + def _cascade_before_substitute( + self, + substitutions: CstSubstitution, + 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() + new_substitutions = self._transform_substitutions(substitutions, child_operation, child_schema) + if len(new_substitutions) == 0: + continue + self._cascade_before_substitute(new_substitutions, child_operation) + child_schema.substitute(new_substitutions) + def _cascade_create_cst(self, cst_list: list[Constituenta], operation: Operation, mapping: CstMapping) -> None: children = self.cache.graph.outputs[operation.pk] if len(children) == 0: @@ -195,7 +135,7 @@ class ChangeManager: self.cache.ensure_loaded() for child_id in children: child_operation = self.cache.operation_by_id[child_id] - successor_id = self.cache.get_successor_for(cst_id, child_id, ignore_substitution=True) + successor_id = self.cache.get_inheritor(cst_id, child_id) if successor_id is None: continue child_schema = self.cache.get_schema(child_operation) @@ -215,7 +155,7 @@ class ChangeManager: self.cache.ensure_loaded() for child_id in children: child_operation = self.cache.operation_by_id[child_id] - successor_id = self.cache.get_successor_for(cst_id, child_id, ignore_substitution=True) + successor_id = self.cache.get_inheritor(cst_id, child_id) if successor_id is None: continue child_schema = self.cache.get_schema(child_operation) @@ -251,7 +191,7 @@ class ChangeManager: child_target_cst = [] child_target_ids = [] for cst in target: - successor_id = self.cache.get_successor_for(cst.pk, child_id, ignore_substitution=True) + successor_id = self.cache.get_inheritor(cst.pk, child_id) if successor_id is not None: child_target_ids.append(successor_id) child_target_cst.append(child_schema.cache.by_id[successor_id]) @@ -264,7 +204,7 @@ class ChangeManager: return mapping result: CstMapping = {} for alias, cst in mapping.items(): - successor_id = self.cache.get_successor_for(cst.pk, operation.pk) + successor_id = self.cache.get_successor(cst.pk, operation.pk) if successor_id is None: continue successor = schema.cache.by_id.get(successor_id) @@ -283,8 +223,7 @@ class ChangeManager: if prototype.order == 1: return 1 prev_cst = source.cache.constituents[prototype.order - 2] - inherited_prev_id = self.cache.get_successor_for( - source.cache.constituents[prototype.order - 2].pk, operation.pk) + inherited_prev_id = self.cache.get_successor(prev_cst.pk, operation.pk) if inherited_prev_id is None: return INSERT_LAST prev_cst = destination.cache.by_id[inherited_prev_id] @@ -320,3 +259,142 @@ class ChangeManager: if replace_entities(old_data['definition_raw'], mapping) == cst.definition_raw: new_data['definition_raw'] = replace_entities(data['definition_raw'], mapping) return new_data + + def _transform_substitutions( + self, + target: CstSubstitution, + operation: Operation, + schema: RSForm + ) -> CstSubstitution: + result: CstSubstitution = [] + for current_sub in target: + sub_replaced = False + new_substitution_id = self.cache.get_inheritor(current_sub[1].pk, operation.pk) + if new_substitution_id is None: + for sub in self.cache.substitutions[operation.pk]: + if sub.original_id == current_sub[1].pk: + sub_replaced = True + new_substitution_id = self.cache.get_inheritor(sub.original_id, operation.pk) + break + + new_original_id = self.cache.get_inheritor(current_sub[0].pk, operation.pk) + original_replaced = False + if new_original_id is None: + for sub in self.cache.substitutions[operation.pk]: + if sub.original_id == current_sub[0].pk: + original_replaced = True + sub.original_id = current_sub[1].pk + sub.save() + new_original_id = new_substitution_id + new_substitution_id = self.cache.get_inheritor(sub.substitution_id, operation.pk) + break + + if sub_replaced and original_replaced: + raise ValidationError({'propagation': 'Substitution breaks OSS substitutions.'}) + + for sub in self.cache.substitutions[operation.pk]: + if sub.substitution_id == current_sub[0].pk: + sub.substitution_id = current_sub[1].pk + sub.save() + + if new_original_id is not None and new_substitution_id is not None: + result.append((schema.cache.by_id[new_original_id], schema.cache.by_id[new_substitution_id])) + return result + + +class OssCache: + ''' Cache for OSS data. ''' + + def __init__(self, oss: OperationSchema): + self._oss = oss + self._schemas: list[RSForm] = [] + self._schema_by_id: dict[int, RSForm] = {} + + self.operations = list(oss.operations().only('result_id')) + self.operation_by_id = {operation.pk: operation for operation in self.operations} + self.graph = Graph[int]() + for operation in self.operations: + self.graph.add_node(operation.pk) + for argument in self._oss.arguments().only('operation_id', 'argument_id'): + self.graph.add_edge(argument.argument_id, argument.operation_id) + + self.is_loaded = False + self.substitutions: dict[int, list[Substitution]] = {} + self.inheritance: dict[int, list[Inheritance]] = {} + + def insert(self, schema: RSForm) -> None: + ''' Insert new schema. ''' + if not self._schema_by_id.get(schema.model.pk): + self._insert_new(schema) + + def get_schema(self, operation: Operation) -> Optional[RSForm]: + ''' Get schema by Operation. ''' + if operation.result_id is None: + return None + if operation.result_id in self._schema_by_id: + return self._schema_by_id[operation.result_id] + else: + schema = RSForm.from_id(operation.result_id) + schema.cache.ensure_loaded() + self._insert_new(schema) + return schema + + def get_operation(self, schema: RSForm) -> Operation: + ''' Get operation by schema. ''' + for operation in self.operations: + if operation.result_id == schema.model.pk: + return operation + raise ValueError(f'Operation for schema {schema.model.pk} not found') + + def ensure_loaded(self) -> None: + ''' Ensure propagation of changes. ''' + if self.is_loaded: + return + self.is_loaded = True + for operation in self.operations: + if operation.operation_type != OperationType.INPUT: + self.inheritance[operation.pk] = [] + self.substitutions[operation.pk] = [] + for sub in self._oss.substitutions().only('operation_id', 'original_id', 'substitution_id'): + self.substitutions[sub.operation_id].append(sub) + for item in self._oss.inheritance().only('operation_id', 'parent_id', 'child_id'): + self.inheritance[item.operation_id].append(item) + + def get_inheritor(self, parent_cst: int, operation: int) -> Optional[int]: + ''' Get child for parent inside target RSFrom. ''' + for item in self.inheritance[operation]: + if item.parent_id == parent_cst: + return item.child_id + return None + + def get_successor(self, parent_cst: int, operation: int) -> Optional[int]: + ''' Get child for parent inside target RSFrom including substitutions. ''' + for sub in self.substitutions[operation]: + if sub.original_id == parent_cst: + return self.get_inheritor(sub.substitution_id, operation) + return self.get_inheritor(parent_cst, operation) + + + def insert_inheritance(self, inheritance: Inheritance) -> None: + ''' Insert new inheritance. ''' + self.inheritance[inheritance.operation_id].append(inheritance) + + def insert_substitution(self, sub: Substitution) -> None: + ''' Insert new inheritance. ''' + self.substitutions[sub.operation_id].append(sub) + + def remove_cst(self, target: list[int], operation: int) -> None: + ''' Remove constituents from operation. ''' + subs_to_delete = [ + sub for sub in self.substitutions[operation] + if sub.original_id in target or sub.substitution_id in target + ] + for sub in subs_to_delete: + self.substitutions[operation].remove(sub) + inherit_to_delete = [item for item in self.inheritance[operation] if item.child_id in target] + for item in inherit_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 diff --git a/rsconcept/backend/apps/oss/models/PropagationFacade.py b/rsconcept/backend/apps/oss/models/PropagationFacade.py index 4017a124..986f0d57 100644 --- a/rsconcept/backend/apps/oss/models/PropagationFacade.py +++ b/rsconcept/backend/apps/oss/models/PropagationFacade.py @@ -40,3 +40,10 @@ class PropagationFacade: hosts = _get_oss_hosts(source.model) for host in hosts: ChangeManager(host).before_delete(target, source) + + @classmethod + def before_substitute(cls, substitutions: list[tuple[Constituenta, Constituenta]], source: RSForm) -> None: + ''' Trigger cascade resolutions before constituents are substituted. ''' + hosts = _get_oss_hosts(source.model) + for host in hosts: + ChangeManager(host).before_substitute(substitutions, source) diff --git a/rsconcept/backend/apps/oss/models/Substitution.py b/rsconcept/backend/apps/oss/models/Substitution.py index 6fdab8b5..e5e39abd 100644 --- a/rsconcept/backend/apps/oss/models/Substitution.py +++ b/rsconcept/backend/apps/oss/models/Substitution.py @@ -29,4 +29,4 @@ class Substitution(Model): verbose_name_plural = 'Таблицы отождествлений' def __str__(self) -> str: - return f'{self.original} -> {self.substitution}' + return f'{self.substitution} -> {self.original}' diff --git a/rsconcept/backend/apps/oss/tests/__init__.py b/rsconcept/backend/apps/oss/tests/__init__.py index a27a6767..c7593b77 100644 --- a/rsconcept/backend/apps/oss/tests/__init__.py +++ b/rsconcept/backend/apps/oss/tests/__init__.py @@ -1,3 +1,4 @@ ''' Tests. ''' from .s_models import * +from .s_propagation import * from .s_views import * diff --git a/rsconcept/backend/apps/oss/tests/s_models/t_Substitution.py b/rsconcept/backend/apps/oss/tests/s_models/t_Substitution.py index c63c5bf5..bc1eb813 100644 --- a/rsconcept/backend/apps/oss/tests/s_models/t_Substitution.py +++ b/rsconcept/backend/apps/oss/tests/s_models/t_Substitution.py @@ -52,7 +52,7 @@ class TestSynthesisSubstitution(TestCase): def test_str(self): - testStr = f'{self.ks1X1} -> {self.ks2X1}' + testStr = f'{self.ks2X1} -> {self.ks1X1}' self.assertEqual(str(self.substitution), testStr) diff --git a/rsconcept/backend/apps/oss/tests/s_propagation/__init__.py b/rsconcept/backend/apps/oss/tests/s_propagation/__init__.py new file mode 100644 index 00000000..3e4ff908 --- /dev/null +++ b/rsconcept/backend/apps/oss/tests/s_propagation/__init__.py @@ -0,0 +1,4 @@ +''' Tests for REST API OSS propagation. ''' +from .t_attributes import * +from .t_constituents import * +from .t_substitutions import * diff --git a/rsconcept/backend/apps/oss/tests/s_views/t_change_attributes.py b/rsconcept/backend/apps/oss/tests/s_propagation/t_attributes.py similarity index 100% rename from rsconcept/backend/apps/oss/tests/s_views/t_change_attributes.py rename to rsconcept/backend/apps/oss/tests/s_propagation/t_attributes.py diff --git a/rsconcept/backend/apps/oss/tests/s_views/t_change_constituents.py b/rsconcept/backend/apps/oss/tests/s_propagation/t_constituents.py similarity index 88% rename from rsconcept/backend/apps/oss/tests/s_views/t_change_constituents.py rename to rsconcept/backend/apps/oss/tests/s_propagation/t_constituents.py index a8b5f09a..0d9ec899 100644 --- a/rsconcept/backend/apps/oss/tests/s_views/t_change_constituents.py +++ b/rsconcept/backend/apps/oss/tests/s_propagation/t_constituents.py @@ -117,3 +117,18 @@ class TestChangeConstituents(EndpointTester): 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') + + @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') + data = {'substitutions': [{ + 'original': self.ks1X1.pk, + 'substitution': self.ks1X2.pk + }]} + self.executeOK(data=data, schema=self.ks1.model.pk) + self.ks1X2.refresh_from_db() + d2.refresh_from_db() + self.assertEqual(self.ks1.constituents().count(), 1) + self.assertEqual(self.ks3.constituents().count(), 4) + self.assertEqual(self.ks1X2.order, 1) + self.assertEqual(d2.definition_formal, r'X2\X2\X3') diff --git a/rsconcept/backend/apps/oss/tests/s_propagation/t_substitutions.py b/rsconcept/backend/apps/oss/tests/s_propagation/t_substitutions.py new file mode 100644 index 00000000..9e6bba1b --- /dev/null +++ b/rsconcept/backend/apps/oss/tests/s_propagation/t_substitutions.py @@ -0,0 +1,160 @@ +''' Testing API: Change substitutions in OSS. ''' + +from apps.oss.models import OperationSchema, OperationType +from apps.rsform.models import Constituenta, CstType, RSForm +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( + title='Test', + alias='T1', + owner=self.user + ) + self.owned_id = self.owned.model.pk + + self.ks1 = RSForm.create( + alias='KS1', + title='Test1', + owner=self.user + ) + self.ks1X1 = self.ks1.insert_new('X1', convention='KS1X1') + self.ks1X2 = self.ks1.insert_new('X2', convention='KS1X2') + self.ks1D1 = self.ks1.insert_new('D1', definition_formal='X1 X2', convention='KS1D1') + + self.ks2 = RSForm.create( + alias='KS2', + title='Test2', + owner=self.user + ) + self.ks2X1 = self.ks2.insert_new('X1', convention='KS2X1') + self.ks2X2 = self.ks2.insert_new('X2', convention='KS2X2') + self.ks2S1 = self.ks2.insert_new( + alias='S1', + definition_formal=r'X1', + convention='KS2S1' + ) + + self.ks3 = RSForm.create( + alias='KS3', + title='Test3', + owner=self.user + ) + self.ks3X1 = self.ks3.insert_new('X1', convention='KS3X1') + self.ks3D1 = self.ks3.insert_new( + alias='D1', + definition_formal='X1 X1', + convention='KS3D1' + ) + + self.operation1 = self.owned.create_operation( + alias='1', + operation_type=OperationType.INPUT, + result=self.ks1.model + ) + self.operation2 = self.owned.create_operation( + alias='2', + operation_type=OperationType.INPUT, + result=self.ks2.model + ) + self.operation3 = self.owned.create_operation( + alias='3', + operation_type=OperationType.INPUT, + result=self.ks3.model + ) + + self.operation4 = self.owned.create_operation( + alias='4', + operation_type=OperationType.SYNTHESIS + ) + self.owned.set_arguments(self.operation4, [self.operation1, self.operation2]) + self.owned.set_substitutions(self.operation4, [{ + 'original': self.ks1X1, + 'substitution': self.ks2S1 + }]) + self.owned.execute_operation(self.operation4) + self.operation4.refresh_from_db() + self.ks4 = RSForm(self.operation4.result) + self.ks4X1 = Constituenta.objects.get(as_child__parent_id=self.ks1X2.pk) + self.ks4S1 = Constituenta.objects.get(as_child__parent_id=self.ks2S1.pk) + self.ks4D1 = Constituenta.objects.get(as_child__parent_id=self.ks1D1.pk) + self.ks4D2 = self.ks4.insert_new( + alias='D2', + definition_formal=r'X1 X2 X3 S1 D1', + convention='KS4D2' + ) + + self.operation5 = self.owned.create_operation( + alias='5', + operation_type=OperationType.SYNTHESIS + ) + self.owned.set_arguments(self.operation5, [self.operation4, self.operation3]) + self.owned.set_substitutions(self.operation5, [{ + 'original': self.ks4X1, + 'substitution': self.ks3X1 + }]) + self.owned.execute_operation(self.operation5) + self.operation5.refresh_from_db() + self.ks5 = RSForm(self.operation5.result) + self.ks5D4 = self.ks5.insert_new( + alias='D4', + definition_formal=r'X1 X2 X3 S1 D1 D2 D3', + convention='KS5D4' + ) + + + def test_oss_setup(self): + self.assertEqual(self.ks1.constituents().count(), 3) + self.assertEqual(self.ks2.constituents().count(), 3) + self.assertEqual(self.ks3.constituents().count(), 2) + self.assertEqual(self.ks4.constituents().count(), 6) + 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': [{ + 'original': self.ks1X1.pk, + 'substitution': self.ks1X2.pk + }]} + self.executeOK(data=data, schema=self.ks1.model.pk) + self.ks4D1.refresh_from_db() + self.ks4D2.refresh_from_db() + self.ks5D4.refresh_from_db() + subs1_2 = self.operation4.getSubstitutions() + self.assertEqual(subs1_2.count(), 1) + self.assertEqual(subs1_2.first().original, self.ks1X2) + self.assertEqual(subs1_2.first().substitution, self.ks2S1) + subs3_4 = self.operation5.getSubstitutions() + self.assertEqual(subs3_4.count(), 1) + self.assertEqual(subs3_4.first().original, self.ks4S1) + self.assertEqual(subs3_4.first().substitution, self.ks3X1) + self.assertEqual(self.ks4D1.definition_formal, r'S1 S1') + self.assertEqual(self.ks4D2.definition_formal, r'S1 X2 X3 S1 D1') + self.assertEqual(self.ks5D4.definition_formal, r'X1 X2 X3 X1 D1 D2 D3') + + @decl_endpoint('/api/rsforms/{schema}/substitute', method='patch') + def test_substitute_substitution(self): + data = {'substitutions': [{ + 'original': self.ks2S1.pk, + 'substitution': self.ks2X1.pk + }]} + self.executeOK(data=data, schema=self.ks2.model.pk) + self.ks4D1.refresh_from_db() + self.ks4D2.refresh_from_db() + self.ks5D4.refresh_from_db() + subs1_2 = self.operation4.getSubstitutions() + self.assertEqual(subs1_2.count(), 1) + self.assertEqual(subs1_2.first().original, self.ks1X1) + self.assertEqual(subs1_2.first().substitution, self.ks2X1) + subs3_4 = self.operation5.getSubstitutions() + self.assertEqual(subs3_4.count(), 1) + self.assertEqual(subs3_4.first().original, self.ks4X1) + self.assertEqual(subs3_4.first().substitution, self.ks3X1) + self.assertEqual(self.ks4D1.definition_formal, r'X2 X1') + self.assertEqual(self.ks4D2.definition_formal, r'X1 X2 X3 X2 D1') + self.assertEqual(self.ks5D4.definition_formal, r'X1 X2 X3 X2 D1 D2 D3') diff --git a/rsconcept/backend/apps/oss/tests/s_views/__init__.py b/rsconcept/backend/apps/oss/tests/s_views/__init__.py index a5a2e6cb..10c776a8 100644 --- a/rsconcept/backend/apps/oss/tests/s_views/__init__.py +++ b/rsconcept/backend/apps/oss/tests/s_views/__init__.py @@ -1,4 +1,2 @@ ''' Tests for REST API. ''' -from .t_change_attributes import * -from .t_change_constituents import * from .t_oss import * diff --git a/rsconcept/backend/apps/rsform/models/RSForm.py b/rsconcept/backend/apps/rsform/models/RSForm.py index 457fc2d9..2f204409 100644 --- a/rsconcept/backend/apps/rsform/models/RSForm.py +++ b/rsconcept/backend/apps/rsform/models/RSForm.py @@ -236,7 +236,7 @@ class RSForm: **kwargs ) self.cache.insert(result) - self.save() + self.save(update_fields=['time_update']) return result def insert_copy(self, items: list[Constituenta], position: int = INSERT_LAST, @@ -271,7 +271,7 @@ class RSForm: new_cst = Constituenta.objects.bulk_create(result) self.cache.insert_multi(new_cst) - self.save() + self.save(update_fields=['time_update']) return result # pylint: disable=too-many-branches @@ -319,7 +319,7 @@ class RSForm: cst.save() if term_changed: self.on_term_change([cst.pk]) - self.save() + self.save(update_fields=['time_update']) return old_data def move_cst(self, target: list[Constituenta], destination: int) -> None: @@ -345,7 +345,7 @@ class RSForm: cst.order = destination + size + count_bot count_bot += 1 Constituenta.objects.bulk_update(cst_list, ['order']) - self.save() + self.save(update_fields=['time_update']) def delete_cst(self, target: Iterable[Constituenta]) -> None: ''' Delete multiple constituents. Do not check if listCst are from this schema. ''' @@ -355,7 +355,7 @@ class RSForm: self.apply_mapping(mapping) Constituenta.objects.filter(pk__in=[cst.pk for cst in target]).delete() self._reset_order() - self.save() + self.save(update_fields=['time_update']) def substitute(self, substitutions: list[tuple[Constituenta, Constituenta]]) -> None: ''' Execute constituenta substitution. ''' @@ -363,12 +363,12 @@ class RSForm: deleted: list[Constituenta] = [] replacements: list[Constituenta] = [] for original, substitution in substitutions: - assert original.pk != substitution.pk mapping[original.alias] = substitution.alias deleted.append(original) replacements.append(substitution) self.cache.remove_multi(deleted) Constituenta.objects.filter(pk__in=[cst.pk for cst in deleted]).delete() + self._reset_order() self.apply_mapping(mapping) self.on_term_change([substitution.pk for substitution in replacements]) @@ -417,7 +417,7 @@ class RSForm: if cst.apply_mapping(mapping, change_aliases): update_list.append(cst) Constituenta.objects.bulk_update(update_list, ['alias', 'definition_formal', 'term_raw', 'definition_raw']) - self.save() + self.save(update_fields=['time_update']) def resolve_all_text(self) -> None: ''' Trigger reference resolution for all texts. ''' @@ -479,7 +479,7 @@ class RSForm: position = position + 1 self.cache.insert_multi(result) - self.save() + self.save(update_fields=['time_update']) return result def _shift_positions(self, start: int, shift: int) -> None: diff --git a/rsconcept/backend/apps/rsform/views/rsforms.py b/rsconcept/backend/apps/rsform/views/rsforms.py index 07ac9960..75a4b32f 100644 --- a/rsconcept/backend/apps/rsform/views/rsforms.py +++ b/rsconcept/backend/apps/rsform/views/rsforms.py @@ -194,6 +194,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr cst.cst_type = serializer.validated_data['cst_type'] cst.save() schema.apply_mapping(mapping=mapping, change_aliases=False) + schema.save() cst.refresh_from_db() if changed_type: PropagationFacade.after_change_cst_type(cst, schema) @@ -232,6 +233,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr original = cast(m.Constituenta, substitution['original']) replacement = cast(m.Constituenta, substitution['substitution']) substitutions.append((original, replacement)) + PropagationFacade.before_substitute(substitutions, schema) schema.substitute(substitutions) return Response( status=c.HTTP_200_OK, @@ -312,7 +314,8 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr def reset_aliases(self, request: Request, pk) -> HttpResponse: ''' Endpoint: Recreate all aliases based on order. ''' model = self._get_item() - m.RSForm(model).reset_aliases() + schema = m.RSForm(model) + schema.reset_aliases() return Response( status=c.HTTP_200_OK, data=s.RSFormParseSerializer(model).data diff --git a/rsconcept/frontend/src/components/ui/TextArea.tsx b/rsconcept/frontend/src/components/ui/TextArea.tsx index 7e165471..47e8f03d 100644 --- a/rsconcept/frontend/src/components/ui/TextArea.tsx +++ b/rsconcept/frontend/src/components/ui/TextArea.tsx @@ -24,6 +24,7 @@ function TextArea({ return (
{ library.localUpdateTimestamp(Number(itemID)); if (callback) callback(newData); + + // TODO: deal with OSS cache invalidation }) }); }, @@ -467,7 +475,11 @@ export const RSFormState = ({ itemID, versionID, children }: RSFormStateProps) = onSuccess: newData => { setSchema(newData.schema); library.localUpdateTimestamp(newData.schema.id); - if (callback) callback(newData.new_cst); + if (library.globalOSS?.schemas.includes(newData.schema.id)) { + library.reloadOSS(() => { + if (callback) callback(newData.new_cst); + }); + } else if (callback) callback(newData.new_cst); } }); }, @@ -485,7 +497,11 @@ export const RSFormState = ({ itemID, versionID, children }: RSFormStateProps) = onSuccess: newData => { setSchema(newData); library.localUpdateTimestamp(newData.id); - if (callback) callback(); + if (library.globalOSS?.schemas.includes(newData.id)) { + library.reloadOSS(() => { + if (callback) callback(); + }); + } else if (callback) callback(); } }); }, @@ -611,6 +627,8 @@ export const RSFormState = ({ itemID, versionID, children }: RSFormStateProps) = setSchema(newData); library.localUpdateTimestamp(Number(itemID)); if (callback) callback(newData); + + // TODO: deal with OSS cache invalidation } }); },