Code

Using Geolocation in a Divio application

ContributionContribution
23. April, 2020

In this guest post, Vuyisile Ndlovu explores building a Divio web application that uses Geolocation.

In this article, our contributor Vuyisile Ndlovu shows how to build and deploy a Divio application that makes use of geolocation, functionality that allows a website to determine where in the world a user is. This example uses Django, but the principles will apply to any stack.

This article shows how to:

Along the way we'll point out some good-practice ways of working with projects of this kind.

Create the project and install packages

Create a new Django project in the Control Panel, and set it up it locally (divio project setup...).

Install Requests

To make API calls and get responses, we’ll use the well-known Python Requests library.

Add requests to the project's requirements.in file. It is always recommended to pin dependencies; at the time of writing, the latest version of Requests is 2.23.0:

# <INSTALLED_ADDONS>  # Warning: text inside the INSTALLED_ADDONS tags is auto-generated. Manual changes will be overwritten.
[...]
# </INSTALLED_ADDONS>

requests==2.23.0

The next time this project is built, requests will be collected and installed. Make sure to list packages outside the automatically generated # <INSTALLED_ADDONS> section.

Create a new Django application

Create a new Django application by running the following in your terminal:

docker-compose run --rm web python manage.py startapp iplocation

This starts the web container and runs python manage.py startapp inside it to create a new app called iplocation.> (The --rm flag means: remove the container when exiting.)

Add the newly created app to settings.py:

INSTALLED_APPS.extend([
    'iplocation',
])

Create a view

The new applications needs a view function to process the geolocation data. Edit the views.pyfile in the iplocation app and create a new view:

from django.shortcuts import render
import requests

def home(request):
    response = requests.get('http://ip-api.com/json/')
    geodata = response.json()
    return render(request, 'home.html', {
        'ip': geodata['query'],
        'country': geodata['country']
    })

This view uses requests to perform a GET request to http://ip-api.com/json/. This website returns IP location data in JSON format. The response data is read into the geodata variable and looks something like this:

ip location json

The view returns the IP address and country data from geodata to the context and then renders the home.html template.

Create templates

Create two HTML templates, base.html and home.html in your project's templates directory and add the following to them:

First base.html:

<html>
    <head>
        <title>Geolocation Service</title>
        <style>
            body{
                    max-width: 600px;
                    margin: auto;
                    font-family: "Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif;
                    font-size: 1rem;
                    font-weight: 400;
                    line-height: 1.5;
                    color:#212529;
                    text-align: left;
                    background-color:#fff;
                }
                h1 {
                    text-align: center;
                }
        </style>
    </head>

    <body>
        <h1>Geolocation Service</h1>
        <hr/>
        {% block content %}

        {% endblock %}
    </body>
</html>

And then home.html:

{% extends 'base.html' %}

{% block content %}
<p>Your IP address is <strong>{{ ip }}</strong> and you are probably in <strong>{{ country }}</strong>. </p>
{% endblock %}

Configure URLs

Edit urls.py to add URL mappings for the views in the iplocation application:

from django.conf.urls import url, include
from django.urls import path

from iplocation import views
from aldryn_django.utils import i18n_patterns
import aldryn_addons.urls

urlpatterns = [
  path('', views.home, name='home'),
] + aldryn_addons.urls.patterns() + i18n_patterns(
  *aldryn_addons.urls.i18n_patterns()
)

This will display the home view you created above in views.py.

Test the project locally

Run:

docker-compose up web

in the terminal. If everything has gone to plan, you should see something similar to this in your browser at http://localhost:8000 (or http://127.0.0.1:8000):

geolocation app

The location that is displayed will depend on your IP address and approximate location. To confirm that this displays a different result when a user’s IP changes, here’s the result after I connect to a VPN:

iplocation hk

Display a map of the user’s location

To develop the geolocation project further, let's display a map showing the user’s location. For this we’ll use a mapping API. In this example, we'll use the TomTom Maps API.

Like many APIs (and unlike the one we used to obtain the geolocation data) this API requires an API key to authenticate the requests we make to it. Register on the TomTom website and obtain an API key before proceeding further.

