From 89bd74ea6020b27ffeb93e3ecdea23d65077c399 Mon Sep 17 00:00:00 2001 From: David Kane Date: Wed, 31 Jan 2024 17:45:52 +0000 Subject: [PATCH] add type suggestion --- reconcile/api/base.py | 48 ++++++++++++--- reconcile/api/reconcile_all.py | 10 ++- reconcile/api/reconcile_company.py | 3 +- reconcile/api/schema.py | 7 ++- reconcile/tests/test_company_api.py | 9 --- reconcile/tests/test_reconcile_all_api.py | 75 ++++++++++++++++++++--- 6 files changed, 121 insertions(+), 31 deletions(-) diff --git a/reconcile/api/base.py b/reconcile/api/base.py index 2ccda7c..ef4028d 100644 --- a/reconcile/api/base.py +++ b/reconcile/api/base.py @@ -1,6 +1,7 @@ import urllib.parse from typing import Dict, List, Literal, Optional, Union +from django.db.models import Q from django.http import Http404 from django.shortcuts import reverse @@ -22,7 +23,8 @@ class Reconcile: name = "Find that Charity Reconciliation API" view_url = "orgid_html" view_url_args = {"org_id": "{{id}}"} - suggest = True + suggest_entity = True + suggest_type = True extend = True preview = True @@ -58,7 +60,9 @@ def get_service_spec( orgtypes = self._get_orgtypes_from_str(orgtypes) if not defaultTypes: if not orgtypes or orgtypes == "all": - defaultTypes = [{"id": "/Organization", "name": "Organisation"}] + defaultTypes = [ + {"id": "registered-charity", "name": "Registered Charity"} + ] elif isinstance(orgtypes, list): defaultTypes = [{"id": o.slug, "name": o.title} for o in orgtypes] @@ -92,13 +96,18 @@ def get_service_spec( }, "property_settings": [], } - if self.suggest: - spec["suggest"] = { - "entity": { + if self.suggest_entity or self.suggest_type: + spec["suggest"] = {} + if self.suggest_entity: + spec["suggest"]["entity"] = { "service_url": request_path, "service_path": "/suggest/entity", - }, - } + } + if self.suggest_type: + spec["suggest"]["type"] = { + "service_url": request_path, + "service_path": "/suggest/type", + } return spec def reconcile( @@ -139,7 +148,6 @@ def suggest_entity( orgtypes = self._get_orgtypes_from_str(orgtypes) SUGGEST_NAME = "name_complete" - # cursor = request.GET.get("cursor") if not prefix: raise Http404("Prefix must be supplied") q = OrganisationGroup.search() @@ -176,6 +184,30 @@ def suggest_entity( ] } + def suggest_type( + self, + request, + prefix: str, + cursor: int = 0, + ): + if not prefix: + raise Http404("Prefix must be supplied") + + results = OrganisationType.objects.filter( + Q(title__icontains=prefix) | Q(slug__icontains=prefix) + )[cursor : cursor + 10] + + return { + "result": [ + { + "id": r.slug, + "name": r.title, + "notable": [], + } + for r in results + ] + } + def propose_properties(self, request, type_, limit=500): if type_ != "Organization": raise Http404("type must be Organization") diff --git a/reconcile/api/reconcile_all.py b/reconcile/api/reconcile_all.py index d60ddca..dd7f633 100644 --- a/reconcile/api/reconcile_all.py +++ b/reconcile/api/reconcile_all.py @@ -15,8 +15,9 @@ ReconciliationQueryBatchForm, ReconciliationResult, ServiceSpec, - SuggestQuery, + SuggestEntityQuery, SuggestResponse, + SuggestTypeQuery, ) api = Router(tags=["Reconciliation (nonprofits)"]) @@ -64,12 +65,17 @@ def preview(request, id: str, response: HttpResponse): @api.get("/suggest/entity", response={200: SuggestResponse}, exclude_none=True) -def suggest_entity(request, query: Query[SuggestQuery]): +def suggest_entity(request, query: Query[SuggestEntityQuery]): return reconcile.suggest_entity( request, query.prefix, query.cursor, orgtypes=query.type ) +@api.get("/suggest/type", response={200: SuggestResponse}, exclude_none=True) +def suggest_type(request, query: Query[SuggestTypeQuery]): + return reconcile.suggest_type(request, query.prefix, query.cursor) + + @api.get( "/extend/propose", response={200: DataExtensionPropertyProposalResponse}, diff --git a/reconcile/api/reconcile_company.py b/reconcile/api/reconcile_company.py index 3d9df0f..8e7e3a4 100644 --- a/reconcile/api/reconcile_company.py +++ b/reconcile/api/reconcile_company.py @@ -20,7 +20,8 @@ class CompanyReconcile(Reconcile): name = "Find that Charity Company Reconciliation API" view_url = "company_detail" view_url_args = {"company_number": "{{id}}"} - suggest = False + suggest_entity = False + suggest_type = False extend = False preview = False diff --git a/reconcile/api/schema.py b/reconcile/api/schema.py index 0d3b07b..ecc148d 100644 --- a/reconcile/api/schema.py +++ b/reconcile/api/schema.py @@ -201,12 +201,17 @@ class ReconciliationResultBatch(RootModel[Dict[str, Dict]], Schema): root: Dict[str, ReconciliationResult] -class SuggestQuery(Schema): +class SuggestEntityQuery(Schema): prefix: str cursor: int = 0 type: Optional[str] = Field(None, alias="type") +class SuggestTypeQuery(Schema): + prefix: str + cursor: int = 0 + + class SuggestResult(Schema): id: str name: str diff --git a/reconcile/tests/test_company_api.py b/reconcile/tests/test_company_api.py index 1be8255..9c4484d 100644 --- a/reconcile/tests/test_company_api.py +++ b/reconcile/tests/test_company_api.py @@ -27,7 +27,6 @@ def setUp(self): super().setUp() self.registry = Registry(retrieve=retrieve_schema_from_filesystem) - # GET request to /api/v1/reconcile/company should return the service spec def test_get_company_service_spec(self): for schema_version, schema in get_schema("manifest.json").items(): with self.subTest(schema_version): @@ -55,9 +54,7 @@ def test_get_company_service_spec(self): registry=self.registry, ) - # POST request to /api/v1/reconcile/company should return a list of candidates def test_company_reconcile_post(self): - # attach RECON RESPONSE to the search() method of self.mock_es self.mock_es.return_value.search.return_value = RECON_RESPONSE for schema_version, schema in get_schema( @@ -83,9 +80,7 @@ def test_company_reconcile_post(self): registry=self.registry, ) - # POST request to /api/v1/reconcile/company should return a list of candidates def test_company_reconcile_get(self): - # attach RECON RESPONSE to the search() method of self.mock_es self.mock_es.return_value.search.return_value = RECON_RESPONSE for schema_version, schema in get_schema( @@ -111,9 +106,7 @@ def test_company_reconcile_get(self): registry=self.registry, ) - # POST request to /api/v1/reconcile should return a list of candidates def test_reconcile_empty_post(self): - # attach RECON RESPONSE to the search() method of self.mock_es self.mock_es.return_value.search.return_value = EMPTY_RESPONSE for schema_version, schema in get_schema( @@ -138,9 +131,7 @@ def test_reconcile_empty_post(self): registry=self.registry, ) - # POST request to /api/v1/reconcile should return a list of candidates def test_reconcile_empty_get(self): - # attach RECON RESPONSE to the search() method of self.mock_es self.mock_es.return_value.search.return_value = EMPTY_RESPONSE for schema_version, schema in get_schema( diff --git a/reconcile/tests/test_reconcile_all_api.py b/reconcile/tests/test_reconcile_all_api.py index 9d1011c..50e2e28 100644 --- a/reconcile/tests/test_reconcile_all_api.py +++ b/reconcile/tests/test_reconcile_all_api.py @@ -30,7 +30,6 @@ RECON_BASE_URLS: list[Tuple[str, list[str]]] = [ ("/api/v1/reconcile/", ["0.2"]), - # ("/api/v1/reconcile/local-authority", ["0.2"]), ("/reconcile", ["0.1"]), ("/reconcile/local-authority", ["0.1"]), ] @@ -49,7 +48,6 @@ def setUp(self): super().setUp() self.registry = Registry(retrieve=retrieve_schema_from_filesystem) - # GET request to /api/v1/reconcile should return the service spec def test_get_service_spec(self): for base_url, schema_version, schema in get_test_cases("manifest.json"): with self.subTest((base_url, schema_version)): @@ -71,7 +69,6 @@ def test_get_service_spec(self): ) def test_reconcile_post(self): - # attach RECON RESPONSE to the search() method of self.mock_es self.mock_es.return_value.search.return_value = RECON_RESPONSE for base_url, schema_version, schema in get_test_cases( @@ -98,7 +95,6 @@ def test_reconcile_post(self): ) def test_reconcile_get(self): - # attach RECON RESPONSE to the search() method of self.mock_es self.mock_es.return_value.search.return_value = RECON_RESPONSE for base_url, schema_version, schema in get_test_cases( @@ -125,7 +121,6 @@ def test_reconcile_get(self): ) def test_reconcile_with_type_post(self): - # attach RECON RESPONSE to the search() method of self.mock_es self.mock_es.return_value.search.return_value = RECON_RESPONSE for base_url, schema_version, schema in get_test_cases( @@ -165,7 +160,6 @@ def test_reconcile_with_type_post(self): ) def test_reconcile_with_type_get(self): - # attach RECON RESPONSE to the search() method of self.mock_es self.mock_es.return_value.search.return_value = RECON_RESPONSE for base_url, schema_version, schema in get_test_cases( @@ -205,7 +199,6 @@ def test_reconcile_with_type_get(self): ) def test_reconcile_empty(self): - # attach RECON RESPONSE to the search() method of self.mock_es self.mock_es.return_value.search.return_value = EMPTY_RESPONSE for base_url, schema_version, schema in get_test_cases( @@ -230,7 +223,6 @@ def test_reconcile_empty(self): registry=self.registry, ) - # POST request to /api/v1/reconcile/suggest/entity should return a list of candidates def test_reconcile_suggest_entity(self): self.mock_es.return_value.search.return_value = SUGGEST_RESPONSE @@ -265,7 +257,6 @@ def test_reconcile_suggest_entity(self): registry=self.registry, ) - # POST request to /api/v1/reconcile/suggest/entity should return a list of candidates def test_reconcile_suggest_entity_type(self): self.mock_es.return_value.search.return_value = SUGGEST_RESPONSE @@ -301,6 +292,71 @@ def test_reconcile_suggest_entity_type(self): registry=self.registry, ) + def test_reconcile_suggest_type(self): + self.mock_es.return_value.search.return_value = SUGGEST_RESPONSE + + for base_url, schema_version, schema in get_test_cases( + "suggest-types-response.json" + ): + with self.subTest((base_url, schema_version)): + service_spec = self.client.get(base_url).json() + if "type" not in service_spec["suggest"]: + continue + suggest_url = "".join(list(service_spec["suggest"]["type"].values())) + + response = self.client.get( + suggest_url, + { + "prefix": "Charity", + }, + ) + self.assertEqual(response.status_code, 200) + data = response.json() + self.assertEqual(list(data.keys()), ["result"]) + self.assertEqual(len(data["result"]), 2) + self.assertEqual( + list(data["result"][0].keys()), ["id", "name", "notable"] + ) + self.assertEqual(data["result"][0]["id"], "registered-charity") + self.assertEqual(len(data["result"][0]["notable"]), 0) + + jsonschema.validate( + instance=data, + schema=schema, + cls=jsonschema.Draft7Validator, + registry=self.registry, + ) + + def test_reconcile_suggest_type_empty(self): + self.mock_es.return_value.search.return_value = SUGGEST_RESPONSE + + for base_url, schema_version, schema in get_test_cases( + "suggest-types-response.json" + ): + with self.subTest((base_url, schema_version)): + service_spec = self.client.get(base_url).json() + if "type" not in service_spec["suggest"]: + continue + suggest_url = "".join(list(service_spec["suggest"]["type"].values())) + + response = self.client.get( + suggest_url, + { + "prefix": "BLAH", + }, + ) + self.assertEqual(response.status_code, 200) + data = response.json() + self.assertEqual(list(data.keys()), ["result"]) + self.assertEqual(len(data["result"]), 0) + + jsonschema.validate( + instance=data, + schema=schema, + cls=jsonschema.Draft7Validator, + registry=self.registry, + ) + def test_reconcile_extend_post(self): self.mock_es.return_value.search.return_value = SUGGEST_RESPONSE @@ -391,7 +447,6 @@ def test_reconcile_extend_get(self): registry=self.registry, ) - # GET request to /api/v1/reconcile/suggest/entity should return a list of candidates def test_reconcile_propose_properties(self): self.mock_es.return_value.search.return_value = SUGGEST_RESPONSE