diff --git a/rsconcept/backend/apps/library/serializers/data_access.py b/rsconcept/backend/apps/library/serializers/data_access.py index 86bf7835..6470673d 100644 --- a/rsconcept/backend/apps/library/serializers/data_access.py +++ b/rsconcept/backend/apps/library/serializers/data_access.py @@ -54,7 +54,7 @@ class LibraryItemCloneSerializer(StrictSerializer): model = LibraryItem exclude = ['id', 'item_type', 'owner', 'read_only'] - items = PKField(many=True, queryset=Constituenta.objects.all().only('pk', 'schema_id')) + items = PKField(many=True, queryset=Constituenta.objects.all().only('schema_id')) item_data = ItemCloneData() def validate_items(self, value): diff --git a/rsconcept/backend/apps/library/views/library.py b/rsconcept/backend/apps/library/views/library.py index fb6af1a4..4a0f167d 100644 --- a/rsconcept/backend/apps/library/views/library.py +++ b/rsconcept/backend/apps/library/views/library.py @@ -14,7 +14,7 @@ from rest_framework.request import Request from rest_framework.response import Response from apps.oss.models import Layout, Operation, OperationSchema, PropagationFacade -from apps.rsform.models import Attribution, RSFormCached +from apps.rsform.models import RSFormCached from apps.rsform.serializers import RSFormParseSerializer from apps.users.models import User from shared import permissions @@ -172,22 +172,7 @@ class LibraryViewSet(viewsets.ModelViewSet): clone.location = data.get('location', m.LocationHead.USER) clone.save() - cst_map: dict[int, int] = {} - cst_list: list[int] = [] - need_filter = 'items' in request.data and request.data['items'] - for cst in RSFormCached(item).constituentsQ(): - if not need_filter or cst.pk in request.data['items']: - old_pk = cst.pk - cst.pk = None - cst.schema = clone - cst.save() - cst_map[old_pk] = cst.pk - cst_list.append(old_pk) - for attr in Attribution.objects.filter(container__in=cst_list, attribute__in=cst_list): - attr.pk = None - attr.container_id = cst_map[attr.container_id] - attr.attribute_id = cst_map[attr.attribute_id] - attr.save() + RSFormCached(clone).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 b719ad24..0a1de7b4 100644 --- a/rsconcept/backend/apps/oss/models/OperationSchema.py +++ b/rsconcept/backend/apps/oss/models/OperationSchema.py @@ -137,11 +137,10 @@ class OperationSchema: parents: dict = {} children: dict = {} for operand in schemas: - items = list(Constituenta.objects.filter(schema_id=operand).order_by('order')) - new_items = receiver.insert_copy(items) - for (i, cst) in enumerate(new_items): - parents[cst.pk] = items[i] - children[items[i].pk] = cst + new_items = receiver.insert_from(operand) + for (old_cst, new_cst) in new_items: + parents[new_cst.pk] = old_cst + children[old_cst.pk] = new_cst translated_substitutions: list[tuple[Constituenta, Constituenta]] = [] for sub in substitutions: diff --git a/rsconcept/backend/apps/oss/models/OperationSchemaCached.py b/rsconcept/backend/apps/oss/models/OperationSchemaCached.py index a6bcee83..8356cbf8 100644 --- a/rsconcept/backend/apps/oss/models/OperationSchemaCached.py +++ b/rsconcept/backend/apps/oss/models/OperationSchemaCached.py @@ -178,11 +178,10 @@ class OperationSchemaCached: parents: dict = {} children: dict = {} for operand in schemas: - items = list(Constituenta.objects.filter(schema_id=operand).order_by('order')) - new_items = receiver.insert_copy(items) - for (i, cst) in enumerate(new_items): - parents[cst.pk] = items[i] - children[items[i].pk] = cst + new_items = receiver.insert_from(operand) + for (old_cst, new_cst) in new_items: + parents[new_cst.pk] = old_cst + children[old_cst.pk] = new_cst translated_substitutions: list[tuple[Constituenta, Constituenta]] = [] for sub in substitutions: @@ -223,7 +222,7 @@ class OperationSchemaCached: Inheritance.objects.filter(operation_id=operation.pk, parent_id__in=items).delete() def relocate_up(self, source: RSFormCached, destination: RSFormCached, - items: list[Constituenta]) -> list[Constituenta]: + item_ids: list[int]) -> list[Constituenta]: ''' Move list of Constituents upstream to destination Schema. ''' self.cache.ensure_loaded_subs() self.cache.insert_schema(source) @@ -237,17 +236,18 @@ class OperationSchemaCached: destination_cst = destination.cache.by_id[item.parent_id] alias_mapping[source_cst.alias] = destination_cst.alias - new_items = destination.insert_copy(items, initial_mapping=alias_mapping) - for index, cst in enumerate(new_items): + new_items = destination.insert_from(source.model.pk, item_ids, alias_mapping) + for (cst, new_cst) in new_items: new_inheritance = Inheritance.objects.create( operation=operation, - child=items[index], - parent=cst + child=cst, + parent=new_cst ) self.cache.insert_inheritance(new_inheritance) - self.after_create_cst(destination, new_items, exclude=[operation.pk]) + 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_items + return new_constituents def after_create_cst( self, source: RSFormCached, diff --git a/rsconcept/backend/apps/oss/models/PropagationEngine.py b/rsconcept/backend/apps/oss/models/PropagationEngine.py index cefd5b5f..eb517116 100644 --- a/rsconcept/backend/apps/oss/models/PropagationEngine.py +++ b/rsconcept/backend/apps/oss/models/PropagationEngine.py @@ -3,7 +3,7 @@ from typing import Optional from rest_framework.serializers import ValidationError -from apps.rsform.models import INSERT_LAST, Attribution, Constituenta, CstType, RSFormCached +from apps.rsform.models import Attribution, Constituenta, CstType, RSFormCached from .Inheritance import Inheritance from .Operation import Operation @@ -76,11 +76,11 @@ class PropagationEngine: alias_mapping = cst_mapping_to_alias(new_mapping) insert_where = self._determine_insert_position(items[0].pk, operation, source, destination) new_cst_list = destination.insert_copy(items, insert_where, alias_mapping) - for index, cst in enumerate(new_cst_list): + for (cst, new_cst) in zip(items, new_cst_list): new_inheritance = Inheritance.objects.create( operation=operation, - child=cst, - parent=items[index] + child=new_cst, + parent=cst ) self.cache.insert_inheritance(new_inheritance) new_mapping = {alias_mapping[alias]: cst for alias, cst in new_mapping.items()} @@ -145,28 +145,28 @@ class PropagationEngine: self.cache.ensure_loaded_subs() - existing_associations = set( + existing_attributions = set( Attribution.objects.filter( container__schema_id=operation.result_id, ).values_list('container_id', 'attribute_id') ) - new_associations: list[Attribution] = [] - for assoc in items: - new_container = self.cache.get_inheritor(assoc.container_id, target) - new_attribute = self.cache.get_inheritor(assoc.attribute_id, target) + new_attributions: list[Attribution] = [] + for attrib in items: + new_container = self.cache.get_inheritor(attrib.container_id, target) + new_attribute = self.cache.get_inheritor(attrib.attribute_id, target) if new_container is None or new_attribute is None \ or new_attribute == new_container \ - or (new_container, new_attribute) in existing_associations: + or (new_container, new_attribute) in existing_attributions: continue - new_associations.append(Attribution( + new_attributions.append(Attribution( container_id=new_container, attribute_id=new_attribute )) - if new_associations: - new_associations = Attribution.objects.bulk_create(new_associations) - self.on_inherit_attribution(target, new_associations) + if new_attributions: + new_attributions = Attribution.objects.bulk_create(new_attributions) + self.on_inherit_attribution(target, new_attributions) def on_before_substitute(self, operationID: int, substitutions: CstSubstitution) -> None: ''' Trigger cascade resolutions when Constituenta substitution is executed. ''' @@ -286,7 +286,7 @@ class PropagationEngine: operation: Operation, source: RSFormCached, destination: RSFormCached - ) -> int: + ) -> Optional[int]: ''' Determine insert_after for new constituenta. ''' prototype = source.cache.by_id[prototype_id] prototype_index = source.cache.constituents.index(prototype) @@ -295,7 +295,7 @@ class PropagationEngine: prev_cst = source.cache.constituents[prototype_index - 1] inherited_prev_id = self.cache.get_successor(prev_cst.pk, operation.pk) if inherited_prev_id is None: - return INSERT_LAST + return None prev_cst = destination.cache.by_id[inherited_prev_id] prev_index = destination.cache.constituents.index(prev_cst) return prev_index + 1 diff --git a/rsconcept/backend/apps/oss/serializers/data_access.py b/rsconcept/backend/apps/oss/serializers/data_access.py index 66cc7e4b..78c8c352 100644 --- a/rsconcept/backend/apps/oss/serializers/data_access.py +++ b/rsconcept/backend/apps/oss/serializers/data_access.py @@ -602,7 +602,7 @@ class RelocateConstituentsSerializer(StrictSerializer): items = PKField( many=True, allow_empty=False, - queryset=Constituenta.objects.all() + queryset=Constituenta.objects.all().only('schema_id') ) def validate(self, attrs): diff --git a/rsconcept/backend/apps/oss/views/oss.py b/rsconcept/backend/apps/oss/views/oss.py index 33972489..51757577 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 Attribution, Constituenta, RSFormCached +from apps.rsform.models import Constituenta, RSFormCached from apps.rsform.serializers import CstTargetSerializer from shared import messages as msg from shared import permissions @@ -23,40 +23,6 @@ from .. import models as m from .. import serializers as s -def _create_clone(prototype: LibraryItem, operation: m.Operation, oss: LibraryItem) -> LibraryItem: - ''' Create clone of prototype schema for operation. ''' - clone = deepcopy(prototype) - clone.pk = None - clone.owner = oss.owner - clone.title = operation.title - clone.alias = operation.alias - clone.description = operation.description - clone.visible = False - clone.read_only = False - clone.access_policy = oss.access_policy - clone.location = oss.location - clone.save() - - cst_map: dict[int, int] = {} - cst_list: list[int] = [] - - for cst in Constituenta.objects.filter(schema_id=prototype.pk): - old_pk = cst.pk - cst_copy = deepcopy(cst) - cst_copy.pk = None - cst_copy.schema = clone - cst_copy.save() - cst_map[old_pk] = cst_copy.pk - cst_list.append(old_pk) - - for attr in Attribution.objects.filter(container__in=cst_list, attribute__in=cst_list): - attr.pk = None - attr.container_id = cst_map[attr.container_id] - attr.attribute_id = cst_map[attr.attribute_id] - attr.save() - return clone - - @extend_schema(tags=['OSS']) @extend_schema_view() class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.RetrieveAPIView): @@ -442,7 +408,21 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev if serializer.validated_data['clone_source']: prototype: LibraryItem = serializer.validated_data['source'] - new_operation.result = _create_clone(prototype, new_operation, item) + + schema_clone = deepcopy(prototype) + schema_clone.pk = None + schema_clone.owner = item.owner + schema_clone.title = new_operation.title + schema_clone.alias = new_operation.alias + schema_clone.description = new_operation.description + schema_clone.visible = False + schema_clone.read_only = False + schema_clone.access_policy = item.access_policy + schema_clone.location = item.location + schema_clone.save() + RSFormCached(schema_clone).insert_from(prototype.pk) + + new_operation.result = schema_clone new_operation.save(update_fields=["result"]) item.save(update_fields=['time_update']) @@ -860,7 +840,7 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev m.PropagationFacade.before_delete_cst(data['source'], ids) source.delete_cst(ids) else: - new_items = oss.relocate_up(source, destination, data['items']) + new_items = oss.relocate_up(source, destination, ids) m.PropagationFacade.after_create_cst(destination, new_items, exclude=[oss.model.pk]) return Response(status=c.HTTP_200_OK) diff --git a/rsconcept/backend/apps/rsform/models/RSForm.py b/rsconcept/backend/apps/rsform/models/RSForm.py index 6ddffb13..ae1804cf 100644 --- a/rsconcept/backend/apps/rsform/models/RSForm.py +++ b/rsconcept/backend/apps/rsform/models/RSForm.py @@ -15,7 +15,6 @@ from .api_RSLanguage import get_type_prefix, guess_type from .Attribution import Attribution from .Constituenta import Constituenta, CstType, extract_entities, extract_globals -INSERT_LAST: int = -1 DELETED_ALIAS = 'DEL' diff --git a/rsconcept/backend/apps/rsform/models/RSFormCached.py b/rsconcept/backend/apps/rsform/models/RSFormCached.py index 160c831a..87b99059 100644 --- a/rsconcept/backend/apps/rsform/models/RSFormCached.py +++ b/rsconcept/backend/apps/rsform/models/RSFormCached.py @@ -14,7 +14,7 @@ from shared import messages as msg from .api_RSLanguage import generate_structure, get_type_prefix, guess_type from .Attribution import Attribution from .Constituenta import Constituenta, CstType -from .RSForm import DELETED_ALIAS, INSERT_LAST, RSForm +from .RSForm import DELETED_ALIAS, RSForm class RSFormCached: @@ -76,7 +76,7 @@ class RSFormCached: def create_cst(self, data: dict, insert_after: Optional[Constituenta] = None) -> Constituenta: ''' Create constituenta from data. ''' self.cache.ensure_loaded_terms() - if insert_after is not None: + if insert_after: position = self.cache.by_id[insert_after.pk].order + 1 else: position = len(self.cache.constituents) @@ -109,52 +109,77 @@ class RSFormCached: RSForm.resolve_term_change(self.cache.constituents, [result.pk], self.cache.by_alias, self.cache.by_id) return result + def insert_from( + self, sourceID: int, + items_list: Optional[list[int]] = None, + initial_mapping: Optional[dict[str, str]] = None + ) -> list[tuple[Constituenta, Constituenta]]: + ''' Insert copy of constituents from source schema. ''' + if not items_list: + items = list(Constituenta.objects.filter(schema_id=sourceID).order_by('order')) + else: + items = list(Constituenta.objects.filter(pk__in=items_list, schema_id=sourceID).order_by('order')) + if not items: + return [] + new_constituents = self.insert_copy(items=items, initial_mapping=initial_mapping) + return list(zip(items, new_constituents)) + def insert_copy( self, items: list[Constituenta], - position: int = INSERT_LAST, + position: Optional[int] = None, initial_mapping: Optional[dict[str, str]] = None ) -> list[Constituenta]: ''' Insert copy of target constituents updating references. ''' - count = len(items) - if count == 0: + if not items: return [] self.cache.ensure_loaded() - lastPosition = len(self.cache.constituents) - if position == INSERT_LAST: - position = lastPosition + last_position = len(self.cache.constituents) + if not position: + position = last_position else: - position = max(0, min(position, lastPosition)) - RSForm.shift_positions(position, count, self.cache.constituents) + position = max(0, min(position, last_position)) - indices: dict[str, int] = {} - for (value, _) in CstType.choices: - indices[value] = -1 + was_empty = last_position == 0 + if not was_empty and position != last_position: + RSForm.shift_positions(position, len(items), self.cache.constituents) - mapping: dict[str, str] = initial_mapping.copy() if initial_mapping else {} - for cst in items: - if indices[cst.cst_type] == -1: - indices[cst.cst_type] = self._get_max_index(cst.cst_type) - indices[cst.cst_type] = indices[cst.cst_type] + 1 - newAlias = f'{get_type_prefix(cst.cst_type)}{indices[cst.cst_type]}' - mapping[cst.alias] = newAlias + mapping_alias: dict[str, str] = initial_mapping.copy() if initial_mapping else {} + if not was_empty: + indices: dict[str, int] = {} + for (value, _) in CstType.choices: + indices[value] = self._get_max_index(value) - result = deepcopy(items) - for cst in result: + for cst in items: + indices[cst.cst_type] = indices[cst.cst_type] + 1 + newAlias = f'{get_type_prefix(cst.cst_type)}{indices[cst.cst_type]}' + mapping_alias[cst.alias] = newAlias + + source_ids = [cst.id for cst in items] + new_constituents = deepcopy(items) + for cst in new_constituents: cst.pk = None cst.schema = self.model cst.order = position - cst.alias = mapping[cst.alias] - cst.apply_mapping(mapping) + if mapping_alias: + cst.alias = mapping_alias[cst.alias] + cst.apply_mapping(mapping_alias) position = position + 1 - new_cst = Constituenta.objects.bulk_create(result) + new_constituents = Constituenta.objects.bulk_create(new_constituents) - # TODO: duplicate attributions + mapping_id: dict[int, int] = {source_ids[i]: new_constituents[i].id for i in range(len(source_ids))} + attributions = list(Attribution.objects.filter(container__in=source_ids, attribute__in=source_ids)) + for attr in attributions: + attr.pk = None + attr.container_id = mapping_id[attr.container_id] + attr.attribute_id = mapping_id[attr.attribute_id] - self.cache.insert_multi(new_cst) - return result + Attribution.objects.bulk_create(attributions) + + self.cache.insert_multi(new_constituents) + return new_constituents # pylint: disable=too-many-branches def update_cst(self, target: int, data: dict) -> dict: diff --git a/rsconcept/backend/apps/rsform/models/__init__.py b/rsconcept/backend/apps/rsform/models/__init__.py index 0d5afb28..0b72a5c9 100644 --- a/rsconcept/backend/apps/rsform/models/__init__.py +++ b/rsconcept/backend/apps/rsform/models/__init__.py @@ -3,5 +3,5 @@ from .Attribution import Attribution from .Constituenta import Constituenta, CstType, extract_globals, replace_entities, replace_globals from .OrderManager import OrderManager -from .RSForm import DELETED_ALIAS, INSERT_LAST, RSForm +from .RSForm import DELETED_ALIAS, RSForm from .RSFormCached import RSFormCached diff --git a/rsconcept/backend/apps/rsform/serializers/data_access.py b/rsconcept/backend/apps/rsform/serializers/data_access.py index ecef5d76..a161c5d6 100644 --- a/rsconcept/backend/apps/rsform/serializers/data_access.py +++ b/rsconcept/backend/apps/rsform/serializers/data_access.py @@ -436,7 +436,7 @@ class InlineSynthesisSerializer(StrictSerializer): ''' Serializer: Inline synthesis operation input. ''' receiver = PKField(many=False, queryset=LibraryItem.objects.all().only('owner_id')) source = PKField(many=False, queryset=LibraryItem.objects.all().only('owner_id')) # type: ignore - items = PKField(many=True, queryset=Constituenta.objects.all()) + items = PKField(many=True, queryset=Constituenta.objects.all().only('schema_id')) substitutions = serializers.ListField( child=SubstitutionSerializerBase() ) diff --git a/rsconcept/backend/apps/rsform/views/rsforms.py b/rsconcept/backend/apps/rsform/views/rsforms.py index 52406a5a..0d958133 100644 --- a/rsconcept/backend/apps/rsform/views/rsforms.py +++ b/rsconcept/backend/apps/rsform/views/rsforms.py @@ -732,25 +732,24 @@ def inline_synthesis(request: Request) -> HttpResponse: serializer.is_valid(raise_exception=True) receiver = m.RSFormCached(serializer.validated_data['receiver']) - items = cast(list[m.Constituenta], serializer.validated_data['items']) - if not items: - source = cast(LibraryItem, serializer.validated_data['source']) - items = list(m.Constituenta.objects.filter(schema=source).order_by('order')) + 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(): - new_items = receiver.insert_copy(items) - PropagationFacade.after_create_cst(receiver, new_items) + 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]) substitutions: list[tuple[m.Constituenta, m.Constituenta]] = [] for substitution in serializer.validated_data['substitutions']: original = cast(m.Constituenta, substitution['original']) replacement = cast(m.Constituenta, substitution['substitution']) - if original in items: - index = next(i for (i, cst) in enumerate(items) if cst.pk == original.pk) - original = new_items[index] + if original.pk in target_ids: + original = mapping_ids[original.pk] else: - index = next(i for (i, cst) in enumerate(items) if cst.pk == replacement.pk) - replacement = new_items[index] + replacement = mapping_ids[replacement.pk] substitutions.append((original, replacement)) PropagationFacade.before_substitute(receiver.model.pk, substitutions)