Next, add the API key to your local environment as an environment variable. Environment variables are dynamic values that can be used by the processes or applications running on the server. One of the advantages in using them is that you can isolate specific values from your codebase. Environment variables are a good place for storing instance-specific configuration, such as Django settings and API keys that you don’t wish to hard-code into your project. For more information on setting and working with environment variables, see the Divio developer handbook.

For local development, use the .env-local file to store environment variables. Edit it and add your TomTom API key:

API_KEY=Sx7F4LkTXMxb5MdVXC8fcDrPsilYrGffp

Then add an <iframe> to display a map in the home.html template:

<iframe width="512"
height="512"
frameborder="0"
style="border:0"
src="https://api.tomtom.com/map/1/staticimage?key={{ api_key }}&zoom=4&center={{ longitude }}, {{ latitude }}&format=png&layer=basic&style=main&view=IN"
allowfullscreen>
</iframe>

An iframe is an inline frame that is commonly used to include content from another source in an HTML document. The iframe in this template has a width and height of 512px and expects three values to be passed to it from the context; an api_key, a longitude, and a latitude.

The longitude and latitude can be extracted from geodata in the view. The API Key will be read from the environment variables. Modify the views.py file to create a new context dictionary that contains the api_key, longitude and latitude key value pairs:

from django.shortcuts import render
import requests
import os


def home(request):
    response = requests.get('http://ip-api.com/json/')
    geodata = response.json()


    context = {
      'ip': geodata['query'],
      'country': geodata['country'],
      'latitude': geodata['lat'],
      'longitude': geodata['lon'],
      'api_key': os.environ['API_KEY'],
  }


    return render(request, 'home.html', context)

The new view and template will add a map to our application:

geolocation with map

Deploy to Divio's cloud

It's time to push the code up to the cloud and deploy our geolocation application. There are a few steps to take before our project is ready for production.

Add the appropriate headers

The code to determine IP location works locally because the application has direct access to the HTTP headers from the client. On the cloud however, the application is not directly exposed to clients: it's always behind Divio's load-balancers. This means that the IP addresses it sees will be those of the load-balancers - not the actual visitors - and it will report the wrong locations.

To address this, the application needs to look at the request headers and extract the original client IP address from there. The X_FORWARDED_FOR (link) header is a common way of identifying the originating IP address of a client. Some Divio projects may also be protected by Cloudflare; in those case, the header will be CF_CONNECTING_IP.

So, the view needs an amendment to get hold of the IP address reliably:

def home(request):
    x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
    cloud_flare_ip = request.META.get('HTTP_CF_CONNECTING_IP')

    if cloud_flare_ip:
        user_ip = cloud_flare_ip

    elif x_forwarded_for:
        user_ip = x_forwarded_for.split(',')[0]

    else:
        user_ip = request.META.get('REMOTE_ADDR')

    response = requests.get(f'http://ip-api.com/json/{user_ip}')
    request.session['geodata'] = response.json()
    geodata = request.session['geodata']

    context = {
        'ip': geodata['query'],
        'country': geodata['country'],
        'latitude': geodata['lat'],
        'longitude': geodata['lon'],
        'api_key': os.environ['API_KEY'],
    }

    return render(request, 'home.html', context)

Add environment variables to the cloud environments

In order to use the TomTom Maps API in the cloud, add the same API key you added to the .env-local file to the control panel. In the project, select Environment Variables. Enter the keys and values, and Save.

settings env variables

Commit and push changes:

git add templates iplocation settings.py urls.py
git commit -m “Add iplocation application”
git push origin master

Deploy the Test Server:

divio project deploy test

and to open the Test site:

divio project test

Conclusion

Geolocation is very useful web functionality, that finds many different applications. This project puts a simple example into practice.

And as well as the key work required to do that in a cloud environment - handling HTTP headers, authenticating to a REST API, integrating IP geolocation and a mapping service - we've covered several other important points too:

  • installing Python packages on Divio
  • local development and cloud deployment
  • managing and using environment variables locally and on the cloud

 Useful references


Back to overview

Recent posts