From 380877e48500f37f2b5b3197e112bb9a7d4dbcd3 Mon Sep 17 00:00:00 2001 From: Ivan <8611739+IRBorisov@users.noreply.github.com> Date: Sun, 9 Nov 2025 13:48:43 +0300 Subject: [PATCH] R: Refactoring caches Improve rsform and oss caching in propagation scenarios --- .../backend/apps/library/views/library.py | 4 +- .../apps/oss/models/OperationSchema.py | 29 +++--- .../apps/oss/models/OperationSchemaCached.py | 85 ++++++++-------- rsconcept/backend/apps/oss/models/OssCache.py | 45 ++------- .../apps/oss/models/PropagationContext.py | 27 +++++ .../apps/oss/models/PropagationEngine.py | 36 +++---- .../apps/oss/models/PropagationFacade.py | 99 ++++++++++--------- rsconcept/backend/apps/oss/models/__init__.py | 1 + .../apps/oss/serializers/data_access.py | 11 --- .../oss/tests/s_propagation/t_attributes.py | 2 +- .../oss/tests/s_propagation/t_operations.py | 10 +- .../oss/tests/s_propagation/t_references.py | 2 +- .../tests/s_propagation/t_substitutions.py | 2 +- .../apps/oss/tests/s_views/t_blocks.py | 3 +- .../apps/oss/tests/s_views/t_operations.py | 8 +- .../backend/apps/oss/tests/s_views/t_oss.py | 3 +- rsconcept/backend/apps/oss/views/oss.py | 49 +++++---- rsconcept/backend/apps/rsform/graph.py | 2 +- .../apps/rsform/models/OrderManager.py | 2 +- .../backend/apps/rsform/models/RSForm.py | 2 +- .../apps/rsform/models/RSFormCached.py | 30 +++--- .../apps/rsform/models/SemanticInfo.py | 2 +- .../apps/rsform/serializers/io_files.py | 15 +-- .../apps/rsform/serializers/io_pyconcept.py | 2 +- .../rsform/tests/s_models/t_RSFormCached.py | 12 +-- .../backend/apps/rsform/views/rsforms.py | 55 ++++++----- .../frontend/src/features/oss/backend/api.ts | 4 +- .../oss/backend/use-relocate-constituents.ts | 2 +- .../oss/dialogs/dlg-relocate-constituents.tsx | 4 +- 29 files changed, 279 insertions(+), 269 deletions(-) create mode 100644 rsconcept/backend/apps/oss/models/PropagationContext.py diff --git a/rsconcept/backend/apps/library/views/library.py b/rsconcept/backend/apps/library/views/library.py index 4a0f167d..ef874607 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..60ae579e 100644 --- a/rsconcept/backend/apps/oss/models/OperationSchema.py +++ b/rsconcept/backend/apps/oss/models/OperationSchema.py @@ -18,7 +18,7 @@ from .Substitution import Substitution class OperationSchema: ''' Operations schema API wrapper. No caching, propagation and minimal side effects. ''' - def __init__(self, model: LibraryItem): + def __init__(self, model: LibraryItem) -> None: self.model = model @staticmethod @@ -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..33ed70cb 100644 --- a/rsconcept/backend/apps/oss/models/OperationSchemaCached.py +++ b/rsconcept/backend/apps/oss/models/OperationSchemaCached.py @@ -11,6 +11,7 @@ from .Inheritance import Inheritance from .Operation import Operation from .OperationSchema import OperationSchema from .OssCache import OssCache +from .PropagationContext import PropagationContext from .PropagationEngine import PropagationEngine from .Substitution import Substitution from .utils import CstMapping, CstSubstitution, create_dependant_mapping, extract_data_references @@ -19,10 +20,11 @@ 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) - self.engine = PropagationEngine(self.cache) + def __init__(self, item_id: int, context: PropagationContext) -> None: + self.pk = item_id + self.context = context + self.cache = OssCache(item_id, context) + self.engine = PropagationEngine(self.cache, context) def delete_replica(self, target: int, keep_connections: bool = False, keep_constituents: bool = False): ''' Delete Replica Operation. ''' @@ -53,7 +55,7 @@ class OperationSchemaCached: inheritance_to_delete: list[Inheritance] = [] for child_id in children: child_operation = self.cache.operation_by_id[child_id] - child_schema = self.cache.get_schema(child_operation) + child_schema = self.cache.get_result(child_operation) if child_schema is None: continue self.engine.undo_substitutions_cst(ids, child_operation, child_schema) @@ -70,15 +72,15 @@ class OperationSchemaCached: ''' Set input schema for operation. ''' operation = self.cache.operation_by_id[target] has_children = bool(self.cache.extend_graph.outputs[target]) - old_schema = self.cache.get_schema(operation) + old_schema = self.cache.get_result(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.cache.remove_schema(old_schema) + self.before_delete_cst(old_schema.pk, [cst.pk for cst in old_schema.cache.constituents]) + self.context.invalidate(old_schema.pk) operation.setQ_result(schema) if schema is not None: @@ -88,8 +90,8 @@ class OperationSchemaCached: operation.save(update_fields=['alias', 'title', 'description']) if schema is not None and has_children: - rsform = RSFormCached(schema) - self.after_create_cst(rsform, list(rsform.constituentsQ().order_by('order'))) + cst_list = list(Constituenta.objects.filter(schema_id=schema.pk).order_by('order')) + self.after_create_cst(schema.pk, cst_list) def set_arguments(self, target: int, arguments: list[Operation]) -> None: ''' Set arguments of target Operation. ''' @@ -126,7 +128,7 @@ class OperationSchemaCached: ''' Clear all arguments for target Operation. ''' self.cache.ensure_loaded_subs() operation = self.cache.operation_by_id[target] - schema = self.cache.get_schema(operation) + schema = self.cache.get_result(operation) processed: list[dict] = [] deleted: list[Substitution] = [] for current in operation.getQ_substitutions(): @@ -172,8 +174,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]) - self.cache.insert_schema(receiver) + new_schema = OperationSchema.create_input(self.pk, self.cache.operation_by_id[operation.pk]) + receiver = self.context.get_schema(new_schema.pk) parents: dict = {} children: dict = {} @@ -190,7 +192,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,31 +206,31 @@ 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')) - self.after_create_cst(receiver, receiver_items) - receiver.model.save(update_fields=['time_update']) + receiver_items = list(Constituenta.objects.filter(schema_id=receiver.pk).order_by('order')) + self.after_create_cst(receiver.pk, receiver_items) return True - def relocate_down(self, source: RSFormCached, destination: RSFormCached, items: list[int]): + def relocate_down(self, destinationID: int, items: list[int]): ''' Move list of Constituents to destination Schema inheritor. ''' 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(destinationID) + destination = self.context.get_schema(destinationID) 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: self.cache.remove_inheritance(item) Inheritance.objects.filter(operation_id=operation.pk, parent_id__in=items).delete() - def relocate_up(self, source: RSFormCached, destination: RSFormCached, + def relocate_up(self, sourceID: int, destinationID: int, item_ids: list[int]) -> list[Constituenta]: ''' Move list of Constituents upstream to destination Schema. ''' self.cache.ensure_loaded_subs() - self.cache.insert_schema(source) - self.cache.insert_schema(destination) - operation = self.cache.get_operation(source.model.pk) + source = self.context.get_schema(sourceID) + destination = self.context.get_schema(destinationID) + + operation = self.cache.get_operation(sourceID) alias_mapping: dict[str, str] = {} for item in self.cache.inheritance[operation.pk]: if item.parent_id in destination.cache.by_id: @@ -236,7 +238,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(sourceID, item_ids, alias_mapping) for (cst, new_cst) in new_items: new_inheritance = Inheritance.objects.create( operation=operation, @@ -245,19 +247,18 @@ 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']) + self.after_create_cst(destinationID, new_constituents, exclude=[operation.pk]) return new_constituents def after_create_cst( - self, source: RSFormCached, + self, sourceID: int, cst_list: list[Constituenta], exclude: Optional[list[int]] = None ) -> None: ''' Trigger cascade resolutions when new Constituenta is created. ''' - self.cache.insert_schema(source) + source = self.context.get_schema(sourceID) 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: @@ -265,10 +266,10 @@ class OperationSchemaCached: operation = self.cache.get_operation(schemaID) self.engine.on_change_cst_type(operation.pk, target, new_type) - def after_update_cst(self, source: RSFormCached, target: int, data: dict, old_data: dict) -> None: + def after_update_cst(self, sourceID: int, 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(sourceID) + source = self.context.get_schema(sourceID) depend_aliases = extract_data_references(data, old_data) alias_mapping: CstMapping = {} for alias in depend_aliases: @@ -298,18 +299,18 @@ class OperationSchemaCached: if target.result_id is None: return for argument in arguments: - parent_schema = self.cache.get_schema(argument) - if parent_schema is not None: + parent_schema = self.cache.get_result(argument) + if parent_schema: self.engine.delete_inherited(target.pk, [cst.pk for cst in parent_schema.cache.constituents]) def after_create_arguments(self, target: Operation, arguments: list[Operation]) -> None: ''' Trigger cascade resolutions after arguments are created. ''' - schema = self.cache.get_schema(target) - if schema is None: + schema = self.cache.get_result(target) + if not schema: return for argument in arguments: - parent_schema = self.cache.get_schema(argument) - if parent_schema is None: + parent_schema = self.cache.get_result(argument) + if not parent_schema: continue self.engine.inherit_cst( target_operation=target.pk, @@ -347,7 +348,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..8ce62845 100644 --- a/rsconcept/backend/apps/oss/models/OssCache.py +++ b/rsconcept/backend/apps/oss/models/OssCache.py @@ -8,6 +8,7 @@ from apps.rsform.models import RSFormCached from .Argument import Argument from .Inheritance import Inheritance from .Operation import Operation, OperationType +from .PropagationContext import PropagationContext from .Replica import Replica from .Substitution import Substitution @@ -15,10 +16,9 @@ from .Substitution import Substitution class OssCache: ''' Cache for OSS data. ''' - def __init__(self, item_id: int): + def __init__(self, item_id: int, context: PropagationContext) -> None: self._item_id = item_id - self._schemas: list[RSFormCached] = [] - self._schema_by_id: dict[int, RSFormCached] = {} + self._context = context self.operations = list(Operation.objects.filter(oss_id=item_id).only('result_id', 'operation_type')) self.operation_by_id = {operation.pk: operation for operation in self.operations} @@ -60,27 +60,11 @@ class OssCache: 'operation_id', 'parent_id', 'child_id'): self.inheritance[item.operation_id].append(item) - def get_schema(self, operation: Operation) -> Optional[RSFormCached]: + def get_result(self, operation: Operation) -> Optional[RSFormCached]: ''' 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 = RSFormCached.from_id(operation.result_id) - schema.cache.ensure_loaded() - self._insert_new(schema) - return schema - - def get_schema_by_id(self, target: int) -> RSFormCached: - ''' Get schema by Operation. ''' - if target in self._schema_by_id: - return self._schema_by_id[target] - else: - schema = RSFormCached.from_id(target) - schema.cache.ensure_loaded() - self._insert_new(schema) - return schema + return self._context.get_schema(operation.result_id) def get_operation(self, schemaID: int) -> Operation: ''' Get operation by schema. ''' @@ -111,12 +95,6 @@ class OssCache: return self.get_inheritor(sub.substitution_id, operation) return self.get_inheritor(parent_cst, operation) - def insert_schema(self, schema: RSFormCached) -> None: - ''' Insert new schema. ''' - if not self._schema_by_id.get(schema.model.pk): - schema.cache.ensure_loaded() - self._insert_new(schema) - def insert_argument(self, argument: Argument) -> None: ''' Insert new argument. ''' self.graph.add_edge(argument.argument_id, argument.operation_id) @@ -145,19 +123,12 @@ class OssCache: for item in inherit_to_delete: self.inheritance[operation].remove(item) - def remove_schema(self, schema: RSFormCached) -> None: - ''' Remove schema from cache. ''' - self._schemas.remove(schema) - del self._schema_by_id[schema.model.pk] - def remove_operation(self, operation: int) -> None: ''' Remove operation from cache. ''' target = self.operation_by_id[operation] self.graph.remove_node(operation) self.extend_graph.remove_node(operation) - if target.result_id in self._schema_by_id: - self._schemas.remove(self._schema_by_id[target.result_id]) - del self._schema_by_id[target.result_id] + self._context.invalidate(target.result_id) self.operations.remove(self.operation_by_id[operation]) del self.operation_by_id[operation] if operation in self.replica_original: @@ -182,7 +153,3 @@ class OssCache: def remove_inheritance(self, target: Inheritance) -> None: ''' Remove inheritance from cache. ''' self.inheritance[target.operation_id].remove(target) - - def _insert_new(self, schema: RSFormCached) -> None: - self._schemas.append(schema) - self._schema_by_id[schema.model.pk] = schema diff --git a/rsconcept/backend/apps/oss/models/PropagationContext.py b/rsconcept/backend/apps/oss/models/PropagationContext.py new file mode 100644 index 00000000..c6828ee3 --- /dev/null +++ b/rsconcept/backend/apps/oss/models/PropagationContext.py @@ -0,0 +1,27 @@ +''' Models: Propagation context. ''' + +from apps.rsform.models import RSFormCached + + +class PropagationContext: + ''' Propagation context. ''' + + def __init__(self) -> None: + self._cache: dict[int, RSFormCached] = {} + + def get_schema(self, item_id: int) -> RSFormCached: + ''' Get schema by ID. ''' + if item_id not in self._cache: + schema = RSFormCached(item_id) + schema.cache.ensure_loaded() + self._cache[item_id] = schema + return self._cache[item_id] + + def clear(self) -> None: + ''' Clear cache. ''' + self._cache = {} + + def invalidate(self, item_id: int | None) -> None: + ''' Invalidate schema by ID. ''' + if item_id in self._cache: + del self._cache[item_id] diff --git a/rsconcept/backend/apps/oss/models/PropagationEngine.py b/rsconcept/backend/apps/oss/models/PropagationEngine.py index 38aad4e0..79af2863 100644 --- a/rsconcept/backend/apps/oss/models/PropagationEngine.py +++ b/rsconcept/backend/apps/oss/models/PropagationEngine.py @@ -8,6 +8,7 @@ from apps.rsform.models import Attribution, Constituenta, CstType, RSFormCached from .Inheritance import Inheritance from .Operation import Operation from .OssCache import OssCache +from .PropagationContext import PropagationContext from .Substitution import Substitution from .utils import ( CstMapping, @@ -21,8 +22,9 @@ from .utils import ( class PropagationEngine: ''' OSS changes propagation engine. ''' - def __init__(self, cache: OssCache): + def __init__(self, cache: OssCache, context: PropagationContext) -> None: self.cache = cache + self.context = context def on_change_cst_type(self, operation_id: int, cst_id: int, ctype: CstType) -> None: ''' Trigger cascade resolutions when Constituenta type is changed. ''' @@ -35,7 +37,7 @@ class PropagationEngine: successor_id = self.cache.get_inheritor(cst_id, child_id) if successor_id is None: continue - child_schema = self.cache.get_schema(child_operation) + child_schema = self.cache.get_result(child_operation) if child_schema is None: continue if child_schema.change_cst_type(successor_id, ctype): @@ -67,7 +69,7 @@ class PropagationEngine: ) -> None: ''' Execute inheritance of Constituenta. ''' operation = self.cache.operation_by_id[target_operation] - destination = self.cache.get_schema(operation) + destination = self.cache.get_result(operation) if destination is None: return @@ -104,7 +106,7 @@ class PropagationEngine: successor_id = self.cache.get_inheritor(cst_id, child_id) if successor_id is None: continue - child_schema = self.cache.get_schema(child_operation) + child_schema = self.cache.get_result(child_operation) assert child_schema is not None new_mapping = self._transform_mapping(mapping, child_operation, child_schema) alias_mapping = cst_mapping_to_alias(new_mapping) @@ -176,7 +178,7 @@ class PropagationEngine: self.cache.ensure_loaded_subs() for child_id in children: child_operation = self.cache.operation_by_id[child_id] - child_schema = self.cache.get_schema(child_operation) + child_schema = self.cache.get_result(child_operation) if child_schema is None: continue new_substitutions = self._transform_substitutions(substitutions, child_id, child_schema) @@ -193,7 +195,7 @@ class PropagationEngine: self.cache.ensure_loaded_subs() for child_id in children: child_operation = self.cache.operation_by_id[child_id] - child_schema = self.cache.get_schema(child_operation) + child_schema = self.cache.get_result(child_operation) if child_schema is None: continue @@ -213,26 +215,26 @@ class PropagationEngine: self.on_delete_attribution(child_id, deleted) Attribution.objects.filter(pk__in=[attrib.pk for attrib in deleted]).delete() - def on_delete_inherited(self, operation: int, target: list[int]) -> None: + def on_delete_inherited(self, operationID: int, target: list[int]) -> None: ''' Trigger cascade resolutions when Constituenta inheritance is deleted. ''' - children = self.cache.extend_graph.outputs[operation] + children = self.cache.extend_graph.outputs[operationID] if not children: return self.cache.ensure_loaded_subs() for child_id in children: self.delete_inherited(child_id, target) - def delete_inherited(self, operation_id: int, parent_ids: list[int]) -> None: + def delete_inherited(self, operationID: int, parents: list[int]) -> None: ''' Execute deletion of Constituenta inheritance. ''' - operation = self.cache.operation_by_id[operation_id] - schema = self.cache.get_schema(operation) + operation = self.cache.operation_by_id[operationID] + schema = self.cache.get_result(operation) if schema is None: return - self.undo_substitutions_cst(parent_ids, operation, schema) - target_ids = self.cache.get_inheritors_list(parent_ids, operation_id) - self.on_delete_inherited(operation_id, target_ids) + self.undo_substitutions_cst(parents, operation, schema) + target_ids = self.cache.get_inheritors_list(parents, operationID) + self.on_delete_inherited(operationID, target_ids) if target_ids: - self.cache.remove_cst(operation_id, target_ids) + self.cache.remove_cst(operationID, target_ids) schema.delete_cst(target_ids) def undo_substitutions_cst(self, target_ids: list[int], operation: Operation, schema: RSFormCached) -> None: @@ -254,7 +256,7 @@ class PropagationEngine: if ignore_parents is None: ignore_parents = [] operation_id = target.operation_id - original_schema = self.cache.get_schema_by_id(target.original.schema_id) + original_schema = self.context.get_schema(target.original.schema_id) dependant = [] for cst_id in original_schema.get_dependant([target.original_id]): if cst_id not in ignore_parents: @@ -374,7 +376,7 @@ class PropagationEngine: self.cache.ensure_loaded_subs() for child_id in children: child_operation = self.cache.operation_by_id[child_id] - child_schema = self.cache.get_schema(child_operation) + child_schema = self.cache.get_result(child_operation) if child_schema is None: continue new_mapping = self._transform_mapping(mapping, child_operation, child_schema) diff --git a/rsconcept/backend/apps/oss/models/PropagationFacade.py b/rsconcept/backend/apps/oss/models/PropagationFacade.py index e5c38c9c..b865f42b 100644 --- a/rsconcept/backend/apps/oss/models/PropagationFacade.py +++ b/rsconcept/backend/apps/oss/models/PropagationFacade.py @@ -1,101 +1,110 @@ ''' 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 +from .PropagationContext import PropagationContext -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).distinct().values_list('pk', flat=True)) class PropagationFacade: ''' Change propagation API. ''' - @staticmethod - def after_create_cst(source: RSFormCached, new_cst: list[Constituenta], + def __init__(self) -> None: + self._context = PropagationContext() + self._oss: dict[int, OperationSchemaCached] = {} + + def get_oss(self, schemaID: int) -> OperationSchemaCached: + ''' Get OperationSchemaCached for schemaID. ''' + if schemaID not in self._oss: + self._oss[schemaID] = OperationSchemaCached(schemaID, self._context) + return self._oss[schemaID] + + def get_schema(self, schemaID: int) -> RSFormCached: + ''' Get RSFormCached for schemaID. ''' + return self._context.get_schema(schemaID) + + def after_create_cst(self, 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) + if not new_cst: + return + source = new_cst[0].schema_id + hosts = _get_oss_hosts(source) 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: + self.get_oss(host).after_create_cst(source, new_cst) - @staticmethod - def after_change_cst_type(sourceID: int, target: int, new_type: CstType, + def after_change_cst_type(self, sourceID: int, target: int, new_type: CstType, exclude: Optional[list[int]] = None) -> None: ''' 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: + self.get_oss(host).after_change_cst_type(sourceID, target, new_type) - @staticmethod + # pylint: disable=too-many-arguments, too-many-positional-arguments def after_update_cst( - source: RSFormCached, - target: int, - data: dict, - old_data: dict, + self, sourceID: int, target: int, + data: dict, old_data: dict, 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(sourceID) 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: + self.get_oss(host).after_update_cst(sourceID, target, data, old_data) - @staticmethod - def before_delete_cst(sourceID: int, target: list[int], + def before_delete_cst(self, sourceID: int, target: list[int], exclude: Optional[list[int]] = None) -> None: ''' 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: + self.get_oss(host).before_delete_cst(sourceID, target) - @staticmethod - def before_substitute(sourceID: int, substitutions: CstSubstitution, + def before_substitute(self, sourceID: int, substitutions: CstSubstitution, exclude: Optional[list[int]] = None) -> None: ''' Trigger cascade resolutions before constituents are substituted. ''' if not substitutions: 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: + self.get_oss(host).before_substitute(sourceID, substitutions) - @staticmethod - def before_delete_schema(item: LibraryItem, exclude: Optional[list[int]] = None) -> None: + def before_delete_schema(self, 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: + self.get_oss(host).before_delete_cst(target, ids) + del self._oss[host] - @staticmethod - def after_create_attribution(sourceID: int, attributions: list[Attribution], + def after_create_attribution(self, 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: + self.get_oss(host).after_create_attribution(sourceID, attributions) - @staticmethod - def before_delete_attribution(sourceID: int, + def before_delete_attribution(self, sourceID: int, attributions: list[Attribution], exclude: Optional[list[int]] = None) -> None: ''' 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: + self.get_oss(host).before_delete_attribution(sourceID, attributions) diff --git a/rsconcept/backend/apps/oss/models/__init__.py b/rsconcept/backend/apps/oss/models/__init__.py index 49e3300d..96a35b96 100644 --- a/rsconcept/backend/apps/oss/models/__init__.py +++ b/rsconcept/backend/apps/oss/models/__init__.py @@ -7,6 +7,7 @@ from .Layout import Layout from .Operation import Operation, OperationType from .OperationSchema import OperationSchema from .OperationSchemaCached import OperationSchemaCached +from .PropagationContext import PropagationContext from .PropagationFacade import PropagationFacade from .Replica import Replica from .Substitution import Substitution diff --git a/rsconcept/backend/apps/oss/serializers/data_access.py b/rsconcept/backend/apps/oss/serializers/data_access.py index 78c8c352..1855127b 100644 --- a/rsconcept/backend/apps/oss/serializers/data_access.py +++ b/rsconcept/backend/apps/oss/serializers/data_access.py @@ -609,8 +609,6 @@ class RelocateConstituentsSerializer(StrictSerializer): attrs['destination'] = attrs['destination'].id attrs['source'] = attrs['items'][0].schema_id - # TODO: check permissions for editing source and destination - if attrs['source'] == attrs['destination']: raise serializers.ValidationError({ 'destination': msg.sourceEqualDestination() @@ -625,15 +623,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..ffa8ff05 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 from apps.rsform.models import RSForm from apps.users.models import User from shared.EndpointTester import EndpointTester, decl_endpoint 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..326fe829 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.rsform.models import Constituenta, RSForm from shared.EndpointTester import EndpointTester, decl_endpoint @@ -388,7 +388,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 +408,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 +418,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 +438,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..ecca7077 100644 --- a/rsconcept/backend/apps/oss/tests/s_propagation/t_references.py +++ b/rsconcept/backend/apps/oss/tests/s_propagation/t_references.py @@ -163,7 +163,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..247ef204 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.rsform.models import Constituenta, RSForm from shared.EndpointTester import EndpointTester, decl_endpoint 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..609ee6e9 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 from shared.EndpointTester import EndpointTester, decl_endpoint 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..a82ae6ba 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,12 @@ ''' 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, + Replica +) from apps.rsform.models import Attribution, RSForm from shared.EndpointTester import EndpointTester, decl_endpoint 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..23d60f21 100644 --- a/rsconcept/backend/apps/oss/tests/s_views/t_oss.py +++ b/rsconcept/backend/apps/oss/tests/s_views/t_oss.py @@ -220,8 +220,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..5530d12f 100644 --- a/rsconcept/backend/apps/oss/views/oss.py +++ b/rsconcept/backend/apps/oss/views/oss.py @@ -14,7 +14,7 @@ from rest_framework.response import Response from apps.library.models import LibraryItem, LibraryItemType from apps.library.serializers import LibraryItemSerializer -from apps.rsform.models import Constituenta, RSFormCached +from apps.rsform.models import Constituenta from apps.rsform.serializers import CstTargetSerializer from shared import messages as msg from shared import permissions @@ -291,7 +291,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 +420,8 @@ 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) + + m.PropagationFacade().get_schema(schema_clone.pk).insert_from(prototype.pk) new_operation.result = schema_clone new_operation.save(update_fields=["result"]) @@ -544,7 +545,8 @@ 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) + propagation = m.PropagationFacade() + oss = propagation.get_oss(item.pk) if 'layout' in serializer.validated_data: layout = serializer.validated_data['layout'] m.Layout.update_data(pk, layout) @@ -599,12 +601,13 @@ 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) + propagation = m.PropagationFacade() + oss = propagation.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) + propagation.before_delete_schema(old_schema.pk) old_schema.delete() elif old_schema.is_synced(item): old_schema.visible = True @@ -640,7 +643,8 @@ 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) + propagation = m.PropagationFacade() + oss = propagation.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 +684,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 +730,8 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev old_schema = target_operation.result with transaction.atomic(): - oss = m.OperationSchemaCached(item) + propagation = m.PropagationFacade() + oss = propagation.get_oss(item.pk) if old_schema is not None: if old_schema.is_synced(item): old_schema.visible = True @@ -769,7 +774,8 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev layout = serializer.validated_data['layout'] with transaction.atomic(): - oss = m.OperationSchemaCached(item) + propagation = m.PropagationFacade() + oss = propagation.get_oss(item.pk) oss.execute_operation(operation) m.Layout.update_data(pk, layout) item.save(update_fields=['time_update']) @@ -823,24 +829,27 @@ 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']] + destinationID = data['destination'] 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'])) + propagation = m.PropagationFacade() + oss = propagation.get_oss(item.pk) + source = propagation.get_schema(data['source']) if data['move_down']: - oss.relocate_down(source, destination, ids) - m.PropagationFacade.before_delete_cst(data['source'], ids) + oss.relocate_down(destinationID, ids) + propagation.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]) + new_items = oss.relocate_up(source.pk, destinationID, ids) + propagation.after_create_cst(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/graph.py b/rsconcept/backend/apps/rsform/graph.py index c379b3ad..1d755087 100644 --- a/rsconcept/backend/apps/rsform/graph.py +++ b/rsconcept/backend/apps/rsform/graph.py @@ -8,7 +8,7 @@ ItemType = TypeVar("ItemType") class Graph(Generic[ItemType]): ''' Directed graph. ''' - def __init__(self, graph: Optional[dict[ItemType, list[ItemType]]] = None): + def __init__(self, graph: Optional[dict[ItemType, list[ItemType]]] = None) -> None: if graph is None: self.outputs: dict[ItemType, list[ItemType]] = {} self.inputs: dict[ItemType, list[ItemType]] = {} diff --git a/rsconcept/backend/apps/rsform/models/OrderManager.py b/rsconcept/backend/apps/rsform/models/OrderManager.py index 61c6bab0..b85ccc14 100644 --- a/rsconcept/backend/apps/rsform/models/OrderManager.py +++ b/rsconcept/backend/apps/rsform/models/OrderManager.py @@ -8,7 +8,7 @@ from .SemanticInfo import SemanticInfo class OrderManager: ''' Ordering helper class ''' - def __init__(self, schema: RSFormCached): + def __init__(self, schema: RSFormCached) -> None: self._semantic = SemanticInfo(schema) self._items = schema.cache.constituents self._cst_by_ID = schema.cache.by_id diff --git a/rsconcept/backend/apps/rsform/models/RSForm.py b/rsconcept/backend/apps/rsform/models/RSForm.py index ae1804cf..b411e2a8 100644 --- a/rsconcept/backend/apps/rsform/models/RSForm.py +++ b/rsconcept/backend/apps/rsform/models/RSForm.py @@ -21,7 +21,7 @@ DELETED_ALIAS = 'DEL' class RSForm: ''' RSForm wrapper. No caching, each mutation requires querying. ''' - def __init__(self, model: LibraryItem): + def __init__(self, model: LibraryItem) -> None: assert model.item_type == LibraryItemType.RSFORM self.model = model diff --git a/rsconcept/backend/apps/rsform/models/RSFormCached.py b/rsconcept/backend/apps/rsform/models/RSFormCached.py index fd6b4ad6..3f49f8ce 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) -> None: + 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] @@ -406,7 +400,7 @@ class RSFormCached: class _RSFormCache: ''' Cache for RSForm constituents. ''' - def __init__(self, schema: 'RSFormCached'): + def __init__(self, schema: 'RSFormCached') -> None: self._schema = schema self.constituents: list[Constituenta] = [] self.by_id: dict[int, Constituenta] = {} diff --git a/rsconcept/backend/apps/rsform/models/SemanticInfo.py b/rsconcept/backend/apps/rsform/models/SemanticInfo.py index c3f39791..de9579a3 100644 --- a/rsconcept/backend/apps/rsform/models/SemanticInfo.py +++ b/rsconcept/backend/apps/rsform/models/SemanticInfo.py @@ -16,7 +16,7 @@ from .RSFormCached import RSFormCached class SemanticInfo: ''' Semantic information derived from constituents. ''' - def __init__(self, schema: RSFormCached): + def __init__(self, schema: RSFormCached) -> None: schema.cache.ensure_loaded() self._items = schema.cache.constituents self._cst_by_ID = schema.cache.by_id 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/serializers/io_pyconcept.py b/rsconcept/backend/apps/rsform/serializers/io_pyconcept.py index 1966538c..4d12e157 100644 --- a/rsconcept/backend/apps/rsform/serializers/io_pyconcept.py +++ b/rsconcept/backend/apps/rsform/serializers/io_pyconcept.py @@ -12,7 +12,7 @@ from ..models import Constituenta, CstType class PyConceptAdapter: ''' RSForm adapter for interacting with pyconcept module. ''' - def __init__(self, data: Union[int, dict]): + def __init__(self, data: Union[int, dict]) -> None: try: if 'items' in cast(dict, data): self.data = self._prepare_request_raw(cast(dict, data)) diff --git a/rsconcept/backend/apps/rsform/tests/s_models/t_RSFormCached.py b/rsconcept/backend/apps/rsform/tests/s_models/t_RSFormCached.py index 4dcacb19..37dbce60 100644 --- a/rsconcept/backend/apps/rsform/tests/s_models/t_RSFormCached.py +++ b/rsconcept/backend/apps/rsform/tests/s_models/t_RSFormCached.py @@ -22,8 +22,8 @@ class TestRSFormCached(DBTester): self.assertFalse(schema1.constituentsQ().exists()) self.assertFalse(schema2.constituentsQ().exists()) - Constituenta.objects.create(alias='X1', schema=schema1.model, order=0) - Constituenta.objects.create(alias='X2', schema=schema1.model, order=1) + Constituenta.objects.create(alias='X1', schema_id=schema1.pk, order=0) + Constituenta.objects.create(alias='X2', schema_id=schema1.pk, order=1) self.assertTrue(schema1.constituentsQ().exists()) self.assertFalse(schema2.constituentsQ().exists()) self.assertEqual(schema1.constituentsQ().count(), 2) @@ -32,7 +32,7 @@ class TestRSFormCached(DBTester): def test_insert_last(self): x1 = self.schema.insert_last('X1') self.assertEqual(x1.order, 0) - self.assertEqual(x1.schema, self.schema.model) + self.assertEqual(x1.schema_id, self.schema.pk) def test_create_cst(self): @@ -115,8 +115,8 @@ class TestRSFormCached(DBTester): definition_formal='X2 = X3' ) test_ks = RSFormCached.create(title='Test') - test_ks.insert_from(self.schema.model.pk) - items = Constituenta.objects.filter(schema=test_ks.model).order_by('order') + test_ks.insert_from(self.schema.pk) + items = Constituenta.objects.filter(schema_id=test_ks.pk).order_by('order') self.assertEqual(len(items), 4) self.assertEqual(items[0].alias, 'X2') self.assertEqual(items[1].alias, 'D2') @@ -200,7 +200,7 @@ class TestRSFormCached(DBTester): self.schema.substitute([(x1, x2)]) self.assertEqual(self.schema.constituentsQ().count(), 3) - self.assertEqual(Attribution.objects.filter(container__schema=self.schema.model).count(), 2) + self.assertEqual(Attribution.objects.filter(container__schema_id=self.schema.pk).count(), 2) self.assertTrue(Attribution.objects.filter(container=x2, attribute=d2).exists()) self.assertTrue(Attribution.objects.filter(container=x2, attribute=d1).exists()) diff --git a/rsconcept/backend/apps/rsform/views/rsforms.py b/rsconcept/backend/apps/rsform/views/rsforms.py index db3798fa..89af3d05 100644 --- a/rsconcept/backend/apps/rsform/views/rsforms.py +++ b/rsconcept/backend/apps/rsform/views/rsforms.py @@ -91,9 +91,10 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr insert_after = data['insert_after'] with transaction.atomic(): - schema = m.RSFormCached(item) + propagation = PropagationFacade() + schema = propagation.get_schema(item.pk) new_cst = schema.create_cst(data, insert_after) - PropagationFacade.after_create_cst(schema, [new_cst]) + propagation.after_create_cst([new_cst]) item.save(update_fields=['time_update']) return Response( @@ -125,9 +126,10 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr data = serializer.validated_data['item_data'] with transaction.atomic(): - schema = m.RSFormCached(item) + propagation = PropagationFacade() + schema = propagation.get_schema(item.pk) old_data = schema.update_cst(cst.pk, data) - PropagationFacade.after_update_cst(schema, cst.pk, data, old_data) + propagation.after_update_cst(item.pk, cst.pk, data, old_data) if 'alias' in data and data['alias'] != cst.alias: cst.refresh_from_db() changed_type = 'cst_type' in data and cst.cst_type != data['cst_type'] @@ -138,7 +140,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr cst.save() schema.apply_mapping(mapping=mapping, change_aliases=False) if changed_type: - PropagationFacade.after_change_cst_type(item.pk, cst.pk, cast(m.CstType, cst.cst_type)) + propagation.after_change_cst_type(item.pk, cst.pk, cast(m.CstType, cst.cst_type)) item.save(update_fields=['time_update']) return Response( status=c.HTTP_200_OK, @@ -208,9 +210,10 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr ) with transaction.atomic(): - schema = m.RSFormCached(item) + propagation = PropagationFacade() + schema = propagation.get_schema(item.pk) new_cst = schema.produce_structure(cst, cst_parse) - PropagationFacade.after_create_cst(schema, new_cst) + propagation.after_create_cst(new_cst) item.save(update_fields=['time_update']) return Response( status=c.HTTP_200_OK, @@ -245,7 +248,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(item.pk, substitutions) + PropagationFacade().before_substitute(item.pk, substitutions) schema.substitute(substitutions) item.save(update_fields=['time_update']) @@ -275,7 +278,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr with transaction.atomic(): schema = m.RSForm(item) - PropagationFacade.before_delete_cst(item.pk, [cst.pk for cst in cst_list]) + PropagationFacade().before_delete_cst(item.pk, [cst.pk for cst in cst_list]) schema.delete_cst(cst_list) item.save(update_fields=['time_update']) @@ -305,11 +308,11 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr attribute = serializer.validated_data['attribute'] with transaction.atomic(): - new_association = m.Attribution.objects.create( + new_attribution = m.Attribution.objects.create( container=container, attribute=attribute ) - PropagationFacade.after_create_attribution(item.pk, [new_association]) + PropagationFacade().after_create_attribution(item.pk, [new_attribution]) item.save(update_fields=['time_update']) return Response( @@ -345,7 +348,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr 'container': msg.invalidAssociation() }) - PropagationFacade.before_delete_attribution(item.pk, target) + PropagationFacade().before_delete_attribution(item.pk, target) m.Attribution.objects.filter(pk__in=[attrib.pk for attrib in target]).delete() item.save(update_fields=['time_update']) @@ -375,7 +378,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr with transaction.atomic(): target = list(m.Attribution.objects.filter(container=serializer.validated_data['target'])) if target: - PropagationFacade.before_delete_attribution(item.pk, target) + PropagationFacade().before_delete_attribution(item.pk, target) m.Attribution.objects.filter(pk__in=[attrib.pk for attrib in target]).delete() item.save(update_fields=['time_update']) @@ -456,7 +459,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( @@ -493,10 +496,10 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr serializer = s.RSFormTRSSerializer(data=data, context={'load_meta': load_metadata}) serializer.is_valid(raise_exception=True) - result: m.RSForm = serializer.save() + result: m.RSFormCached = serializer.save() return Response( status=c.HTTP_200_OK, - data=s.RSFormParseSerializer(result.model).data + data=s.RSFormParseSerializer(LibraryItem.objects.get(pk=result.pk)).data ) @extend_schema( @@ -651,10 +654,10 @@ class TrsImportView(views.APIView): _prepare_rsform_data(data, request, owner) serializer = s.RSFormTRSSerializer(data=data, context={'load_meta': True}) serializer.is_valid(raise_exception=True) - schema: m.RSForm = serializer.save() + schema: m.RSFormCached = serializer.save() return Response( status=c.HTTP_201_CREATED, - data=LibraryItemSerializer(schema.model).data + data=LibraryItemSerializer(LibraryItem.objects.get(pk=schema.pk)).data ) @@ -687,10 +690,10 @@ def create_rsform(request: Request) -> HttpResponse: _prepare_rsform_data(data, request, owner) serializer_rsform = s.RSFormTRSSerializer(data=data, context={'load_meta': True}) serializer_rsform.is_valid(raise_exception=True) - schema: m.RSForm = serializer_rsform.save() + schema: m.RSFormCached = serializer_rsform.save() return Response( status=c.HTTP_201_CREATED, - data=LibraryItemSerializer(schema.model).data + data=LibraryItemSerializer(LibraryItem.objects.get(pk=schema.pk)).data ) @@ -731,16 +734,18 @@ 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']) 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 with transaction.atomic(): + propagation = PropagationFacade() + receiver = propagation.get_schema(item.pk) new_items = receiver.insert_from(source.pk, target_ids) target_ids = [item[0].pk for item in new_items] mapping_ids = {cst.pk: new_cst for (cst, new_cst) in new_items} - PropagationFacade.after_create_cst(receiver, [item[1] for item in new_items]) + propagation.after_create_cst([item[1] for item in new_items]) substitutions: list[tuple[m.Constituenta, m.Constituenta]] = [] for substitution in serializer.validated_data['substitutions']: @@ -752,11 +757,11 @@ def inline_synthesis(request: Request) -> HttpResponse: replacement = mapping_ids[replacement.pk] substitutions.append((original, replacement)) - PropagationFacade.before_substitute(receiver.model.pk, substitutions) + propagation.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 })); } }