diff --git a/rsconcept/backend/apps/library/views/library.py b/rsconcept/backend/apps/library/views/library.py index 4a0f167d..8142771f 100644 --- a/rsconcept/backend/apps/library/views/library.py +++ b/rsconcept/backend/apps/library/views/library.py @@ -67,7 +67,7 @@ class LibraryViewSet(viewsets.ModelViewSet): def perform_destroy(self, instance: m.LibraryItem) -> None: if instance.item_type == m.LibraryItemType.RSFORM: - PropagationFacade.before_delete_schema(instance) + PropagationFacade.before_delete_schema(instance.pk) super().perform_destroy(instance) if instance.item_type == m.LibraryItemType.OPERATION_SCHEMA: schemas = list(OperationSchema.owned_schemasQ(instance)) @@ -172,7 +172,7 @@ class LibraryViewSet(viewsets.ModelViewSet): clone.location = data.get('location', m.LocationHead.USER) clone.save() - RSFormCached(clone).insert_from(item.pk, request.data['items'] if 'items' in request.data else None) + RSFormCached(clone.pk).insert_from(item.pk, request.data['items'] if 'items' in request.data else None) return Response( status=c.HTTP_201_CREATED, diff --git a/rsconcept/backend/apps/oss/models/OperationSchema.py b/rsconcept/backend/apps/oss/models/OperationSchema.py index 0a1de7b4..924d8d3e 100644 --- a/rsconcept/backend/apps/oss/models/OperationSchema.py +++ b/rsconcept/backend/apps/oss/models/OperationSchema.py @@ -43,19 +43,18 @@ class OperationSchema: return Layout.objects.get(oss_id=itemID) @staticmethod - def create_input(oss: LibraryItem, operation: Operation) -> RSFormCached: + def create_input(oss_id: int, operation: Operation) -> LibraryItem: ''' Create input RSForm for given Operation. ''' - schema = RSFormCached.create( - owner=oss.owner, - alias=operation.alias, - title=operation.title, - description=operation.description, - visible=False, - access_policy=oss.access_policy, - location=oss.location - ) - Editor.set(schema.model.pk, oss.getQ_editors().values_list('pk', flat=True)) - operation.setQ_result(schema.model) + oss = LibraryItem.objects.get(pk=oss_id) + schema = LibraryItem.objects.create(item_type=LibraryItemType.RSFORM, owner=oss.owner, + alias=operation.alias, + title=operation.title, + description=operation.description, + visible=False, + access_policy=oss.access_policy, + location=oss.location) + Editor.set(schema.pk, oss.getQ_editors().values_list('pk', flat=True)) + operation.setQ_result(schema) return schema def refresh_from_db(self) -> None: @@ -132,7 +131,7 @@ class OperationSchema: if not schemas: return substitutions = operation.getQ_substitutions() - receiver = OperationSchema.create_input(self.model, operation) + receiver = RSFormCached(OperationSchema.create_input(self.model.pk, operation).pk) parents: dict = {} children: dict = {} @@ -149,7 +148,7 @@ class OperationSchema: translated_substitutions.append((original, replacement)) receiver.substitute(translated_substitutions) - for cst in Constituenta.objects.filter(schema=receiver.model).order_by('order'): + for cst in Constituenta.objects.filter(schema_id=receiver.pk).order_by('order'): parent = parents.get(cst.pk) assert parent is not None Inheritance.objects.create( diff --git a/rsconcept/backend/apps/oss/models/OperationSchemaCached.py b/rsconcept/backend/apps/oss/models/OperationSchemaCached.py index dcf6a526..ca53cdf0 100644 --- a/rsconcept/backend/apps/oss/models/OperationSchemaCached.py +++ b/rsconcept/backend/apps/oss/models/OperationSchemaCached.py @@ -19,9 +19,9 @@ from .utils import CstMapping, CstSubstitution, create_dependant_mapping, extrac class OperationSchemaCached: ''' Operations schema API with caching. ''' - def __init__(self, model: LibraryItem): - self.model = model - self.cache = OssCache(model.pk) + def __init__(self, item_id: int): + self.pk = item_id + self.cache = OssCache(item_id) self.engine = PropagationEngine(self.cache) def delete_replica(self, target: int, keep_connections: bool = False, keep_constituents: bool = False): @@ -72,12 +72,12 @@ class OperationSchemaCached: has_children = bool(self.cache.extend_graph.outputs[target]) old_schema = self.cache.get_schema(operation) if schema is None and old_schema is None or \ - (schema is not None and old_schema is not None and schema.pk == old_schema.model.pk): + (schema is not None and old_schema is not None and schema.pk == old_schema.pk): return if old_schema is not None: if has_children: - self.before_delete_cst(old_schema.model.pk, [cst.pk for cst in old_schema.cache.constituents]) + self.before_delete_cst(old_schema.pk, [cst.pk for cst in old_schema.cache.constituents]) self.cache.remove_schema(old_schema) operation.setQ_result(schema) @@ -88,7 +88,7 @@ class OperationSchemaCached: operation.save(update_fields=['alias', 'title', 'description']) if schema is not None and has_children: - rsform = RSFormCached(schema) + rsform = RSFormCached(schema.pk) self.after_create_cst(rsform, list(rsform.constituentsQ().order_by('order'))) def set_arguments(self, target: int, arguments: list[Operation]) -> None: @@ -172,7 +172,8 @@ class OperationSchemaCached: if not schemas: return False substitutions = operation.getQ_substitutions() - receiver = OperationSchema.create_input(self.model, self.cache.operation_by_id[operation.pk]) + new_schema = OperationSchema.create_input(self.pk, self.cache.operation_by_id[operation.pk]) + receiver = RSFormCached(new_schema.pk) self.cache.insert_schema(receiver) parents: dict = {} @@ -190,7 +191,7 @@ class OperationSchemaCached: translated_substitutions.append((original, replacement)) receiver.substitute(translated_substitutions) - for cst in Constituenta.objects.filter(schema=receiver.model).order_by('order'): + for cst in Constituenta.objects.filter(schema_id=receiver.pk).order_by('order'): parent = parents.get(cst.pk) assert parent is not None Inheritance.objects.create( @@ -204,9 +205,8 @@ class OperationSchemaCached: receiver.resolve_all_text() if self.cache.extend_graph.outputs[operation.pk]: - receiver_items = list(Constituenta.objects.filter(schema=receiver.model).order_by('order')) + receiver_items = list(Constituenta.objects.filter(schema_id=receiver.pk).order_by('order')) self.after_create_cst(receiver, receiver_items) - receiver.model.save(update_fields=['time_update']) return True def relocate_down(self, source: RSFormCached, destination: RSFormCached, items: list[int]): @@ -214,7 +214,7 @@ class OperationSchemaCached: self.cache.ensure_loaded_subs() self.cache.insert_schema(source) self.cache.insert_schema(destination) - operation = self.cache.get_operation(destination.model.pk) + operation = self.cache.get_operation(destination.pk) self.engine.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: @@ -228,7 +228,7 @@ class OperationSchemaCached: self.cache.insert_schema(source) self.cache.insert_schema(destination) - operation = self.cache.get_operation(source.model.pk) + operation = self.cache.get_operation(source.pk) alias_mapping: dict[str, str] = {} for item in self.cache.inheritance[operation.pk]: if item.parent_id in destination.cache.by_id: @@ -236,7 +236,7 @@ class OperationSchemaCached: destination_cst = destination.cache.by_id[item.parent_id] alias_mapping[source_cst.alias] = destination_cst.alias - new_items = destination.insert_from(source.model.pk, item_ids, alias_mapping) + new_items = destination.insert_from(source.pk, item_ids, alias_mapping) for (cst, new_cst) in new_items: new_inheritance = Inheritance.objects.create( operation=operation, @@ -246,7 +246,6 @@ class OperationSchemaCached: self.cache.insert_inheritance(new_inheritance) new_constituents = [item[1] for item in new_items] self.after_create_cst(destination, new_constituents, exclude=[operation.pk]) - destination.model.save(update_fields=['time_update']) return new_constituents def after_create_cst( @@ -257,7 +256,7 @@ class OperationSchemaCached: ''' Trigger cascade resolutions when new Constituenta is created. ''' self.cache.insert_schema(source) alias_mapping = create_dependant_mapping(source, cst_list) - operation = self.cache.get_operation(source.model.pk) + operation = self.cache.get_operation(source.pk) self.engine.on_inherit_cst(operation.pk, source, cst_list, alias_mapping, exclude) def after_change_cst_type(self, schemaID: int, target: int, new_type: CstType) -> None: @@ -268,7 +267,7 @@ class OperationSchemaCached: def after_update_cst(self, source: RSFormCached, target: int, data: dict, old_data: dict) -> None: ''' Trigger cascade resolutions when Constituenta data is changed. ''' self.cache.insert_schema(source) - operation = self.cache.get_operation(source.model.pk) + operation = self.cache.get_operation(source.pk) depend_aliases = extract_data_references(data, old_data) alias_mapping: CstMapping = {} for alias in depend_aliases: @@ -347,7 +346,7 @@ class OperationSchemaCached: original_cst = schema.cache.by_id[original_id] substitution_cst = schema.cache.by_id[substitution_id] cst_mapping.append((original_cst, substitution_cst)) - self.before_substitute(schema.model.pk, cst_mapping) + self.before_substitute(schema.pk, cst_mapping) schema.substitute(cst_mapping) for sub in added: self.cache.insert_substitution(sub) diff --git a/rsconcept/backend/apps/oss/models/OssCache.py b/rsconcept/backend/apps/oss/models/OssCache.py index 7daa4f74..1113d794 100644 --- a/rsconcept/backend/apps/oss/models/OssCache.py +++ b/rsconcept/backend/apps/oss/models/OssCache.py @@ -67,7 +67,7 @@ class OssCache: if operation.result_id in self._schema_by_id: return self._schema_by_id[operation.result_id] else: - schema = RSFormCached.from_id(operation.result_id) + schema = RSFormCached(operation.result_id) schema.cache.ensure_loaded() self._insert_new(schema) return schema @@ -77,7 +77,7 @@ class OssCache: if target in self._schema_by_id: return self._schema_by_id[target] else: - schema = RSFormCached.from_id(target) + schema = RSFormCached(target) schema.cache.ensure_loaded() self._insert_new(schema) return schema @@ -113,7 +113,7 @@ class OssCache: def insert_schema(self, schema: RSFormCached) -> None: ''' Insert new schema. ''' - if not self._schema_by_id.get(schema.model.pk): + if not self._schema_by_id.get(schema.pk): schema.cache.ensure_loaded() self._insert_new(schema) @@ -148,7 +148,7 @@ class OssCache: def remove_schema(self, schema: RSFormCached) -> None: ''' Remove schema from cache. ''' self._schemas.remove(schema) - del self._schema_by_id[schema.model.pk] + del self._schema_by_id[schema.pk] def remove_operation(self, operation: int) -> None: ''' Remove operation from cache. ''' @@ -185,4 +185,4 @@ class OssCache: def _insert_new(self, schema: RSFormCached) -> None: self._schemas.append(schema) - self._schema_by_id[schema.model.pk] = schema + self._schema_by_id[schema.pk] = schema diff --git a/rsconcept/backend/apps/oss/models/PropagationFacade.py b/rsconcept/backend/apps/oss/models/PropagationFacade.py index e5c38c9c..bba5b2d8 100644 --- a/rsconcept/backend/apps/oss/models/PropagationFacade.py +++ b/rsconcept/backend/apps/oss/models/PropagationFacade.py @@ -1,28 +1,42 @@ ''' 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 from apps.rsform.models import Attribution, Constituenta, CstType, RSFormCached from .OperationSchemaCached import CstSubstitution, OperationSchemaCached -def _get_oss_hosts(schemaID: int) -> list[LibraryItem]: - ''' Get all hosts for LibraryItem. ''' - return list(LibraryItem.objects.filter(operations__result_id=schemaID).only('pk').distinct()) +def _get_oss_hosts(schemaID: int) -> list[int]: + ''' Get all hosts for schema. ''' + return list(LibraryItem.objects.filter(operations__result_id=schemaID).values_list('pk', flat=True)) class PropagationFacade: ''' Change propagation API. ''' + _oss: dict[int, OperationSchemaCached] = {} + + @staticmethod + def get_oss(schemaID: int) -> OperationSchemaCached: + ''' Get OperationSchemaCached for schemaID. ''' + if schemaID not in PropagationFacade._oss: + PropagationFacade._oss[schemaID] = OperationSchemaCached(schemaID) + return PropagationFacade._oss[schemaID] + + @staticmethod + def reset_cache() -> None: + ''' Reset cache. ''' + PropagationFacade._oss = {} + @staticmethod def after_create_cst(source: RSFormCached, new_cst: list[Constituenta], exclude: Optional[list[int]] = None) -> None: ''' Trigger cascade resolutions when new constituenta is created. ''' - hosts = _get_oss_hosts(source.model.pk) + hosts = _get_oss_hosts(source.pk) for host in hosts: - if exclude is None or host.pk not in exclude: - OperationSchemaCached(host).after_create_cst(source, new_cst) + if exclude is None or host not in exclude: + PropagationFacade.get_oss(host).after_create_cst(source, new_cst) @staticmethod def after_change_cst_type(sourceID: int, target: int, new_type: CstType, @@ -30,8 +44,8 @@ class PropagationFacade: ''' Trigger cascade resolutions when constituenta type is changed. ''' hosts = _get_oss_hosts(sourceID) for host in hosts: - if exclude is None or host.pk not in exclude: - OperationSchemaCached(host).after_change_cst_type(sourceID, target, new_type) + if exclude is None or host not in exclude: + PropagationFacade.get_oss(host).after_change_cst_type(sourceID, target, new_type) @staticmethod def after_update_cst( @@ -42,10 +56,10 @@ class PropagationFacade: exclude: Optional[list[int]] = None ) -> None: ''' Trigger cascade resolutions when constituenta data is changed. ''' - hosts = _get_oss_hosts(source.model.pk) + hosts = _get_oss_hosts(source.pk) for host in hosts: - if exclude is None or host.pk not in exclude: - OperationSchemaCached(host).after_update_cst(source, target, data, old_data) + if exclude is None or host not in exclude: + PropagationFacade.get_oss(host).after_update_cst(source, target, data, old_data) @staticmethod def before_delete_cst(sourceID: int, target: list[int], @@ -53,8 +67,8 @@ class PropagationFacade: ''' Trigger cascade resolutions before constituents are deleted. ''' hosts = _get_oss_hosts(sourceID) for host in hosts: - if exclude is None or host.pk not in exclude: - OperationSchemaCached(host).before_delete_cst(sourceID, target) + if exclude is None or host not in exclude: + PropagationFacade.get_oss(host).before_delete_cst(sourceID, target) @staticmethod def before_substitute(sourceID: int, substitutions: CstSubstitution, @@ -64,31 +78,31 @@ class PropagationFacade: return hosts = _get_oss_hosts(sourceID) for host in hosts: - if exclude is None or host.pk not in exclude: - OperationSchemaCached(host).before_substitute(sourceID, substitutions) + if exclude is None or host not in exclude: + PropagationFacade.get_oss(host).before_substitute(sourceID, substitutions) @staticmethod - def before_delete_schema(item: LibraryItem, exclude: Optional[list[int]] = None) -> None: + def before_delete_schema(target: int, exclude: Optional[list[int]] = None) -> None: ''' Trigger cascade resolutions before schema is deleted. ''' - if item.item_type != LibraryItemType.RSFORM: - return - hosts = _get_oss_hosts(item.pk) + hosts = _get_oss_hosts(target) if not hosts: return - ids = list(Constituenta.objects.filter(schema=item).order_by('order').values_list('pk', flat=True)) + ids = list(Constituenta.objects.filter(schema_id=target).order_by('order').values_list('pk', flat=True)) for host in hosts: - if exclude is None or host.pk not in exclude: - OperationSchemaCached(host).before_delete_cst(item.pk, ids) + if exclude is None or host not in exclude: + PropagationFacade.get_oss(host).before_delete_cst(target, ids) + del PropagationFacade._oss[host] @staticmethod - def after_create_attribution(sourceID: int, attributions: list[Attribution], + def after_create_attribution(sourceID: int, + attributions: list[Attribution], exclude: Optional[list[int]] = None) -> None: ''' Trigger cascade resolutions when Attribution is created. ''' hosts = _get_oss_hosts(sourceID) for host in hosts: - if exclude is None or host.pk not in exclude: - OperationSchemaCached(host).after_create_attribution(sourceID, attributions) + if exclude is None or host not in exclude: + PropagationFacade.get_oss(host).after_create_attribution(sourceID, attributions) @staticmethod def before_delete_attribution(sourceID: int, @@ -97,5 +111,5 @@ class PropagationFacade: ''' Trigger cascade resolutions before Attribution is deleted. ''' hosts = _get_oss_hosts(sourceID) for host in hosts: - if exclude is None or host.pk not in exclude: - OperationSchemaCached(host).before_delete_attribution(sourceID, attributions) + if exclude is None or host not in exclude: + PropagationFacade.get_oss(host).before_delete_attribution(sourceID, attributions) diff --git a/rsconcept/backend/apps/oss/serializers/data_access.py b/rsconcept/backend/apps/oss/serializers/data_access.py index 78c8c352..7bf614e3 100644 --- a/rsconcept/backend/apps/oss/serializers/data_access.py +++ b/rsconcept/backend/apps/oss/serializers/data_access.py @@ -625,15 +625,6 @@ class RelocateConstituentsSerializer(StrictSerializer): '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'] diff --git a/rsconcept/backend/apps/oss/tests/s_propagation/t_attributes.py b/rsconcept/backend/apps/oss/tests/s_propagation/t_attributes.py index 5878e4d5..521e09e0 100644 --- a/rsconcept/backend/apps/oss/tests/s_propagation/t_attributes.py +++ b/rsconcept/backend/apps/oss/tests/s_propagation/t_attributes.py @@ -1,6 +1,6 @@ ''' Testing API: Change attributes of OSS and RSForms. ''' from apps.library.models import AccessPolicy, Editor, LibraryItem, LocationHead -from apps.oss.models import Operation, OperationSchema, OperationType +from apps.oss.models import OperationSchema, OperationType, PropagationFacade from apps.rsform.models import RSForm from apps.users.models import User from shared.EndpointTester import EndpointTester, decl_endpoint @@ -11,6 +11,7 @@ class TestChangeAttributes(EndpointTester): def setUp(self): + PropagationFacade.reset_cache() super().setUp() self.user3 = User.objects.create( username='UserTest3', diff --git a/rsconcept/backend/apps/oss/tests/s_propagation/t_constituents.py b/rsconcept/backend/apps/oss/tests/s_propagation/t_constituents.py index 79996245..7712b4e3 100644 --- a/rsconcept/backend/apps/oss/tests/s_propagation/t_constituents.py +++ b/rsconcept/backend/apps/oss/tests/s_propagation/t_constituents.py @@ -1,6 +1,6 @@ ''' Testing API: Change constituents in OSS. ''' -from apps.oss.models import OperationSchema, OperationType +from apps.oss.models import OperationSchema, OperationType, PropagationFacade from apps.rsform.models import Attribution, Constituenta, CstType, RSForm from shared.EndpointTester import EndpointTester, decl_endpoint @@ -9,6 +9,7 @@ class TestChangeConstituents(EndpointTester): ''' Testing Constituents change propagation in OSS. ''' def setUp(self): + PropagationFacade.reset_cache() super().setUp() self.owned = OperationSchema.create( title='Test', diff --git a/rsconcept/backend/apps/oss/tests/s_propagation/t_operations.py b/rsconcept/backend/apps/oss/tests/s_propagation/t_operations.py index b4ba907d..ff94ef38 100644 --- a/rsconcept/backend/apps/oss/tests/s_propagation/t_operations.py +++ b/rsconcept/backend/apps/oss/tests/s_propagation/t_operations.py @@ -1,7 +1,7 @@ ''' Testing API: Change substitutions in OSS. ''' -from apps.oss.models import OperationSchema, OperationType -from apps.rsform.models import Constituenta, CstType, RSForm +from apps.oss.models import OperationSchema, OperationType, PropagationFacade +from apps.rsform.models import Constituenta, RSForm from shared.EndpointTester import EndpointTester, decl_endpoint @@ -9,6 +9,7 @@ class TestChangeOperations(EndpointTester): ''' Testing Operations change propagation in OSS. ''' def setUp(self): + PropagationFacade.reset_cache() super().setUp() self.owned = OperationSchema.create( title='Test', @@ -388,7 +389,7 @@ class TestChangeOperations(EndpointTester): self.assertEqual(self.ks5.constituentsQ().count(), 8) - @decl_endpoint('/api/oss/relocate-constituents', method='post') + @decl_endpoint('/api/oss/{item}/relocate-constituents', method='post') def test_relocate_constituents_up(self): ks1_old_count = self.ks1.constituentsQ().count() ks4_old_count = self.ks4.constituentsQ().count() @@ -408,7 +409,7 @@ class TestChangeOperations(EndpointTester): 'items': [ks6A1.pk] } - self.executeOK(data) + self.executeOK(data, item=self.owned_id) ks6.model.refresh_from_db() self.ks1.model.refresh_from_db() self.ks4.model.refresh_from_db() @@ -418,7 +419,7 @@ class TestChangeOperations(EndpointTester): self.assertEqual(self.ks4.constituentsQ().count(), ks4_old_count + 1) - @decl_endpoint('/api/oss/relocate-constituents', method='post') + @decl_endpoint('/api/oss/{item}/relocate-constituents', method='post') def test_relocate_constituents_down(self): ks1_old_count = self.ks1.constituentsQ().count() ks4_old_count = self.ks4.constituentsQ().count() @@ -438,7 +439,7 @@ class TestChangeOperations(EndpointTester): 'items': [self.ks1X2.pk] } - self.executeOK(data) + self.executeOK(data, item=self.owned_id) ks6.model.refresh_from_db() self.ks1.model.refresh_from_db() self.ks4.model.refresh_from_db() diff --git a/rsconcept/backend/apps/oss/tests/s_propagation/t_references.py b/rsconcept/backend/apps/oss/tests/s_propagation/t_references.py index 3d3e7d6c..fac912d0 100644 --- a/rsconcept/backend/apps/oss/tests/s_propagation/t_references.py +++ b/rsconcept/backend/apps/oss/tests/s_propagation/t_references.py @@ -1,6 +1,6 @@ ''' Testing API: Propagate changes through references in OSS. ''' -from apps.oss.models import Inheritance, OperationSchema, OperationType +from apps.oss.models import Inheritance, OperationSchema, OperationType, PropagationFacade from apps.rsform.models import Constituenta, CstType, RSForm from shared.EndpointTester import EndpointTester, decl_endpoint @@ -9,6 +9,7 @@ class ReferencePropagationTestCase(EndpointTester): ''' Test propagation through references in OSS. ''' def setUp(self): + PropagationFacade.reset_cache() super().setUp() self.owned = OperationSchema.create( @@ -163,7 +164,7 @@ class ReferencePropagationTestCase(EndpointTester): @decl_endpoint('/api/rsforms/{schema}/delete-multiple-cst', method='patch') def test_delete_constituenta(self): data = {'items': [self.ks1X1.pk]} - response = self.executeOK(data, schema=self.ks1.model.pk) + self.executeOK(data, schema=self.ks1.model.pk) self.ks4D2.refresh_from_db() self.ks5D4.refresh_from_db() self.ks6D2.refresh_from_db() diff --git a/rsconcept/backend/apps/oss/tests/s_propagation/t_substitutions.py b/rsconcept/backend/apps/oss/tests/s_propagation/t_substitutions.py index da550077..6d4f65e2 100644 --- a/rsconcept/backend/apps/oss/tests/s_propagation/t_substitutions.py +++ b/rsconcept/backend/apps/oss/tests/s_propagation/t_substitutions.py @@ -1,7 +1,7 @@ ''' Testing API: Change substitutions in OSS. ''' -from apps.oss.models import OperationSchema, OperationType -from apps.rsform.models import Constituenta, CstType, RSForm +from apps.oss.models import OperationSchema, OperationType, PropagationFacade +from apps.rsform.models import Constituenta, RSForm from shared.EndpointTester import EndpointTester, decl_endpoint @@ -10,6 +10,7 @@ class TestChangeSubstitutions(EndpointTester): def setUp(self): + PropagationFacade.reset_cache() super().setUp() self.owned = OperationSchema.create( title='Test', diff --git a/rsconcept/backend/apps/oss/tests/s_views/t_blocks.py b/rsconcept/backend/apps/oss/tests/s_views/t_blocks.py index d05fc5c7..86486bb8 100644 --- a/rsconcept/backend/apps/oss/tests/s_views/t_blocks.py +++ b/rsconcept/backend/apps/oss/tests/s_views/t_blocks.py @@ -1,6 +1,5 @@ ''' Testing API: Operation Schema - blocks manipulation. ''' -from apps.library.models import AccessPolicy, Editor, LibraryItem, LibraryItemType -from apps.oss.models import Operation, OperationSchema, OperationType +from apps.oss.models import OperationSchema, OperationType, PropagationFacade from shared.EndpointTester import EndpointTester, decl_endpoint @@ -9,6 +8,7 @@ class TestOssBlocks(EndpointTester): def setUp(self): + PropagationFacade.reset_cache() super().setUp() self.owned = OperationSchema.create(title='Test', alias='T1', owner=self.user) self.owned_id = self.owned.model.pk diff --git a/rsconcept/backend/apps/oss/tests/s_views/t_operations.py b/rsconcept/backend/apps/oss/tests/s_views/t_operations.py index 24fbbf1e..62cc49dd 100644 --- a/rsconcept/backend/apps/oss/tests/s_views/t_operations.py +++ b/rsconcept/backend/apps/oss/tests/s_views/t_operations.py @@ -1,6 +1,13 @@ ''' Testing API: Operation Schema - operations manipulation. ''' from apps.library.models import Editor, LibraryItem -from apps.oss.models import Argument, Operation, OperationSchema, OperationType, Replica +from apps.oss.models import ( + Argument, + Operation, + OperationSchema, + OperationType, + PropagationFacade, + Replica +) from apps.rsform.models import Attribution, RSForm from shared.EndpointTester import EndpointTester, decl_endpoint @@ -10,6 +17,7 @@ class TestOssOperations(EndpointTester): def setUp(self): + PropagationFacade.reset_cache() super().setUp() self.owned = OperationSchema.create(title='Test', alias='T1', owner=self.user) self.owned_id = self.owned.model.pk diff --git a/rsconcept/backend/apps/oss/tests/s_views/t_oss.py b/rsconcept/backend/apps/oss/tests/s_views/t_oss.py index 430143e7..386cd881 100644 --- a/rsconcept/backend/apps/oss/tests/s_views/t_oss.py +++ b/rsconcept/backend/apps/oss/tests/s_views/t_oss.py @@ -1,6 +1,6 @@ ''' Testing API: Operation Schema. ''' from apps.library.models import AccessPolicy, LibraryItemType -from apps.oss.models import OperationSchema, OperationType +from apps.oss.models import OperationSchema, OperationType, PropagationFacade from apps.rsform.models import Constituenta, RSForm from shared.EndpointTester import EndpointTester, decl_endpoint @@ -9,6 +9,7 @@ class TestOssViewset(EndpointTester): ''' Testing OSS view. ''' def setUp(self): + PropagationFacade.reset_cache() super().setUp() self.owned = OperationSchema.create(title='Test', alias='T1', owner=self.user) self.owned_id = self.owned.model.pk @@ -220,8 +221,9 @@ class TestOssViewset(EndpointTester): self.executeBadData(data) - @decl_endpoint('/api/oss/relocate-constituents', method='post') + @decl_endpoint('/api/oss/{item}/relocate-constituents', method='post') def test_relocate_constituents(self): + self.set_params(item=self.owned_id) self.populateData() self.ks1X2 = self.ks1.insert_last('X2', convention='test') diff --git a/rsconcept/backend/apps/oss/views/oss.py b/rsconcept/backend/apps/oss/views/oss.py index 51757577..01f5382e 100644 --- a/rsconcept/backend/apps/oss/views/oss.py +++ b/rsconcept/backend/apps/oss/views/oss.py @@ -14,6 +14,7 @@ from rest_framework.response import Response from apps.library.models import LibraryItem, LibraryItemType from apps.library.serializers import LibraryItemSerializer +from apps.oss.models import PropagationFacade from apps.rsform.models import Constituenta, RSFormCached from apps.rsform.serializers import CstTargetSerializer from shared import messages as msg @@ -291,7 +292,7 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev 'height': position['height'] }) m.Layout.update_data(pk, layout) - m.OperationSchema.create_input(item, new_operation) + m.OperationSchema.create_input(item.pk, new_operation) item.save(update_fields=['time_update']) return Response( @@ -420,7 +421,7 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev schema_clone.access_policy = item.access_policy schema_clone.location = item.location schema_clone.save() - RSFormCached(schema_clone).insert_from(prototype.pk) + RSFormCached(schema_clone.pk).insert_from(prototype.pk) new_operation.result = schema_clone new_operation.save(update_fields=["result"]) @@ -544,7 +545,7 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev operation: m.Operation = cast(m.Operation, serializer.validated_data['target']) with transaction.atomic(): - oss = m.OperationSchemaCached(item) + oss = PropagationFacade.get_oss(item.pk) if 'layout' in serializer.validated_data: layout = serializer.validated_data['layout'] m.Layout.update_data(pk, layout) @@ -599,12 +600,12 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev layout = [x for x in layout if x['nodeID'] != 'o' + str(operation.pk)] with transaction.atomic(): - oss = m.OperationSchemaCached(item) + oss = PropagationFacade.get_oss(item.pk) oss.delete_operation(operation.pk, serializer.validated_data['keep_constituents']) m.Layout.update_data(pk, layout) if old_schema is not None: if serializer.validated_data['delete_schema']: - m.PropagationFacade.before_delete_schema(old_schema) + m.PropagationFacade.before_delete_schema(old_schema.pk) old_schema.delete() elif old_schema.is_synced(item): old_schema.visible = True @@ -640,7 +641,7 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev layout = [x for x in layout if x['nodeID'] != 'o' + str(operation.pk)] with transaction.atomic(): - oss = m.OperationSchemaCached(item) + oss = PropagationFacade.get_oss(item.pk) m.Layout.update_data(pk, layout) oss.delete_replica(operation.pk, keep_connections, keep_constituents) item.save(update_fields=['time_update']) @@ -680,13 +681,13 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev with transaction.atomic(): m.Layout.update_data(pk, layout) - schema = m.OperationSchema.create_input(item, operation) + schema = m.OperationSchema.create_input(item.pk, operation) item.save(update_fields=['time_update']) return Response( status=c.HTTP_200_OK, data={ - 'new_schema': LibraryItemSerializer(schema.model).data, + 'new_schema': LibraryItemSerializer(schema).data, 'oss': s.OperationSchemaSerializer(item).data } ) @@ -726,7 +727,7 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev old_schema = target_operation.result with transaction.atomic(): - oss = m.OperationSchemaCached(item) + oss = PropagationFacade.get_oss(item.pk) if old_schema is not None: if old_schema.is_synced(item): old_schema.visible = True @@ -769,7 +770,7 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev layout = serializer.validated_data['layout'] with transaction.atomic(): - oss = m.OperationSchemaCached(item) + oss = PropagationFacade.get_oss(item.pk) oss.execute_operation(operation) m.Layout.update_data(pk, layout) item.save(update_fields=['time_update']) @@ -823,24 +824,26 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev c.HTTP_404_NOT_FOUND: None } ) - @action(detail=False, methods=['post'], url_path='relocate-constituents') - def relocate_constituents(self, request: Request) -> Response: + @action(detail=True, methods=['post'], url_path='relocate-constituents') + def relocate_constituents(self, request: Request, pk) -> Response: ''' Relocate constituents from one schema to another. ''' + item = self._get_item() serializer = s.RelocateConstituentsSerializer(data=request.data) serializer.is_valid(raise_exception=True) data = serializer.validated_data ids = [cst.pk for cst in data['items']] with transaction.atomic(): - oss = m.OperationSchemaCached(LibraryItem.objects.get(pk=data['oss'])) - source = RSFormCached(LibraryItem.objects.get(pk=data['source'])) - destination = RSFormCached(LibraryItem.objects.get(pk=data['destination'])) + oss = PropagationFacade.get_oss(item.pk) + source = RSFormCached(data['source']) + destination = RSFormCached(data['destination']) if data['move_down']: oss.relocate_down(source, destination, ids) - m.PropagationFacade.before_delete_cst(data['source'], ids) + m.PropagationFacade.before_delete_cst(source.pk, ids) source.delete_cst(ids) else: new_items = oss.relocate_up(source, destination, ids) - m.PropagationFacade.after_create_cst(destination, new_items, exclude=[oss.model.pk]) + m.PropagationFacade.after_create_cst(destination, new_items, exclude=[oss.pk]) + item.save(update_fields=['time_update']) return Response(status=c.HTTP_200_OK) diff --git a/rsconcept/backend/apps/rsform/models/RSFormCached.py b/rsconcept/backend/apps/rsform/models/RSFormCached.py index fd6b4ad6..aa7ed50d 100644 --- a/rsconcept/backend/apps/rsform/models/RSFormCached.py +++ b/rsconcept/backend/apps/rsform/models/RSFormCached.py @@ -20,21 +20,15 @@ from .RSForm import DELETED_ALIAS, RSForm class RSFormCached: ''' RSForm cached. Caching allows to avoid querying for each method call. ''' - def __init__(self, model: LibraryItem): - self.model = model + def __init__(self, item_id: int): + self.pk = item_id self.cache: _RSFormCache = _RSFormCache(self) @staticmethod def create(**kwargs) -> 'RSFormCached': ''' Create LibraryItem via RSForm. ''' model = LibraryItem.objects.create(item_type=LibraryItemType.RSFORM, **kwargs) - return RSFormCached(model) - - @staticmethod - def from_id(pk: int) -> 'RSFormCached': - ''' Get LibraryItem by pk. ''' - model = LibraryItem.objects.get(pk=pk) - return RSFormCached(model) + return RSFormCached(model.pk) def get_dependant(self, target: Iterable[int]) -> set[int]: ''' Get list of constituents depending on target (only 1st degree). ''' @@ -51,7 +45,7 @@ class RSFormCached: def constituentsQ(self) -> QuerySet[Constituenta]: ''' Get QuerySet containing all constituents of current RSForm. ''' - return Constituenta.objects.filter(schema=self.model) + return Constituenta.objects.filter(schema_id=self.pk) def insert_last( self, @@ -62,9 +56,9 @@ class RSFormCached: ''' Insert new constituenta at last position. ''' if cst_type is None: cst_type = guess_type(alias) - position = Constituenta.objects.filter(schema=self.model).count() + position = Constituenta.objects.filter(schema_id=self.pk).count() result = Constituenta.objects.create( - schema=self.model, + schema_id=self.pk, order=position, alias=alias, cst_type=cst_type, @@ -83,7 +77,7 @@ class RSFormCached: RSForm.shift_positions(position, 1, self.cache.constituents) result = Constituenta.objects.create( - schema=self.model, + schema_id=self.pk, order=position, alias=data['alias'], cst_type=data['cst_type'], @@ -160,7 +154,7 @@ class RSFormCached: new_constituents = deepcopy(items) for cst in new_constituents: cst.pk = None - cst.schema = self.model + cst.schema_id = self.pk cst.order = position if mapping_alias: cst.alias = mapping_alias[cst.alias] @@ -263,7 +257,7 @@ class RSFormCached: deleted.append(original) replacements.append(substitution.pk) - attributions = list(Attribution.objects.filter(container__schema=self.model)) + attributions = list(Attribution.objects.filter(container__schema_id=self.pk)) if attributions: orig_to_sub = {original.pk: substitution.pk for original, substitution in substitutions} orig_pks = set(orig_to_sub.keys()) @@ -374,7 +368,7 @@ class RSFormCached: prefix = get_type_prefix(cst_type) for text in expressions: new_item = Constituenta.objects.create( - schema=self.model, + schema_id=self.pk, order=position, alias=f'{prefix}{free_index}', definition_formal=text, @@ -392,7 +386,7 @@ class RSFormCached: cst_list: Iterable[Constituenta] = [] if not self.cache.is_loaded: cst_list = Constituenta.objects \ - .filter(schema=self.model, cst_type=cst_type) \ + .filter(schema_id=self.pk, cst_type=cst_type) \ .only('alias') else: cst_list = [cst for cst in self.cache.constituents if cst.cst_type == cst_type] diff --git a/rsconcept/backend/apps/rsform/serializers/io_files.py b/rsconcept/backend/apps/rsform/serializers/io_files.py index 0b3ad047..8983551a 100644 --- a/rsconcept/backend/apps/rsform/serializers/io_files.py +++ b/rsconcept/backend/apps/rsform/serializers/io_files.py @@ -123,7 +123,7 @@ class RSFormTRSSerializer(serializers.Serializer): result['description'] = data.get('description', '') if 'id' in data: result['id'] = data['id'] - self.instance = RSFormCached.from_id(result['id']) + self.instance = RSFormCached(result['id']) return result def validate(self, attrs: dict): @@ -151,7 +151,7 @@ class RSFormTRSSerializer(serializers.Serializer): for cst_data in validated_data['items']: cst = Constituenta( alias=cst_data['alias'], - schema=self.instance.model, + schema_id=self.instance.pk, order=order, cst_type=cst_data['cstType'], ) @@ -163,12 +163,13 @@ class RSFormTRSSerializer(serializers.Serializer): @transaction.atomic def update(self, instance: RSFormCached, validated_data) -> RSFormCached: + model = LibraryItem.objects.get(pk=instance.pk) if 'alias' in validated_data: - instance.model.alias = validated_data['alias'] + model.alias = validated_data['alias'] if 'title' in validated_data: - instance.model.title = validated_data['title'] + model.title = validated_data['title'] if 'description' in validated_data: - instance.model.description = validated_data['description'] + model.description = validated_data['description'] order = 0 prev_constituents = instance.constituentsQ() @@ -185,7 +186,7 @@ class RSFormTRSSerializer(serializers.Serializer): else: cst = Constituenta( alias=cst_data['alias'], - schema=instance.model, + schema_id=instance.pk, order=order, cst_type=cst_data['cstType'], ) @@ -199,7 +200,7 @@ class RSFormTRSSerializer(serializers.Serializer): prev_cst.delete() instance.resolve_all_text() - instance.model.save() + model.save() return instance @staticmethod diff --git a/rsconcept/backend/apps/rsform/views/rsforms.py b/rsconcept/backend/apps/rsform/views/rsforms.py index db3798fa..36fca4a4 100644 --- a/rsconcept/backend/apps/rsform/views/rsforms.py +++ b/rsconcept/backend/apps/rsform/views/rsforms.py @@ -91,7 +91,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr insert_after = data['insert_after'] with transaction.atomic(): - schema = m.RSFormCached(item) + schema = m.RSFormCached(item.pk) new_cst = schema.create_cst(data, insert_after) PropagationFacade.after_create_cst(schema, [new_cst]) item.save(update_fields=['time_update']) @@ -125,7 +125,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr data = serializer.validated_data['item_data'] with transaction.atomic(): - schema = m.RSFormCached(item) + schema = m.RSFormCached(item.pk) old_data = schema.update_cst(cst.pk, data) PropagationFacade.after_update_cst(schema, cst.pk, data, old_data) if 'alias' in data and data['alias'] != cst.alias: @@ -208,7 +208,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr ) with transaction.atomic(): - schema = m.RSFormCached(item) + schema = m.RSFormCached(item.pk) new_cst = schema.produce_structure(cst, cst_parse) PropagationFacade.after_create_cst(schema, new_cst) item.save(update_fields=['time_update']) @@ -456,7 +456,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr item = self._get_item() with transaction.atomic(): - m.OrderManager(m.RSFormCached(item)).restore_order() + m.OrderManager(m.RSFormCached(item.pk)).restore_order() item.save(update_fields=['time_update']) return Response( @@ -731,7 +731,8 @@ def inline_synthesis(request: Request) -> HttpResponse: serializer = s.InlineSynthesisSerializer(data=request.data, context={'user': request.user}) serializer.is_valid(raise_exception=True) - receiver = m.RSFormCached(serializer.validated_data['receiver']) + item = cast(LibraryItem, serializer.validated_data['receiver']) + receiver = m.RSFormCached(item.pk) target_cst = cast(list[m.Constituenta], serializer.validated_data['items']) source = cast(LibraryItem, serializer.validated_data['source']) target_ids = [item.pk for item in target_cst] if target_cst else None @@ -752,11 +753,11 @@ def inline_synthesis(request: Request) -> HttpResponse: replacement = mapping_ids[replacement.pk] substitutions.append((original, replacement)) - PropagationFacade.before_substitute(receiver.model.pk, substitutions) + PropagationFacade.before_substitute(receiver.pk, substitutions) receiver.substitute(substitutions) - receiver.model.save(update_fields=['time_update']) + item.save(update_fields=['time_update']) return Response( status=c.HTTP_200_OK, - data=s.RSFormParseSerializer(receiver.model).data + data=s.RSFormParseSerializer(item).data ) diff --git a/rsconcept/frontend/src/features/oss/backend/api.ts b/rsconcept/frontend/src/features/oss/backend/api.ts index 0918cc28..326738c1 100644 --- a/rsconcept/frontend/src/features/oss/backend/api.ts +++ b/rsconcept/frontend/src/features/oss/backend/api.ts @@ -215,9 +215,9 @@ export const ossApi = { } }), - relocateConstituents: (data: IRelocateConstituentsDTO) => + relocateConstituents: ({ itemID, data }: { itemID: number; data: IRelocateConstituentsDTO }) => axiosPost({ - endpoint: `/api/oss/relocate-constituents`, + endpoint: `/api/oss/${itemID}/relocate-constituents`, request: { data: data, successMessage: infoMsg.changesSaved diff --git a/rsconcept/frontend/src/features/oss/backend/use-relocate-constituents.ts b/rsconcept/frontend/src/features/oss/backend/use-relocate-constituents.ts index 505ebbf0..05bf036f 100644 --- a/rsconcept/frontend/src/features/oss/backend/use-relocate-constituents.ts +++ b/rsconcept/frontend/src/features/oss/backend/use-relocate-constituents.ts @@ -19,6 +19,6 @@ export const useRelocateConstituents = () => { onError: () => client.invalidateQueries() }); return { - relocateConstituents: (data: IRelocateConstituentsDTO) => mutation.mutateAsync(data) + relocateConstituents: (data: { itemID: number; data: IRelocateConstituentsDTO }) => mutation.mutateAsync(data) }; }; diff --git a/rsconcept/frontend/src/features/oss/dialogs/dlg-relocate-constituents.tsx b/rsconcept/frontend/src/features/oss/dialogs/dlg-relocate-constituents.tsx index 7a736dd0..8c1edaa2 100644 --- a/rsconcept/frontend/src/features/oss/dialogs/dlg-relocate-constituents.tsx +++ b/rsconcept/frontend/src/features/oss/dialogs/dlg-relocate-constituents.tsx @@ -106,13 +106,13 @@ export function DlgRelocateConstituents() { function onSubmit(data: IRelocateConstituentsDTO) { data.items = moveTarget; if (!layout || JSON.stringify(layout) === JSON.stringify(oss.layout)) { - return relocateConstituents(data); + return relocateConstituents({ itemID: oss.id, data: data }); } else { return updatePositions({ isSilent: true, itemID: oss.id, data: layout - }).then(() => relocateConstituents(data)); + }).then(() => relocateConstituents({ itemID: oss.id, data: data })); } }