diff --git a/core/collections/models.py b/core/collections/models.py index a48ba0476..c8c345f44 100644 --- a/core/collections/models.py +++ b/core/collections/models.py @@ -92,24 +92,32 @@ class Meta: expansion_uri = models.TextField(null=True, blank=True) def get_standard_checksum_fields(self): + return self.get_standard_checksum_fields_for_resource(self) + + def get_smart_checksum_fields(self): + return self.get_smart_checksum_fields_for_resource(self) + + @staticmethod + def get_standard_checksum_fields_for_resource(data): return { - 'collection_type': self.collection_type, - 'canonical_url': self.canonical_url, - 'custom_validation_schema': self.custom_validation_schema, - 'default_locale': self.default_locale, - 'supported_locales': self.supported_locales, - 'website': self.website, - 'extras': self.extras, + 'collection_type': get(data, 'collection_type'), + 'canonical_url': get(data, 'canonical_url'), + 'custom_validation_schema': get(data, 'custom_validation_schema'), + 'default_locale': get(data, 'default_locale'), + 'supported_locales': get(data, 'supported_locales'), + 'website': get(data, 'website'), + 'extras': get(data, 'extras'), } - def get_smart_checksum_fields(self): + @staticmethod + def get_smart_checksum_fields_for_resource(data): return { - 'collection_type': self.collection_type, - 'canonical_url': self.canonical_url, - 'custom_validation_schema': self.custom_validation_schema, - 'default_locale': self.default_locale, - 'released': self.released, - 'retired': self.retired, + 'collection_type': get(data, 'collection_type'), + 'canonical_url': get(data, 'canonical_url'), + 'custom_validation_schema': get(data, 'custom_validation_schema'), + 'default_locale': get(data, 'default_locale'), + 'released': get(data, 'released'), + 'retired': get(data, 'retired'), } def set_active_concepts(self): diff --git a/core/common/checksums.py b/core/common/checksums.py index 691d6e1e2..69f43a8a8 100644 --- a/core/common/checksums.py +++ b/core/common/checksums.py @@ -21,20 +21,18 @@ class Meta: STANDARD_CHECKSUM_KEY = 'standard' SMART_CHECKSUM_KEY = 'smart' - def get_checksums(self, standard=False, queue=False, recalculate=False): + def get_checksums(self, queue=False, recalculate=False): + _checksums = None if Toggle.get('CHECKSUMS_TOGGLE'): - if not recalculate and self.checksums and self.has_checksums(standard): - return self.checksums - if queue: + if not recalculate and self.checksums and self.has_all_checksums(): + _checksums = self.checksums + elif queue: self.queue_checksum_calculation() - return self.checksums or {} - if standard: - self.set_standard_checksums() + _checksums = self.checksums or {} else: self.set_checksums() - - return self.checksums - return None + _checksums = self.checksums + return _checksums def queue_checksum_calculation(self): from core.common.tasks import calculate_checksums @@ -44,14 +42,6 @@ def queue_checksum_calculation(self): else: calculate_checksums.delay(self.__class__.__name__, self.id) - def set_specific_checksums(self, checksum_type, checksum): - self.checksums = self.checksums or {} - self.checksums[checksum_type] = checksum - self.save(update_fields=['checksums']) - - def has_checksums(self, standard=False): - return self.has_standard_checksum() if standard else self.has_all_checksums() - def has_all_checksums(self): return self.has_standard_checksum() and self.has_smart_checksum() @@ -66,21 +56,17 @@ def set_checksums(self): self.checksums = self._calculate_checksums() self.save(update_fields=['checksums']) - def set_standard_checksums(self): - if Toggle.get('CHECKSUMS_TOGGLE'): - self.checksums = self.get_standard_checksums() - self.save(update_fields=['checksums']) - @property def checksum(self): """Returns the checksum of the model instance or standard only checksum.""" + _checksum = None if Toggle.get('CHECKSUMS_TOGGLE'): if get(self, f'checksums.{self.STANDARD_CHECKSUM_KEY}'): - return self.checksums[self.STANDARD_CHECKSUM_KEY] - self.get_checksums() - - return self.checksums.get(self.STANDARD_CHECKSUM_KEY) - return None + _checksum = self.checksums[self.STANDARD_CHECKSUM_KEY] + else: + self.get_checksums() + _checksum = self.checksums.get(self.STANDARD_CHECKSUM_KEY) + return _checksum def get_checksum_fields(self): return {field: getattr(self, field) for field in self.CHECKSUM_INCLUSIONS} @@ -91,28 +77,31 @@ def get_standard_checksum_fields(self): def get_smart_checksum_fields(self): return {} - def get_standard_checksums(self): - if Toggle.get('CHECKSUMS_TOGGLE'): - checksums = self.checksums or {} - if self.STANDARD_CHECKSUM_KEY: - checksums[self.STANDARD_CHECKSUM_KEY] = self._calculate_standard_checksum() - return checksums - return None - def get_all_checksums(self): + checksums = None if Toggle.get('CHECKSUMS_TOGGLE'): checksums = {} if self.STANDARD_CHECKSUM_KEY: checksums[self.STANDARD_CHECKSUM_KEY] = self._calculate_standard_checksum() if self.SMART_CHECKSUM_KEY: checksums[self.SMART_CHECKSUM_KEY] = self._calculate_smart_checksum() - return checksums - return None + return checksums @staticmethod def generate_checksum(data): return Checksum.generate(ChecksumModel._cleanup(data)) + @staticmethod + def generate_checksum_from_many(data): + checksums = [ + Checksum.generate(ChecksumModel._cleanup(_data)) for _data in data + ] if isinstance(data, list) else [ + Checksum.generate(ChecksumModel._cleanup(data)) + ] + if len(checksums) == 1: + return checksums[0] + return Checksum.generate(checksums) + def _calculate_standard_checksum(self): fields = self.get_standard_checksum_fields() return None if fields is None else self.generate_checksum(fields) @@ -123,16 +112,18 @@ def _calculate_smart_checksum(self): @staticmethod def _cleanup(fields): + result = fields if isinstance(fields, dict): - new_fields = {} + result = {} for key, value in fields.items(): if value is None: continue if key in ['is_active', 'retired'] and not value: continue - new_fields[key] = value - return new_fields - return fields + if key in ['extras'] and not value: + continue + result[key] = value + return result def _calculate_checksums(self): return self.get_all_checksums() diff --git a/core/common/mixins.py b/core/common/mixins.py index c4d24c7c1..804b56f74 100644 --- a/core/common/mixins.py +++ b/core/common/mixins.py @@ -125,7 +125,9 @@ def checksums(self): smart.append(get(result.checksums, 'smart')) standard = compact(standard) smart = compact(smart) - return Checksum.generate(standard) if standard else None, Checksum.generate(smart) if smart else None + standard = Checksum.generate(standard) if len(standard) > 1 else get(standard, '0') + smart = Checksum.generate(smart) if len(smart) > 1 else get(smart, '0') + return standard, smart class ListWithHeadersMixin(ListModelMixin): @@ -474,10 +476,6 @@ class Meta: def is_strictly_equal(instance1, instance2): return instance1.get_checksums() == instance2.get_checksums() - @staticmethod - def is_equal(instance1, instance2): - return instance1.get_standard_checksums() == instance2.get_standard_checksums() - @staticmethod def apply_user_criteria(queryset, user): queryset = queryset.exclude( diff --git a/core/common/swagger_parameters.py b/core/common/swagger_parameters.py index 73b2f9e04..495587b05 100644 --- a/core/common/swagger_parameters.py +++ b/core/common/swagger_parameters.py @@ -114,6 +114,15 @@ 'resource', openapi.IN_PATH, type=openapi.TYPE_STRING, enum=['mappings', 'concepts', 'sources', 'orgs', 'users', 'collections'] ) +all_resource_query_param = openapi.Parameter( + 'resource', + openapi.IN_QUERY, + description="Resource type to generate checksum", + type=openapi.TYPE_STRING, + default='concept_version', + enum=['concept_version', 'mapping_version', 'source_version', 'collection_version', 'org', 'user'], + required=True +) parallel_threads_param = openapi.Parameter( 'parallel', openapi.IN_FORM, description="Parallel threads count (default: 5, max: 10)", type=openapi.TYPE_INTEGER ) diff --git a/core/common/tests.py b/core/common/tests.py index 2eaebaed5..070ff503d 100644 --- a/core/common/tests.py +++ b/core/common/tests.py @@ -14,6 +14,7 @@ from django.core.management import call_command from django.test import TestCase from django.test.runner import DiscoverRunner +from mock.mock import call from moto import mock_s3 from requests.auth import HTTPBasicAuth from rest_framework.exceptions import ValidationError @@ -1347,8 +1348,28 @@ def setUp(self): @patch('core.common.checksums.Checksum.generate') def test_post_400(self, checksum_generate_mock): response = self.client.post( - '/$checksum/', - data={}, + '/$checksum/standard/', + {}, + HTTP_AUTHORIZATION=f"Token {self.token}", + format='json' + ) + + self.assertEqual(response.status_code, 400) + checksum_generate_mock.assert_not_called() + + response = self.client.post( + '/$checksum/smart/', + {"foo": "bar"}, + HTTP_AUTHORIZATION=f"Token {self.token}", + format='json' + ) + + self.assertEqual(response.status_code, 400) + checksum_generate_mock.assert_not_called() + + response = self.client.post( + '/$checksum/smart/?resource=foobar', + {"foo": "bar"}, HTTP_AUTHORIZATION=f"Token {self.token}", format='json' ) @@ -1357,16 +1378,143 @@ def test_post_400(self, checksum_generate_mock): checksum_generate_mock.assert_not_called() @patch('core.common.checksums.Checksum.generate') - def test_post_200(self, checksum_generate_mock): + def test_post_200_concept(self, checksum_generate_mock): checksum_generate_mock.return_value = 'checksum' response = self.client.post( - '/$checksum/', - data={'foo': 'bar'}, + '/$checksum/standard/?resource=concept_version', + data={'foo': 'bar', 'concept_class': 'foobar', 'extras': {}}, HTTP_AUTHORIZATION=f"Token {self.token}", format='json' ) self.assertEqual(response.status_code, 200) self.assertEqual(response.data, 'checksum') - checksum_generate_mock.assert_called_once_with({'foo': 'bar'}) + checksum_generate_mock.assert_called_once_with({'concept_class': 'foobar', 'names': []}) + + @patch('core.common.checksums.Checksum.generate') + def test_post_200_mapping_standard(self, checksum_generate_mock): + checksum_generate_mock.side_effect = ['checksum1', 'checksum2', 'checksum3'] + + response = self.client.post( + '/$checksum/standard/?resource=mapping', + data=[ + { + 'id': 'bar', + 'map_type': 'foobar', + 'from_concept_url': '/foo/', + 'to_source_url': '/bar/', + 'from_concept_code': 'foo', + 'to_concept_code': 'bar', + 'from_concept_name': 'fooName', + 'to_concept_name': 'barName', + 'retired': False, + 'extras': { + 'foo': 'bar' + } + }, + { + 'id': 'barbara', + 'map_type': 'foobarbara', + 'from_concept_url': '/foobara/', + 'to_source_url': '/barbara/', + 'from_concept_code': 'foobara', + 'to_concept_code': 'barbara', + 'from_concept_name': 'foobaraName', + 'to_concept_name': 'barbaraName', + 'retired': True, + 'extras': { + 'foo': 'barbara' + } + } + ], + HTTP_AUTHORIZATION=f"Token {self.token}", + format='json' + ) + + self.assertEqual(response.status_code, 200) + self.assertEqual(response.data, 'checksum3') + self.assertEqual(checksum_generate_mock.call_count, 3) + self.assertEqual( + checksum_generate_mock.mock_calls, + [ + call({ + 'map_type': 'foobar', + 'from_concept_code': 'foo', + 'to_concept_code': 'bar', + 'from_concept_name': 'fooName', + 'to_concept_name': 'barName', + 'extras': {'foo': 'bar'} + }), + call({ + 'map_type': 'foobarbara', + 'from_concept_code': 'foobara', + 'to_concept_code': 'barbara', + 'from_concept_name': 'foobaraName', + 'to_concept_name': 'barbaraName', + 'extras': {'foo': 'barbara'} + }), + call(['checksum1', 'checksum2']) + ] + ) + + @patch('core.common.checksums.Checksum.generate') + def test_post_200_mapping_smart(self, checksum_generate_mock): + checksum_generate_mock.side_effect = ['checksum1', 'checksum2', 'checksum3'] + + response = self.client.post( + '/$checksum/smart/?resource=mapping', + data=[ + { + 'id': 'bar', + 'map_type': 'foobar', + 'from_concept_url': '/foo/', + 'to_source_url': '/bar/', + 'from_concept_code': 'foo', + 'to_concept_code': 'bar', + 'from_concept_name': 'fooName', + 'to_concept_name': 'barName', + 'retired': False, + 'extras': {'foo': 'bar'} + }, + { + 'id': 'barbara', + 'map_type': 'foobarbara', + 'from_concept_url': '/foobara/', + 'to_source_url': '/barbara/', + 'from_concept_code': 'foobara', + 'to_concept_code': 'barbara', + 'from_concept_name': 'foobaraName', + 'to_concept_name': 'barbaraName', + 'retired': True, + 'extras': {'foo': 'barbara'} + } + ], + HTTP_AUTHORIZATION=f"Token {self.token}", + format='json' + ) + + self.assertEqual(response.status_code, 200) + self.assertEqual(response.data, 'checksum3') + self.assertEqual(checksum_generate_mock.call_count, 3) + self.assertEqual( + checksum_generate_mock.mock_calls, + [ + call({ + 'map_type': 'foobar', + 'from_concept_code': 'foo', + 'to_concept_code': 'bar', + 'from_concept_name': 'fooName', + 'to_concept_name': 'barName' + }), + call({ + 'map_type': 'foobarbara', + 'from_concept_code': 'foobara', + 'to_concept_code': 'barbara', + 'from_concept_name': 'foobaraName', + 'to_concept_name': 'barbaraName', + 'retired': True + }), + call(['checksum1', 'checksum2']) + ] + ) diff --git a/core/common/utils.py b/core/common/utils.py index 928b5d896..1e19cee7e 100644 --- a/core/common/utils.py +++ b/core/common/utils.py @@ -586,24 +586,30 @@ def get_resource_class_from_resource_name(resource): # pylint: disable=too-many return resource name = resource.lower() - if name in ['concepts', 'concept']: + if 'concept' in name: from core.concepts.models import Concept return Concept - if name in ['mappings', 'mapping']: + if 'mapping' in name: from core.mappings.models import Mapping return Mapping - if name in ['users', 'user', 'user_profiles', 'user_profile', 'userprofiles', 'userprofile']: + if 'user' in name: from core.users.models import UserProfile return UserProfile - if name in ['orgs', 'org', 'organizations', 'organization']: + if 'org' in name: from core.orgs.models import Organization return Organization - if name in ['sources', 'source']: + if 'source' in name: from core.sources.models import Source return Source - if name in ['collections', 'collection']: + if 'collection' in name and 'reference' not in name and 'expansion' not in name: from core.collections.models import Collection return Collection + if 'reference' in name: + from core.collections.models import CollectionReference + return CollectionReference + if 'expansion' in name: + from core.collections.models import Expansion + return Expansion return None diff --git a/core/common/views.py b/core/common/views.py index 9852d04df..7924a5861 100644 --- a/core/common/views.py +++ b/core/common/views.py @@ -12,7 +12,7 @@ from drf_yasg.utils import swagger_auto_schema from elasticsearch import RequestError, TransportError from elasticsearch_dsl import Q -from pydash import get, compact +from pydash import get, compact, flatten from rest_framework import response, generics, status from rest_framework.generics import ListAPIView, RetrieveUpdateDestroyAPIView from rest_framework.permissions import AllowAny, IsAuthenticated @@ -20,7 +20,6 @@ from rest_framework.views import APIView from core import __version__ -from core.common.checksums import Checksum from core.common.constants import SEARCH_PARAM, LIST_DEFAULT_LIMIT, CSV_DEFAULT_LIMIT, \ LIMIT_PARAM, NOT_FOUND, MUST_SPECIFY_EXTRA_PARAM_IN_BODY, INCLUDE_RETIRED_PARAM, VERBOSE_PARAM, HEAD, LATEST, \ BRIEF_PARAM, ES_REQUEST_TIMEOUT, INCLUDE_INACTIVE, FHIR_LIMIT_PARAM, RAW_PARAM, SEARCH_MAP_CODES_PARAM, \ @@ -29,8 +28,9 @@ from core.common.mixins import PathWalkerMixin from core.common.search import CustomESSearch from core.common.serializers import RootSerializer +from core.common.swagger_parameters import all_resource_query_param from core.common.utils import compact_dict_by_values, to_snake_case, parse_updated_since_param, \ - to_int, get_user_specific_task_id, get_falsy_values, get_truthy_values + to_int, get_user_specific_task_id, get_falsy_values, get_truthy_values, get_resource_class_from_resource_name from core.concepts.permissions import CanViewParentDictionary, CanEditParentDictionary from core.orgs.constants import ORG_OBJECT_TYPE from core.tasks.constants import TASK_NOT_COMPLETED @@ -999,22 +999,50 @@ def delete(self, request, *args, **kwargs): return Response({'detail': NOT_FOUND}, status=status.HTTP_404_NOT_FOUND) -class ChecksumView(APIView): +class AbstractChecksumView(APIView): permission_classes = (IsAuthenticated,) + smart = False @swagger_auto_schema( - request_body=openapi.Schema(type=openapi.TYPE_OBJECT), + manual_parameters=[all_resource_query_param], + request_body=openapi.Schema( + type=openapi.TYPE_OBJECT, + description='Data to generate checksum', + ), responses={ 200: openapi.Response( - 'MD5 checksum of the request body', + 'MD5 checksum of the request body for a resource', openapi.Schema(type=openapi.TYPE_STRING), ) }, ) def post(self, request): - if not request.data: - return Response({'error': 'Request body is required'}, status=status.HTTP_400_BAD_REQUEST) - return Response(Checksum.generate(request.data)) + resource = request.query_params.get('resource') + data = request.data + if not resource or not data: + return Response({'error': 'resource and data are both required.'}, status=status.HTTP_400_BAD_REQUEST) + + klass = get_resource_class_from_resource_name(resource) + + if not klass: + return Response({'error': 'Invalid resource.'}, status=status.HTTP_400_BAD_REQUEST) + + method = 'get_smart_checksum_fields_for_resource' if self.smart else 'get_standard_checksum_fields_for_resource' + func = get(klass, method) + + if not func: + return Response( + {'error': 'Checksums for this resource is not yet implemented.'}, status=status.HTTP_400_BAD_REQUEST) + + return Response(klass.generate_checksum_from_many([func(_data) for _data in flatten([data])])) + + +class StandardChecksumView(AbstractChecksumView): + smart = False + + +class SmartChecksumView(AbstractChecksumView): + smart = True class TaskMixin: diff --git a/core/concepts/models.py b/core/concepts/models.py index 737f25f44..ce3499fb1 100644 --- a/core/concepts/models.py +++ b/core/concepts/models.py @@ -69,13 +69,21 @@ def build(cls, params): @property def is_fully_specified(self): - return self.type in LOCALES_FULLY_SPECIFIED or self.is_fully_specified_after_clean + return self.is_fully_specified_type(self.type) + + @staticmethod + def is_fully_specified_type(_type): + return _type in LOCALES_FULLY_SPECIFIED or AbstractLocalizedText.is_fully_specified_type_after_clean(_type) @property def is_fully_specified_after_clean(self): # needed for OpenMRS schema content created from TermBrowser - if not self.type: + return self.is_fully_specified_type_after_clean(self.type) + + @staticmethod + def is_fully_specified_type_after_clean(_type): + if not _type: return False - _type = self.type.replace(' ', '').replace('-', '').replace('_', '').lower() + _type = _type.replace(' ', '').replace('-', '').replace('_', '').lower() return _type == 'fullyspecified' @property @@ -254,19 +262,37 @@ class Meta: } def get_standard_checksum_fields(self): + return self.get_standard_checksum_fields_for_resource(self) + + def get_smart_checksum_fields(self): + return self.get_smart_checksum_fields_for_resource(self) + + @staticmethod + def get_standard_checksum_fields_for_resource(data): + names = data.names.filter() if isinstance(data, Concept) else get(data, 'names', []) return { - 'concept_class': self.concept_class, - 'datatype': self.datatype, - 'names': [name.get_checksum_fields() for name in self.names.filter()], - 'extras': self.extras, + 'concept_class': get(data, 'concept_class'), + 'datatype': get(data, 'datatype'), + 'names': [ + { + field: get(name, field) for field in ConceptName.CHECKSUM_INCLUSIONS + } for name in names + ], + 'extras': get(data, 'extras'), } - def get_smart_checksum_fields(self): + @staticmethod + def get_smart_checksum_fields_for_resource(data): + names = data.names.filter() if isinstance(data, Concept) else get(data, 'names', []) return { - 'concept_class': self.concept_class, - 'datatype': self.datatype, - 'names': [name.get_checksum_fields() for name in self.names.filter() if name.is_fully_specified], - 'retired': self.retired, + 'concept_class': get(data, 'concept_class'), + 'datatype': get(data, 'datatype'), + 'names': [ + { + field: get(name, field) for field in ConceptName.CHECKSUM_INCLUSIONS + } for name in names if ConceptName.is_fully_specified_type(get(name, 'name_type')) + ], + 'retired': get(data, 'retired'), } @staticmethod diff --git a/core/mappings/models.py b/core/mappings/models.py index 6ed4dc0bd..b98bf0793 100644 --- a/core/mappings/models.py +++ b/core/mappings/models.py @@ -132,23 +132,31 @@ class Meta: } def get_standard_checksum_fields(self): + return self.get_standard_checksum_fields_for_resource(self) + + def get_smart_checksum_fields(self): + return self.get_smart_checksum_fields_for_resource(self) + + @staticmethod + def get_standard_checksum_fields_for_resource(data): return { - 'map_type': self.map_type, - 'from_concept_code': self.from_concept_code, - 'to_concept_code': self.to_concept_code, - 'from_concept_name': self.from_concept_name, - 'to_concept_name': self.to_concept_name, - 'extras': self.extras, + 'map_type': get(data, 'map_type'), + 'from_concept_code': get(data, 'from_concept_code'), + 'to_concept_code': get(data, 'to_concept_code'), + 'from_concept_name': get(data, 'from_concept_name'), + 'to_concept_name': get(data, 'to_concept_name'), + 'extras': get(data, 'extras'), } - def get_smart_checksum_fields(self): + @staticmethod + def get_smart_checksum_fields_for_resource(data): return { - 'map_type': self.map_type, - 'from_concept_code': self.from_concept_code, - 'to_concept_code': self.to_concept_code, - 'from_concept_name': self.from_concept_name, - 'to_concept_name': self.to_concept_name, - 'retired': self.retired + 'map_type': get(data, 'map_type'), + 'from_concept_code': get(data, 'from_concept_code'), + 'to_concept_code': get(data, 'to_concept_code'), + 'from_concept_name': get(data, 'from_concept_name'), + 'to_concept_name': get(data, 'to_concept_name'), + 'retired': get(data, 'retired') } @staticmethod diff --git a/core/orgs/models.py b/core/orgs/models.py index f73534c78..198adb4c2 100644 --- a/core/orgs/models.py +++ b/core/orgs/models.py @@ -1,6 +1,7 @@ from django.contrib.contenttypes.fields import GenericRelation from django.core.validators import RegexValidator from django.db import models, transaction +from pydash import get from core.client_configs.models import ClientConfig from core.common.checksums import ChecksumModel @@ -42,21 +43,29 @@ class Meta: overview = models.JSONField(default=dict) def get_standard_checksum_fields(self): + return self.get_standard_checksum_fields_for_resource(self) + + def get_smart_checksum_fields(self): + return self.get_smart_checksum_fields_for_resource(self) + + @staticmethod + def get_standard_checksum_fields_for_resource(data): return { - 'name': self.name, - 'company': self.company, - 'location': self.location, - 'website': self.website, - 'extras': self.extras, + 'name': get(data, 'name'), + 'company': get(data, 'company'), + 'location': get(data, 'location'), + 'website': get(data, 'website'), + 'extras': get(data, 'extras'), } - def get_smart_checksum_fields(self): + @staticmethod + def get_smart_checksum_fields_for_resource(data): return { - 'name': self.name, - 'company': self.company, - 'location': self.location, - 'website': self.website, - 'is_active': self.is_active + 'name': get(data, 'name'), + 'company': get(data, 'company'), + 'location': get(data, 'location'), + 'website': get(data, 'website'), + 'is_active': get(data, 'is_active') } def calculate_uri(self): diff --git a/core/sources/models.py b/core/sources/models.py index 15a191c4f..c54bfa114 100644 --- a/core/sources/models.py +++ b/core/sources/models.py @@ -90,25 +90,33 @@ class Meta: OBJECT_VERSION_TYPE = SOURCE_VERSION_TYPE def get_standard_checksum_fields(self): + return self.get_standard_checksum_fields_for_resource(self) + + def get_smart_checksum_fields(self): + return self.get_smart_checksum_fields_for_resource(self) + + @staticmethod + def get_standard_checksum_fields_for_resource(data): return { - 'source_type': self.source_type, - 'canonical_url': self.canonical_url, - 'custom_validation_schema': self.custom_validation_schema, - 'default_locale': self.default_locale, - 'supported_locales': self.supported_locales, - 'website': self.website, - 'hierarchy_meaning': self.hierarchy_meaning, - 'extras': self.extras, + 'source_type': get(data, 'source_type'), + 'canonical_url': get(data, 'canonical_url'), + 'custom_validation_schema': get(data, 'custom_validation_schema'), + 'default_locale': get(data, 'default_locale'), + 'supported_locales': get(data, 'supported_locales'), + 'website': get(data, 'website'), + 'hierarchy_meaning': get(data, 'hierarchy_meaning'), + 'extras': get(data, 'extras'), } - def get_smart_checksum_fields(self): + @staticmethod + def get_smart_checksum_fields_for_resource(data): return { - 'source_type': self.source_type, - 'canonical_url': self.canonical_url, - 'custom_validation_schema': self.custom_validation_schema, - 'default_locale': self.default_locale, - 'released': self.released, - 'retired': self.retired, + 'source_type': get(data, 'source_type'), + 'canonical_url': get(data, 'canonical_url'), + 'custom_validation_schema': get(data, 'custom_validation_schema'), + 'default_locale': get(data, 'default_locale'), + 'released': get(data, 'released'), + 'retired': get(data, 'retired'), } @property diff --git a/core/urls.py b/core/urls.py index e878af1f1..402ca6eb8 100644 --- a/core/urls.py +++ b/core/urls.py @@ -26,7 +26,8 @@ from core.collections.views import ReferenceExpressionResolveView from core.common.constants import NAMESPACE_PATTERN from core.common.utils import get_api_base_url -from core.common.views import RootView, FeedbackView, APIVersionView, ChangeLogView, ChecksumView +from core.common.views import RootView, FeedbackView, APIVersionView, ChangeLogView, StandardChecksumView, \ + SmartChecksumView from core.concepts.views import ConceptsHierarchyAmendAdminView from core.importers.views import BulkImportView @@ -56,7 +57,8 @@ 'admin/reports/monthly-usage/job/', report_views.ResourcesReportJobView.as_view(), name='monthly-usage-job'), path('admin/concepts/amend-hierarchy/', ConceptsHierarchyAmendAdminView.as_view(), name='concepts-amend-hierarchy'), re_path(r'^\$resolveReference/$', ReferenceExpressionResolveView.as_view(), name='$resolveReference'), - re_path(r'^\$checksum/$', ChecksumView.as_view(), name='$checksum'), + re_path(r'^\$checksum/standard/$', StandardChecksumView.as_view(), name='$checksum-standard'), + re_path(r'^\$checksum/smart/$', SmartChecksumView.as_view(), name='$checksum-smart'), path('users/', include('core.users.urls'), name='users_urls'), path('user/', include('core.users.user_urls'), name='current_user_urls'), path('orgs/', include('core.orgs.urls'), name='orgs_url'), diff --git a/core/users/models.py b/core/users/models.py index a4fca74cf..855a6f023 100644 --- a/core/users/models.py +++ b/core/users/models.py @@ -5,6 +5,7 @@ from django.contrib.auth.password_validation import validate_password from django.core.exceptions import ValidationError from django.db import models +from pydash import get from rest_framework.authtoken.models import Token from core.common.mixins import SourceContainerMixin @@ -52,26 +53,39 @@ class Meta: 'is_admin': {'sortable': False, 'filterable': False, 'exact': False, 'facet': True} } + STANDARD_CHECKSUM_INCLUSIONS = [ + 'first_name', 'last_name', 'username', 'company', 'location', 'website', 'preferred_locale', 'extras'] + SMART_CHECKSUM_INCLUSIONS = [ + 'first_name', 'last_name', 'username', 'company', 'location', 'website', 'is_active'] + def get_standard_checksum_fields(self): + return self.get_standard_checksum_fields_for_resource(self) + + def get_smart_checksum_fields(self): + return self.get_smart_checksum_fields_for_resource(self) + + @staticmethod + def get_standard_checksum_fields_for_resource(data): return { - 'first_name': self.first_name, - 'last_name': self.last_name, - 'username': self.username, - 'company': self.company, - 'location': self.location, - 'website': self.website, - 'preferred_locale': self.preferred_locale, - 'extras': self.extras, + 'first_name': get(data, 'first_name'), + 'last_name': get(data, 'last_name'), + 'username': get(data, 'username'), + 'company': get(data, 'company'), + 'location': get(data, 'location'), + 'website': get(data, 'website'), + 'preferred_locale': get(data, 'preferred_locale'), + 'extras': get(data, 'extras') } - def get_smart_checksum_fields(self): + @staticmethod + def get_smart_checksum_fields_for_resource(data): return { - 'first_name': self.first_name, - 'last_name': self.last_name, - 'username': self.username, - 'company': self.company, - 'location': self.location, - 'is_active': self.is_active + 'first_name': get(data, 'first_name'), + 'last_name': get(data, 'last_name'), + 'username': get(data, 'username'), + 'company': get(data, 'company'), + 'location': get(data, 'location'), + 'is_active': get(data, 'is_active') } def calculate_uri(self):