diff --git a/core/collections/models.py b/core/collections/models.py index 1f90496f..fb01e5af 100644 --- a/core/collections/models.py +++ b/core/collections/models.py @@ -43,6 +43,7 @@ from core.mappings.models import Mapping from core.tasks.models import Task from core.toggles.models import Toggle +from core.url_registry.constants import URL_REGISTRY_IN_RESOLVE_REFERENCE_TOGGLE TRUTHY = get_truthy_values() FALSEY = get_falsy_values() @@ -273,11 +274,11 @@ def add_references(self, references, user=None, _async=False): is_locked = CollectionReference.is_expansion_locked_repo_version_on() for reference in references: reference.expression = reference.build_expression() + # before generating, so cascade-and-transform resolves in this collection's namespace and locks + reference.collection = self if is_locked: - # before generating, so cascade-and-transform resolves against this collection's locks - reference.collection = self reference._async = _async # pylint: disable=protected-access - reference.clear_resolution_cache() + reference.clear_resolution_cache() total_references += reference.generate_references() total_references = CollectionReference.dedupe_by_expression(total_references) for reference in total_references: @@ -942,6 +943,7 @@ def __generate_references_for_type(self, queryset, resource_type, locked_version created_by=self.created_by, resource_version=resource_version, system=self.system, + namespace=self.namespace, version=repo_version )) return references @@ -1138,11 +1140,18 @@ def get_locked_system_versions(self, version): if drop_version(locked.uri) == drop_version(version.uri) ] + @cached_property + def resolution_namespace(self): + """Its own namespace, else its collection owner's, as the URL registries resolve within an owner""" + if self.namespace or not self.collection_id: + return self.namespace + return Expansion.get_system_version_namespace(self.collection.parent_url) + @cached_property def resolve_expression_system_version(self): if self.system: from core.sources.models import Source - version, _ = Source.resolve_reference_expression(self.system, self.namespace, self.version) + version, _ = Source.resolve_reference_expression(self.system, self.resolution_namespace, self.version) if version.id: return version return None @@ -1273,7 +1282,7 @@ def is_explicit_valueset(valueset, all_forms=True): def resolve_valueset_version(self, valueset): """A value set named without a version resolves to the collection's locked version of it, if any""" - version, _ = Collection.resolve_reference_expression(valueset, self.namespace) + version, _ = Collection.resolve_reference_expression(valueset, self.resolution_namespace) if version.id and self.collection_id and not self.is_explicit_valueset(valueset): return next(( locked for locked in self.collection.locked_collection_versions @@ -1282,8 +1291,8 @@ def resolve_valueset_version(self, valueset): return version def clear_resolution_cache(self): - for attr in ('resolve_system_version', 'resolve_expression_system_version', 'resolve_valueset_versions', - 'resolve_valueset_versions_with_unresolved'): + for attr in ('resolution_namespace', 'resolve_system_version', 'resolve_expression_system_version', + 'resolve_valueset_versions', 'resolve_valueset_versions_with_unresolved'): self.__dict__.pop(attr, None) @cached_property @@ -1301,7 +1310,8 @@ def resolve_valueset_versions_with_unresolved(self): if version.id: to_add.append(version) else: - unresolved.append({'url': valueset, 'namespace': self.namespace, 'type': 'reference.valueset'}) + unresolved.append( + {'url': valueset, 'namespace': self.resolution_namespace, 'type': 'reference.valueset'}) return explicit, evaluated, unresolved def clean(self): @@ -1414,10 +1424,10 @@ def _get_expression_system_version(self, system_version_cache=None): if system_version_cache is None: return self.resolve_expression_system_version - cache_key = (self.namespace, self.system, self.version) + cache_key = (self.resolution_namespace, self.system, self.version) if cache_key not in system_version_cache: from core.sources.models import Source - version, _ = Source.resolve_reference_expression(self.system, self.namespace, self.version) + version, _ = Source.resolve_reference_expression(self.system, self.resolution_namespace, self.version) system_version_cache[cache_key] = version if version.id else None return system_version_cache[cache_key] @@ -1456,9 +1466,9 @@ def _get_resolved_valueset_versions(self, valueset_version_cache=None): if isinstance(self.valueset, list): for valueset in self.valueset: # pylint: disable=not-an-iterable if valueset: - cache_key = (self.namespace, valueset, None) + cache_key = (self.resolution_namespace, valueset, None) if cache_key not in valueset_version_cache: - version, _ = Collection.resolve_reference_expression(valueset, self.namespace) + version, _ = Collection.resolve_reference_expression(valueset, self.resolution_namespace) valueset_version_cache[cache_key] = version if version.id else None if valueset_version_cache[cache_key]: versions.append(valueset_version_cache[cache_key]) @@ -1772,6 +1782,42 @@ def delete_expressions(self, expressions): # Deprecated: Old way, must use dele CollectionReference.on_commit_when_locked(lambda: batch_index_resources.apply_async( ('mapping', mappings_filters), queue='indexing', permanent=False)) + @staticmethod + def get_system_version_namespace(owner_url): + # without the URL registries a namespace would limit canonical URLs to the owner's repositories + return owner_url if Toggle.get(URL_REGISTRY_IN_RESOLVE_REFERENCE_TOGGLE) else None + + @staticmethod + def resolve_system_version_entries(value, namespace=None): + """Each system-version entry with the repo version it resolves to (one without an id when unresolved).""" + from core.sources.models import Source + return [ + (entry, Source.resolve_reference_expression(entry, namespace)[0]) + for entry in compact([entry.strip() for entry in (value or '').split(',')]) + ] + + @classmethod + def get_system_version_conflicts(cls, value, namespace=None): + """The system-version entries naming different versions of one repository, by the repository's URL. + An entry that doesn't resolve still counts, under the repository its URL names and the version it asks for.""" + from core.sources.models import Source + entries_by_repo = {} + for entry, version in cls.resolve_system_version_entries(value, namespace): + url, requested = canonical_url_to_url_and_version(entry) + if version.id: + repo_url, requested = drop_version(version.uri), version.version + elif is_canonical_uri(url): + repo, _ = Source.resolve_reference_expression(url, namespace) + repo_url = drop_version(repo.uri) if repo.id else url + else: + path = url if url.endswith('/') else url + '/' + repo_url = drop_version(path) + requested = requested or path[len(repo_url):].strip('/') or None + entries_by_repo.setdefault(repo_url, {}).setdefault(requested, entry) + return { + repo_url: list(entries.values()) for repo_url, entries in entries_by_repo.items() if len(entries) > 1 + } + def add_references( # pylint: disable=too-many-locals,too-many-statements,too-many-branches,too-many-arguments self, references, index=True, is_adding_all=False, attempt_reevaluate=True, force_reevaluate=False): include_refs, exclude_refs = self.to_ref_list_separated(references) @@ -1828,11 +1874,10 @@ def add_references( # pylint: disable=too-many-locals,too-many-statements,too-m include_system_versions = [] system_versions = self.parameters.get(ExpansionParameters.INCLUDE_SYSTEM) if should_reevaluate and system_versions: - for system_version in compact(system_versions.split(',')): - _system = system_version.strip() - _cache_key = f"NONE-{_system}-NONE" - version, _ = ConceptContainerModel.resolve_reference_expression(_system) - _system_version_cache[_cache_key] = version + namespace = self.get_system_version_namespace(self.collection_version.parent_url) + for _system, version in self.resolve_system_version_entries(system_versions, namespace): + # unresolved is None here too, as a reference resolving the same URL itself would get + _system_version_cache[f"{namespace or 'NONE'}-{_system}-NONE"] = version if version.id else None if version.id: include_system_versions.append(version) else: @@ -1840,7 +1885,7 @@ def add_references( # pylint: disable=too-many-locals,too-many-statements,too-m def get_ref_system_version(ref): if ref.system: - __cache_key = f"{ref.namespace or 'NONE'}-{ref.system}-{ref.version or 'NONE'}" + __cache_key = f"{ref.resolution_namespace or 'NONE'}-{ref.system}-{ref.version or 'NONE'}" if __cache_key not in _system_version_cache: _system_version_cache[__cache_key] = ref.resolve_expression_system_version _version = _system_version_cache[__cache_key] @@ -1894,7 +1939,7 @@ def get_ref_results(ref): # pylint: disable=too-many-branches,too-many-locals unresolved_repo_versions.append( { 'url': ref.system, - 'namespace': ref.namespace, + 'namespace': ref.resolution_namespace, 'version': ref.version, 'type': 'reference.system' } diff --git a/core/collections/tests/tests.py b/core/collections/tests/tests.py index ef33b928..f0f96259 100644 --- a/core/collections/tests/tests.py +++ b/core/collections/tests/tests.py @@ -44,6 +44,9 @@ from core.sources.models import Source from core.sources.tests.factories import OrganizationSourceFactory from core.tasks.models import Task +from core.toggles.models import Toggle +from core.url_registry.constants import URL_REGISTRY_IN_RESOLVE_REFERENCE_TOGGLE +from core.url_registry.factories import GlobalURLRegistryFactory, OrganizationURLRegistryFactory from core.users.models import UserProfile from core.users.tests.factories import UserProfileFactory @@ -2133,9 +2136,11 @@ def test_add_references_task(self): expansion.unresolved_repo_versions, [ {'url': 'http://foo-system.com', 'type': 'reference.system', 'version': None, 'namespace': 'barbar'}, - {'url': 'http://foo-valueset.com', 'type': 'reference.valueset', 'namespace': None}, - {'url': '/orgs/Org/collections/Collection/123/', 'type': 'reference.valueset', 'namespace': None}, - {'url': 'http://foo-system2.com|v1', 'type': 'reference.system', 'version': None, 'namespace': None} + {'url': 'http://foo-valueset.com', 'type': 'reference.valueset', 'namespace': collection.parent_url}, + {'url': '/orgs/Org/collections/Collection/123/', 'type': 'reference.valueset', + 'namespace': collection.parent_url}, + {'url': 'http://foo-system2.com|v1', 'type': 'reference.system', 'version': None, + 'namespace': collection.parent_url} ] ) self.assertEqual(expansion.explicit_collection_versions.count(), 0) @@ -2457,16 +2462,27 @@ def test_atomic_when_locked(self, atomic_mock): CollectionReference.atomic_when_locked() atomic_mock.assert_not_called() - def test_add_references_sets_lock_context_before_generating_only_with_toggle_on(self): + @staticmethod + def context_when_generating(concept, is_locked): + collection = OrganizationCollectionFactory() + seen = {} + + def generate(reference): + seen.update(collection_id=reference.collection_id, is_async=bool(getattr(reference, '_async', False))) + return [reference] + + with patch.object(CollectionReference, 'is_expansion_locked_repo_version_on', return_value=is_locked), \ + patch.object(CollectionReference, 'generate_references', autospec=True, side_effect=generate): + collection.add_references( + [CollectionReference(expression=concept.uri, created_by=collection.created_by)], + collection.created_by, True) + return collection, seen + + def test_add_references_sets_the_collection_before_generating_and_async_only_with_toggle_on(self): concept = ConceptFactory() for is_locked in (True, False): - collection = OrganizationCollectionFactory() - with patch.object(CollectionReference, 'is_expansion_locked_repo_version_on', return_value=is_locked), \ - patch.object(CollectionReference, 'clear_resolution_cache') as clear_mock: - collection.add_references( - [CollectionReference(expression=concept.uri, created_by=collection.created_by)], - collection.created_by, True) - self.assertEqual(clear_mock.called, is_locked) + collection, seen = self.context_when_generating(concept, is_locked) + self.assertEqual(seen, {'collection_id': collection.id, 'is_async': is_locked}) def test_cascade_children_to_expansion_claims_default_only_with_toggle_on(self): for is_locked in (True, False): @@ -5989,3 +6005,204 @@ def test_migration_moves_public_edit_collections_and_expansions_to_view(self): self.assertEqual(instance.public_access, ACCESS_TYPE_VIEW) private.refresh_from_db() self.assertEqual(private.public_access, ACCESS_TYPE_NONE) + + +class ExpansionSystemVersionNamespaceTest(OCLTestCase): + URL = 'https://registry-only.org/cs' + + @staticmethod + def set_registry_toggle(on): + Toggle.objects.filter(name=URL_REGISTRY_IN_RESOLVE_REFERENCE_TOGGLE).update( + dev=on, qa=on, demo=on, staging=on, who_staging=on, production=on) + + def setUp(self): + super().setUp() + self.owner = OrganizationFactory(mnemonic='owner') + self.other = OrganizationFactory(mnemonic='other') + self.publisher = OrganizationFactory(mnemonic='publisher') + # the lowest-id repository with the URL, found without the owner's registry + self.other_source = OrganizationSourceFactory( + organization=self.other, mnemonic='cs', canonical_url=self.URL) + self.other_source_v1 = OrganizationSourceFactory( + organization=self.other, mnemonic='cs', version='v1', canonical_url=self.URL) + self.source = OrganizationSourceFactory(organization=self.publisher, mnemonic='cs', canonical_url=self.URL) + self.source_v1 = OrganizationSourceFactory( + organization=self.publisher, mnemonic='cs', version='v1', canonical_url=self.URL) + self.source_v2 = OrganizationSourceFactory( + organization=self.publisher, mnemonic='cs', version='v2', canonical_url=self.URL) + self.collection = OrganizationCollectionFactory(organization=self.owner, mnemonic='vs') + self.expansion = ExpansionFactory(mnemonic='e1', collection_version=self.collection) + + def tearDown(self): + self.set_registry_toggle(False) + super().tearDown() + + def add_owner_registry_entry(self): + OrganizationURLRegistryFactory(organization=self.owner, url=self.URL, namespace=self.publisher.uri) + + def evaluate(self, system_version, namespace=None): + self.expansion.parameters = {'system-version': system_version} + self.expansion.add_references(CollectionReference(system=self.URL, code='c1', namespace=namespace)) + + def unresolved_system_versions(self): + return [ + entry['url'] for entry in self.expansion.unresolved_repo_versions or [] + if entry['type'] == 'expansion.parameters.system-version' + ] + + def test_resolves_in_owner_registry_with_toggle_on(self): + self.set_registry_toggle(True) + self.add_owner_registry_entry() + + self.evaluate(f'{self.URL}|v1', self.owner.uri) + + self.assertEqual(self.unresolved_system_versions(), []) + self.assertEqual(list(self.expansion.evaluated_source_versions.all()), [self.source_v1]) + + def test_owner_registry_ignored_with_toggle_off(self): + self.set_registry_toggle(False) + self.add_owner_registry_entry() + + self.evaluate(f'{self.URL}|v1') + + self.assertEqual(self.unresolved_system_versions(), []) + self.assertEqual(list(self.expansion.evaluated_source_versions.all()), [self.other_source_v1]) + + def test_unresolved_without_any_registry_entry_with_toggle_on(self): + self.set_registry_toggle(True) + + self.evaluate(f'{self.URL}|v1', self.owner.uri) + + self.assertEqual(self.unresolved_system_versions(), [f'{self.URL}|v1']) + + def test_resolves_in_global_registry_with_toggle_on(self): + self.set_registry_toggle(True) + GlobalURLRegistryFactory(url=self.URL, namespace=self.publisher.uri) + + self.evaluate(f'{self.URL}|v2', self.owner.uri) + + self.assertEqual(self.unresolved_system_versions(), []) + self.assertEqual(list(self.expansion.evaluated_source_versions.all()), [self.source_v2]) + + def test_entry_applies_whether_or_not_the_reference_names_the_owner_namespace(self): + self.set_registry_toggle(True) + self.add_owner_registry_entry() + GlobalURLRegistryFactory(url=self.URL, namespace=self.other.uri) + + for namespace in (self.owner.uri, None): + with self.subTest(namespace=namespace): + self.expansion.evaluated_source_versions.clear() + self.expansion.parameters = {'system-version': f'{self.URL}|v1'} + self.expansion.add_references(CollectionReference( + system=self.URL, code='c1', namespace=namespace, collection=self.collection)) + + self.assertEqual(self.unresolved_system_versions(), []) + self.assertEqual(list(self.expansion.evaluated_source_versions.all()), [self.source_v1]) + + def test_reference_resolution_namespace(self): + reference = CollectionReference(system=self.URL, code='c1', collection=self.collection) + own = CollectionReference(system=self.URL, code='c1', namespace=self.other.uri, collection=self.collection) + + self.set_registry_toggle(False) + self.assertIsNone(reference.resolution_namespace) + self.assertEqual(own.resolution_namespace, self.other.uri) + + reference.clear_resolution_cache() + self.set_registry_toggle(True) + self.assertEqual(reference.resolution_namespace, self.owner.uri) + self.assertEqual(CollectionReference(system=self.URL, code='c1').resolution_namespace, None) + + def test_unresolved_entry_matching_a_references_system_leaves_it_unresolved(self): + self.set_registry_toggle(True) + for namespace in (self.owner.uri, None): + with self.subTest(namespace=namespace): + expansion = ExpansionFactory(collection_version=self.collection) + expansion.parameters = {'system-version': 'https://example.org/cs|v1'} + + expansion.add_references(CollectionReference( + system='https://example.org/cs|v1', code='c1', namespace=namespace, collection=self.collection)) + + self.assertEqual( + sorted(entry['type'] for entry in expansion.unresolved_repo_versions), + ['expansion.parameters.system-version', 'reference.system']) + self.assertFalse(expansion.evaluated_source_versions.exists()) + + def test_get_system_version_conflicts_counts_versions_that_dont_resolve(self): + self.set_registry_toggle(True) + self.add_owner_registry_entry() + namespace = Expansion.get_system_version_namespace(self.owner.uri) + + self.assertEqual( + Expansion.get_system_version_conflicts(f'{self.URL}|v1,{self.URL}|v9', namespace), + {self.source.uri: [f'{self.URL}|v1', f'{self.URL}|v9']}) + self.assertEqual( + Expansion.get_system_version_conflicts(f'{self.source_v1.uri},{self.URL}|v9', namespace), + {self.source.uri: [self.source_v1.uri, f'{self.URL}|v9']}) + self.assertEqual( + Expansion.get_system_version_conflicts(f'{self.source.uri}v8/,{self.source_v1.uri}', namespace), + {self.source.uri: [f'{self.source.uri}v8/', self.source_v1.uri]}) + self.assertEqual( + Expansion.get_system_version_conflicts('https://none.org/cs|v8,https://none.org/cs|v9', namespace), + {'https://none.org/cs': ['https://none.org/cs|v8', 'https://none.org/cs|v9']}) + self.assertEqual( + Expansion.get_system_version_conflicts('https://none.org/cs|v8,https://none.org/cs|v8', namespace), {}) + + def test_get_system_version_conflicts_matches_missing_versions_across_url_forms(self): + self.set_registry_toggle(True) + self.add_owner_registry_entry() + namespace = Expansion.get_system_version_namespace(self.owner.uri) + + self.assertEqual( + Expansion.get_system_version_conflicts(f'{self.URL}|v8,{self.source.uri}v9/', namespace), + {self.source.uri: [f'{self.URL}|v8', f'{self.source.uri}v9/']}) + self.assertEqual( + Expansion.get_system_version_conflicts(f'{self.URL}|v8,{self.source.uri}v8/', namespace), {}) + self.assertEqual( + Expansion.get_system_version_conflicts(f'{self.URL}|v1,{self.source.uri}v1/', namespace), {}) + + def add_cascade_reference(self, **kwargs): + ConceptFactory(parent=self.source, mnemonic='c1') + ConceptFactory(parent=self.other_source, mnemonic='c1') + GlobalURLRegistryFactory(url=self.URL, namespace=self.other.uri) + self.add_owner_registry_entry() + self.collection.add_references([CollectionReference( + system=self.URL, code='c1', reference_type='concepts', created_by=self.collection.created_by, + cascade={'method': SOURCE_TO_CONCEPTS}, transform=TRANSFORM_TO_RESOURCE_VERSIONS, **kwargs)], + self.collection.created_by) + return list(self.collection.references.values_list('expression', flat=True)) + + def test_cascade_generates_from_the_owners_repository_with_the_lock_toggle_off(self): + self.set_registry_toggle(True) + + with patch.object(CollectionReference, 'is_expansion_locked_repo_version_on', return_value=False): + expressions = self.add_cascade_reference() + + self.assertTrue(expressions) + self.assertTrue(all(expression.startswith(self.source.uri) for expression in expressions), expressions) + + def test_cascade_keeps_an_explicit_global_namespace_on_generated_references(self): + self.set_registry_toggle(True) + + expressions = self.add_cascade_reference(namespace='/') + + self.assertTrue(expressions) + self.assertTrue(all(expression.startswith(self.other_source.uri) for expression in expressions), expressions) + self.assertEqual(set(self.collection.references.values_list('namespace', flat=True)), {'/'}) + + def test_get_system_version_conflicts(self): + self.set_registry_toggle(True) + self.add_owner_registry_entry() + namespace = Expansion.get_system_version_namespace(self.owner.uri) + self.assertEqual(namespace, self.owner.uri) + + self.assertEqual(Expansion.get_system_version_conflicts(f'{self.URL}|v1', namespace), {}) + self.assertEqual( + Expansion.get_system_version_conflicts(f'{self.URL}|v1, {self.source_v1.uri}', namespace), {}) + self.assertEqual( + Expansion.get_system_version_conflicts(f'{self.URL}|v1,{self.URL}|v2', namespace), + {self.source.uri: [f'{self.URL}|v1', f'{self.URL}|v2']}) + self.assertEqual( + Expansion.get_system_version_conflicts(f'{self.URL}|v1,https://unknown.org|v1', namespace), {}) + + self.set_registry_toggle(False) + self.assertIsNone(Expansion.get_system_version_namespace(self.owner.uri)) diff --git a/core/collections/views.py b/core/collections/views.py index 715a5490..0a52364f 100644 --- a/core/collections/views.py +++ b/core/collections/views.py @@ -25,7 +25,7 @@ SOURCE_MAPPINGS, UNKNOWN_REFERENCE_ADDED_TO_COLLECTION_FMT, TRANSFORM_TO_RESOURCE_VERSIONS, TRANSFORM_TO_EXTENSIONAL) from core.collections.documents import CollectionDocument -from core.collections.models import Collection, CollectionReference, Expansion +from core.collections.models import Collection, CollectionReference, Expansion, ExpansionParameters from core.collections.search import CollectionFacetedSearch from core.collections.serializers import ( CollectionDetailSerializer, CollectionListSerializer, @@ -879,6 +879,10 @@ def create(self, request, *args, **kwargs): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) version = self.get_object() + errors = self.get_system_version_errors(serializer.validated_data.get('parameters'), version) + if errors: + return Response({'parameters': {ExpansionParameters.INCLUDE_SYSTEM: errors}}, + status=status.HTTP_400_BAD_REQUEST) user = request.user expansion = version.cascade_children_to_expansion( expansion_data={**serializer.validated_data, 'created_by_id': user.id, 'updated_by_id': user.id}, @@ -889,6 +893,22 @@ def create(self, request, *args, **kwargs): return Response(self.get_response_serializer_class()(expansion).data, status=status.HTTP_201_CREATED, headers=headers) + @staticmethod + def get_system_version_errors(parameters, version): + """As FHIR's $expand, system-version may name only one version of a source.""" + value = get(parameters, ExpansionParameters.INCLUDE_SYSTEM) + if value is None or value == '': + return [] + if not isinstance(value, str): + return ['Must be a comma-separated string of canonical URLs (url|version) or repository version URLs.'] + conflicts = Expansion.get_system_version_conflicts( + value, Expansion.get_system_version_namespace(version.parent_url)) + return [ + f"Names more than one version of {repo_url}: {', '.join(entries)}. " + "Use only one version of a source in system-version." + for repo_url, entries in conflicts.items() + ] + class CollectionExpansionsView(CollectionBaseView, ListWithHeadersMixin): """List all expansions across all versions of a collection.""" diff --git a/core/integration_tests/tests_collections.py b/core/integration_tests/tests_collections.py index 84bffb6f..868afa36 100644 --- a/core/integration_tests/tests_collections.py +++ b/core/integration_tests/tests_collections.py @@ -4379,6 +4379,48 @@ def test_post(self): self.assertEqual(self.collection.expansions.count(), 1) self.assertIsNotNone(self.collection.expansion_uri) + def test_post_system_version_with_two_versions_of_one_source_400(self): + source = OrganizationSourceFactory() + v1 = OrganizationSourceFactory(organization=source.organization, mnemonic=source.mnemonic, version='v1') + v2 = OrganizationSourceFactory(organization=source.organization, mnemonic=source.mnemonic, version='v2') + + response = self.client.post( + self.collection.url + 'HEAD/expansions/', + {'mnemonic': 'e1', 'parameters': {'system-version': f'{v1.uri},{v2.uri}'}}, + HTTP_AUTHORIZATION='Token ' + self.token, + format='json' + ) + + self.assertEqual(response.status_code, 400) + self.assertEqual( + response.data, + {'parameters': {'system-version': [ + f'Names more than one version of {source.uri}: {v1.uri}, {v2.uri}. ' + 'Use only one version of a source in system-version.' + ]}}) + self.assertEqual(self.collection.expansions.count(), 0) + + for value in ([v1.uri], [], {}, 0, False): + response = self.client.post( + self.collection.url + 'HEAD/expansions/', + {'mnemonic': 'e1', 'parameters': {'system-version': value}}, + HTTP_AUTHORIZATION='Token ' + self.token, + format='json' + ) + + self.assertEqual(response.status_code, 400, value) + self.assertEqual(self.collection.expansions.count(), 0) + + response = self.client.post( + self.collection.url + 'HEAD/expansions/', + {'mnemonic': 'e1', 'parameters': {'system-version': f'{v1.uri},{v1.uri}'}}, + HTTP_AUTHORIZATION='Token ' + self.token, + format='json' + ) + + self.assertEqual(response.status_code, 201) + self.assertEqual(self.collection.expansions.count(), 1) + def test_get(self): response = self.client.get( self.collection.url + 'HEAD/expansions/', diff --git a/core/integration_tests/tests_value_sets.py b/core/integration_tests/tests_value_sets.py index a9b23063..40a081ec 100644 --- a/core/integration_tests/tests_value_sets.py +++ b/core/integration_tests/tests_value_sets.py @@ -118,6 +118,7 @@ def test_posting_value_sets(self): url = f"/orgs/{self.organization.mnemonic}/CodeSystem/" json_file = self.load_json('code_systems/code_systems_who_core.json') self.remove_duplicate_codes(json_file) + source_version = json_file.get('version') json_response = self.post(url, json_file) source_url = json_response.get('url') @@ -125,6 +126,9 @@ def test_posting_value_sets(self): json_file = self.load_json(test_file) response = self.post(url, json_file) json_response = response.json() + # the code system resolves in the value set owner's namespace, so each include names its version + for include in json_file['compose']['include']: + include['version'] = source_version self.ignore_paths(json_file, json_response, ['jurisdiction', 'date', 'copyright', 'purpose', 'identifier', 'meta', 'text', 'revisionDate', 'count',