Skip to content

Commit

Permalink
Merge pull request #402 from bento-platform/features/beacon-public-co…
Browse files Browse the repository at this point in the history
…nfig-search

Beacon search endpoint
  • Loading branch information
gsfk authored May 17, 2023
2 parents 445a02f + bcb4f01 commit 08d8e87
Show file tree
Hide file tree
Showing 3 changed files with 111 additions and 0 deletions.
68 changes: 68 additions & 0 deletions chord_metadata_service/patients/api_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -213,3 +213,71 @@ def get(self, request, *args, **kwargs):
"experiment_type": experiment_types
}
})


class BeaconListIndividuals(APIView):
"""
View to return lists of individuals filtered using search terms from katsu's config.json.
Uncensored equivalent of PublicListIndividuals.
"""
def filter_queryset(self, queryset):
# Check query parameters validity
qp = self.request.query_params
if len(qp) > settings.CONFIG_PUBLIC["rules"]["max_query_parameters"]:
raise ValidationError(f"Wrong number of fields: {len(qp)}")

search_conf = settings.CONFIG_PUBLIC["search"]
field_conf = settings.CONFIG_PUBLIC["fields"]
queryable_fields = {
f: field_conf[f] for section in search_conf for f in section["fields"]
}

for field, value in qp.items():
if field not in queryable_fields:
raise ValidationError(f"Unsupported field used in query: {field}")

field_props = queryable_fields[field]
options = get_field_options(field_props)
if value not in options \
and not (
# case-insensitive search on categories
field_props["datatype"] == "string"
and value.lower() in [o.lower() for o in options]
) \
and not (
# no restriction when enum is not set for categories
field_props["datatype"] == "string"
and field_props["config"]["enum"] is None
):
raise ValidationError(f"Invalid value used in query: {value}")

# recursion
queryset = filter_queryset_field_value(queryset, field_props, value)

return queryset

def get(self, request, *args, **kwargs):
if not settings.CONFIG_PUBLIC:
return Response(settings.NO_PUBLIC_DATA_AVAILABLE, status=404)

base_qs = Individual.objects.all()
try:
filtered_qs = self.filter_queryset(base_qs)
except ValidationError as e:
return Response(errors.bad_request_error(
*(e.error_list if hasattr(e, "error_list") else e.error_dict.items())), status=400)

tissues_count, sampled_tissues = biosample_tissue_stats(filtered_qs)
experiments_count, experiment_types = experiment_type_stats(filtered_qs)

return Response({
"matches": filtered_qs.values_list("id", flat=True),
"biosamples": {
"count": tissues_count,
"sampled_tissue": sampled_tissues
},
"experiments": {
"count": experiments_count,
"experiment_type": experiment_types
}
})
40 changes: 40 additions & 0 deletions chord_metadata_service/patients/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -650,3 +650,43 @@ def test_public_filtering_age_range_min_and_max_no_config(self):
self.assertIsInstance(response_obj, dict)
self.assertIsInstance(response_obj, dict)
self.assertEqual(response_obj, settings.NO_PUBLIC_DATA_AVAILABLE)


class BeaconSearchTest(APITestCase):

random_range = 20

def setUp(self):
individuals = [c.generate_valid_individual() for _ in range(self.random_range)]
for individual in individuals:
Individual.objects.create(**individual)

# test beacon formatted response
@override_settings(CONFIG_PUBLIC=CONFIG_PUBLIC_TEST)
def test_beacon_search_response(self):
response = self.client.get('/api/beacon_search?sex=MALE')
male_count = Individual.objects.filter(sex="MALE").count()
self.assertEqual(response.status_code, status.HTTP_200_OK)
response_obj = response.json()
self.assertEqual(len(response_obj["matches"]), male_count)

@override_settings(CONFIG_PUBLIC={})
def test_beacon_search_response_no_config(self):
# test when config is not provided, returns NOT FOUND
response = self.client.get('/api/beacon_search?sex=MALE')
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)

@override_settings(CONFIG_PUBLIC=CONFIG_PUBLIC_TEST)
def test_beacon_search_response_invalid_search_key(self):
response = self.client.get('/api/beacon_search?birdwatcher=yes')
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)

@override_settings(CONFIG_PUBLIC=CONFIG_PUBLIC_TEST)
def test_beacon_search_response_invalid_search_value(self):
response = self.client.get('/api/beacon_search?smoking=on_Sundays')
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)

@override_settings(CONFIG_PUBLIC=CONFIG_PUBLIC_TEST)
def test_beacon_search_too_many_params(self):
response = self.client.get('/api/beacon_search?sex=MALE&smoking=Non-smoker&death_dc=Deceased')
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
3 changes: 3 additions & 0 deletions chord_metadata_service/restapi/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,4 +102,7 @@
path('public_search_fields', public_search_fields, name='public-search-fields',),
path('public_overview', public_overview, name='public-overview',),
path('public_dataset', public_dataset, name='public-dataset'),

# uncensored endpoint for beacon search using fields from config.json
path('beacon_search', individual_views.BeaconListIndividuals.as_view(), name='beacon-search'),
]

0 comments on commit 08d8e87

Please sign in to comment.