Skip to content

Commit e0bc793

Browse files
Fix/update mappings inside concept without concept changes (#819)
* fix(graphql): align search behavior with REST for HEAD versions * Adopt a permissive base QuerySet for textual GraphQL searches to rely on Elasticsearch's version filtering logic. * Apply the same Elasticsearch filters as REST (source_version=HEAD and is_latest_version=True). * Use strict 'id = versioned_object_id' filtering only when listing without search terms. * Remove SQL fallback logic to ensure consistent ES-only behavior across REST and GraphQL search flows. * fix(concepts): allow mapping-only patches without duplicate checks * Introduce skip_duplicate_version_check in SourceChildMixin and pass it for mappings-only payloads so concept PATCH requests that only modify mappings bypass duplicate-version prevention. * Add an integration test covering mapping upsert and delete during concept PATCH, asserting expected versioning behavior and mapping counts. * chore: revert graphql updates on this branch
1 parent 45da7ed commit e0bc793

File tree

3 files changed

+68
-2
lines changed

3 files changed

+68
-2
lines changed

core/common/mixins.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -893,6 +893,7 @@ def save_as_new_version(self, user, **kwargs): # pylint: disable=too-many-branc
893893
parent_concept_uris = kwargs.pop('parent_concept_uris', None)
894894
add_prev_version_children = kwargs.pop('add_prev_version_children', True)
895895
_hierarchy_processing = kwargs.pop('_hierarchy_processing', False)
896+
skip_duplicate_version_check = kwargs.pop('skip_duplicate_version_check', False)
896897
errors = {}
897898
self.created_by = self.updated_by = user
898899
self.version = self.version or generate_temp_version()
@@ -919,7 +920,7 @@ def save_as_new_version(self, user, **kwargs): # pylint: disable=too-many-branc
919920
self.set_checksums()
920921
if Toggle.get(
921922
'PREVENT_DUPLICATE_VERSION_TOGGLE'
922-
) and not _hierarchy_processing:
923+
) and not _hierarchy_processing and not skip_duplicate_version_check:
923924
standard_checksum = prev_latest.checksums.get('standard')
924925
if not standard_checksum:
925926
standard_checksum = prev_latest.get_checksums(recalculate=True).get('standard')

core/concepts/models.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -566,7 +566,8 @@ def create_new_version_for(
566566
create_parent_version=create_parent_version,
567567
parent_concept_uris=parent_concept_uris,
568568
add_prev_version_children=add_prev_version_children,
569-
_hierarchy_processing=_hierarchy_processing
569+
_hierarchy_processing=_hierarchy_processing,
570+
skip_duplicate_version_check=bool(mappings_payload)
570571
)
571572

572573
if errors or mappings_payload is None:

core/integration_tests/tests_concepts.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -631,6 +631,70 @@ def test_put_200_with_mappings_upsert_and_delete(self):
631631
)
632632
self.assertEqual(concept.get_unidirectional_mappings().filter(retired=False).count(), 2)
633633

634+
def test_patch_200_with_mappings_only_upsert_and_delete(self):
635+
concept = ConceptFactory(
636+
parent=self.source,
637+
names=[ConceptNameFactory.build(locale='en', locale_preferred=True)]
638+
)
639+
concepts_url = f"/orgs/{self.organization.mnemonic}/sources/{self.source.mnemonic}/concepts/{concept.mnemonic}/"
640+
update_target = ConceptFactory(parent=self.source)
641+
update_target_new = ConceptFactory(parent=self.source)
642+
delete_target = ConceptFactory(parent=self.source)
643+
new_target = ConceptFactory(parent=self.source)
644+
645+
mapping_to_update = MappingFactory(
646+
parent=self.source, from_concept=concept, to_concept=update_target, map_type='Same As'
647+
).versioned_object
648+
mapping_to_delete = MappingFactory(
649+
parent=self.source, from_concept=concept, to_concept=delete_target, map_type='BROADER-THAN'
650+
).versioned_object
651+
652+
initial_versions_count = concept.versions.count()
653+
654+
response = self.client.patch(
655+
concepts_url,
656+
{
657+
'mappings': [
658+
{
659+
'id': mapping_to_update.mnemonic,
660+
'map_type': 'NARROWER-THAN',
661+
'update_comment': 'updated map type',
662+
'to_concept_url': update_target_new.url
663+
},
664+
{
665+
'to_concept_url': new_target.url,
666+
'map_type': 'Same As'
667+
},
668+
{
669+
'id': mapping_to_delete.mnemonic,
670+
'action': '__delete',
671+
'update_comment': 'Deleted from concept patch'
672+
}
673+
]
674+
},
675+
HTTP_AUTHORIZATION='Token ' + self.token,
676+
format='json'
677+
)
678+
679+
self.assertEqual(response.status_code, 200, response.data)
680+
mapping_to_update.refresh_from_db()
681+
mapping_to_delete.refresh_from_db()
682+
concept.refresh_from_db()
683+
684+
self.assertEqual(mapping_to_update.map_type, 'NARROWER-THAN')
685+
self.assertEqual(mapping_to_update.to_concept_id, update_target_new.id)
686+
self.assertEqual(mapping_to_update.versions.count(), 2)
687+
self.assertTrue(mapping_to_delete.retired)
688+
self.assertEqual(concept.versions.count(), initial_versions_count + 1)
689+
self.assertTrue(
690+
concept.get_unidirectional_mappings().filter(
691+
to_concept_id=new_target.id,
692+
map_type='Same As',
693+
retired=False
694+
).exists()
695+
)
696+
self.assertEqual(concept.get_unidirectional_mappings().filter(retired=False).count(), 2)
697+
634698
def test_put_400_with_mappings_everything_or_nothing(self):
635699
concept = ConceptFactory(parent=self.source)
636700
concepts_url = f"/orgs/{self.organization.mnemonic}/sources/{self.source.mnemonic}/concepts/{concept.mnemonic}/"

0 commit comments

Comments
 (0)