diff --git a/docs/api.md b/docs/api.md index 4bc018a..8280dbc 100644 --- a/docs/api.md +++ b/docs/api.md @@ -418,3 +418,14 @@ Examples: - https://vial-staging.calltheshots.us/api/counties/CA - https://vial-staging.calltheshots.us/api/counties/OR - https://vial-staging.calltheshots.us/api/counties/RI + +## GET /api/export-mapbox/Locations.geojson + +This returns a GeoJSON file for use with Mapbox. This streams out records for ALL of our locations, so it can be very large! + +You can control which locations are returned (useful for debugging) with the following parameters: + +- `?limit=10` - only return 10 locations +- `?id=recXXX&id=lxx` - just return GeoJSON for specific location IDs (multiple allowed) + +Try this API at https://vial-staging.calltheshots.us/api/export-mapbox/Locations.geojson?limit=10 diff --git a/vaccinate/api/test_mapbox_export.py b/vaccinate/api/test_mapbox_export.py new file mode 100644 index 0000000..e30ed8a --- /dev/null +++ b/vaccinate/api/test_mapbox_export.py @@ -0,0 +1,28 @@ +import json + +import pytest + + +@pytest.mark.django_db +def test_mapbox_export(client, ten_locations): + response = client.get("/api/export-mapbox/Locations.geojson") + joined = b"".join(response.streaming_content) + assert json.loads(joined) == { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { + "id": location.public_id, + "name": location.name, + "location_type": "Hospital / Clinic", + "website": None, + "address": None, + "hours": None, + "public_notes": None, + }, + "geometry": {"type": "Point", "coordinates": [40.0, 30.0]}, + } + for location in ten_locations + ], + } diff --git a/vaccinate/api/views.py b/vaccinate/api/views.py index 9838470..2034449 100644 --- a/vaccinate/api/views.py +++ b/vaccinate/api/views.py @@ -2,6 +2,7 @@ import os import pathlib import random +import textwrap from datetime import datetime, timedelta from typing import List, Optional @@ -32,10 +33,11 @@ Reporter, State, ) +from core.utils import keyset_pagination_iterator from dateutil import parser from django.conf import settings from django.db import transaction -from django.http import JsonResponse +from django.http import JsonResponse, StreamingHttpResponse from django.shortcuts import render from django.utils import timezone from django.utils.timezone import localdate @@ -771,3 +773,59 @@ def api_export_preview_locations(request): @beeline.traced(name="location_metrics") def location_metrics(request): return LocationMetricsReport().serve() + + +@beeline.traced(name="export_mapbox_geojson") +def export_mapbox_geojson(request): + locations = Location.objects.all().select_related( + "location_type", "dn_latest_non_skip_report" + ) + location_ids = request.GET.getlist("id") + if location_ids: + locations = locations.filter(public_id__in=location_ids) + limit = None + if request.GET.get("limit", "").isdigit(): + limit = int(request.GET["limit"]) + start = textwrap.dedent( + """ + { + "type": "FeatureCollection", + "features": [ + """ + ) + + def chunks(): + yield start + started = False + for location in keyset_pagination_iterator(locations, stop_after=limit): + if started: + yield "," + started = True + yield json.dumps( + { + "type": "Feature", + "properties": { + "id": location.public_id, + "name": location.name, + "location_type": location.location_type.name, + "website": location.website, + "address": location.full_address, + # "provider": "County", + # "appointment_information": "", + # "date_added": "2021-01-15T21:49:00.000Z", + # "last_contacted_date": "2021-04-14T16:07:00.000Z", + # "vaccines_offered": [], + "hours": location.hours, + "public_notes": location.dn_latest_non_skip_report.public_notes + if location.dn_latest_non_skip_report + else None, + }, + "geometry": { + "type": "Point", + "coordinates": [location.longitude, location.latitude], + }, + } + ) + yield "]}" + + return StreamingHttpResponse(chunks(), content_type="application/json") diff --git a/vaccinate/config/urls.py b/vaccinate/config/urls.py index d30a787..12edd5e 100644 --- a/vaccinate/config/urls.py +++ b/vaccinate/config/urls.py @@ -73,6 +73,7 @@ path("api/availabilityTags", api_views.availability_tags), path("api/export", api_views.api_export), path("api/export-preview/Locations.json", api_views.api_export_preview_locations), + path("api/export-mapbox/Locations.geojson", api_views.export_mapbox_geojson), path("api/location_metrics", api_views.location_metrics), path("api/counties/", api_views.counties), path("", include("django.contrib.auth.urls")), diff --git a/vaccinate/core/admin_actions.py b/vaccinate/core/admin_actions.py index 63f7cf3..0595933 100644 --- a/vaccinate/core/admin_actions.py +++ b/vaccinate/core/admin_actions.py @@ -4,20 +4,7 @@ from django.db.models.fields.related import ForeignKey from django.http import StreamingHttpResponse - -def keyset_pagination_iterator(input_queryset, batch_size=500): - all_queryset = input_queryset.order_by("pk") - last_pk = None - while True: - queryset = all_queryset - if last_pk is not None: - queryset = all_queryset.filter(pk__gt=last_pk) - queryset = queryset[:batch_size] - for row in queryset: - last_pk = row.pk - yield row - if not queryset: - break +from .utils import keyset_pagination_iterator def export_as_csv_action( diff --git a/vaccinate/core/utils.py b/vaccinate/core/utils.py new file mode 100644 index 0000000..fa862a3 --- /dev/null +++ b/vaccinate/core/utils.py @@ -0,0 +1,17 @@ +def keyset_pagination_iterator(input_queryset, batch_size=500, stop_after=None): + all_queryset = input_queryset.order_by("pk") + last_pk = None + i = 0 + while True: + queryset = all_queryset + if last_pk is not None: + queryset = all_queryset.filter(pk__gt=last_pk) + queryset = queryset[:batch_size] + for row in queryset: + last_pk = row.pk + yield row + i += 1 + if stop_after and i >= stop_after: + return + if not queryset: + break