<?xml version="1.0" encoding="utf-8"?>
<feed xml:lang="en-us" xmlns="http://www.w3.org/2005/Atom"><title>Simon Willison's Weblog: geodjango</title><link href="http://simonwillison.net/" rel="alternate"/><link href="http://simonwillison.net/tags/geodjango.atom" rel="self"/><id>http://simonwillison.net/</id><updated>2021-05-03T06:38:12+00:00</updated><author><name>Simon Willison</name></author><entry><title>Adding GeoDjango to an existing Django project</title><link href="https://simonwillison.net/2021/May/3/adding-geodjango-to-an-existing-django-project/#atom-tag" rel="alternate"/><published>2021-05-03T06:38:12+00:00</published><updated>2021-05-03T06:38:12+00:00</updated><id>https://simonwillison.net/2021/May/3/adding-geodjango-to-an-existing-django-project/#atom-tag</id><summary type="html">
    &lt;p&gt;Work on VIAL for &lt;a href="https://www.vaccinatethestates.com/"&gt;Vaccinate The States&lt;/a&gt; continues.&lt;/p&gt;
&lt;p&gt;I talked about &lt;a href="https://simonwillison.net/2021/Apr/26/vaccinate-the-states/"&gt;matching&lt;/a&gt; last week. I've been building more features to support figuring out if a newly detected location is already listed or not, with one of the most significant being the ability to search for locations within a radius of a specific point.&lt;/p&gt;
&lt;p&gt;I've experimented with a PostgreSQL/Django version of &lt;a href="https://til.simonwillison.net/postgresql/closest-locations-to-a-point"&gt;the classic cos/sin/radians query for this&lt;/a&gt; but if you're going to do this over a larger dataset it's worth using a proper spatial index for it - and &lt;a href="https://docs.djangoproject.com/en/3.2/ref/contrib/gis/"&gt;GeoDjango&lt;/a&gt; has provided tools for this since Django 1.0 in 2008!&lt;/p&gt;
&lt;p&gt;I have to admit that outside of a few prototypes I've never used GeoDjango extensively myself - partly I've not had the right project for it, and in the past I've also been put off by the difficulty involved in installing all of the components.&lt;/p&gt;
&lt;p&gt;That's a lot easier in 2021 than it was in 2008. But VIAL is a project in-flight, so here are some notes on what it took to get GeoDjango added to an existing Django project.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/alexmv"&gt;Alex Vandiver&lt;/a&gt; has been working with me on VIAL and helped figure out quite a few of these steps.&lt;/p&gt;
&lt;h4&gt;Activating PostgreSQL&lt;/h4&gt;
&lt;p&gt;The first step was to install the PostGIS PostgreSQL extension. This can be achieved using a Django migration:&lt;/p&gt;
&lt;pre&gt;&lt;span class="pl-k"&gt;from&lt;/span&gt; &lt;span class="pl-s1"&gt;django&lt;/span&gt;.&lt;span class="pl-s1"&gt;contrib&lt;/span&gt;.&lt;span class="pl-s1"&gt;postgres&lt;/span&gt;.&lt;span class="pl-s1"&gt;operations&lt;/span&gt; &lt;span class="pl-k"&gt;import&lt;/span&gt; &lt;span class="pl-v"&gt;CreateExtension&lt;/span&gt;
&lt;span class="pl-k"&gt;from&lt;/span&gt; &lt;span class="pl-s1"&gt;django&lt;/span&gt;.&lt;span class="pl-s1"&gt;db&lt;/span&gt; &lt;span class="pl-k"&gt;import&lt;/span&gt; &lt;span class="pl-s1"&gt;migrations&lt;/span&gt;


&lt;span class="pl-k"&gt;class&lt;/span&gt; &lt;span class="pl-v"&gt;Migration&lt;/span&gt;(&lt;span class="pl-s1"&gt;migrations&lt;/span&gt;.&lt;span class="pl-v"&gt;Migration&lt;/span&gt;):

    &lt;span class="pl-s1"&gt;dependencies&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; [
        (&lt;span class="pl-s"&gt;"my_app"&lt;/span&gt;, &lt;span class="pl-s"&gt;"0108_previous-migration"&lt;/span&gt;),
    ]

    &lt;span class="pl-s1"&gt;operations&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; [
        &lt;span class="pl-v"&gt;CreateExtension&lt;/span&gt;(&lt;span class="pl-s"&gt;"postgis"&lt;/span&gt;),
    ]&lt;/pre&gt;
