server/fgs/views.py

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({})