From 2cb7c44f3adcfa60345ea70072e45dc11c586316 Mon Sep 17 00:00:00 2001 From: Ivan <8611739+IRBorisov@users.noreply.github.com> Date: Thu, 17 Jul 2025 19:17:08 +0300 Subject: [PATCH] B: Fix cyclic block hierarchy --- .../apps/oss/serializers/data_access.py | 2 +- .../apps/oss/tests/s_views/t_blocks.py | 33 +++++++++++++++++++ .../features/oss/dialogs/dlg-edit-block.tsx | 20 ++++++----- 3 files changed, 46 insertions(+), 9 deletions(-) diff --git a/rsconcept/backend/apps/oss/serializers/data_access.py b/rsconcept/backend/apps/oss/serializers/data_access.py index 20eff108..fd916371 100644 --- a/rsconcept/backend/apps/oss/serializers/data_access.py +++ b/rsconcept/backend/apps/oss/serializers/data_access.py @@ -123,7 +123,7 @@ class UpdateBlockSerializer(StrictSerializer): raise serializers.ValidationError({ 'parent': msg.parentNotInOSS() }) - if parent == attrs['target']: + if attrs['target'].pk in _collect_ancestors(parent): raise serializers.ValidationError({ 'parent': msg.blockCyclicHierarchy() }) 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 e319c458..202ee455 100644 --- a/rsconcept/backend/apps/oss/tests/s_views/t_blocks.py +++ b/rsconcept/backend/apps/oss/tests/s_views/t_blocks.py @@ -266,3 +266,36 @@ class TestOssBlocks(EndpointTester): data['layout'] = self.layout_data self.executeOK(data=data) + + + @decl_endpoint('/api/oss/{item}/update-block', method='patch') + def test_update_block_cyclic_parent(self): + self.populateData() + # block1 -> block2 + # Try to set block1's parent to block2 (should fail, direct cycle) + data = { + 'target': self.block1.pk, + 'item_data': { + 'title': self.block1.title, + 'description': self.block1.description, + 'parent': self.block2.pk + }, + } + self.executeBadData(data=data, item=self.owned_id) + + # Create a deeper hierarchy: block1 -> block2 -> block3 + self.block3 = self.owned.create_block(title='3', parent=self.block2) + # Try to set block1's parent to block3 (should fail, indirect cycle) + data['item_data']['parent'] = self.block3.pk + self.executeBadData(data=data, item=self.owned_id) + + # Setting block2's parent to block1 (valid, as block1 is not a descendant) + data = { + 'target': self.block2.pk, + 'item_data': { + 'title': self.block2.title, + 'description': self.block2.description, + 'parent': self.block1.pk + }, + } + self.executeOK(data=data, item=self.owned_id) diff --git a/rsconcept/frontend/src/features/oss/dialogs/dlg-edit-block.tsx b/rsconcept/frontend/src/features/oss/dialogs/dlg-edit-block.tsx index 20898abb..1409a5bd 100644 --- a/rsconcept/frontend/src/features/oss/dialogs/dlg-edit-block.tsx +++ b/rsconcept/frontend/src/features/oss/dialogs/dlg-edit-block.tsx @@ -66,14 +66,18 @@ export function DlgEditBlock() { ( - block.id !== target.id)} - value={field.value ? manager.oss.blockByID.get(field.value) ?? null : null} - placeholder='Родительский блок' - onChange={value => field.onChange(value ? value.id : null)} - /> - )} + render={({ field }) => { + const descendantNodeIDs = manager.oss.hierarchy.expandAllOutputs([target.nodeID]); + descendantNodeIDs.push(target.nodeID); + return ( + !descendantNodeIDs.includes(block.nodeID))} + value={field.value ? manager.oss.blockByID.get(field.value) ?? null : null} + placeholder='Родительский блок' + onChange={value => field.onChange(value ? value.id : null)} + /> + ); + }} />