Location API - bounswe/2021SpringGroup9 GitHub Wiki

Location API Documentation


1. Glossary

  • 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.

2. API Reference

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.

3. How to Use Location API

Get Location of All Stories

This returns Location objects. It consists of stories' id, formatted address, longitude, and latitude.

Request:

Parameters:

  • No parameters are needed for this.

Response:

  • "GET /api/location/ HTTP/1.1" 200

Data:

[
    {
        "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.

Request:

Parameters:

{
    story_id = The id of the story
}

Response:

  • "GET /api/location/1/ HTTP/1.1" 200

Data:

{
    "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.

Request:

Parameters:

{
    story_id = The id of the story
}

Response:

  • "GET /api/location/map/1/ HTTP/1.1" 200

Data:

Figure 1: Location of the Story with ID 1

4. Authentication

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.

6. Errors and Successes

  • 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.

7. Code Documentation

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 calls get() 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 inherits GenericAPIView 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 through GenericAPIView. 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 and params parameter is a dictionary. A base URL should be passed to url parameter and parameters for this base URL should be passed to params 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')
⚠️ **GitHub.com Fallback** ⚠️