@khjrtbrg

Skip to main content

Simple Map Project with Django

We're a little less than two months away from the end of the classroom portion of Ada, and the deadline to decide on our Capstone projects is coming up quickly. Since I'd like to make an app that uses maps in some kind of way, I used our Capstone Spike week to explore basic mapping with Leaflet and took Django for a test ride.

This guide will outline the project I worked on this week: a simple, map-based website. Users of the site can submit a form to add new locations, which are then added to a Leaflet map displayed on the homepage. For reference, I used Python version 2.7.9 with Django version 1.7.

Speak Django To Me

Why Django? I'd heard that Django and Python handle data well, particularly for visualizations and maps. Since I spent most of my time this week wrestling with Django, I didn't get to explore these features very deeply. From glancing through documentation, Django does appear to have some neat features for handling GIS data, but I can't speak to how it compares to Rails on that front. My impression is that Python/Django is more established and better documented for geospatial data, but Ruby/Rails can also handle those kinds of tasks elegantly.

So how is Django different from Rails? Basically, Django is much more lightweight and focused on efficiency. A Django project is essentially a collection of module-like mini-apps, peppered with core Django methods, enabled as needed. Where a brand new Rails app's skeleton file structure provies a structure for almost any kind of basic functionality, a new Django project forces you to select exactly what modules and methods you want to use, when you want to use them.

One of the major differences between Rails' and Django's structure is the level of segmentation. While Rails is a single app that can incorporate gems and third party code, Django is a project that incorporates many apps and packets. Each app in Django is completely separate and can be reused in other projects. Recyclable apps mean your code will be more dry, but it's a strange idea to wrap your head around if you're comfortable with Rails' structure.

Django is also DRY, to a degree that can make it confusing to read at first. While most Rails people avoid scaffolds-esque solutions, Django not only encourages it, it lets you use pre-written methods for menial tasks like displaying a list of objects or the details of an object (why write essentially the same method more than once, or even at all?). It's very much a utilitarian framework, focused on fast results and automating anything tedious, but it doesn't feel as elegant as Rails. Everything in Rails is laid out in front of you and your app's code weaves in and out of the framework as needed. Django hides its core functionality and forces efficiency over clarity (at least in my opinion, as a total beginner).

Before we get started, there's one more key difference to keep in mind: what you would call a "controller" in Rails is called a "view" in Django, and what you would call a "view" in Rails is called a "template" in Django. That difference takes some getting used to, and from a Rails perspective, it feels a little like abandoning convention.

Step 1: Set Up Django

In order to use Django, you'll need to do some basic installation and configuration of your environment. Follow this tutorial to set up your virtual environment, Python, and Django.

Warning: Skipping setting up a virtual environment lead to hours of headaches for me. Just do it.

Once you have an environment up and running, take some time to work through this tutorial from Django Project to get yourself oriented. If you want to use PostgreSQL instead of SQLite, check out this guide for getting that set up. Note that when you set up anything other than SQLite for your database, you'll need to add your database name, username, and password. In order to not share your access information with all of Github, you'll want to use environmental variables, either with a package or global on your environment.

Environmental Variables in Django

To use global environment variables, use os.environ['KEY_HERE']. For example, your database section of your settings.py file might look like this:

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql_psycopg2',
        'NAME': os.environ['DJANGO_DB_NAME'],
        'USER': os.environ['DJANGO_DB_USER'],
        'PASSWORD': os.environ['DJANGO_DB_PASSWORD'],
        'HOST': 'localhost',
        'PORT': '5432',
}

Then just set your global variables:

$ export DJANGO_DB_NAME=databasenamestring
$ echo $DJANGO_DB_NAME
=> databasenamestring

If you'd prefer to use a package (Python's equivalent of a Ruby gem), use django-dotenv:

$ pip install django-dotenv

Then add the following to your manage.py file:

import dotenv
dotenv.read_dotenv()

Create your .env file and add it to your .gitignore. Add your environment variables to your .env as so:

DJANGO_DB_NAME='databasenamestring'
DJANGO_DB_USER='databaseuserstring'
DJANGO_DB_PASSWORD='databasepasswordstring'

