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