Location API - bounswe/2021SpringGroup9 GitHub Wiki
-
Story: A model in the database. It is a model for user's stories that they post in our app. Its fields are:
- id: Story's id to be kept in the database.
- title: Story's title with maximum char length 200
- story: Story's content with maximum char length 1000
- name: The user's name who posted the story. It has a maximum char length of 200.
- longitude: Story's location's longitude.
- latitude: Story's location's latitude.
- location: Story's location name with maximum char length 200.
- tag: Story's tag with maximum char length 200,
- date: Story's post date.
- notifyAdmin: Thi shows if a story needs to notify the admin or not.
-
Location: A model that is used for serializing the data that Google Maps returns. This model is not stored in the database.
- id: Corresponding story's id.
- location_name: Formatted address.
- location_longitude: Longitude of the location.
- location_latitude: Latitude of the location.
- Google Maps API returns the geocode of a location. It is used for getting the exact coordinates of a story's location. The geocode of a location consists of a lot of data and it should be handled so that it can make sense easily. Location model is used for this. It only serializes formatted address, longitude, and latitude from geocode.
Google Maps API has a lot of functionalities and these functionalities are categorized into 3 parts. These are Maps, Routes, and Places.
In Maps category:
-
Developers can access and use Google Maps in their applications. The Maps could be either static or dynamic.
-
Static or dynamic street view imagery can also be added.
In Routes category:
-
Nearby roads can be identified by using coordinates.
-
Travel times can be calculated.
-
Directions for transportation and traffic information can be obtained.
In Places category:
-
The data of places that are in the Google Maps application can be accessed.
-
Geocode of a certain place can be obtained.
-
Approximate location of a device can be obtained.
-
Time zone of a specific coordinate can be obtained.
In my API functionality, I used geocoding and static map feature of Google Maps API. By using geocode, I obtained the coordinates and formatted address of a location. By using the static map, I put a marker on the location of the story.
In general, Google Maps API works really well. However, it is better to be as specific as possible to get an accurate location.
Get Location of All Stories
This returns Location objects. It consists of stories' id, formatted address, longitude, and latitude.
- No parameters are needed for this.
"GET /api/location/ HTTP/1.1" 200
[
{
"story_id": 1,
"location_name": "Rome, Metropolitan City of Rome, Italy",
"location_latitude": 41.9027835,
"location_longitude": 12.4963655
},
{
"story_id": 15,
"location_name": "Berlin, Germany",
"location_latitude": 52.52000659999999,
"location_longitude": 13.404954
},
{
"story_id": 16,
"location_name": "Rumeli Hisarı, Hisar Üstü Nispetiye Cd No:7, 34342 Sarıyer/İstanbul, Turkey",
"location_latitude": 41.0863067,
"location_longitude": 29.0441352
},
{
"story_id": 17,
"location_name": "Bebek, 34342 Beşiktaş/İstanbul, Turkey",
"location_latitude": 41.0847571,
"location_longitude": 29.0510399
},
{
"story_id": 18,
"location_name": "London, UK",
"location_latitude": 51.5073509,
"location_longitude": -0.1277583
},
{
"story_id": 19,
"location_name": "Grand Kremlin Palace, Moskva, Russia, 103132",
"location_latitude": 55.7505944,
"location_longitude": 37.6153441
},
{
"story_id": 20,
"location_name": "Kemer, Belgrade Forest, 34450 Sarıyer/İstanbul, Turkey",
"location_latitude": 41.184012,
"location_longitude": 28.9885021
},
{
"story_id": 21,
"location_name": "Adelaide SA, Australia",
"location_latitude": -34.9284989,
"location_longitude": 138.6007456
},
{
"story_id": 22,
"location_name": "Bebek, 34342 Beşiktaş/İstanbul, Turkey",
"location_latitude": 41.0847571,
"location_longitude": 29.0510399
},
{
"story_id": 23,
"location_name": "Bengaluru, Karnataka, India",
"location_latitude": 12.9715987,
"location_longitude": 77.5945627
},
{
"story_id": 24,
"location_name": "İstanbul, Turkey",
"location_latitude": 41.0082376,
"location_longitude": 28.9783589
}
]
Get Location of a Specific Story
This returns a Location object. It consists of a specific story's id, formatted address, longitude, and latitude.
-
GET
http://3.129.194.233/api/location/int:story_id/
{
story_id = The id of the story
}
"GET /api/location/1/ HTTP/1.1" 200
{
"story_id": 1,
"location_name": "Rome, Metropolitan City of Rome, Italy",
"location_latitude": 41.9027835,
"location_longitude": 12.4963655
}
Get Location of a Specific Story (MAP)
This returns an image of a map. A marker is put on the location of the story.
-
GET
http://3.129.194.233/api/location/map/int:story_id/
{
story_id = The id of the story
}
"GET /api/location/map/1/ HTTP/1.1" 200
Figure 1: Location of the Story with ID 1
Google Maps API requires a key. I acquired the key and shared it with team members. It allows 200 dollars worth of usage per month. This amount is quite enough for our application.
-
Location API returns HTTP_200_OK if the request is successful.
-
If there is no such story with the given id, then it returns HTTP_404_NOT_FOUND.
Models Used in Location API
Location Model is used for serializing the data Google Maps returns. Django creates tables in the database for models and the data is stored in the database. Location Model is only used for its serializer. Thus, it was not necessary for it to be stored in the database. manage = False
in class Meta
provides this functionality.
class Story(models.Model):
title = models.CharField(max_length=200)
story = models.CharField(max_length=1000)
name = models.CharField(max_length=200)
longitude = models.FloatField()
latitude = models.FloatField()
location = models.CharField(max_length=200)
tag = models.CharField(max_length=200)
date = models.DateTimeField(auto_now_add=True)
notifyAdmin = models.BooleanField(default=False)
def __str__(self):
return self.title
class Location(models.Model):
story_id = models.IntegerField()
location_name = models.CharField(max_length=200)
location_longitude = models.FloatField()
location_latitude = models.FloatField()
class Meta:
managed = False
Serializers
Serializers make it easier to return a JSON object. Also, if there is a bad request, such as invalid input, then, it will give errors automatically. This was an important concept that Django REST Framework provides.
class StorySerializer(serializers.ModelSerializer):
class Meta:
model = Story
fields = ['id' ,'date', 'name', 'location', 'tag', 'title', 'story', 'notifyAdmin','latitude', 'longitude']
read_only_fields = ['notifyAdmin','latitude', 'longitude']
class LocationSerializer(serializers.ModelSerializer):
class Meta:
model = Location
fields = ['story_id', 'location_name', 'location_latitude', 'location_longitude']
Views
-
api/location/
endpoint callsget()
function of following class. It returns the Location objects of all the stories in the database. If there are not any stories in the database, it will return an empty array. This class inheritsGenericAPIView
from Django REST Framework.GenericAPIView
and serializers work together. Serializers take specified fields in a dictionary as a parameter. These fields are shown in the previous section. Then, it converts them to JSON Objects. If there are many JSON Objects, it returns them as an array. Thanks to these functionalities, it is easy to serialize all of the locations in stories. -
There is another good advantage of using
GenericAPIView
and serializers. They make endpoints visible on Swagger UI. Also, it shows the example response data on Swagger UI. Without using these features, example response data is not shown on Swagger UI because the example response data gets the information from the serializer throughGenericAPIView
. You can check it from here. You should click on/location/
endpoint to see example response data. -
If there is invalid input, the serializer notices that by
is_valid()
function and the program can give 400 BAD REQUEST error. -
create_url(string, dict)
helper function is used for joining URL and parameters. After joining them, an API call is made to Google Maps API.
class Locations(GenericAPIView):
"""
List locations of stories.
"""
queryset = Location.objects.all()
serializer_class = LocationSerializer
def get(self, request, format=None):
story = Story.objects.all()
dic = []
for s in story:
location = s.location
location = location.split(' ')
location = "+".join(location)
url = "https://maps.googleapis.com/maps/api/geocode/json"
params = {"address":location, "key":env('GOOGLE_MAPS_API_KEY')}
new_url = create_url(url,params)
response = requests.get(new_url)
json_data = json.loads(response.content)
data = {'story_id': s.id,
'location_name' : json_data['results'][0]['formatted_address'],
'location_latitude' : json_data['results'][0]['geometry']['location']['lat'],
'location_longitude': json_data['results'][0]['geometry']['location']['lng']}
dic.append(data)
serializer = LocationSerializer(data=dic, many=True)
if serializer.is_valid():
return Response(serializer.data)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
-
api/location/<int:story_id>/
endpoint calls `get() function of following class. If there is no such story, it will give 404 NOT FOUND error. Otherwise, it will return a JSON Object that consists of corresponding story's id, formatted address, longitude, and latitude. -
If there is invalid input, the serializer notices that by
is_valid()
function and the program can give 400 BAD REQUEST error. -
create_url(string, dict)
helper function is used for joining URL and parameters. After joining them, an API call is made to Google Maps API.
class LocationDetail(GenericAPIView):
"""
Retrieve the location of a story instance.
"""
queryset = Location.objects.all()
serializer_class = LocationSerializer
def get_object(self, pk):
try:
return Story.objects.get(pk=pk)
except Story.DoesNotExist:
raise Http404
def get(self, request, pk, format=None):
story = self.get_object(pk)
location = story.location
location = location.split(' ')
location = "+".join(location)
url = "https://maps.googleapis.com/maps/api/geocode/json"
params = {"address":location, "key":env('GOOGLE_MAPS_API_KEY')}
new_url = create_url(url,params)
response = requests.get(new_url)
json_data = json.loads(response.content)
data = {'story_id': story.id,
'location_name' : json_data['results'][0]['formatted_address'],
'location_latitude' : json_data['results'][0]['geometry']['location']['lat'],
'location_longitude': json_data['results'][0]['geometry']['location']['lng']}
serializer = LocationSerializer(data=data)
if serializer.is_valid():
return Response(serializer.data)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
-
api/location/map/<int:story_id>/
endpoint calls following function. If there is no such story, it will give a 404 NOT FOUND error. Otherwise, it will return an image. This image is a map that has a marker on its center. The location of the map is the corresponding story's location. -
@api_view(['GET'])
is a decorater that Django REST Framework provides. It is used when working with function-based views in Django REST Framework. Also, it makes the endpoint visible on Swagger UI. -
create_url(string, dict)
helper function is used for joining URL and parameters. After joining them, an API call is made to Google Maps API.
@api_view(['GET'])
def locationMap(request, pk, format=None):
"""
Retrieve the location map of a story instance.
"""
try:
story = Story.objects.get(pk=pk)
except Story.DoesNotExist:
return HttpResponse(status = 404)
location = story.location
location = location.split(' ')
location = "+".join(location)
url = "https://maps.googleapis.com/maps/api/staticmap"
params = {"markers":location, "center":location, "zoom":"13", "size":"800x400", "key":env('GOOGLE_MAPS_API_KEY')}
new_url = create_url(url,params)
response = requests.get(new_url)
return HttpResponse(response.content, content_type="image/png", status = 200)
Helper Function
-
create_url(url,params)
function is used for joining URLs and parameters.url
parameter is a string andparams
parameter is a dictionary. A base URL should be passed tourl
parameter and parameters for this base URL should be passed toparams
parameter of the function. After joining the base URL and parameters, it returns the new URL.
def create_url(url, params):
url_parse = urlparse.urlparse(url)
query = url_parse.query
url_dict = dict(urlparse.parse_qsl(query))
url_dict.update(params)
url_new_query = urlparse.urlencode(url_dict)
url_parse = url_parse._replace(query=url_new_query)
new_url = urlparse.urlunparse(url_parse)
return new_url
Tests
- URLs of the Location API are tested to see whether the intended URLs are working correctly.
class TestUrls(TestCase):
def test_location_story_url_resolve(self):
url = reverse('location_story')
self.assertEqual(resolve(url).func.view_class, view_locationAPI.Locations)
def test_location_detail_story_url_resolve(self):
url = reverse('location_detail_story', args=[1])
self.assertEqual(resolve(url).func.view_class, view_locationAPI.LocationDetail)
def test_location_map_story_url_resolve(self):
url = reverse('location_map_story', args=[1])
self.assertEqual(resolve(url).func, view_locationAPI.locationMap)
- Views of the Location API are tested to see whether they return the expected HTTP status code.
class TestViews(TestCase):
def setUp(self):
self.client = Client()
self.mock_data = {"name":"melih",
"location":"Karaköy",
"tag":"Chilling",
"title":"Kahve",
"story":"Kahve Keyfi"}
self.mock_data_1 = view_locationAPI.Story.objects.create(title = "Çay",
story="Çay keyfi",
name ="emre",
longitude=10,
latitude=20,
location="Beşiktaş",
tag="mutlu")
def test_story_GET_location_list(self):
response = self.client.get(reverse('location_story'))
self.assertEqual(response.status_code, 200)
self.assertEqual(json.loads(response.content)[0]['story_id'], 1)
def test_story_GET_location_detail_1(self):
response = self.client.get(reverse('location_detail_story', args = [2]))
self.assertEqual(response.status_code, 404)
def test_story_GET_location_detail_2(self):
response = self.client.get(reverse('location_detail_story', args = [1]))
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data['story_id'], 1)
def test_story_GET_location_map_1(self):
response = self.client.get(reverse('location_map_story', args = [2]))
self.assertEqual(response.status_code, 404)
def test_story_GET_location_map_2(self):
response = self.client.get(reverse('location_map_story', args = [1]))
self.assertEqual(response.status_code, 200)
self.assertEqual(response.headers['Content-type'], 'image/png')