From bfcc43457cbede7ac9a17ae58c36d9e7b2b5d669 Mon Sep 17 00:00:00 2001 From: Ivan <8611739+IRBorisov@users.noreply.github.com> Date: Mon, 27 Oct 2025 13:02:12 +0300 Subject: [PATCH] F: Implement attribution merge on substitution --- .../backend/apps/rsform/models/RSForm.py | 20 ++++++++ .../apps/rsform/models/RSFormCached.py | 23 ++++++++++ .../apps/rsform/tests/s_views/t_rsforms.py | 46 ++++++++++++++++++- 3 files changed, 88 insertions(+), 1 deletion(-) diff --git a/rsconcept/backend/apps/rsform/models/RSForm.py b/rsconcept/backend/apps/rsform/models/RSForm.py index 8b245dc3..6ddffb13 100644 --- a/rsconcept/backend/apps/rsform/models/RSForm.py +++ b/rsconcept/backend/apps/rsform/models/RSForm.py @@ -12,6 +12,7 @@ from shared import messages as msg from ..graph import Graph 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 @@ -271,6 +272,25 @@ class RSForm: mapping[original.alias] = substitution.alias deleted.append(original.pk) replacements.append(substitution.pk) + + attributions = list(Attribution.objects.filter(container__schema=self.model)) + if attributions: + orig_to_sub = {original.pk: substitution.pk for original, substitution in substitutions} + orig_pks = set(orig_to_sub.keys()) + + for attr in attributions: + if attr.container_id not in orig_pks and attr.attribute_id not in orig_pks: + continue + + container_id = orig_to_sub.get(attr.container_id) + container_id = container_id if container_id is not None else attr.container_id + attr_id = orig_to_sub.get(attr.attribute_id) + attr_id = attr_id if attr_id is not None else attr.attribute_id + if not any(a.container_id == container_id and a.attribute_id == attr_id for a in attributions): + attr.attribute_id = attr_id + attr.container_id = container_id + attr.save() + Constituenta.objects.filter(pk__in=deleted).delete() cst_list = Constituenta.objects.filter(schema=self.model).only( 'alias', 'cst_type', 'definition_formal', diff --git a/rsconcept/backend/apps/rsform/models/RSFormCached.py b/rsconcept/backend/apps/rsform/models/RSFormCached.py index ab8305ad..160c831a 100644 --- a/rsconcept/backend/apps/rsform/models/RSFormCached.py +++ b/rsconcept/backend/apps/rsform/models/RSFormCached.py @@ -12,6 +12,7 @@ from apps.library.models import LibraryItem, LibraryItemType 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 @@ -149,6 +150,9 @@ class RSFormCached: position = position + 1 new_cst = Constituenta.objects.bulk_create(result) + + # TODO: duplicate attributions + self.cache.insert_multi(new_cst) return result @@ -233,6 +237,25 @@ class RSFormCached: mapping[original.alias] = substitution.alias deleted.append(original) replacements.append(substitution.pk) + + attributions = list(Attribution.objects.filter(container__schema=self.model)) + if attributions: + orig_to_sub = {original.pk: substitution.pk for original, substitution in substitutions} + orig_pks = set(orig_to_sub.keys()) + + for attr in attributions: + if attr.container_id not in orig_pks and attr.attribute_id not in orig_pks: + continue + + container_id = orig_to_sub.get(attr.container_id) + container_id = container_id if container_id is not None else attr.container_id + attr_id = orig_to_sub.get(attr.attribute_id) + attr_id = attr_id if attr_id is not None else attr.attribute_id + if not any(a.container_id == container_id and a.attribute_id == attr_id for a in attributions): + attr.attribute_id = attr_id + attr.container_id = container_id + attr.save() + self.cache.remove_multi(deleted) Constituenta.objects.filter(pk__in=[cst.pk for cst in deleted]).delete() RSForm.save_order(self.cache.constituents) diff --git a/rsconcept/backend/apps/rsform/tests/s_views/t_rsforms.py b/rsconcept/backend/apps/rsform/tests/s_views/t_rsforms.py index 388a2681..0927dc54 100644 --- a/rsconcept/backend/apps/rsform/tests/s_views/t_rsforms.py +++ b/rsconcept/backend/apps/rsform/tests/s_views/t_rsforms.py @@ -238,11 +238,55 @@ class TestRSFormViewset(EndpointTester): 'substitution': d2.pk } ]} - response = self.executeOK(data, item=self.owned_id) + self.executeOK(data, item=self.owned_id) d3.refresh_from_db() self.assertEqual(d3.definition_formal, r'D1 \ D2') + @decl_endpoint('/api/rsforms/{item}/substitute', method='patch') + def test_substitute_with_attributions(self): + self.set_params(item=self.owned_id) + + # Create two base items + x1 = self.owned.insert_last('X1') + x2 = self.owned.insert_last('X2') + + # Create two attributes to be attributions + a1 = self.owned.insert_last('A1', cst_type=CstType.BASE) + a2 = self.owned.insert_last('A2', cst_type=CstType.BASE) + + # Create attributions: X1 -> A1, X2 -> A2 + Attribution = self.owned.constituentsQ().model._meta.apps.get_model('rsform', 'Attribution') + Attribution.objects.create(container=x1, attribute=a1) + Attribution.objects.create(container=x2, attribute=a2) + + # Substitute x1 with x2 + data = { + 'substitutions': [{ + 'original': x1.pk, + 'substitution': x2.pk + }] + } + self.executeOK(data, item=self.owned_id) + + # Fetch updated attributions + attributions = Attribution.objects.filter( + container__in=[x1.pk, x2.pk], + attribute__in=[a1.pk, a2.pk] + ) + self.assertEqual(len(attributions), 2) + + # Confirm the attribution with container originally x1 is now x2, and there are no duplicates + containers = set() + attributes = set() + for attr in attributions: + containers.add(attr.container_id) + attributes.add(attr.attribute_id) + self.assertIn(x2.pk, containers) + self.assertIn(a1.pk, attributes) + self.assertIn(a2.pk, attributes) + + @decl_endpoint('/api/rsforms/{item}/create-cst', method='post') def test_create_constituenta_data(self): data = {