&lt;p&gt;Most good PostgreSQL hosting already makes this extension available - in our case we are using Google Cloud SQL which &lt;a href="https://cloud.google.com/sql/docs/postgres/extensions#postgresql-extensions-supported-by-cloud-sql"&gt;supports various extensions&lt;/a&gt;, including PostGIS. I use &lt;a href="https://postgresapp.com/"&gt;Postgres.app&lt;/a&gt; for my personal development environment which bundles PostGIS too.&lt;/p&gt;
&lt;p&gt;So far, so painless!&lt;/p&gt;
&lt;h4&gt;System packages needed by GeoDjango&lt;/h4&gt;
&lt;p&gt;GeoDjango &lt;a href="https://docs.djangoproject.com/en/3.2/ref/contrib/gis/install/geolibs/"&gt;needs the GEOS, GDAL and PROJ&lt;/a&gt; system libraries. Alex added these to our Dockerfile (used for our production deployments) like so:&lt;/p&gt;
&lt;div class="highlight highlight-source-dockerfile"&gt;&lt;pre&gt;&lt;span class="pl-k"&gt;RUN&lt;/span&gt; apt-get update &amp;amp;&amp;amp; apt-get install -y \
    binutils \
    gdal-bin \
    libproj-dev \
    &amp;amp;&amp;amp; rm -rf /var/lib/apt/lists/*&lt;/pre&gt;&lt;/div&gt;
&lt;h4&gt;Adding a point field to a Django model&lt;/h4&gt;
&lt;p&gt;I already had a &lt;code&gt;Location&lt;/code&gt; model, which looked something like this:&lt;/p&gt;
&lt;pre&gt;&lt;span class="pl-k"&gt;class&lt;/span&gt; &lt;span class="pl-v"&gt;Location&lt;/span&gt;(&lt;span class="pl-s1"&gt;models&lt;/span&gt;.&lt;span class="pl-v"&gt;Model&lt;/span&gt;):
    &lt;span class="pl-s1"&gt;name&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;models&lt;/span&gt;.&lt;span class="pl-v"&gt;CharField&lt;/span&gt;()
    &lt;span class="pl-c"&gt;# ...&lt;/span&gt;
    &lt;span class="pl-s1"&gt;latitude&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;models&lt;/span&gt;.&lt;span class="pl-v"&gt;DecimalField&lt;/span&gt;(
        &lt;span class="pl-s1"&gt;max_digits&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-c1"&gt;9&lt;/span&gt;, &lt;span class="pl-s1"&gt;decimal_places&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-c1"&gt;5&lt;/span&gt;
    )
    &lt;span class="pl-s1"&gt;longitude&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;models&lt;/span&gt;.&lt;span class="pl-v"&gt;DecimalField&lt;/span&gt;(
        &lt;span class="pl-s1"&gt;max_digits&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-c1"&gt;9&lt;/span&gt;, &lt;span class="pl-s1"&gt;decimal_places&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-c1"&gt;5&lt;/span&gt;
    )&lt;/pre&gt;
&lt;p&gt;I made three changes to this class: I changed the base class to this:&lt;/p&gt;
&lt;pre&gt;&lt;span class="pl-k"&gt;from&lt;/span&gt; &lt;span class="pl-s1"&gt;django&lt;/span&gt;.&lt;span class="pl-s1"&gt;contrib&lt;/span&gt;.&lt;span class="pl-s1"&gt;gis&lt;/span&gt;.&lt;span class="pl-s1"&gt;db&lt;/span&gt; &lt;span class="pl-k"&gt;import&lt;/span&gt; &lt;span class="pl-s1"&gt;models&lt;/span&gt; &lt;span class="pl-k"&gt;as&lt;/span&gt; &lt;span class="pl-s1"&gt;gis_models&lt;/span&gt;

&lt;span class="pl-k"&gt;class&lt;/span&gt; &lt;span class="pl-v"&gt;Location&lt;/span&gt;(&lt;span class="pl-s1"&gt;gis_models&lt;/span&gt;.&lt;span class="pl-v"&gt;Model&lt;/span&gt;):
    &lt;span class="pl-c"&gt;# ...&lt;/span&gt;&lt;/pre&gt;
&lt;p&gt;I added a &lt;code&gt;point&lt;/code&gt; column:&lt;/p&gt;
&lt;pre&gt;    &lt;span class="pl-s1"&gt;point&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;gis_models&lt;/span&gt;.&lt;span class="pl-v"&gt;PointField&lt;/span&gt;(
        &lt;span class="pl-s1"&gt;blank&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-c1"&gt;True&lt;/span&gt;,
        &lt;span class="pl-s1"&gt;null&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-c1"&gt;True&lt;/span&gt;,
        &lt;span class="pl-s1"&gt;spatial_index&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-c1"&gt;True&lt;/span&gt;
    )&lt;/pre&gt;
&lt;p&gt;And I set up a custom &lt;code&gt;save()&lt;/code&gt; method to populate that &lt;code&gt;point&lt;/code&gt; field with a point representing the latitude and longitude every time the object was saved:&lt;/p&gt;
&lt;pre&gt;&lt;span class="pl-k"&gt;from&lt;/span&gt; &lt;span class="pl-s1"&gt;django&lt;/span&gt;.&lt;span class="pl-s1"&gt;contrib&lt;/span&gt;.&lt;span class="pl-s1"&gt;gis&lt;/span&gt;.&lt;span class="pl-s1"&gt;geos&lt;/span&gt; &lt;span class="pl-k"&gt;import&lt;/span&gt; &lt;span class="pl-v"&gt;Point&lt;/span&gt;

&lt;span class="pl-c"&gt;# ...&lt;/span&gt;

    &lt;span class="pl-k"&gt;def&lt;/span&gt; &lt;span class="pl-en"&gt;save&lt;/span&gt;(&lt;span class="pl-s1"&gt;self&lt;/span&gt;, &lt;span class="pl-c1"&gt;*&lt;/span&gt;&lt;span class="pl-s1"&gt;args&lt;/span&gt;, &lt;span class="pl-c1"&gt;**&lt;/span&gt;&lt;span class="pl-s1"&gt;kwargs&lt;/span&gt;):
        &lt;span class="pl-c"&gt;# Point is derived from latitude/longitude&lt;/span&gt;
        &lt;span class="pl-k"&gt;if&lt;/span&gt; &lt;span class="pl-s1"&gt;self&lt;/span&gt;.&lt;span class="pl-s1"&gt;longitude&lt;/span&gt; &lt;span class="pl-c1"&gt;and&lt;/span&gt; &lt;span class="pl-s1"&gt;self&lt;/span&gt;.&lt;span class="pl-s1"&gt;latitude&lt;/span&gt;:
            &lt;span class="pl-s1"&gt;self&lt;/span&gt;.&lt;span class="pl-s1"&gt;point&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-v"&gt;Point&lt;/span&gt;(
                &lt;span class="pl-en"&gt;float&lt;/span&gt;(&lt;span class="pl-s1"&gt;self&lt;/span&gt;.&lt;span class="pl-s1"&gt;longitude&lt;/span&gt;),
                &lt;span class="pl-en"&gt;float&lt;/span&gt;(&lt;span class="pl-s1"&gt;self&lt;/span&gt;.&lt;span class="pl-s1"&gt;latitude&lt;/span&gt;),
                &lt;span class="pl-s1"&gt;srid&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-c1"&gt;4326&lt;/span&gt;
            )
        &lt;span class="pl-k"&gt;else&lt;/span&gt;:
            &lt;span class="pl-s1"&gt;self&lt;/span&gt;.&lt;span class="pl-s1"&gt;point&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-c1"&gt;None&lt;/span&gt;
        &lt;span class="pl-en"&gt;super&lt;/span&gt;().&lt;span class="pl-en"&gt;save&lt;/span&gt;(&lt;span class="pl-c1"&gt;*&lt;/span&gt;&lt;span class="pl-s1"&gt;args&lt;/span&gt;, &lt;span class="pl-c1"&gt;**&lt;/span&gt;&lt;span class="pl-s1"&gt;kwargs&lt;/span&gt;)&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;srid=4326&lt;/code&gt; ensures the point is stored using WGS84 - the most common coordinate system for latitude and longitude values across our planet.&lt;/p&gt;
&lt;p&gt;Running &lt;code&gt;./manage.py makemigrations&lt;/code&gt; identified the new &lt;code&gt;point&lt;/code&gt; Point column and created the corresponding migration for me.&lt;/p&gt;
&lt;h4&gt;Backfilling the point column with a migration&lt;/h4&gt;
&lt;p&gt;The &lt;code&gt;.save()&lt;/code&gt; method would populate &lt;code&gt;point&lt;/code&gt; for changes going forward, but I had 40,000 records that already existed which I needed to backfill. I used this migration to do that:&lt;/p&gt;
&lt;pre&gt;&lt;span class="pl-k"&gt;from&lt;/span&gt; &lt;span class="pl-s1"&gt;django&lt;/span&gt;.&lt;span class="pl-s1"&gt;db&lt;/span&gt; &lt;span class="pl-k"&gt;import&lt;/span&gt; &lt;span class="pl-s1"&gt;migrations&lt;/span&gt;

&lt;span class="pl-k"&gt;class&lt;/span&gt; &lt;span class="pl-v"&gt;Migration&lt;/span&gt;(&lt;span class="pl-s1"&gt;migrations&lt;/span&gt;.&lt;span class="pl-v"&gt;Migration&lt;/span&gt;):

    &lt;span class="pl-s1"&gt;dependencies&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; [
        (&lt;span class="pl-s"&gt;"core"&lt;/span&gt;, &lt;span class="pl-s"&gt;"0110_location_point"&lt;/span&gt;),
    ]

    &lt;span class="pl-s1"&gt;operations&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; [
        &lt;span class="pl-s1"&gt;migrations&lt;/span&gt;.&lt;span class="pl-v"&gt;RunSQL&lt;/span&gt;(
            &lt;span class="pl-s1"&gt;sql&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-s"&gt;"""&lt;/span&gt;
&lt;span class="pl-s"&gt;            update location&lt;/span&gt;
&lt;span class="pl-s"&gt;            set point = ST_SetSRID(&lt;/span&gt;
&lt;span class="pl-s"&gt;                ST_MakePoint(&lt;/span&gt;
&lt;span class="pl-s"&gt;                    longitude, latitude&lt;/span&gt;
&lt;span class="pl-s"&gt;                ),&lt;/span&gt;
&lt;span class="pl-s"&gt;                4326&lt;/span&gt;
&lt;span class="pl-s"&gt;            );"""&lt;/span&gt;,
            &lt;span class="pl-s1"&gt;reverse_sql&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-s1"&gt;migrations&lt;/span&gt;.&lt;span class="pl-v"&gt;RunSQL&lt;/span&gt;.&lt;span class="pl-s1"&gt;noop&lt;/span&gt;,
        )
    ]&lt;/pre&gt;
&lt;h4&gt;latitude/longitude/radius queries&lt;/h4&gt;
&lt;p&gt;With the new &lt;code&gt;point&lt;/code&gt; column created and populated, here's the code I wrote to support simple latitude/longitude/radius queries:&lt;/p&gt;
&lt;pre&gt;&lt;span class="pl-k"&gt;from&lt;/span&gt; &lt;span class="pl-s1"&gt;django&lt;/span&gt;.&lt;span class="pl-s1"&gt;contrib&lt;/span&gt;.&lt;span class="pl-s1"&gt;gis&lt;/span&gt;.&lt;span class="pl-s1"&gt;geos&lt;/span&gt; &lt;span class="pl-k"&gt;import&lt;/span&gt; &lt;span class="pl-v"&gt;Point&lt;/span&gt;
&lt;span class="pl-k"&gt;from&lt;/span&gt; &lt;span class="pl-s1"&gt;django&lt;/span&gt;.&lt;span class="pl-s1"&gt;contrib&lt;/span&gt;.&lt;span class="pl-s1"&gt;gis&lt;/span&gt;.&lt;span class="pl-s1"&gt;measure&lt;/span&gt; &lt;span class="pl-k"&gt;import&lt;/span&gt; &lt;span class="pl-v"&gt;Distance&lt;/span&gt;

&lt;span class="pl-k"&gt;def&lt;/span&gt; &lt;span class="pl-en"&gt;search_locations&lt;/span&gt;(&lt;span class="pl-s1"&gt;request&lt;/span&gt;):
    &lt;span class="pl-s1"&gt;qs&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-v"&gt;Location&lt;/span&gt;.&lt;span class="pl-s1"&gt;objects&lt;/span&gt;.&lt;span class="pl-en"&gt;filter&lt;/span&gt;(&lt;span class="pl-s1"&gt;soft_deleted&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-c1"&gt;False&lt;/span&gt;)
    &lt;span class="pl-s1"&gt;latitude&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;request&lt;/span&gt;.&lt;span class="pl-v"&gt;GET&lt;/span&gt;.&lt;span class="pl-en"&gt;get&lt;/span&gt;(&lt;span class="pl-s"&gt;"latitude"&lt;/span&gt;)
    &lt;span class="pl-s1"&gt;longitude&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;request&lt;/span&gt;.&lt;span class="pl-v"&gt;GET&lt;/span&gt;.&lt;span class="pl-en"&gt;get&lt;/span&gt;(&lt;span class="pl-s"&gt;"longitude"&lt;/span&gt;)
    &lt;span class="pl-s1"&gt;radius&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;request&lt;/span&gt;.&lt;span class="pl-v"&gt;GET&lt;/span&gt;.&lt;span class="pl-en"&gt;get&lt;/span&gt;(&lt;span class="pl-s"&gt;"radius"&lt;/span&gt;)
    &lt;span class="pl-k"&gt;if&lt;/span&gt; &lt;span class="pl-s1"&gt;latitude&lt;/span&gt; &lt;span class="pl-c1"&gt;and&lt;/span&gt; &lt;span class="pl-s1"&gt;longitude&lt;/span&gt; &lt;span class="pl-c1"&gt;and&lt;/span&gt; &lt;span class="pl-s1"&gt;radius&lt;/span&gt;:
        &lt;span class="pl-c"&gt;# Validate latitude/longitude/radius&lt;/span&gt;
        &lt;span class="pl-k"&gt;for&lt;/span&gt; &lt;span class="pl-s1"&gt;value&lt;/span&gt; &lt;span class="pl-c1"&gt;in&lt;/span&gt; (&lt;span class="pl-s1"&gt;latitude&lt;/span&gt;, &lt;span class="pl-s1"&gt;longitude&lt;/span&gt;, &lt;span class="pl-s1"&gt;radius&lt;/span&gt;):
            &lt;span class="pl-k"&gt;try&lt;/span&gt;:
                &lt;span class="pl-en"&gt;float&lt;/span&gt;(&lt;span class="pl-s1"&gt;value&lt;/span&gt;)
            &lt;span class="pl-k"&gt;except&lt;/span&gt; &lt;span class="pl-v"&gt;ValueError&lt;/span&gt;:
                &lt;span class="pl-k"&gt;return&lt;/span&gt; &lt;span class="pl-v"&gt;JsonResponse&lt;/span&gt;(
                    {&lt;span class="pl-s"&gt;"error"&lt;/span&gt;: &lt;span class="pl-s"&gt;"latitude/longitude/radius should be numbers"&lt;/span&gt;}, &lt;span class="pl-s1"&gt;status&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-c1"&gt;400&lt;/span&gt;
                )
        &lt;span class="pl-s1"&gt;qs&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;qs&lt;/span&gt;.&lt;span class="pl-en"&gt;filter&lt;/span&gt;(
            &lt;span class="pl-s1"&gt;point__distance_lt&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;(
                &lt;span class="pl-v"&gt;Point&lt;/span&gt;(
                    &lt;span class="pl-en"&gt;float&lt;/span&gt;(&lt;span class="pl-s1"&gt;longitude&lt;/span&gt;),
                    &lt;span class="pl-en"&gt;float&lt;/span&gt;(&lt;span class="pl-s1"&gt;latitude&lt;/span&gt;)
                ),
                &lt;span class="pl-v"&gt;Distance&lt;/span&gt;(&lt;span class="pl-s1"&gt;m&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-en"&gt;float&lt;/span&gt;(&lt;span class="pl-s1"&gt;radius&lt;/span&gt;)),
            )
        )
    &lt;span class="pl-c"&gt;# ... return JSON for locations&lt;/span&gt;&lt;/pre&gt;
&lt;p&gt;In writing up these notes I realize that this isn't actually the best way to do this, because it fails to take advantage of the spatial index on that column! I've filed myself an issue to switch to the spatial-index-friendly &lt;a href="https://docs.djangoproject.com/en/3.1/ref/contrib/gis/geoquerysets/#dwithin"&gt;dwithin&lt;/a&gt; instead.&lt;/p&gt;
&lt;h4&gt;Getting CI to work&lt;/h4&gt;
&lt;p&gt;The hardest part of all of this turned out to be getting our CI suites to pass.&lt;/p&gt;
&lt;p&gt;We run CI in two places at the moment: GitHub Actions and Google Cloud Build (as part of our continuous deployment setup).&lt;/p&gt;
&lt;p&gt;The first error I hit was this one:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;psycopg2.errors.UndefinedFile: could not open extension control file "/usr/share/postgresql/13/extension/postgis.control": No such file or directory&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;It turns out that's what happens when your PostgreSQL server doesn't have the PostGIS extension available.&lt;/p&gt;
&lt;p&gt;Our GitHub Actions configuration started like this:&lt;/p&gt;
&lt;div class="highlight highlight-source-yaml"&gt;&lt;pre&gt;&lt;span class="pl-ent"&gt;name&lt;/span&gt;: &lt;span class="pl-s"&gt;Run tests&lt;/span&gt;

&lt;span class="pl-ent"&gt;on&lt;/span&gt;: &lt;span class="pl-s"&gt;[push]&lt;/span&gt;

&lt;span class="pl-ent"&gt;jobs&lt;/span&gt;:
  &lt;span class="pl-ent"&gt;test&lt;/span&gt;:
    &lt;span class="pl-ent"&gt;runs-on&lt;/span&gt;: &lt;span class="pl-s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="pl-ent"&gt;services&lt;/span&gt;:
      &lt;span class="pl-ent"&gt;postgres&lt;/span&gt;:
        &lt;span class="pl-ent"&gt;image&lt;/span&gt;: &lt;span class="pl-s"&gt;postgres:13&lt;/span&gt;
        &lt;span class="pl-ent"&gt;env&lt;/span&gt;:
          &lt;span class="pl-ent"&gt;POSTGRES_USER&lt;/span&gt;: &lt;span class="pl-s"&gt;postgres&lt;/span&gt;
          &lt;span class="pl-ent"&gt;POSTGRES_PASSWORD&lt;/span&gt;: &lt;span class="pl-s"&gt;postgres&lt;/span&gt;
          &lt;span class="pl-ent"&gt;POSTGRES_DB&lt;/span&gt;: &lt;span class="pl-s"&gt;vaccinate&lt;/span&gt;
        &lt;span class="pl-ent"&gt;options&lt;/span&gt;:
          &lt;span class="pl-s"&gt;--health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5&lt;/span&gt;
        &lt;span class="pl-ent"&gt;ports&lt;/span&gt;:
        - &lt;span class="pl-c1"&gt;5432:5432&lt;/span&gt;
    &lt;span class="pl-ent"&gt;steps&lt;/span&gt;:&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The &lt;code&gt;postgres:13&lt;/code&gt; image doesn't have PostGIS. Swapping that out for &lt;code&gt;postgis/postgis:13-3.1&lt;/code&gt; fixed that (using &lt;a href="https://registry.hub.docker.com/r/postgis/postgis/"&gt;this image&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;Our Cloud Build configuration included this:&lt;/p&gt;
&lt;div class="highlight highlight-source-yaml"&gt;&lt;pre&gt;  &lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; Start up a postgres for tests&lt;/span&gt;
  - &lt;span class="pl-ent"&gt;id&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;start postgres&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
    &lt;span class="pl-ent"&gt;name&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;gcr.io/cloud-builders/docker&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
    &lt;span class="pl-ent"&gt;args&lt;/span&gt;:
      - &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;run&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
      - &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;-d&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
      - &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;--network=cloudbuild&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
      - &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;-e&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
      - &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;POSTGRES_HOST_AUTH_METHOD=trust&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
      - &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;--name&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
      - &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;vaccinate-db&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
      - &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;postgres&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;

  - &lt;span class="pl-ent"&gt;id&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;test image&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
    &lt;span class="pl-ent"&gt;name&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;gcr.io/cloud-builders/docker&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
    &lt;span class="pl-ent"&gt;args&lt;/span&gt;:
      - &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;run&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
      - &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;-t&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
      - &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;--network=cloudbuild&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
      - &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;-e&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
      - &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;DATABASE_URL=postgres://postgres@vaccinate-db:5432/vaccinate&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
      - &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;${_IMAGE_NAME}:latest&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
      - &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;pytest&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
      - &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;-v&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;I tried swapping out that last &lt;code&gt;postgres&lt;/code&gt; argument for &lt;code&gt;postgis/postgis:13-3.1&lt;/code&gt;, like I had with the GitHub Actions one... and it failed with this error instead:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;django.db.utils.OperationalError: could not connect to server: Connection refused&lt;/code&gt;
&lt;code&gt;Is the server running on host "vaccinate-db" (192.168.10.3) and accepting&lt;/code&gt;
&lt;code&gt;TCP/IP connections on port 5432?&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This one stumped me. Eventually Alex figured out the problem: the extra extension meant the PostgreSQL was taking slightly longer to start - something that was covered in our GitHub Actions configuration by the &lt;code&gt;pg_isready&lt;/code&gt; line. He added this step to our Cloud Build configuration:&lt;/p&gt;
&lt;div class="highlight highlight-source-yaml"&gt;&lt;pre&gt;  - &lt;span class="pl-ent"&gt;id&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;wait for postgres&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
    &lt;span class="pl-ent"&gt;name&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;jwilder/dockerize&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
    &lt;span class="pl-ent"&gt;args&lt;/span&gt;: &lt;span class="pl-s"&gt;["dockerize", "-timeout=60s", "-wait=tcp://vaccinate-db:5432"]&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;It uses &lt;a href="https://github.com/jwilder/dockerize"&gt;jwilder/dockerize&lt;/a&gt; to wait until the database container starts accepting connections on port 5432.&lt;/p&gt;
&lt;h4&gt;Next steps&lt;/h4&gt;
&lt;p&gt;Now that we have GeoDjango I'm excited to start exploring new capabilities for our software. One thing in particular that interests me is teaching VIAL to backfill the county for a location based on its latitude and longitude - the US Census provide a shapefile of county polygons which I use with Datasette and SpatiaLite in my &lt;a href="https://github.com/simonw/us-counties-datasette"&gt;simonw/us-counties-datasette&lt;/a&gt; project, so I'm confident it would work well using PostGIS instead.&lt;/p&gt;
&lt;h4&gt;Releases this week&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/django-sql-dashboard"&gt;django-sql-dashboard&lt;/a&gt;&lt;/strong&gt;: &lt;a href="https://github.com/simonw/django-sql-dashboard/releases/tag/0.11a0"&gt;0.11a0&lt;/a&gt; - (&lt;a href="https://github.com/simonw/django-sql-dashboard/releases"&gt;22 total releases&lt;/a&gt;) - 2021-04-26
&lt;br /&gt;Django app for building dashboards using raw SQL queries&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;TIL this week&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/simonw/til/blob/main/django/migrations-runsql-noop.md"&gt;migrations.RunSQL.noop for reversible SQL migrations&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/simonw/til/blob/main/datasette/datasette-on-replit.md"&gt;Running Datasette on Replit&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/django"&gt;django&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/geodjango"&gt;geodjango&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/geospatial"&gt;geospatial&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/postgresql"&gt;postgresql&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/weeknotes"&gt;weeknotes&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/vaccinate-ca"&gt;vaccinate-ca&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="django"/><category term="geodjango"/><category term="geospatial"/><category term="postgresql"/><category term="weeknotes"/><category term="vaccinate-ca"/></entry><entry><title>Installing GeoDjango Dependencies with Homebrew</title><link href="https://simonwillison.net/2010/May/7/homebrew/#atom-tag" rel="alternate"/><published>2010-05-07T14:40:00+00:00</published><updated>2010-05-07T14:40:00+00:00</updated><id>https://simonwillison.net/2010/May/7/homebrew/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://lincolnloop.com/blog/2010/apr/30/installing-geodjango-dependencies-homebrew/"&gt;Installing GeoDjango Dependencies with Homebrew&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
brew update &amp;amp;&amp;amp; brew install postgis &amp;amp;&amp;amp; brew install gdal


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/django"&gt;django&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/geodjango"&gt;geodjango&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/homebrew"&gt;homebrew&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/macos"&gt;macos&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/postgis"&gt;postgis&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/postgresql"&gt;postgresql&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/recovered"&gt;recovered&lt;/a&gt;&lt;/p&gt;



</summary><category term="django"/><category term="geodjango"/><category term="homebrew"/><category term="macos"/><category term="postgis"/><category term="postgresql"/><category term="recovered"/></entry><entry><title>GeoDjango and the UK postcode database</title><link href="https://simonwillison.net/2009/Sep/30/geodjango/#atom-tag" rel="alternate"/><published>2009-09-30T14:25:19+00:00</published><updated>2009-09-30T14:25:19+00:00</updated><id>https://simonwillison.net/2009/Sep/30/geodjango/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://chris-lamb.co.uk/2009/09/30/geodjango-and-uk-postcode-database/"&gt;GeoDjango and the UK postcode database&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Excellent introduction to GeoDjango using the recently leaked UK postcode database. Obviously, you should only follow the steps in this tutorial using the officially licensed database, available for a mere £1,700.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/chris-lamb"&gt;chris-lamb&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/django"&gt;django&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/geodjango"&gt;geodjango&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/geospatial"&gt;geospatial&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/postcodes"&gt;postcodes&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/uk"&gt;uk&lt;/a&gt;&lt;/p&gt;



</summary><category term="chris-lamb"/><category term="django"/><category term="geodjango"/><category term="geospatial"/><category term="postcodes"/><category term="uk"/></entry><entry><title>Install Django, GeoDjango, PostgreSQL and PostGIS on OSX Leopard</title><link href="https://simonwillison.net/2009/Jul/24/geodjango/#atom-tag" rel="alternate"/><published>2009-07-24T11:47:49+00:00</published><updated>2009-07-24T11:47:49+00:00</updated><id>https://simonwillison.net/2009/Jul/24/geodjango/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://www.tokumine.com/2009/06/01/install-django-geodjango-postgresql-postgis-on-osx-leopard/"&gt;Install Django, GeoDjango, PostgreSQL and PostGIS on OSX Leopard&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
This tutorial worked perfectly for me.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/django"&gt;django&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/geodjango"&gt;geodjango&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/macos"&gt;macos&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/postgis"&gt;postgis&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/postgresql"&gt;postgresql&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;&lt;/p&gt;



</summary><category term="django"/><category term="geodjango"/><category term="macos"/><category term="postgis"/><category term="postgresql"/><category term="python"/></entry><entry><title>Represent</title><link href="https://simonwillison.net/2008/Dec/29/represent/#atom-tag" rel="alternate"/><published>2008-12-29T22:10:26+00:00</published><updated>2008-12-29T22:10:26+00:00</updated><id>https://simonwillison.net/2008/Dec/29/represent/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://open.blogs.nytimes.com/2008/12/22/represent/"&gt;Represent&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Andrei Scheinkman and Derek Willis describe how they built the NYTimes Represent feature using GeoDjango and PostGIS.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/andrei-scheinkman"&gt;andrei-scheinkman&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/derek-willis"&gt;derek-willis&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/django"&gt;django&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/geodjango"&gt;geodjango&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/geospatial"&gt;geospatial&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/new-york-times"&gt;new-york-times&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/postgis"&gt;postgis&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/postgresql"&gt;postgresql&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;&lt;/p&gt;



</summary><category term="andrei-scheinkman"/><category term="derek-willis"/><category term="django"/><category term="geodjango"/><category term="geospatial"/><category term="new-york-times"/><category term="postgis"/><category term="postgresql"/><category term="python"/></entry><entry><title>Represent and GeoDjango</title><link href="https://simonwillison.net/2008/Dec/20/represent/#atom-tag" rel="alternate"/><published>2008-12-20T21:07:55+00:00</published><updated>2008-12-20T21:07:55+00:00</updated><id>https://simonwillison.net/2008/Dec/20/represent/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://blog.thescoop.org/archives/2008/12/19/represent-and-geodjango/"&gt;Represent and GeoDjango&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
The NYTimes new Represent application is built on GeoDjango.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/derek-willis"&gt;derek-willis&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/django"&gt;django&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/geodjango"&gt;geodjango&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/new-york-times"&gt;new-york-times&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/represent"&gt;represent&lt;/a&gt;&lt;/p&gt;



</summary><category term="derek-willis"/><category term="django"/><category term="geodjango"/><category term="new-york-times"/><category term="python"/><category term="represent"/></entry><entry><title>Django 1.0 alpha 2 release notes</title><link href="https://simonwillison.net/2008/Aug/8/django/#atom-tag" rel="alternate"/><published>2008-08-08T23:57:21+00:00</published><updated>2008-08-08T23:57:21+00:00</updated><id>https://simonwillison.net/2008/Aug/8/django/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://www.djangoproject.com/documentation/release_notes_1.0_alpha_2/"&gt;Django 1.0 alpha 2 release notes&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
The last preview release before the 1.0 beta. Big new features are GeoDjango, pluggable file storage (which went in earlier today) and Jython compatibility. The beta is scheduled for August 14th.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="http://www.djangoproject.com/weblog/2008/aug/08/10-alpha-2/"&gt;Django 1.0 alpha 2 released!&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/alpha"&gt;alpha&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/django"&gt;django&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/geodjango"&gt;geodjango&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/jython"&gt;jython&lt;/a&gt;&lt;/p&gt;



</summary><category term="alpha"/><category term="django"/><category term="geodjango"/><category term="jython"/></entry><entry><title>GeoDjango Documentation</title><link href="https://simonwillison.net/2008/Aug/5/geodjango/#atom-tag" rel="alternate"/><published>2008-08-05T23:06:28+00:00</published><updated>2008-08-05T23:06:28+00:00</updated><id>https://simonwillison.net/2008/Aug/5/geodjango/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://geodjango.org/docs/"&gt;GeoDjango Documentation&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Merged to Django trunk a few hours ago. The tutorial isn’t there yet, but the rest of the docs are worth exploring.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/django"&gt;django&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/documentation"&gt;documentation&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/geodjango"&gt;geodjango&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;&lt;/p&gt;



</summary><category term="django"/><category term="documentation"/><category term="geodjango"/><category term="python"/></entry></feed>