From 5e40eef21a7bc98f1a2b0db143ca5de20174defc Mon Sep 17 00:00:00 2001 From: Filipe Lopes Date: Mon, 22 Jun 2026 12:35:27 -0300 Subject: [PATCH 1/5] feat(concepts): add detection for locale status changes impacting version creation * Introduce static method to detect retired status edits not affecting checksum * Update version cloning logic to consider locale status changes via * Add integration test verifying PUT returns 200 when only name retired changes * Ensure proper handling of active vs retired name transitions in versioning --- core/concepts/models.py | 54 +++++++++++++++++++++- core/integration_tests/tests_concepts.py | 58 ++++++++++++++++++++++++ 2 files changed, 111 insertions(+), 1 deletion(-) diff --git a/core/concepts/models.py b/core/concepts/models.py index 681f9090..582bb2b6 100644 --- a/core/concepts/models.py +++ b/core/concepts/models.py @@ -574,6 +574,48 @@ def create_initial_version(cls, concept, **kwargs): initial_version.save() return initial_version + @staticmethod + def _locale_status_changed(previous_locales, new_locales, locale_payloads): + """Detect locale status edits that the standard checksum intentionally ignores.""" + previous_by_identity = { + ( + locale.external_id, + locale.name, + locale.type, + locale.locale, + locale.locale_preferred, + ): locale + for locale in previous_locales + } + previous_by_content = { + (locale.name, locale.type, locale.locale, locale.locale_preferred): locale + for locale in previous_locales + } + status_fields = {'retired', 'retire_reason'} + for new_locale, locale_payload in zip(new_locales, locale_payloads): + if not status_fields.intersection(locale_payload): + continue + previous_locale = previous_by_identity.get(( + new_locale.external_id, + new_locale.name, + new_locale.type, + new_locale.locale, + new_locale.locale_preferred, + )) + if not previous_locale: + previous_locale = previous_by_content.get(( + new_locale.name, + new_locale.type, + new_locale.locale, + new_locale.locale_preferred, + )) + if previous_locale and ( + previous_locale.retired != new_locale.retired or + previous_locale.retire_reason != new_locale.retire_reason + ): + return True + return False + @classmethod def create_new_version_for( cls, instance, data, user, create_parent_version=True, add_prev_version_children=True, @@ -583,6 +625,8 @@ def create_new_version_for( prev_latest = Concept.objects.filter( mnemonic=instance.mnemonic, parent_id=instance.parent_id, is_latest_version=True ).first() + names_payload = data.get('names', []) + descriptions_payload = data.get('descriptions', []) instance.id = None # Clear id so it is persisted as a new object instance.version = data.get('version', None) instance.concept_class = data.get('concept_class', instance.concept_class) @@ -611,13 +655,21 @@ def create_new_version_for( if not parent_concept_uris and has_parent_concept_uris_attr: parent_concept_uris = [] + has_locale_status_change = False + if prev_latest: + has_locale_status_change = cls._locale_status_changed( + prev_latest.clone_name_locales(), instance.cloned_names, names_payload + ) or cls._locale_status_changed( + prev_latest.clone_description_locales(), instance.cloned_descriptions, descriptions_payload + ) + errors = instance.save_as_new_version( user=user, create_parent_version=create_parent_version, parent_concept_uris=parent_concept_uris, add_prev_version_children=add_prev_version_children, _hierarchy_processing=_hierarchy_processing, - skip_duplicate_version_check=bool(mappings_payload) + skip_duplicate_version_check=bool(mappings_payload) or has_locale_status_change ) if errors or mappings_payload is None: diff --git a/core/integration_tests/tests_concepts.py b/core/integration_tests/tests_concepts.py index d1d311ed..997ceb0a 100644 --- a/core/integration_tests/tests_concepts.py +++ b/core/integration_tests/tests_concepts.py @@ -494,6 +494,64 @@ def test_put_200(self): # pylint: disable=too-many-statements self.assertEqual(response.data['display_name'], prev_version.display_name) self.assertEqual(concept.datatype, "N/A") + def test_put_200_when_name_retired_changes_only(self): + names = [ + ConceptNameFactory.build(name='Active name', locale='es', locale_preferred=True), + ConceptNameFactory.build(name='Retirable name', locale='es', locale_preferred=False), + ] + concept = ConceptFactory( + parent=self.source, + concept_class='Procedure', + datatype='Coded', + names=names, + descriptions=[ + ConceptDescriptionFactory.build( + name='Concept description', locale='es', locale_preferred=True + ) + ] + ) + concepts_url = f"/orgs/{self.organization.mnemonic}/sources/{self.source.mnemonic}/concepts/{concept.mnemonic}/" + names_payload = [ + { + 'external_id': name.external_id, + 'name': name.name, + 'locale': name.locale, + 'locale_preferred': name.locale_preferred, + 'name_type': name.type, + 'retired': name.name == 'Retirable name', + } + for name in concept.names.order_by('id') + ] + + response = self.client.put( + concepts_url, + { + 'datatype': concept.datatype, + 'concept_class': concept.concept_class, + 'extras': concept.extras, + 'descriptions': [ + { + 'description': description.name, + 'description_type': description.type, + 'external_id': description.external_id, + 'locale': description.locale, + 'locale_preferred': description.locale_preferred, + } + for description in concept.descriptions.order_by('id') + ], + 'external_id': concept.external_id or '', + 'id': concept.mnemonic, + 'names': names_payload, + }, + HTTP_AUTHORIZATION='Token ' + self.token, + format='json' + ) + + self.assertEqual(response.status_code, 200) + self.assertEqual(concept.versions.count(), 2) + self.assertFalse(concept.get_latest_version().active_names.get(name='Active name').retired) + self.assertTrue(concept.get_latest_version().retired_names.get(name='Retirable name').retired) + def test_put_200_with_mappings(self): # pylint: disable=too-many-statements concept = ConceptFactory(parent=self.source, datatype="N/A") self.assertEqual(concept.versions.count(), 1) From e7aff85f52211ff6dc150e8f8474827608f12dc4 Mon Sep 17 00:00:00 2001 From: Filipe Lopes Date: Mon, 22 Jun 2026 18:27:46 -0300 Subject: [PATCH 2/5] feat(checksum): add locale status handling to standard checksum * Introduce ChecksumBase subclass to include locale retirement fields in standard checksum * Update tests to verify checksum variations when locale status changes * Remove unused _locale_status_changed method from Concept model --- core/common/checksums.py | 14 +++++++- core/common/tests.py | 71 ++++++++++++++++++++++++++++++++++++++-- core/concepts/models.py | 54 +----------------------------- 3 files changed, 83 insertions(+), 56 deletions(-) diff --git a/core/common/checksums.py b/core/common/checksums.py index c36966de..b45d952e 100644 --- a/core/common/checksums.py +++ b/core/common/checksums.py @@ -1,6 +1,6 @@ from django.conf import settings from django.db import models -from ocldev.checksum import Checksum as ChecksumBase +from ocldev.checksum import Checksum as OCLDevChecksum, getvalue from pydash import get @@ -20,6 +20,18 @@ CHANGELOG_ENRICHMENT_VERBOSITY = 4 +class ChecksumBase(OCLDevChecksum): + def _locales_for_checksums(self, data, relation, predicate_func): + """Include locale status in standard concept checksums.""" + locales = list(getvalue(data, relation, [])) + checksum_locales = super()._locales_for_checksums(data, relation, predicate_func) + if self.checksum_type == 'standard': + included_locales = [locale for locale in locales if predicate_func(locale)] + for checksum_locale, locale in zip(checksum_locales, included_locales): + checksum_locale.update({field: get(locale, field, None) for field in ['retired', 'retire_reason']}) + return checksum_locales + + class ChecksumModel(models.Model): class Meta: abstract = True diff --git a/core/common/tests.py b/core/common/tests.py index 2a7de7a6..9dc78486 100644 --- a/core/common/tests.py +++ b/core/common/tests.py @@ -50,7 +50,7 @@ from core.users.models import UserProfile from core.users.tests.factories import UserProfileFactory from .backends import OCLOIDCAuthenticationBackend -from .checksums import Checksum +from .checksums import Checksum, ChecksumBase from .fhir_helpers import translate_fhir_query from .serializers import IdentifierSerializer from .validators import URIValidator @@ -1340,7 +1340,6 @@ def test_generate(self): concept1 = ConceptFactory(mnemonic=encode_string('Foo/bar', safe=' ')) - from ocldev.checksum import Checksum as ChecksumBase self.assertEqual( ChecksumBase('concept', ConceptDetailSerializer(concept1).data, 'standard').generate(), concept1.checksums['standard'] @@ -1361,6 +1360,74 @@ def test_generate(self): mapping1.checksums['smart'] ) + def test_concept_standard_checksum_includes_locale_status(self): + concept_data = { + 'concept_class': 'Diagnosis', + 'datatype': 'None', + 'retired': False, + 'names': [ + { + 'locale': 'es', + 'locale_preferred': False, + 'name': 'Nombre de prueba OCL local', + 'name_type': 'FULLY_SPECIFIED', + 'external_id': None, + 'retired': False, + 'retire_reason': None, + } + ], + 'descriptions': [ + { + 'locale': 'es', + 'locale_preferred': False, + 'description': 'Descripcion de prueba', + 'description_type': 'Description', + 'external_id': None, + 'retired': False, + 'retire_reason': None, + } + ], + } + concept_data_with_retired_name = { + **concept_data, + 'names': [{**concept_data['names'][0], 'retired': True}], + } + concept_data_with_name_retire_reason = { + **concept_data, + 'names': [{**concept_data['names'][0], 'retire_reason': 'Duplicate'}], + } + concept_data_with_retired_description = { + **concept_data, + 'descriptions': [{**concept_data['descriptions'][0], 'retired': True}], + } + concept_data_with_description_retire_reason = { + **concept_data, + 'descriptions': [{**concept_data['descriptions'][0], 'retire_reason': 'Duplicate'}], + } + standard_checksum = ChecksumBase('concept', concept_data, 'standard').generate() + smart_checksum = ChecksumBase('concept', concept_data, 'smart').generate() + + self.assertNotEqual( + standard_checksum, + ChecksumBase('concept', concept_data_with_retired_name, 'standard').generate() + ) + self.assertNotEqual( + standard_checksum, + ChecksumBase('concept', concept_data_with_name_retire_reason, 'standard').generate() + ) + self.assertNotEqual( + standard_checksum, + ChecksumBase('concept', concept_data_with_retired_description, 'standard').generate() + ) + self.assertNotEqual( + standard_checksum, + ChecksumBase('concept', concept_data_with_description_retire_reason, 'standard').generate() + ) + self.assertEqual( + smart_checksum, + ChecksumBase('concept', concept_data_with_retired_name, 'smart').generate() + ) + class ChecksumViewTest(OCLAPITestCase): def setUp(self): diff --git a/core/concepts/models.py b/core/concepts/models.py index 582bb2b6..681f9090 100644 --- a/core/concepts/models.py +++ b/core/concepts/models.py @@ -574,48 +574,6 @@ def create_initial_version(cls, concept, **kwargs): initial_version.save() return initial_version - @staticmethod - def _locale_status_changed(previous_locales, new_locales, locale_payloads): - """Detect locale status edits that the standard checksum intentionally ignores.""" - previous_by_identity = { - ( - locale.external_id, - locale.name, - locale.type, - locale.locale, - locale.locale_preferred, - ): locale - for locale in previous_locales - } - previous_by_content = { - (locale.name, locale.type, locale.locale, locale.locale_preferred): locale - for locale in previous_locales - } - status_fields = {'retired', 'retire_reason'} - for new_locale, locale_payload in zip(new_locales, locale_payloads): - if not status_fields.intersection(locale_payload): - continue - previous_locale = previous_by_identity.get(( - new_locale.external_id, - new_locale.name, - new_locale.type, - new_locale.locale, - new_locale.locale_preferred, - )) - if not previous_locale: - previous_locale = previous_by_content.get(( - new_locale.name, - new_locale.type, - new_locale.locale, - new_locale.locale_preferred, - )) - if previous_locale and ( - previous_locale.retired != new_locale.retired or - previous_locale.retire_reason != new_locale.retire_reason - ): - return True - return False - @classmethod def create_new_version_for( cls, instance, data, user, create_parent_version=True, add_prev_version_children=True, @@ -625,8 +583,6 @@ def create_new_version_for( prev_latest = Concept.objects.filter( mnemonic=instance.mnemonic, parent_id=instance.parent_id, is_latest_version=True ).first() - names_payload = data.get('names', []) - descriptions_payload = data.get('descriptions', []) instance.id = None # Clear id so it is persisted as a new object instance.version = data.get('version', None) instance.concept_class = data.get('concept_class', instance.concept_class) @@ -655,21 +611,13 @@ def create_new_version_for( if not parent_concept_uris and has_parent_concept_uris_attr: parent_concept_uris = [] - has_locale_status_change = False - if prev_latest: - has_locale_status_change = cls._locale_status_changed( - prev_latest.clone_name_locales(), instance.cloned_names, names_payload - ) or cls._locale_status_changed( - prev_latest.clone_description_locales(), instance.cloned_descriptions, descriptions_payload - ) - errors = instance.save_as_new_version( user=user, create_parent_version=create_parent_version, parent_concept_uris=parent_concept_uris, add_prev_version_children=add_prev_version_children, _hierarchy_processing=_hierarchy_processing, - skip_duplicate_version_check=bool(mappings_payload) or has_locale_status_change + skip_duplicate_version_check=bool(mappings_payload) ) if errors or mappings_payload is None: From 094807f1963306ff3238aedbb98d03c85fed7eb1 Mon Sep 17 00:00:00 2001 From: Filipe Lopes Date: Mon, 22 Jun 2026 18:39:32 -0300 Subject: [PATCH 3/5] refactor(core): rename ChecksumBase to OCLAPIChecksum and add locale status fields * Replace ChecksumBase with OCLAPIChecksum throughout core * Introduce LOCALE_STATUS_CHECKSUM_FIELDS constant for retired/status tracking * Update checksum generation calls in ChecksumModel and related methods * Adjust tests to import OCLAPIChecksum and use the new class name * Update patch decorator target for checksum generation mocks --- core/common/checksums.py | 14 +++++---- core/common/tests.py | 59 +++++++++++------------------------- core/concepts/tests/tests.py | 2 +- 3 files changed, 27 insertions(+), 48 deletions(-) diff --git a/core/common/checksums.py b/core/common/checksums.py index b45d952e..7ba7b8fb 100644 --- a/core/common/checksums.py +++ b/core/common/checksums.py @@ -1,6 +1,6 @@ from django.conf import settings from django.db import models -from ocldev.checksum import Checksum as OCLDevChecksum, getvalue +from ocldev.checksum import Checksum as ChecksumBase, getvalue from pydash import get @@ -19,8 +19,10 @@ # Include expanded changelog fields such as names, descriptions, and previous values. CHANGELOG_ENRICHMENT_VERBOSITY = 4 +LOCALE_STATUS_CHECKSUM_FIELDS = ('retired', 'retire_reason') -class ChecksumBase(OCLDevChecksum): + +class OCLAPIChecksum(ChecksumBase): def _locales_for_checksums(self, data, relation, predicate_func): """Include locale status in standard concept checksums.""" locales = list(getvalue(data, relation, [])) @@ -28,7 +30,7 @@ def _locales_for_checksums(self, data, relation, predicate_func): if self.checksum_type == 'standard': included_locales = [locale for locale in locales if predicate_func(locale)] for checksum_locale, locale in zip(checksum_locales, included_locales): - checksum_locale.update({field: get(locale, field, None) for field in ['retired', 'retire_reason']}) + checksum_locale.update({field: get(locale, field, None) for field in LOCALE_STATUS_CHECKSUM_FIELDS}) return checksum_locales @@ -47,7 +49,7 @@ def get_checksum_base(self, resource=None, data=None, checksum_type='standard'): resource_name = 'user' if resource_name == 'org': resource_name = 'organization' - return ChecksumBase(resource_name, data or self, checksum_type) + return OCLAPIChecksum(resource_name, data or self, checksum_type) def get_checksums(self, queue=False, recalculate=False): _checksums = None @@ -110,7 +112,7 @@ def generate_checksum(self, checksum_type='standard'): @staticmethod def generate_checksum_from_many(resource, data, checksum_type='standard'): - return ChecksumBase(resource, data, checksum_type).generate() + return OCLAPIChecksum(resource, data, checksum_type).generate() def _calculate_standard_checksum(self): return self.generate_checksum('standard') @@ -125,7 +127,7 @@ def _calculate_checksums(self): class Checksum: @classmethod def generate(cls, obj): - return ChecksumBase(None, obj).generate() + return OCLAPIChecksum(None, obj).generate() class ChecksumDiff: diff --git a/core/common/tests.py b/core/common/tests.py index 9dc78486..bcee40d5 100644 --- a/core/common/tests.py +++ b/core/common/tests.py @@ -50,7 +50,7 @@ from core.users.models import UserProfile from core.users.tests.factories import UserProfileFactory from .backends import OCLOIDCAuthenticationBackend -from .checksums import Checksum, ChecksumBase +from .checksums import Checksum, OCLAPIChecksum from .fhir_helpers import translate_fhir_query from .serializers import IdentifierSerializer from .validators import URIValidator @@ -1341,7 +1341,7 @@ def test_generate(self): concept1 = ConceptFactory(mnemonic=encode_string('Foo/bar', safe=' ')) self.assertEqual( - ChecksumBase('concept', ConceptDetailSerializer(concept1).data, 'standard').generate(), + OCLAPIChecksum('concept', ConceptDetailSerializer(concept1).data, 'standard').generate(), concept1.checksums['standard'] ) concept2 = ConceptFactory(mnemonic=encode_string('bar/bar', safe=' ')) @@ -1352,11 +1352,11 @@ def test_generate(self): mapping_data = MappingDetailSerializer(mapping1).data self.assertEqual( - ChecksumBase('mapping', mapping_data, 'standard').generate(), + OCLAPIChecksum('mapping', mapping_data, 'standard').generate(), mapping1.checksums['standard'] ) self.assertEqual( - ChecksumBase('mapping', mapping_data, 'smart').generate(), + OCLAPIChecksum('mapping', mapping_data, 'smart').generate(), mapping1.checksums['smart'] ) @@ -1388,44 +1388,21 @@ def test_concept_standard_checksum_includes_locale_status(self): } ], } - concept_data_with_retired_name = { - **concept_data, - 'names': [{**concept_data['names'][0], 'retired': True}], - } - concept_data_with_name_retire_reason = { - **concept_data, - 'names': [{**concept_data['names'][0], 'retire_reason': 'Duplicate'}], - } - concept_data_with_retired_description = { - **concept_data, - 'descriptions': [{**concept_data['descriptions'][0], 'retired': True}], - } - concept_data_with_description_retire_reason = { - **concept_data, - 'descriptions': [{**concept_data['descriptions'][0], 'retire_reason': 'Duplicate'}], - } - standard_checksum = ChecksumBase('concept', concept_data, 'standard').generate() - smart_checksum = ChecksumBase('concept', concept_data, 'smart').generate() - - self.assertNotEqual( - standard_checksum, - ChecksumBase('concept', concept_data_with_retired_name, 'standard').generate() - ) - self.assertNotEqual( - standard_checksum, - ChecksumBase('concept', concept_data_with_name_retire_reason, 'standard').generate() - ) - self.assertNotEqual( - standard_checksum, - ChecksumBase('concept', concept_data_with_retired_description, 'standard').generate() - ) - self.assertNotEqual( - standard_checksum, - ChecksumBase('concept', concept_data_with_description_retire_reason, 'standard').generate() - ) + standard_checksum = OCLAPIChecksum('concept', concept_data, 'standard').generate() + smart_checksum = OCLAPIChecksum('concept', concept_data, 'smart').generate() + locale_status_variants = [ + {**concept_data, 'names': [{**concept_data['names'][0], 'retired': True}]}, + {**concept_data, 'names': [{**concept_data['names'][0], 'retire_reason': 'Duplicate'}]}, + {**concept_data, 'descriptions': [{**concept_data['descriptions'][0], 'retired': True}]}, + {**concept_data, 'descriptions': [{**concept_data['descriptions'][0], 'retire_reason': 'Duplicate'}]}, + ] + + for concept_data_variant in locale_status_variants: + self.assertNotEqual(standard_checksum, OCLAPIChecksum( + 'concept', concept_data_variant, 'standard').generate()) self.assertEqual( smart_checksum, - ChecksumBase('concept', concept_data_with_retired_name, 'smart').generate() + OCLAPIChecksum('concept', locale_status_variants[0], 'smart').generate() ) @@ -1466,7 +1443,7 @@ def test_post_400(self, checksum_generate_mock): self.assertEqual(response.data, {'error': 'Invalid resource: foobar'}) checksum_generate_mock.assert_not_called() - @patch('core.common.checksums.ChecksumBase.generate') + @patch('core.common.checksums.OCLAPIChecksum.generate') def test_post_200_concept(self, checksum_generate_mock): checksum_generate_mock.return_value = 'checksum' diff --git a/core/concepts/tests/tests.py b/core/concepts/tests/tests.py index d4355a37..97c83338 100644 --- a/core/concepts/tests/tests.py +++ b/core/concepts/tests/tests.py @@ -1435,7 +1435,7 @@ def test_cascade_as_hierarchy_reverse(self): root.url ) - @patch('core.common.checksums.ChecksumBase.generate') + @patch('core.common.checksums.OCLAPIChecksum.generate') def test_checksum(self, checksum_generate_mock): checksum_generate_mock.side_effect = [ 'standard-checksum', 'smart-checksum' From 329718b9b8ad19244ae64b6e444f14b4ce1b0abf Mon Sep 17 00:00:00 2001 From: Filipe Lopes Date: Mon, 22 Jun 2026 18:44:17 -0300 Subject: [PATCH 4/5] refactor(core/common): replace OCLAPIChecksum with ChecksumBase * Update import statements from OCLAPIChecksum to ChecksumBase * Change class inheritance from OCLAPIChecksum to ChecksumBase * Adjust test code and patches to use ChecksumBase --- core/common/checksums.py | 10 +++++----- core/common/tests.py | 18 +++++++++--------- core/concepts/tests/tests.py | 2 +- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/core/common/checksums.py b/core/common/checksums.py index 7ba7b8fb..28d3fa0f 100644 --- a/core/common/checksums.py +++ b/core/common/checksums.py @@ -1,6 +1,6 @@ from django.conf import settings from django.db import models -from ocldev.checksum import Checksum as ChecksumBase, getvalue +from ocldev.checksum import Checksum as OCLDevChecksum, getvalue from pydash import get @@ -22,7 +22,7 @@ LOCALE_STATUS_CHECKSUM_FIELDS = ('retired', 'retire_reason') -class OCLAPIChecksum(ChecksumBase): +class ChecksumBase(OCLDevChecksum): def _locales_for_checksums(self, data, relation, predicate_func): """Include locale status in standard concept checksums.""" locales = list(getvalue(data, relation, [])) @@ -49,7 +49,7 @@ def get_checksum_base(self, resource=None, data=None, checksum_type='standard'): resource_name = 'user' if resource_name == 'org': resource_name = 'organization' - return OCLAPIChecksum(resource_name, data or self, checksum_type) + return ChecksumBase(resource_name, data or self, checksum_type) def get_checksums(self, queue=False, recalculate=False): _checksums = None @@ -112,7 +112,7 @@ def generate_checksum(self, checksum_type='standard'): @staticmethod def generate_checksum_from_many(resource, data, checksum_type='standard'): - return OCLAPIChecksum(resource, data, checksum_type).generate() + return ChecksumBase(resource, data, checksum_type).generate() def _calculate_standard_checksum(self): return self.generate_checksum('standard') @@ -127,7 +127,7 @@ def _calculate_checksums(self): class Checksum: @classmethod def generate(cls, obj): - return OCLAPIChecksum(None, obj).generate() + return ChecksumBase(None, obj).generate() class ChecksumDiff: diff --git a/core/common/tests.py b/core/common/tests.py index bcee40d5..401f9e29 100644 --- a/core/common/tests.py +++ b/core/common/tests.py @@ -50,7 +50,7 @@ from core.users.models import UserProfile from core.users.tests.factories import UserProfileFactory from .backends import OCLOIDCAuthenticationBackend -from .checksums import Checksum, OCLAPIChecksum +from .checksums import Checksum, ChecksumBase from .fhir_helpers import translate_fhir_query from .serializers import IdentifierSerializer from .validators import URIValidator @@ -1341,7 +1341,7 @@ def test_generate(self): concept1 = ConceptFactory(mnemonic=encode_string('Foo/bar', safe=' ')) self.assertEqual( - OCLAPIChecksum('concept', ConceptDetailSerializer(concept1).data, 'standard').generate(), + ChecksumBase('concept', ConceptDetailSerializer(concept1).data, 'standard').generate(), concept1.checksums['standard'] ) concept2 = ConceptFactory(mnemonic=encode_string('bar/bar', safe=' ')) @@ -1352,11 +1352,11 @@ def test_generate(self): mapping_data = MappingDetailSerializer(mapping1).data self.assertEqual( - OCLAPIChecksum('mapping', mapping_data, 'standard').generate(), + ChecksumBase('mapping', mapping_data, 'standard').generate(), mapping1.checksums['standard'] ) self.assertEqual( - OCLAPIChecksum('mapping', mapping_data, 'smart').generate(), + ChecksumBase('mapping', mapping_data, 'smart').generate(), mapping1.checksums['smart'] ) @@ -1388,8 +1388,8 @@ def test_concept_standard_checksum_includes_locale_status(self): } ], } - standard_checksum = OCLAPIChecksum('concept', concept_data, 'standard').generate() - smart_checksum = OCLAPIChecksum('concept', concept_data, 'smart').generate() + standard_checksum = ChecksumBase('concept', concept_data, 'standard').generate() + smart_checksum = ChecksumBase('concept', concept_data, 'smart').generate() locale_status_variants = [ {**concept_data, 'names': [{**concept_data['names'][0], 'retired': True}]}, {**concept_data, 'names': [{**concept_data['names'][0], 'retire_reason': 'Duplicate'}]}, @@ -1398,11 +1398,11 @@ def test_concept_standard_checksum_includes_locale_status(self): ] for concept_data_variant in locale_status_variants: - self.assertNotEqual(standard_checksum, OCLAPIChecksum( + self.assertNotEqual(standard_checksum, ChecksumBase( 'concept', concept_data_variant, 'standard').generate()) self.assertEqual( smart_checksum, - OCLAPIChecksum('concept', locale_status_variants[0], 'smart').generate() + ChecksumBase('concept', locale_status_variants[0], 'smart').generate() ) @@ -1443,7 +1443,7 @@ def test_post_400(self, checksum_generate_mock): self.assertEqual(response.data, {'error': 'Invalid resource: foobar'}) checksum_generate_mock.assert_not_called() - @patch('core.common.checksums.OCLAPIChecksum.generate') + @patch('core.common.checksums.ChecksumBase.generate') def test_post_200_concept(self, checksum_generate_mock): checksum_generate_mock.return_value = 'checksum' diff --git a/core/concepts/tests/tests.py b/core/concepts/tests/tests.py index 97c83338..d4355a37 100644 --- a/core/concepts/tests/tests.py +++ b/core/concepts/tests/tests.py @@ -1435,7 +1435,7 @@ def test_cascade_as_hierarchy_reverse(self): root.url ) - @patch('core.common.checksums.OCLAPIChecksum.generate') + @patch('core.common.checksums.ChecksumBase.generate') def test_checksum(self, checksum_generate_mock): checksum_generate_mock.side_effect = [ 'standard-checksum', 'smart-checksum' From 7b3d41176e4bc7c5091faf6a5c6a201af6345395 Mon Sep 17 00:00:00 2001 From: Filipe Lopes Date: Mon, 22 Jun 2026 21:36:41 -0300 Subject: [PATCH 5/5] refactor(core): drop legacy checksum helper * remove unused ChecksumBase subclass and related methods * simplify checksum import to use public API * bump ocldev dependency to 0.2.4 --- core/common/checksums.py | 17 +-------------- core/common/tests.py | 46 ---------------------------------------- requirements.txt | 2 +- 3 files changed, 2 insertions(+), 63 deletions(-) diff --git a/core/common/checksums.py b/core/common/checksums.py index 28d3fa0f..d3d4c5d7 100644 --- a/core/common/checksums.py +++ b/core/common/checksums.py @@ -1,6 +1,6 @@ from django.conf import settings from django.db import models -from ocldev.checksum import Checksum as OCLDevChecksum, getvalue +from ocldev.checksum import Checksum as ChecksumBase from pydash import get @@ -19,21 +19,6 @@ # Include expanded changelog fields such as names, descriptions, and previous values. CHANGELOG_ENRICHMENT_VERBOSITY = 4 -LOCALE_STATUS_CHECKSUM_FIELDS = ('retired', 'retire_reason') - - -class ChecksumBase(OCLDevChecksum): - def _locales_for_checksums(self, data, relation, predicate_func): - """Include locale status in standard concept checksums.""" - locales = list(getvalue(data, relation, [])) - checksum_locales = super()._locales_for_checksums(data, relation, predicate_func) - if self.checksum_type == 'standard': - included_locales = [locale for locale in locales if predicate_func(locale)] - for checksum_locale, locale in zip(checksum_locales, included_locales): - checksum_locale.update({field: get(locale, field, None) for field in LOCALE_STATUS_CHECKSUM_FIELDS}) - return checksum_locales - - class ChecksumModel(models.Model): class Meta: abstract = True diff --git a/core/common/tests.py b/core/common/tests.py index 401f9e29..e42e8676 100644 --- a/core/common/tests.py +++ b/core/common/tests.py @@ -1360,52 +1360,6 @@ def test_generate(self): mapping1.checksums['smart'] ) - def test_concept_standard_checksum_includes_locale_status(self): - concept_data = { - 'concept_class': 'Diagnosis', - 'datatype': 'None', - 'retired': False, - 'names': [ - { - 'locale': 'es', - 'locale_preferred': False, - 'name': 'Nombre de prueba OCL local', - 'name_type': 'FULLY_SPECIFIED', - 'external_id': None, - 'retired': False, - 'retire_reason': None, - } - ], - 'descriptions': [ - { - 'locale': 'es', - 'locale_preferred': False, - 'description': 'Descripcion de prueba', - 'description_type': 'Description', - 'external_id': None, - 'retired': False, - 'retire_reason': None, - } - ], - } - standard_checksum = ChecksumBase('concept', concept_data, 'standard').generate() - smart_checksum = ChecksumBase('concept', concept_data, 'smart').generate() - locale_status_variants = [ - {**concept_data, 'names': [{**concept_data['names'][0], 'retired': True}]}, - {**concept_data, 'names': [{**concept_data['names'][0], 'retire_reason': 'Duplicate'}]}, - {**concept_data, 'descriptions': [{**concept_data['descriptions'][0], 'retired': True}]}, - {**concept_data, 'descriptions': [{**concept_data['descriptions'][0], 'retire_reason': 'Duplicate'}]}, - ] - - for concept_data_variant in locale_status_variants: - self.assertNotEqual(standard_checksum, ChecksumBase( - 'concept', concept_data_variant, 'standard').generate()) - self.assertEqual( - smart_checksum, - ChecksumBase('concept', locale_status_variants[0], 'smart').generate() - ) - - class ChecksumViewTest(OCLAPITestCase): def setUp(self): self.token = UserProfile.objects.get(username='ocladmin').get_token() diff --git a/requirements.txt b/requirements.txt index ca1c8ced..d3563bcf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -23,7 +23,7 @@ kombu==5.4.2 django-elasticsearch-dsl==8.0 drf-yasg==1.21.15 git+https://github.com/snyaggarwal/django-queryset-csv -ocldev==0.2.3 +ocldev==0.2.4 coverage==7.3.0 tblib==2.0.0 django-ordered-model==3.7.4