Once those are in place, you can refer to these variables like so:

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql_psycopg2',
        'NAME': os.environ.get('DJANGO_DB_NAME'),
        'USER': os.environ.get('DJANGO_DB_USER'),
        'PASSWORD': os.environ.get('DJANGO_DB_PASSWORD'),
        'HOST': 'localhost',
        'PORT': '5432',
}

If this is your first time using Django, finishing those tutorials before moving on will make this guide easier to follow along with.

Step 2: Setting Up the App

In order to get started, start your virtual environment, generate a project called "AwesomeMap", and configure your preferred database. Once that's completed, you'll need to generate a new app for your project. I called my app locations, but feel free to call it whatever, non-reserved word you like:

$ python manage.py startapp locations

This should have generated a new directory in your project called locations. In order for your Django project to know that this app exists and should be included in your project, add 'locations' to your INSTALLED_APPS tuple in settings.py.

Next, let's create a model for this new app. For this app, we'll need a model for each location. Replace your locations/models.py file's contents with the following code:

from django.db import models

class Location(models.Model):
    location_name = models.CharField(max_length=200)
    street_address = models.CharField(max_length=200)
    state = models.CharField(max_length=2)
    latitude = models.FloatField()
    longitude = models.FloatField()
    city = models.CharField(max_length=200)

To update your database, generate a migration file and run the migration:

$ python manage.py makemigrations locations
$ python manage.py migrate

Next, update your project's AwesomeMap/urls.py file to:

from django.conf.urls import patterns, include, url
from django.contrib import admin

urlpatterns = patterns('',
    url(r'^', include('locations.urls', namespace="locations")),
    url(r'^admin/', include(admin.site.urls)),
)

Notice that we're directing anything other than /admin to our new app. Now, update your app's urls.py file to:

from django.conf.urls import patterns, url

from locations import views

urlpatterns = patterns('',
    url(r'^$', views.index, name='index'),
    url(r'^new$', views.new, name='new'),
    url(r'^create$', views.create, name='create')
)

Next, update your locations/views.py file to the following:

from django.shortcuts import render
from django.http import HttpResponseRedirect
from django.core.urlresolvers import reverse
from locations.form import LocationForm

from locations.models import Location

def index(request):
    locations = Location.objects.all()
    context = { 'locations': locations }
    return render(request, "locations/index.html", context)

def new(request):
    form = LocationForm()
    context = { 'form': form }
    return render(request, "locations/new.html", context)

def create(request):
    if request.method == 'POST':
        form = LocationForm(request.POST)
        form.set_lat_long()

        if form.is_valid():
            print form
            form.save()
            return HttpResponseRedirect(reverse('locations:index'))
    else:
        form = LocationForm()
    return render(request, 'locations/new.html', {'form': form})

Remember, these are the methods that would normally go in a Rails controller. In order for these to work, we'll also need some templates (called "views" in Rails):

$ mkdir locations/templates
$ mkdir locations/templates/locations
$ touch index.html
$ touch new.html

Finally, we'll also need the form referred to on line 4 of locations/views.py. Like Rails, Django gives you some nifty forms generators. You have less flexibility and controll with them, but also less opportunities to screw anything up.

To make your form, create locations/form.py:

from locations.models import Location
from django.forms import ModelForm
import requests
import os


class LocationForm(ModelForm):
    class Meta:
        model = Location
        fields = ['location_name', 'street_address', 'city', 'state', 'latitude', 'longitude']

    def set_lat_long(self):
        data = self.data.copy()

        key = os.environ.get('GOOGLE_API')
        address = "{0}, {1}, {2}".format(data['street_address'], data['city'], data['state'])
        payload = { 'address': address, 'key': key }
        r = requests.get('https://maps.googleapis.com/maps/api/geocode/json', params=payload).json()

        data['latitude'] = r['results'][0]['geometry']['location']['lat']
        data['longitude'] = r['results'][0]['geometry']['location']['lng']

        self.data = data

The first part of the LocationForm class specifies which fields should be available to the form from the specified model. The second part is a method for using Google's Geocodding API to generate the latitude and longitude. In order to use that API, you'll need to register with Google's Developers Console:

  • Register an account
  • Create a new project
  • Enable the Geocodding API for that project
  • Generate your access key
  • Add the key to your .env file with a key of GOOGLE_API

At this point you could fire up your server with $ python manage.py runserver, but it won't be very exciting yet. Our templates are still blank! Update them with the following code:

