From 1977393714f3ec7d93b22f4fdd938a5015351477 Mon Sep 17 00:00:00 2001 From: jrhoads Date: Thu, 26 Feb 2026 16:49:42 +0100 Subject: [PATCH 1/3] feat: add SINGLE_SEARCH_DEFAULT setting for affiliation matching --- rorapi/settings.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/rorapi/settings.py b/rorapi/settings.py index 6ae0cd5..d49ede2 100644 --- a/rorapi/settings.py +++ b/rorapi/settings.py @@ -299,6 +299,10 @@ # Toggle for behavior-based rate limiting ENABLE_BEHAVIORAL_LIMITING = os.getenv("ENABLE_BEHAVIORAL_LIMITING", "False") == "True" +# When True, affiliation matching defaults to single search; otherwise multisearch. +# Request params single_search and multisearch override this. +SINGLE_SEARCH_DEFAULT = os.getenv("SINGLE_SEARCH_DEFAULT", "False") == "True" + # Email settings for Django EMAIL_BACKEND = 'django_ses.SESBackend' AWS_ACCESS_KEY_ID = os.environ.get('AWS_ACCESS_KEY_ID') From 15d62b083df4ab519973221061bb70304d527a53 Mon Sep 17 00:00:00 2001 From: jrhoads Date: Thu, 26 Feb 2026 16:51:34 +0100 Subject: [PATCH 2/3] feat: add multisearch support and single search default to affiliation matching --- rorapi/common/views.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/rorapi/common/views.py b/rorapi/common/views.py index cd699aa..557965a 100644 --- a/rorapi/common/views.py +++ b/rorapi/common/views.py @@ -14,6 +14,7 @@ from rorapi.common.create_update import new_record_from_json, update_record_from_json from rorapi.common.csv_bulk import process_csv from rorapi.common.csv_utils import validate_csv +from django.conf import settings from rorapi.settings import REST_FRAMEWORK, ES7, ES_VARS from rorapi.common.matching import match_organizations from rorapi.common.matching_single_search import match_organizations as single_search_match_organizations @@ -153,6 +154,10 @@ def list(self, request, version=REST_FRAMEWORK["DEFAULT_VERSION"]): if "affiliation" in params: if "single_search" in params: errors, organizations = single_search_match_organizations(params) + elif "multisearch" in params: + errors, organizations = match_organizations(params) + elif settings.SINGLE_SEARCH_DEFAULT: + errors, organizations = single_search_match_organizations(params) else: errors, organizations = match_organizations(params) else: From d81a97b22b29bd6c02f7dd9e1d6f5071e0ad2a72 Mon Sep 17 00:00:00 2001 From: jrhoads Date: Thu, 26 Feb 2026 16:51:56 +0100 Subject: [PATCH 3/3] test: add unit tests for affiliation single search and multisearch logic --- rorapi/tests/tests_unit/tests_views_v2.py | 88 ++++++++++++++++++++++- 1 file changed, 87 insertions(+), 1 deletion(-) diff --git a/rorapi/tests/tests_unit/tests_views_v2.py b/rorapi/tests/tests_unit/tests_views_v2.py index 8811197..88649e0 100644 --- a/rorapi/tests/tests_unit/tests_views_v2.py +++ b/rorapi/tests/tests_unit/tests_views_v2.py @@ -2,7 +2,7 @@ import mock import os -from django.test import SimpleTestCase, Client +from django.test import SimpleTestCase, Client, override_settings from rest_framework.test import APIRequestFactory from rorapi.common import views @@ -88,6 +88,92 @@ def test_query_redirect(self, search_mock): response = client.get('/v2/organizations?query.names=query') self.assertRedirects(response, '/v2/organizations?query=query') + +class _MockMatchingResult: + """Minimal object matching MatchingResultSerializer expectations.""" + number_of_results = 0 + items = [] + + +class AffiliationDefaultMatchingTestCase(SimpleTestCase): + """Test SINGLE_SEARCH_DEFAULT and single_search/multisearch params.""" + V2_VERSION = 'v2' + + @override_settings(SINGLE_SEARCH_DEFAULT=False) + @mock.patch('rorapi.common.views.single_search_match_organizations') + @mock.patch('rorapi.common.views.match_organizations') + def test_affiliation_default_uses_multisearch_when_setting_off( + self, match_organizations_mock, single_search_mock): + mock_result = (None, _MockMatchingResult()) + match_organizations_mock.return_value = mock_result + single_search_mock.return_value = mock_result + + view = views.OrganizationViewSet.as_view({'get': 'list'}) + request = factory.get('/v2/organizations', {'affiliation': 'Harvard University'}) + response = view(request, version=self.V2_VERSION) + response.render() + + match_organizations_mock.assert_called_once() + single_search_mock.assert_not_called() + + @override_settings(SINGLE_SEARCH_DEFAULT=True) + @mock.patch('rorapi.common.views.single_search_match_organizations') + @mock.patch('rorapi.common.views.match_organizations') + def test_affiliation_default_uses_single_search_when_setting_on( + self, match_organizations_mock, single_search_mock): + mock_result = (None, _MockMatchingResult()) + match_organizations_mock.return_value = mock_result + single_search_mock.return_value = mock_result + + view = views.OrganizationViewSet.as_view({'get': 'list'}) + request = factory.get('/v2/organizations', {'affiliation': 'Harvard University'}) + response = view(request, version=self.V2_VERSION) + response.render() + + single_search_mock.assert_called_once() + match_organizations_mock.assert_not_called() + + @override_settings(SINGLE_SEARCH_DEFAULT=True) + @mock.patch('rorapi.common.views.single_search_match_organizations') + @mock.patch('rorapi.common.views.match_organizations') + def test_affiliation_multisearch_param_overrides_setting( + self, match_organizations_mock, single_search_mock): + mock_result = (None, _MockMatchingResult()) + match_organizations_mock.return_value = mock_result + single_search_mock.return_value = mock_result + + view = views.OrganizationViewSet.as_view({'get': 'list'}) + request = factory.get( + '/v2/organizations', + {'affiliation': 'Harvard University', 'multisearch': ''} + ) + response = view(request, version=self.V2_VERSION) + response.render() + + match_organizations_mock.assert_called_once() + single_search_mock.assert_not_called() + + @override_settings(SINGLE_SEARCH_DEFAULT=False) + @mock.patch('rorapi.common.views.single_search_match_organizations') + @mock.patch('rorapi.common.views.match_organizations') + def test_affiliation_single_search_param_uses_single_search( + self, match_organizations_mock, single_search_mock): + mock_result = (None, _MockMatchingResult()) + match_organizations_mock.return_value = mock_result + single_search_mock.return_value = mock_result + + view = views.OrganizationViewSet.as_view({'get': 'list'}) + request = factory.get( + '/v2/organizations', + {'affiliation': 'Harvard University', 'single_search': ''} + ) + response = view(request, version=self.V2_VERSION) + response.render() + + single_search_mock.assert_called_once() + match_organizations_mock.assert_not_called() + + class ViewRetrievalTestCase(SimpleTestCase): V2_VERSION = 'v2'