As a Django developer, you might be tasked with a project that requires working with and manipulating spatial data. Spatial data refers to information that includes geographical or location-based attributes, representing the physical location and shape of objects on the Earth's surface or in three-dimensional space. Django provides a way to do that using PostGIS.
PostGIS is an extension of PostgreSQL that adds support for spatial data. It enables you to store, query, and manipulate geographic and geometric data in your database.
This tutorial will guide you on how to work with spatial data in Django using the PostGIS extension. You will learn how to render it on the web using the Django admin. Finally, you will learn how to run the Django and PostGIS applications together on Docker.
You can access the complete app on GitHub.
Prerequisites
You should have the following for this tutorial:
- PostgreSQL installed locally.
- A basic understanding of Django.
- Docker Engine (optional).
Local PostgreSQL setup
Django doesn't have the capability to create a PostgreSQL database and user automatically. Hence, we'll set up a local database and make note of the details, which will be used in the Django configuration for connecting to the database. Once this is done, Django will establish an automatic connection to it.
To interact with the database engine, we'll use a command-line tool called "psql." To start an interactive session with your local database, run the following command, assuming you have the necessary permissions (such as sudo access):
sudo -u postgres psql
After entering the command above, you'll enter the psql
session, where you can execute SQL commands to create a new user and database.
To create a new user, execute the following SQL command:
CREATE USER db_user WITH PASSWORD 'secure-password';
Next, create a new database and assign the newly created user as the owner:
CREATE DATABASE locations_db OWNER db_user;
Finally, you'll need to grant all privileges to your database, allowing your Django application to modify tables in the database:
GRANT ALL PRIVILEGES ON DATABASE locations_db TO db_user;
With these steps completed, your PostgreSQL database is set up locally, and Django will be able to connect to it automatically using the provided configuration.
Configure PostGIS for spatial data in Django
The configuration will start with Django and Psycopg2 (a PostgreSQL database adapter for Python) installation. To do this, open your terminal or command prompt and run the following.
pip install django==4.2.1 psycopg2-binary==2.9.5
Now we will create a new Django project to work on. Open a terminal and navigate to the desired directory where you want to create a Django project then run the following command:
django-admin startproject project
Now cd
into project
and create a Django app:
cd project
python manage.py startapp locations
To set up the app, open the project's settings.py
file located in the main project directory (/project). Then, add the newly created app to the INSTALLED_APPS
list. In the INSTALLED_APPS
section, add the following line:
'locations',
To configure the database settings, open the settings.py
file located in the project
directory. In the DATABASES
section, update the configuration to make Django aware of the database (PostGIS) you want to use and other necessary details.
DATABASES = {
'default': {
'ENGINE': 'django.contrib.gis.db.backends.postgis',
'NAME': 'locations_db',
'USER': 'db_user',
'PASSWORD': 'secure-password',
'HOST': 'localhost',
'PORT': '5432',
}
}
You need to install the PostGIS extension for PostgreSQL to use it on your database. This step may vary depending on your operating system. Refer to the PostGIS documentation for installation instructions specific to your platform.
For Ubuntu, run the following command:
sudo apt install postgis
Now enable PostGIS in the database in your PostgreSQL database by running the following commands:
sudo -u postgres psql
CREATE EXTENSION IF NOT EXISTS postgis;
ALTER ROLE db_user SUPERUSER; # Django requires the database user is a superuser
Creating models for spatial objects
Django provides integration with PostGIS through the django.contrib.gis
module. You can define spatial fields in your models, such as PointField
, LineStringField
, and PolygonField
. You can also perform spatial queries using Django's spatial lookup functions.
We will work with PointField
, LineStringField
, and PolygonField
in this tutorial, but you can check out the spatial field types available and use them as required.
PointField
: Represents a single point in space with X and Y coordinates (latitude and longitude or projected coordinates). Common use cases include storing the locations of landmarks, addresses, or any other single geographical point of interest.
LineStringField
: Represents a series of connected points, forming a continuous line or curve. It is used to store and work with linear features, including roads, rivers, trails, or any other connected path on the Earth's surface.
PolygonField
: Represents an enclosed area defined by a series of connected points, forming a closed loop. It is used to store and work with polygons that represent areas, such as countries, states, buildings, lakes, or any other closed geographic region.
Now we will create the models. Open the models.py
file in the project/locations
directory and paste the following:
from django.contrib.gis.db import models
from django.contrib.gis.geos import Point, LineString, Polygon
class LonLat(models.Model):
name = models.CharField(max_length=100)
location = models.PointField()
def __str__(self):
return self.name
class Road(models.Model):
name = models.CharField(max_length=100)
path = models.LineStringField()
def __str__(self):
return self.name
class Country(models.Model):
name = models.CharField(max_length=100)
area = models.PolygonField()
def __str__(self):
return self.name
This code defines three Django models: LonLat
, Road
, and Country
. Each model represents a different geographical entity. The LonLat
model contains a name and location represented by a point with latitude and longitude coordinates. The Road
model has a name and a path represented by a LineString
, describing a road's route. The Country
model includes a name and an area represented by a Polygon, defining the boundary of a country. The __str__
methods are implemented in each model to provide a readable representation of the object when printed.
Now we need to apply the new changes to the database with the migration commands.
python manage.py makemigrations
python manage.py migrate
Spatial data in the admin interface
To use the PostGIS in Django admin, 'django.contrib.gis'
must be in your INSTALLED_APPS
.
Since we are using the Django admin, we need to create a superuser. Create a superuser by navigating to the root directory of your Django project, where the manage.py
file is located, and running the following command. Fill in the requested details.
python manage.py createsuperuser
Next, to make the new model visible on the Django admin, paste the following code in locations/admin.py
:
from django.contrib import admin
from .models import LonLat, Road, Country
from django.contrib.gis.admin import OSMGeoAdmin
@admin.register(LonLat)
class LonLatAdmin(OSMGeoAdmin):
list_display = ('name', 'location')
@admin.register(Road)
class RoadsAdmin(OSMGeoAdmin):
list_display = ('name', 'path')
@admin.register(Country)
class CountriesAdmin(OSMGeoAdmin):
list_display = ('name', 'area')
You can run the server and add data using the Django admin. In the example illustrated below, I used a Polygon to highlight Nigeria on the map.
When you save, the data will be represented as you see in the image below. This data can be extracted using an API endpoint, and the boundaries will appear exactly as shown in the image above.
How to run Django with the PostGIS app on Docker
In this section, we'll delve into the process of running a Django application with a PostGIS database using Docker. Docker offers a convenient way to package our application and its dependencies into containers, simplifying deployment and management. By the end of this guide, you'll have a Dockerized Django application up and running, utilizing PostgreSQL as the database backend.
Creating the Dockerfile
The Dockerfile
is a crucial component when building a Docker image, because it defines the properties. By providing step-by-step instructions within the Dockerfile
, we can specify the operating system, required environment variables, and dependencies essential for the image. Each instruction is executed in sequence, generating an intermediate image that serves as the foundation for subsequent steps.
To get started, create a file named Dockerfile
at the root of your project and include the following configuration:
FROM python:3.9.2
ENV PYTHONUNBUFFERED 1
RUN apt-get update \\
&& apt-get -y install netcat gcc postgresql \\
&& apt-get clean
RUN apt-get update \\
&& apt-get install -y binutils libproj-dev gdal-bin python-gdal python3-gdal
RUN pip install django==4.2.1 psycopg2-binary==2.9.5
RUN mkdir /project
WORKDIR /project
COPY . .
The given Dockerfile is used to build a Docker image based on Python 3.9.2. It sets an environment variable PYTHONUNBUFFERED
to 1, installs necessary packages, including psycopg2-binary and Django, and copies the project files into the container's /project
directory.
Defining services in Docker Compose
Docker Compose is a tool used for defining and managing multi-container Docker applications. It allows us to specify a set of services and their dependencies in a YAML file (docker-compose.yml
) and then run them with a single command. In our case, we require two services, one for Django and another for PostGIS.
Let's define the services that our application will utilize in the docker-compose.yml
file. Create a new file named docker-compose.yml
in the root of your project and add the following:
version: '3.9'
services:
db:
image: kartoza/postgis:12.0
ports:
- 5432:5432 # Maps the container's PostgreSQL port to the host's port
volumes:
- postgres_data:/var/lib/postgresql # Mounts a volume to persist PostgreSQL data
environment:
POSTGRES_DB: locations_db
POSTGRES_USER: db_user
POSTGRES_PASSWORD: secure-password
web:
build: .
command: python manage.py runserver 0.0.0.0:8000
volumes:
- .:/project
ports:
- "8000:8000"
depends_on:
- db
volumes:
postgres_data: # Defines a named volume for persisting PostgreSQL data
The provided Docker Compose configuration defines two services, db
and web
. The db
service utilizes the kartoza/postgis:12.0
image to set up a PostGIS-enabled PostgreSQL database with specified environment variables and data persistence. The web
service builds a Docker image from the current directory, runs the Django development server, and establishes a dependency on the db
service. This configuration enables running a Django application with PostGIS support within a Docker environment.
To connect to the database service we just created (db
), we need to update the settings.py
file:
DATABASES = {
'default': {
...,
'HOST': 'db',
'PORT': '5432',
}
}
Building and running the application
Before running the application, we need to add host 0.0.0.0
to ALLOWED_HOSTS
in your settings.py
file. We need to do this because Docker runs on that host, and it is not allowed by default in Django.
ALLOWED_HOSTS = ['0.0.0.0']
You can build and run the application with a single command:
docker-compose up --build
This command builds the Docker images for our application and starts the containers defined in our docker-compose.yml
file. You should see log output from both the db
and web
services in your terminal.
Docker has created a new database for us, so we will need to run the migration commands. The following are the commands you will use to apply migrations:
docker compose run web sh -c "python manage.py makemigrations"
docker compose run web sh -c "python manage.py migrate"
You can now add data as before, but this time using Docker.
Building with PostGIS on your own
In this tutorial, we have learned how to work with spatial data in Django web applications using the PostGIS extension. You learned how to the maps and use the features we built. And finally, we saw how to run the whole application on Docker.
You can build upon this by building a frontend that can render maps with PostGIS data from Django. If you'd like to get more articles like this one to level up your Django skills delivered straight to your inbox, sign up for the Honeybadger newsletter.