## locationsindex.html
{% extends "base.html" %}
{% load staticfiles %}

{% block title %}Home{% endblock %}

{% block content %}
    <script src="http://cdn.leafletjs.com/leaflet-0.7.3/leaflet.js"></script>

    <div id="map"></div>

    <script>
      function screenWidthZoom(){
        var width = document.documentElement.clientWidth;
        if (width < 768) {
          return 3;
        } else {
          return 4;
        }
      }

      var map = L.map('map', {
        scrollWheelZoom: false,
        center: [39.2855955, -98.0330969],
        scrollWheelZoom: false,
        zoom: screenWidthZoom()
      });

      L.tileLayer('http://server.arcgisonline.com/ArcGIS/rest/services/Canvas/World_Light_Gray_Base/MapServer/tile/{z}/{y}/{x}', {
        attribution: 'Tiles © Esri — Esri, DeLorme, NAVTEQ',
        maxZoom: 16
      }).addTo(map);

      {% for location in locations %}
          var marker = L.marker([{{ location.latitude }}, {{ location.longitude }}]).addTo(map);
          marker.bindPopup("<b>{{ location.location_name }}</b><br>{{ location.street_address }}");
      {% endfor %}

      window.addEventListener('resize', function(event){
        map.setZoom(screenWidthZoom());
      });
    </script>
{% endblock %}
## locations/new.html
{% extends "base.html" %}
{% load widget_tweaks %}

{% block title %}New Location{% endblock %}

{% block content %}
    <div class="container">
        <div class="row">
            <div class="col-lg-12 text-center">
                <h1>Create New Location</h1>

                <form action="{% url 'locations:create' %}" method="post">
                    {% csrf_token %}
                    
                    <div class="form-group">
                        {{ form.location_name|add_class:"form-control"|attr:"placeholder:Location Name" }}
                    </div>

                    <div class="form-group">
                        {{ form.street_address|add_class:"form-control"|attr:"placeholder:Street Address" }}
                    </div>

                    <div class="form-group">
                        {{ form.city|add_class:"form-control"|attr:"placeholder:City" }}
                    </div>

                    <div class="form-group">
                        {{ form.state|add_class:"form-control"|attr:"placeholder:State" }}
                    </div>

                    <input type="submit" value="Save Location" class="btn btn-default" />
                </form>
            </div>
        </div>
    </div>
{% endblock %}

They look a teensy bit like an ERB file, right? It turns out they're pretty similar: where you would use <%= "foo" %> in Rails, you'd use {{ "foo" }} in Django; <% "foo" %> in Rails becomes {% "foo" %} in Django.

You'll need to do two more things before moving on:

$ mkdir AwesomeMap/templates
$ touch AwesomeMap/templates/base.html
$ pip install django-widget-tweaks

Add the following to base.html:

{% load staticfiles %}

<!DOCTYPE html>
<html lang="en">
<head>
    <title>{% block title %}{% endblock %} | Location-O-Matic</title>
    <link href="{% static 'css/bootstrap.min.css' %}" rel="stylesheet" media="screen">
    <link rel="stylesheet" href="http://cdn.leafletjs.com/leaflet-0.7.3/leaflet.css">
    <link href="{% static 'css/style.css' %}" rel="stylesheet" type="text/css">
    <meta name="viewport" content="width=device-width, initial-scale=1">
</head>

<body>
        <!-- Navigation -->
    <nav class="navbar navbar-inverse navbar-fixed-top" role="navigation">
        <div class="container">
            <!-- Brand and toggle get grouped for better mobile display -->
            <div class="navbar-header">
                <button type="button" class="navbar-toggle" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1">
                    <span class="sr-only">Toggle navigation</span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                </button>
                <a class="navbar-brand" href="/">Location-O-Matic</a>
            </div>
            <!-- Collect the nav links, forms, and other content for toggling -->
            <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
                <ul class="nav navbar-nav">
                    <li>
                        <a href="{% url 'locations:new' %}">Add Location</a>
                    </li>
                </ul>
            </div>
            <!-- /.navbar-collapse -->
        </div>
        <!-- /.container -->
    </nav>

    <!-- Page Content -->
    {% block content %}{% endblock %}

    <!-- Javascript Files -->
    <script src="{% static 'js/jquery.js' %}" type="text/javascript"></script>
    <script src="{% static 'js/bootstrap.min.js' %}" type="text/javascript"></script>
