Skip to content
This repository has been archived by the owner on Jun 1, 2022. It is now read-only.

Commit

Permalink
Initial prototype of /api/searchLocations, refs #367
Browse files Browse the repository at this point in the history
  • Loading branch information
simonw committed Apr 20, 2021
1 parent d8c1d7c commit e9289e3
Show file tree
Hide file tree
Showing 6 changed files with 204 additions and 0 deletions.
18 changes: 18 additions & 0 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
100 changes: 100 additions & 0 deletions vaccinate/api/search.py
Original file line number Diff line number Diff line change
@@ -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],
},
}
28 changes: 28 additions & 0 deletions vaccinate/api/test_search.py
Original file line number Diff line number Diff line change
@@ -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
)
2 changes: 2 additions & 0 deletions vaccinate/config/urls.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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",
Expand Down
7 changes: 7 additions & 0 deletions vaccinate/templates/api/search_locations_debug.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<html>
<head><title>Debug search locations</title>
</head>
<body>
<pre>{{ json_results }}</pre>
</body>
</html>
49 changes: 49 additions & 0 deletions vaccinate/templates/api/search_locations_map.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<html>
<head>
<title>Search location results on a map</title>
<link
rel="stylesheet"
href="https://unpkg.com/[email protected]/dist/leaflet.css"
integrity="sha512-xodZBNTC5n17Xt2atTPuE1HxjVMSvLVW9ocqUKLsCC5CXdbqCmblAshOMAS6/keqq/sMZMZ19scR4PsZChSR7A=="
crossorigin=""
/>
<script
src="https://unpkg.com/[email protected]/dist/leaflet.js"
integrity="sha512-XQoYMqMTK8LvdxXYG3nZ448hOEQiglfqkJs1NOQV44cWnUrBc8PkAOcXy20w0vlaXaVUearIOBhiXZ5V3ynxwA=="
crossorigin=""
></script>
<style>
.leaflet-popup-content {
white-space: pre-wrap;
}
</style>
</head>
<body>
<h1>{{ query_string }}</h1>
<div id="themap" style="width: 98%; height: 90vh"></div>
<script>
var themapdiv = document.getElementById("themap");
var attribution =
'&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors';
var tilesUrl = "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png";
var themap = L.map(themapdiv).setView([0, 0], 3);
L.tileLayer(tilesUrl, {
maxZoom: 18,
attribution: attribution,
}).addTo(themap);
fetch("?{{ query_string|safe }}")
.then((r) => r.json())
.then((d) => {
var layer = L.geoJSON(d, {
onEachFeature: (feature, layer) => {
layer.bindPopup(JSON.stringify(feature.properties, null, 2));
},
});
layer.addTo(themap);
themap.fitBounds(layer.getBounds(), {
maxZoom: 14,
});
});
</script>
</body>
</html>

0 comments on commit e9289e3

Please sign in to comment.