diff --git a/docs/api.md b/docs/api.md index 8a3d438..af05d67 100644 --- a/docs/api.md +++ b/docs/api.md @@ -8,6 +8,24 @@ The base URL for every API is https://vial-staging.calltheshots.us/ Not a JSON API, but this is a convenient way to link to the edit page for a specific location. You can contruct this URL with the public ID of the location and VIAL will redirect the authenticated user to the corresponding edit interface for that location. +## GET /api/searchLocations + +Under active development at the moment. This lets you search all of our locations, excluding those that have been soft-deleted. + +Optional query string parameters: + +- `q=` - a term to search for in the `name` field +- `size=` - the number of results to return, up to 1000 +- `output=` - the output format, see below. + +The following output formats are supported: + +- `json` - the default. [Example JSON](https://vial-staging.calltheshots.us/api/searchLocations?q=walgreens&format=json) +- `geojson` - a GeoJSON Feature Collection. [Example GeoJSON](https://vial-staging.calltheshots.us/api/searchLocations?q=walgreens&format=geojson) +- `map` - a basic Leaflet map that renders that GeoJSON. [Example map](https://vial-staging.calltheshots.us/api/searchLocations?q=walgreens&format=map) + +You can also add `debug=1` to the JSON output to wrap them in an HTML page. This is primarily useful in development as it enables the Django Debug Toolbar for those results. + ## POST /api/submitReport This API records a new "report" in our database. A report is when someone checks with a vaccination location - usually by calling them - to find out their current status. diff --git a/vaccinate/api/search.py b/vaccinate/api/search.py new file mode 100644 index 0000000..8e6f7e0 --- /dev/null +++ b/vaccinate/api/search.py @@ -0,0 +1,100 @@ +import json +from html import escape + +import beeline +from core.models import Location +from django.http import JsonResponse +from django.shortcuts import render +from django.utils.safestring import mark_safe + + +@beeline.traced("search_locations") +def search_locations(request): + format = request.GET.get("format") or "json" + size = min(int(request.GET.get("size", "10")), 1000) + q = (request.GET.get("q") or "").strip().lower() + # debug wraps in HTML so we can run django-debug-toolbar + debug = request.GET.get("debug") + if format == "map": + get = request.GET.copy() + get["format"] = "geojson" + if "debug" in get: + del get["debug"] + return render( + request, "api/search_locations_map.html", {"query_string": get.urlencode()} + ) + qs = Location.objects.filter(soft_deleted=False) + if q: + qs = qs.filter(name__icontains=q) + qs = location_json_queryset(qs) + page_qs = qs[:size] + json_results = lambda: { + "results": [location_json(location) for location in page_qs], + "total": qs.count(), + } + output = None + if format == "geojson": + output = { + "type": "FeatureCollection", + "features": [location_geojson(location) for location in qs], + } + + else: + output = json_results() + if debug: + return render( + request, + "api/search_locations_debug.html", + { + "json_results": mark_safe(escape(json.dumps(output, indent=2))), + }, + ) + else: + return JsonResponse(output) + + +def location_json_queryset(queryset): + return queryset.select_related( + "state", "county", "location_type", "provider__provider_type" + ) + + +def location_json(location): + return { + "id": location.public_id, + "name": location.name, + "state": location.state.abbreviation, + "latitude": location.latitude, + "longitude": location.longitude, + "location_type": location.location_type.name, + "import_ref": location.import_ref, + "phone_number": location.phone_number, + "full_address": location.full_address, + "city": location.city, + "county": location.county.name if location.county else None, + "google_places_id": location.google_places_id, + "vaccinefinder_location_id": location.vaccinefinder_location_id, + "vaccinespotter_location_id": location.vaccinespotter_location_id, + "zip_code": location.zip_code, + "hours": location.hours, + "website": location.website, + "preferred_contact_method": location.preferred_contact_method, + "provider": { + "name": location.provider.name, + "type": location.provider.provider_type.name, + } + if location.provider + else None, + } + + +def location_geojson(location): + properties = location_json(location) + return { + "type": "Feature", + "properties": properties, + "geometry": { + "type": "Point", + "coordinates": [location.longitude, location.latitude], + }, + } diff --git a/vaccinate/api/test_search.py b/vaccinate/api/test_search.py new file mode 100644 index 0000000..9da0fa1 --- /dev/null +++ b/vaccinate/api/test_search.py @@ -0,0 +1,28 @@ +import json + +import pytest +from core.models import Location + + +@pytest.mark.parametrize( + "query_string,expected", (("q=location+1", ["Location 1", "Location 10"]),) +) +def test_search_locations(client, query_string, expected, ten_locations): + response = client.get("/api/searchLocations?" + query_string) + assert response.status_code == 200 + data = json.loads(response.content) + names = [r["name"] for r in data["results"]] + assert names == expected + assert data["total"] == len(expected) + + +def test_search_locations_ignores_soft_deleted(client, ten_locations): + assert ( + json.loads(client.get("/api/searchLocations?q=Location+1").content)["total"] + == 2 + ) + Location.objects.filter(name="Location 10").update(soft_deleted=True) + assert ( + json.loads(client.get("/api/searchLocations?q=Location+1").content)["total"] + == 1 + ) diff --git a/vaccinate/config/urls.py b/vaccinate/config/urls.py index 1d550c5..246e92d 100644 --- a/vaccinate/config/urls.py +++ b/vaccinate/config/urls.py @@ -1,5 +1,6 @@ import debug_toolbar import django_sql_dashboard +from api import search as search_views from api import views as api_views from auth0login.views import login, logout from core import tool_views @@ -46,6 +47,7 @@ ), ), path("api/verifyToken", api_views.verify_token), + path("api/searchLocations", search_views.search_locations), path("api/importLocations", api_views.import_locations), path( "api/importLocations/debug", diff --git a/vaccinate/templates/api/search_locations_debug.html b/vaccinate/templates/api/search_locations_debug.html new file mode 100644 index 0000000..4e31819 --- /dev/null +++ b/vaccinate/templates/api/search_locations_debug.html @@ -0,0 +1,7 @@ + +
{{ json_results }}+ + diff --git a/vaccinate/templates/api/search_locations_map.html b/vaccinate/templates/api/search_locations_map.html new file mode 100644 index 0000000..f2e97fb --- /dev/null +++ b/vaccinate/templates/api/search_locations_map.html @@ -0,0 +1,49 @@ + + +