Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Beacon search endpoint #402

Merged
merged 14 commits into from
May 17, 2023
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'),
]