174 lines
5.9 KiB
Python
174 lines
5.9 KiB
Python
from . import app, db
|
|
from .flatten import flatten, unflatten
|
|
from .jwt import jwt_required
|
|
from .model import *
|
|
from flask import g, jsonify, request, abort
|
|
from sqlalchemy import func
|
|
from geopy.distance import geodesic
|
|
from datetime import datetime, timedelta
|
|
|
|
@app.route('/')
|
|
def index():
|
|
"""
|
|
Index function, mostly used to ensure that the API is up and running.
|
|
Does not require authentication.
|
|
"""
|
|
return 'Hello, world!'
|
|
|
|
@app.route('/login', methods=['POST'])
|
|
def login():
|
|
"""
|
|
Authentication function.
|
|
|
|
The form data containing the username
|
|
and password is retrieved and compared against the database; if the
|
|
username does not exist, or the password doesn't match,
|
|
the service responds with a "bad request". A JSON token, unique
|
|
to the user, is returned when the authentication succeeds. This
|
|
token can then be used by the routes protected with @jwt_required.
|
|
"""
|
|
username = request.form.get('username')
|
|
password = request.form.get('password')
|
|
if username is None or password is None: abort(400)
|
|
user = User.query.filter_by(username=username).first()
|
|
if user is None or not user.check_password(password): abort(400)
|
|
return jsonify({ 'token': user.get_jwt().decode() })
|
|
|
|
@app.route('/me')
|
|
@jwt_required
|
|
def me():
|
|
"""
|
|
Function used to ensure that a token is valid with the API.
|
|
|
|
Simply returns the username of the user to whom the token belongs.
|
|
"""
|
|
return jsonify({ 'username' : g.user.username })
|
|
|
|
@app.route('/collars')
|
|
@jwt_required
|
|
def collars():
|
|
"""
|
|
Function to retrieve the initial list of collars, to be shown on
|
|
the application's main page.
|
|
|
|
The response contains the identifier, name, and position of each
|
|
currently active collar.
|
|
"""
|
|
active_collars = []
|
|
for collar in Collar.query.filter_by(active=True).all():
|
|
max_point = DataPoint.query.filter_by(collar_id=collar.id).\
|
|
order_by(DataPoint.datetime.desc()).\
|
|
first()
|
|
active_collars.append(
|
|
{'id': collar.id, 'name': collar.name, 'pos':
|
|
{'longitude': max_point.longitude, 'latitude': max_point.latitude}}
|
|
)
|
|
|
|
return jsonify(active_collars)
|
|
|
|
@app.route('/collars/stats/distance')
|
|
@jwt_required
|
|
def collars_distance():
|
|
"""
|
|
One of the functions used for displaying graphs / statistics to the user.
|
|
Computes the total distance traveled by an animal.
|
|
|
|
Returns, for each active collar, the identifier, name,
|
|
and total distance traveled. The name is included for convenience,
|
|
to prevent further API requests.
|
|
"""
|
|
|
|
active_collars = []
|
|
for collar in Collar.query.filter_by(active=True).all():
|
|
# Can't use SQLAlchemy's data_points field
|
|
# to further restrict query (without some tinkering, which would hurt elsewhere)
|
|
data_points = DataPoint.query.filter_by(collar_id=collar.id).\
|
|
order_by(DataPoint.datetime.desc())
|
|
|
|
# Extract longitude and latitude
|
|
coords_pairs = list(map(lambda e : (e.latitude, e.longitude), data_points))
|
|
|
|
# The animal has not traveled at all; skip.
|
|
if len(coords_pairs) < 1: continue
|
|
|
|
# Start with the current location, and work backwards
|
|
# one data point at a time.
|
|
distance_kilometers = 0
|
|
coord_last = coords_pairs.pop(0)
|
|
for coord in coords_pairs:
|
|
distance_kilometers += geodesic(coord_last,coord).km
|
|
coord_last = coord
|
|
|
|
active_collars.append(
|
|
{'id': collar.id, 'name': collar.name,
|
|
'distance': distance_kilometers }
|
|
)
|
|
|
|
return jsonify(active_collars)
|
|
|
|
@app.route('/collars/<int:id>/history')
|
|
@jwt_required
|
|
def collar_history(id):
|
|
"""
|
|
Function to retrieve the complete history of a collar, sorted newest to oldest.
|
|
|
|
Returns a list of coordinates.
|
|
"""
|
|
collar = Collar.query.filter_by(id=id).first()
|
|
if collar is None: abort(404)
|
|
|
|
data_points = DataPoint.query.\
|
|
filter_by(collar_id=id).\
|
|
order_by(DataPoint.datetime.desc()).\
|
|
all()
|
|
return jsonify([point.to_dict() for point in data_points])
|
|
|
|
@app.route('/collars/<int:id>/details')
|
|
@jwt_required
|
|
def collar_detail(id):
|
|
"""
|
|
Function to get the information needed for a single-collar view.
|
|
|
|
At the moment, this information includes the collar's name, location,
|
|
valid grazing area, and how many times it had triggered the
|
|
out-of-grazing-area stimulus in the last 24 hours.
|
|
"""
|
|
collar = Collar.query.filter_by(id=id).first()
|
|
if collar is None: abort(404)
|
|
|
|
# get stimulus activation reports from within last 24 hours
|
|
since = datetime.now() - timedelta(hours=24)
|
|
stimulus_points = StimulusActivation.query.\
|
|
filter_by(collar_id=id).\
|
|
filter(StimulusActivation.datetime > since).\
|
|
order_by(StimulusActivation.datetime.desc())
|
|
n_stimulus = stimulus_points.count()
|
|
|
|
return jsonify({'id': collar.id, 'name': collar.name, 'stimilus': n_stimulus, 'boundary': unflatten(collar.boundary) })
|
|
|
|
@app.route('/collars/<int:id>/boundary/set', methods=['POST'])
|
|
@jwt_required
|
|
def collar_set_boundary(id):
|
|
"""
|
|
Function to configure a collar's grazing boundary.
|
|
|
|
A grazing boundary is received from the application as a list of
|
|
vertices; we hide the the 10-point constraint from that part
|
|
of the system to prevent coupling. However, this means
|
|
that we need to convert an arbitrary list of points into
|
|
the flattened BoundingBox instance, which is handled
|
|
by flatten. Setting a collar boundary removes the old
|
|
boundary from the database.
|
|
"""
|
|
collar = Collar.query.filter_by(id=id).first()
|
|
if collar is None: abort(404)
|
|
|
|
new_boundary = flatten(request.json)
|
|
new_boundary.needs_push = True
|
|
if collar.boundary is not None:
|
|
db.session.delete(collar.boundary)
|
|
collar.boundary = new_boundary
|
|
db.session.add(new_boundary)
|
|
db.session.commit()
|
|
return jsonify({})
|