Boilerplate with Geodjango and Leaflet - learn while using it and deploy to Heroku
The topic we will cover here. Click to go to code examples:
- Get geolocation from the IP - we will get coordinates of the IP that we extract from the request itself. I mention loading IP data from other DB to not use external API.
- Searching via Address - we will use q django query to search location fields.
- Adding new objects on map - we will add a few objects using demo VueJS component with api from django rest framework.
- Closest object - we will use geodjango point to find closest object
Plus useful snippets:
- Drawing polygons on map - we check here geodjango area.
- List of objects within radius
- Calculate distance between two points - backend and frontend solution.
ExTRA :
- Deployable boilerplate with all above - how to prepare PostgreSQL database for geodjango on Heroku.
Important: We will be using vuejs with geodjango (connecting django rest framework api with javascript) throughout this tutorial - django-rest-framework-gis. This django package is compatible with both currently used django versions - Django 2 and Django 3. For more compatibility check geodjango documentation.
- Intercepting requests - using proxy and interceptor to hack your workflow ( no swagger? no problem! )
Get geolocation from the IP
There are 3 ways of obtaining geolocation based on IP address: (easy) ⚪ Call an external geocoding webservices API that returns location data (done from frontend or backend) (harder) 🟠 Use geo IP database with mapped IP.
First let’s get IP address from the django request. Let’s install django-ipware
pip install django-ipware
and use it in class views :
from rest_framework.generics import ListAPIView
from rest_framework.response import Response
from rest_framework import status
from ipware import get_client_ip
class ShowUsersIP(ListPIView):
"""
Sample API endpoint that shows user's IP
"""
queryset = Message.objects.all()
serializer_class = UserSerializer
def get(self, request):
client_ip, is_routable = get_client_ip(request)
print('IP_INFO: ',client_ip, is_routable)
data = {
'client_ip': client_ip,
'is_routable': is_routable
}
return Response(data, status=status.HTTP_200_OK)
or function based views :
from django.http.response import JsonResponse
from ipware import get_client_ip
def show_users_ip(request):
client_ip, is_routable = get_client_ip(request)
data = {
'client_ip': client_ip,
'is_routable': is_routable
}
return JsonResponse(data)
(The easy one) ⚪
Now that we know how to get the IP of the user we can track it’s location. With ip.info we get 50k calls on freemium which is more than enough. In order to obtain the location you need to pass the IP to the request:
import sys
import urllib.request
import json
ACCESS_TOKEN = 'YOUR_TOKEN'
def get_lat_lng_from_ip(ip_address):
"""Returns array that consists of latitude and longitude"""
URL = 'https://ipinfo.io/{}?token={}'.format(ip_address, ACCESS_TOKEN)
try:
result = urllib.request.urlopen(URL).read()
result = json.loads(result)
result = result['loc']
lat, lng = result.split(',')
return [lat, lng]
except:
print("Could not load: ", URL)
return None
location = get_lat_lng_from_ip('208.80.152.201')
print('Latitude: {0}, Longitude: {1}'.format(*location))
# Prints: Latitude: 32.7831, Longitude: -96.8067
The response we get and transform to get lat and lng is this:
{
"ip": "208.80.152.201",
"city": "Dallas",
"region": "Texas",
"country": "US",
"loc": "32.7831,-96.8067",
"org": "AS14907 Wikimedia Foundation Inc.",
"postal": "75270",
"timezone": "America/Chicago"
}
Responses vary between providers. Some of other service providers are: Google, OpenCage
Now let’s save the latitude and longitude we got from user’s IP to his account ! All examples are shown in the boilerplate code. The User model we have there extends Abstract user.
# User model
class User(AbstractUser):
#...
coordinates = models.PointField(blank=True, null=True, srid=4326)
# Update view
# ...
from rest_framework_gis.pagination import GeoJsonPagination
from backend.accounts.api.serializers import UpdateLocationSerializer
from backend.utils.coordinates import get_lat_lng_from_ip
from rest_framework.response import Response
from django.contrib.gis.geos import GEOSGeometry
from django.contrib.gis.geos import Point
from rest_framework import status
import json
class UpdateLocation(UpdateAPIView):
""" Updates location of a user using it's IP taken from request"""
model = User
serializer_class = UpdateLocationSerializer
pagination_class = GeoJsonPagination
def update(self, request, *args, **kwargs):
"""Here we:
- get location from the IP
- change coordinates to a randomly
close one in order to anonymize users location.
"""
# Using ipware library
client_ip, is_routable = get_client_ip(request)
# Using our function created previously in utils
latitude, longitude = get_lat_lng_from_ip(client_ip)
if not latitude:
return Response({'message': 'IP or location was not found.'}, status=status.HTTP_406_NOT_ACCEPTABLE)
# Anonimizing users location before saving
latitude, longitude = self.anonymize_location(latitude, longitude)
print('Users latitude {} and longitude {}'.format(latitude, longitude))
# For GeoDjango PointField we define y and x
# more in docs: https://docs.djangoproject.com/en/3.1/ref/contrib/gis/geos/#point
point = Point(float(longitude), float(latitude), srid=4326)
# Requires at least 1 user in DB (e.g. admin)
user_obj = User.objects.all().first()
# Using partial update to not create a new user obj
anonymize_profile = UpdateLocationSerializer(user_obj, data={'coordinates': point}, partial=True)
if not anonymize_profile.is_valid():
return Response(anonymize_profile.errors, status=status.HTTP_400_BAD_REQUEST)
anonymize_profile.save()
pnt = GEOSGeometry(point)
geojson = json.loads(pnt.geojson)
return Response({'coordinates': reversed(geojson['coordinates'])}, status=status.HTTP_201_CREATED)
For your purposes there is a high chance you should use the first solution. There is many providers with free tiers offering many calls with high degree of accuracy.
(The hard one) 🟠
This option is based on downloading databases of IP and locations and querying it with the obtained IP of the user. You can check IP2location page to see how to do it. Install python package for querying the DB and download the BINs with data to query.
(for more loading dbsync data check out this realpython django tutorial)
Searching via Address
Here we talk about geocoding and reverse geocoding. You can imagine that although with IP we could have location data in our DB - here, with addresses it is not going to be possible because we need much higher accuracy and the amount of data that is needed to achieve it is a lot bigger. You will be forced to use external service’s API.
What if you don’t want to use Google Geocoding API? There is a geopy library that provides easy API access to Nominatim geocoding information that is open source. You just need to specify the name when using it. It can be name of your app.
Let’s see how easy it is:
>>> from geopy.geocoders import Nominatim
>>> geolocator = Nominatim(user_agent="mysuperapp")
>>> location = geolocator.geocode("Warsaw Culture Palace")
>>> print(location.address)
Pałac Kultury, 1, Plac Defilad, Śródmieście Północne, Warszawa, województwo mazowieckie, 00-110, Polska
>>> print((location.latitude, location.longitude))
(52.2317641, 21.005799675616117)
In order to implement it we need to follow good REST API practices! How to implement it into django search endpoint?
First it’s important to follow a good URL naming practices:
yourapp.io/coordinates?address=QUERY_ADDRESS
In Django we can implement it like this starting from urls.py:
# urls.py
from django.urls import path
from backend.accounts.api.views import GetCoordinatesFromAddress
app_name = 'accounts'
urlpatterns = [
# ...
path("coordinates", # the rest will be in views: ?address=QUERY_ADDRESS
GetCoordinatesFromAddress.as_view(), name="get-coordinates"),
]
and in views.py:
from geopy.geocoders import Nominatim
# ...
class GetCoordinatesFromAddress(ListAPIView, GeoFilterSet):
""" Shows nearby Users"""
def get(self, *args, **kwargs):
# We can check on the server side the location of the users, using request
# point = self.request.user.coordinates
# ?address=QUERY_ADDRESS
# QUERY_ADDRESS is the information user passes to the query
QUERY_ADDRESS = self.request.query_params.get('address', None)
if QUERY_ADDRESS not in [None, '']:
# here we can use the geopy library:
geolocator = Nominatim(user_agent="mysuperapp")
location = geolocator.geocode(QUERY_ADDRESS)
return Response({'coordinates': [location.latitude ,location.longitude]}, status=status.HTTP_200_OK)
else:
return Response({'message': 'No address was passed in the query'}, status=status.HTTP_400_BAD_REQUEST)
Now if we call our app’s API with:
https://geodjango-rest-vue-boilerplate.herokuapp.com/api/accounts/nearbyusers?address=warsaw
We get in response body:
{
"coordinates": [52.2319581, 21.0067249]
}
There is a front end example if you want to [try it][] or [see the code][].
Adding new objects on map
🚧🏗️👷 It will be here soon!
Closest object
🚧🏗️👷 It will be here soon!
Drawing polygons on map
🚧🏗️👷 It will be here soon!
List of objects within radius
from geopy.geocoders import Nominatim
from rest_framework_gis.filterset import GeoFilterSet
from rest_framework_gis import filters as geofilters
from django.db.models import Q
from django.contrib.gis.measure import Distance
# ...
class ListNearbyUsers(ListAPIView, GeoFilterSet):
""" Shows nearby Users"""
model = User
serializer_class = NearbyUsersSerializer
pagination_class = GeoJsonPagination
contains_geom = geofilters.GeometryFilter(name='coordinates', lookup_expr='exists')
def get_queryset(self, *args, **kwargs):
QUERY_ADDRESS = self.request.query_params.get('address', None)
queryset = []
if QUERY_ADDRESS not in [None, '']:
queryset = User.objects.all()
# here we can use the geopy library:
geolocator = Nominatim(user_agent="mysuperapp.com")
location = geolocator.geocode(QUERY_ADDRESS)
# Let's use the obtained information to create a geodjango Point
point = Point(float(location.longitude), float(location.latitude), srid=4326)
# and query for 10 Users objects to find active users within radius
queryset = queryset.filter(Q(coordinates__distance_lt=(
point, Distance(km=settings.RADIUS_SEARCH_IN_KM))) & Q(is_active=True)).order_by('coordinates')[0:10]
return queryset
else:
return queryset
Calculate distance between two points
🚧🏗️👷 It will be here soon!
Deployable boilerplate with all above
🚧🏗️👷 It will be here soon!
Did you make any mistakes when using REPLACETHIS or you’ve seen one here? Tell me about your insights. Leave a comment with YOUR opinion.
Top comments (0)