</body>
</html>

Much like Rails' application.html.erb file, Django's templating system lets you set a base layout file called base.html. You might have noticed the Bootstrap references. We'll have to do a little more to make that work.

Step 3: Styling Your App

Create directories in your project's root for static files:

$ mkdir static/
$ mkdir static/css/
$ touch static/css/style.css
$ mkdir static/js/

Download the latest version of Boostrap and put the minified CSS and Javascript files in their appropriate directories. While you're at it, update add the following to your style.css file:

body {
  padding-top: 50px;
}

.navbar-inverse {
  background-color: #18bc9c;
  border-color: transparent;
}

.navbar.navbar-inverse a,
.navbar ul.nav.navbar-nav a {
  color: white;
}

.navbar ul.nav.navbar-nav a {
  font-weight: bold;
}

.navbar.navbar-inverse a:hover,
.navbar ul.nav.navbar-nav a:hover,
.navbar.navbar-inverse a:focus,
.navbar ul.nav.navbar-nav a:focus {
  color: #2c3e50;
}

#map {
  width: 100%;
  height: 620px;
}

.leaflet-popup-content {
  text-align: center;
}

.leaflet-popup-content-wrapper {
  border-radius: 3px;
  color: white;
}

.leaflet-popup .leaflet-popup-content-wrapper,
.leaflet-popup .leaflet-popup-tip {
  background-color: #2c3e50;
}

@media (max-width: 768px) {
  .navbar-inverse .navbar-collapse,
  .navbar-inverse .navbar-form {
    background-color: #2c3e50;
    border-color: transparent;
    text-align: center;
  }

  .navbar-inverse .navbar-toggle {
    border-color: transparent;
  }

  .navbar-inverse .navbar-toggle:hover,
  .navbar-inverse .navbar-toggle:focus {
    background-color: #2c3e50;
  }

  .navbar.navbar-inverse a:hover,
  .navbar ul.nav.navbar-nav a:hover,
  .navbar.navbar-inverse a:focus,
  .navbar ul.nav.navbar-nav a:focus {
    color: #18bc9c;
  }
}

In order for Django to serve static files, you'll need to update your settings.py file to tell Django how to find them. This guide will walk you through setting this up properly.

Once these are in place, your project should be able to run locally and should be looking snazzy.

Step 4: Setting up Leaflet Map

The above code already set up your Leaflet Map and its interactions with your app, but let's go over it in a little more detail.

Leaflet requires a few a CSS file and a Javascript file. The CSS is required on base.html, whereas the Javascript is only loaded on index.html (why bother globally loading Javascript when you only need it on one page?). Leaflet also requires a div with an id of "map" to load itself in, which we've added on your index.html file.

The code for displaying the map and adding markers follows this flow:

  • Set up Leaflet map
  • Add tiles to the map
  • Loop through all the locations stored in the database and add them to the map
  • Listen for changes to the browser's screen width and adjust the map's zoom level if necessary

You'll notice that the initial zoom level of the map is also determined by screenWidthZoom(), letting the map load relatively seamlessly if you're viewing the site on mobile or if you're on a larger screen.

The final touch for the map was to style the marker pop-ups to be more consistent with the rest of the site. I made them a little less rounded on the corners and a nice shade of navy blue:

.leaflet-popup-content {
  text-align: center;
}

.leaflet-popup-content-wrapper {
  border-radius: 3px;
  color: white;
}

.leaflet-popup .leaflet-popup-content-wrapper,
.leaflet-popup .leaflet-popup-tip {
  background-color: #2c3e50;
}

Step 5: Deploying On Heroku

Assuming you've been committing with git along the way, deploying to Heroku is fairly straight forward, but you'll need to do some more configuration. Follow the guide Heroku created for deployment. After you deploy, remember to also run:

heroku run python manage.py migrate

I had some difficulties with dj_database_url when running the project locally, but simply commenting out those lines when working locally seemed to do the trick.

Final Thoughts

I'm glad to have gotten time to play around with Django. I still strongly prefer Rails, but I'm curious about what advantages Python/Django have on the geospatial front. I suspect the main advantages might matter more to geography professionals, and Rails might be sufficient for basic mapping with a service like Leaflet or Google Maps.

Check out the code on GitHub or check out the live finished project.