cygnal/server/routes.py

350 lines
8.9 KiB
Python

#!/usr/bin/env python
# coding: utf-8
"""
Routes definitions
"""
import arrow
import json
import os
import bottle
from server.models import Report
from server.tools import UTC_now
from server import jsonapi
class AuthenticationError(Exception):
pass
def check_auth():
"""
Check authentication.
:return: Abort and return a HTTP 403 page if authentication is not ok.
"""
if not os.getenv('API_TOKEN'):
return
auth = bottle.request.headers.get('Authorization', None)
if not auth:
raise AuthenticationError()
parts = auth.split()
if parts[0].lower() != 'bearer' or parts[1] != os.getenv('API_TOKEN'):
raise AuthenticationError()
return
@bottle.route('/api/v1/reports', ["GET", "OPTIONS"])
def get_all_reports():
"""
API v1 GET reports route. Get all reports.
Example::
> GET /api/v1/reports
{
"data": [
{
"attributes": {
"expiration_datetime": null,
"downvotes": 0,
"datetime": "2018-06-27T16:44:12+00:00",
"lat": 48.842005,
"upvotes": 1,
"lng": 2.386278,
"type": "interrupt",
},
"type": "reports",
"id": 1
},
]
}
.. note::
Filtering can be done through the ``filter`` GET param, according
to JSON API spec (http://jsonapi.org/recommendations/#filtering).
.. note::
By default no pagination is done. Pagination can be forced using
``page[size]`` to specify a number of items per page and
``page[number]`` to specify which page to return. Pages are numbered
starting from 0.
.. note::
Sorting can be handled through the ``sort`` GET param, according to
JSON API spec (http://jsonapi.org/format/#fetching-sorting).
:return: The available reports objects in a JSON ``data`` dict.
"""
# Handle CORS
if bottle.request.method == 'OPTIONS':
return {}
# Handle filtering, pagination and sorting
try:
filters, page_number, page_size, sorting = jsonapi.JsonApiParseQuery(
bottle.request.query,
Report,
default_sorting='id'
)
except ValueError as exc:
return jsonapi.JsonApiError(400, "Invalid parameters: " + str(exc))
# Query
query = Report.select()
if filters:
query = query.where(*filters)
query = query.order_by(*sorting)
if page_number is not None and page_size is not None:
query = query.paginate(page_number, page_size)
if (
'format' in bottle.request.query and
bottle.request.query['format'] == 'geojson'
):
return {
"type": "FeatureCollection",
"features": [
r.to_geojson_feature()
for r in query
]
}
else:
return {
"data": [
r.to_json()
for r in query
]
}
@bottle.route('/api/v1/reports', ["POST", "OPTIONS"])
def post_report():
"""
API v1 POST reports route.
Example::
> POST /api/v1/reports
> {
> "type": "pothole",
> "lat": 48.84219652060494,
> "lng": 2.385234797066081
> }
{
"data": {
"attributes": {
"expiration_datetime": null,
"downvotes": 0,
"datetime": "2018-10-17T13:42:35+00:00",
"first_report_datetime": "2018-10-17T13:42:35+00:00",
"lat": 48.84219652060494,
"upvotes": 0,
"lng": 2.385234797066081,
"type": "pothole",
"source": "survey"
},
"type": "reports",
"id": 1161
}
}
:return: The newly created report object in a JSON ``data`` dict.
"""
# Handle CORS
if bottle.request.method == 'OPTIONS':
return {}
# Check authentication
try:
check_auth()
except AuthenticationError:
return jsonapi.JsonApiError(403, "Invalid authentication.")
try:
payload = json.load(bottle.request.body)
except ValueError as exc:
return jsonapi.JsonApiError(400, "Invalid JSON payload: " + str(exc))
try:
r = Report(
type=payload['type'],
lat=payload['lat'],
lng=payload['lng'],
source=payload.get('source', 'unknown'),
shape_geojson=payload.get('shape_geojson', None)
)
# Handle expiration
if r.type in ['accident', 'gcum']:
r.expiration_datetime = (
arrow.get(UTC_now()).shift(hours=+1).naive
)
r.save()
except KeyError as exc:
return jsonapi.JsonApiError(400, "Invalid report payload: " + str(exc))
return {
"data": r.to_json()
}
@bottle.route('/api/v1/reports/:id/upvote', ["POST", "OPTIONS"])
def upvote_report(id):
"""
API v1 POST upvote route.
Example::
> POST /api/v1/reports/1/upvote
{
"data": {
"attributes": {
"expiration_datetime": null,
"downvotes": 0,
"datetime": "2018-10-17T13:42:35+00:00",
"first_report_datetime": "2018-10-17T13:42:35+00:00",
"lat": 48.84219652060494,
"upvotes": 1,
"lng": 2.385234797066081,
"type": "pothole",
},
"type": "reports",
"id": 1161
}
}
:return: The updated report object in a JSON ``data`` dict.
"""
# Handle CORS
if bottle.request.method == 'OPTIONS':
return {}
# Check authentication
try:
check_auth()
except AuthenticationError:
return jsonapi.JsonApiError(403, "Invalid authentication.")
r = Report.get(Report.id == id)
if not r:
return jsonapi.JsonApiError(404, "Invalid report id.")
# Increase upvotes
r.upvotes += 1
# Update report datetime
r.datetime = UTC_now()
# Update expiration datetime
if r.type in ['accident', 'gcum']:
r.expiration_datetime = (
arrow.get(UTC_now()).shift(hours=+1).naive
)
r.save()
return {
"data": r.to_json()
}
@bottle.route('/api/v1/reports/:id/downvote', ["POST", "OPTIONS"])
def downvote_report(id):
"""
API v1 POST downvote route.
Example::
> POST /api/v1/reports/1/downvote
{
"data": {
"attributes": {
"expiration_datetime": null,
"downvotes": 1,
"datetime": "2018-10-17T13:42:35+00:00",
"first_report_datetime": "2018-10-17T13:42:35+00:00",
"lat": 48.84219652060494,
"upvotes": 0,
"lng": 2.385234797066081,
"type": "pothole",
},
"type": "reports",
"id": 1161
}
}
:return: The updated report object in a JSON ``data`` dict.
"""
# Handle CORS
if bottle.request.method == 'OPTIONS':
return {}
# Check authentication
try:
check_auth()
except AuthenticationError:
return jsonapi.JsonApiError(403, "Invalid authentication.")
r = Report.get(Report.id == id)
if not r:
return jsonapi.JsonApiError(404, "Invalid report id.")
r.downvotes += 1
r.save()
return {
"data": r.to_json()
}
@bottle.route('/api/v1/stats', ["GET", "OPTIONS"])
def get_stats():
"""
API v1 GET stats about this instance.
Example::
> GET /api/v1/stats
{
"data": {
"nb_active_reports": 606,
"nb_reports": 1162,
"last_added_report_datetime": "2018-10-17T13:44:16+00:00",
}
}
:return: The available stats about the instance in a JSON ``data`` dict.
"""
# Handle CORS
if bottle.request.method == 'OPTIONS':
return {}
nb_reports = Report.select().count()
nb_active_reports = Report.select().where(
(Report.expiration_datetime == None) |
(Report.expiration_datetime > UTC_now())
).count()
last_added_report_datetime = Report.select().order_by(
Report.datetime.desc()
).get().datetime
return {
"data": {
"nb_reports": nb_reports,
"nb_active_reports": nb_active_reports,
"last_added_report_datetime": last_added_report_datetime
}
}