API: add an activity (w/ gpx file)
This commit is contained in:
parent
ba613079f4
commit
f4f0d5a2aa
@ -1,7 +1,15 @@
|
|||||||
from flask import Blueprint, jsonify
|
import json
|
||||||
|
import os
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
from ..users.utils import authenticate
|
from flask import Blueprint, current_app, jsonify, request
|
||||||
|
from mpwo_api import appLog, db
|
||||||
|
from sqlalchemy import exc
|
||||||
|
from werkzeug.utils import secure_filename
|
||||||
|
|
||||||
|
from ..users.utils import authenticate, verify_extension
|
||||||
from .models import Activity
|
from .models import Activity
|
||||||
|
from .utils import get_gpx_info
|
||||||
|
|
||||||
activities_blueprint = Blueprint('activities', __name__)
|
activities_blueprint = Blueprint('activities', __name__)
|
||||||
|
|
||||||
@ -29,3 +37,77 @@ def get_activities(auth_user_id):
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return jsonify(response_object), 200
|
return jsonify(response_object), 200
|
||||||
|
|
||||||
|
|
||||||
|
@activities_blueprint.route('/activities', methods=['POST'])
|
||||||
|
@authenticate
|
||||||
|
def post_activity(auth_user_id):
|
||||||
|
"""Post an activity"""
|
||||||
|
code = 400
|
||||||
|
response_object = verify_extension('activity', request)
|
||||||
|
if response_object['status'] != 'success':
|
||||||
|
return jsonify(response_object), code
|
||||||
|
|
||||||
|
activity_data = json.loads(request.form["data"])
|
||||||
|
if not activity_data or activity_data.get('sport_id') is None:
|
||||||
|
response_object = {
|
||||||
|
'status': 'error',
|
||||||
|
'message': 'Invalid payload.'
|
||||||
|
}
|
||||||
|
return jsonify(response_object), 400
|
||||||
|
|
||||||
|
activity_file = request.files['file']
|
||||||
|
filename = secure_filename(activity_file.filename)
|
||||||
|
dirpath = os.path.join(
|
||||||
|
current_app.config['UPLOAD_FOLDER'],
|
||||||
|
'activities',
|
||||||
|
str(auth_user_id)
|
||||||
|
)
|
||||||
|
if not os.path.exists(dirpath):
|
||||||
|
os.makedirs(dirpath)
|
||||||
|
filepath = os.path.join(dirpath, filename)
|
||||||
|
|
||||||
|
try:
|
||||||
|
activity_file.save(filepath)
|
||||||
|
gpx_data = get_gpx_info(filepath)
|
||||||
|
except Exception as e:
|
||||||
|
appLog.error(e)
|
||||||
|
response_object = {
|
||||||
|
'status': 'error',
|
||||||
|
'message': 'Error during activity file save.'
|
||||||
|
}
|
||||||
|
return jsonify(response_object), 500
|
||||||
|
|
||||||
|
try:
|
||||||
|
new_activity = Activity(
|
||||||
|
user_id=auth_user_id,
|
||||||
|
sport_id=activity_data.get('sport_id'),
|
||||||
|
activity_date=gpx_data['start'],
|
||||||
|
duration=timedelta(seconds=gpx_data['duration'])
|
||||||
|
)
|
||||||
|
new_activity.gpx = filepath
|
||||||
|
new_activity.pauses = timedelta(seconds=gpx_data['stop_time'])
|
||||||
|
new_activity.moving = timedelta(seconds=gpx_data['moving_time'])
|
||||||
|
new_activity.distance = gpx_data['distance']
|
||||||
|
new_activity.min_alt = gpx_data['elevation_min']
|
||||||
|
new_activity.max_alt = gpx_data['elevation_max']
|
||||||
|
new_activity.descent = gpx_data['downhill']
|
||||||
|
new_activity.ascent = gpx_data['uphill']
|
||||||
|
new_activity.max_speed = gpx_data['max_speed']
|
||||||
|
new_activity.ave_speed = gpx_data['average_speed']
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
response_object = {
|
||||||
|
'status': 'created',
|
||||||
|
'message': 'Activity added.'
|
||||||
|
}
|
||||||
|
return jsonify(response_object), 201
|
||||||
|
|
||||||
|
except (exc.IntegrityError, ValueError) as e:
|
||||||
|
db.session.rollback()
|
||||||
|
appLog.error(e)
|
||||||
|
response_object = {
|
||||||
|
'status': 'fail',
|
||||||
|
'message': 'Error during activity save.'
|
||||||
|
}
|
||||||
|
return jsonify(response_object), 500
|
||||||
|
49
mpwo_api/mpwo_api/activities/utils.py
Normal file
49
mpwo_api/mpwo_api/activities/utils.py
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import gpxpy.gpx
|
||||||
|
|
||||||
|
|
||||||
|
def get_gpx_info(gpx_file):
|
||||||
|
|
||||||
|
gpx_data = {'filename': gpx_file}
|
||||||
|
|
||||||
|
gpx_file = open(gpx_file, 'r')
|
||||||
|
gpx = gpxpy.parse(gpx_file)
|
||||||
|
|
||||||
|
max_speed = 0
|
||||||
|
start = 0
|
||||||
|
|
||||||
|
for track in gpx.tracks:
|
||||||
|
for segment in track.segments:
|
||||||
|
for point_idx, point in enumerate(segment.points):
|
||||||
|
if point_idx == 0:
|
||||||
|
start = point.time
|
||||||
|
speed = segment.get_speed(point_idx)
|
||||||
|
try:
|
||||||
|
if speed > max_speed:
|
||||||
|
max_speed = speed
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
gpx_data['max_speed'] = (max_speed / 1000) * 3600
|
||||||
|
gpx_data['start'] = start
|
||||||
|
|
||||||
|
duration = gpx.get_duration()
|
||||||
|
gpx_data['duration'] = duration
|
||||||
|
|
||||||
|
ele = gpx.get_elevation_extremes()
|
||||||
|
gpx_data['elevation_max'] = ele.maximum
|
||||||
|
gpx_data['elevation_min'] = ele.minimum
|
||||||
|
|
||||||
|
hill = gpx.get_uphill_downhill()
|
||||||
|
gpx_data['uphill'] = hill.uphill
|
||||||
|
gpx_data['downhill'] = hill.downhill
|
||||||
|
|
||||||
|
mv = gpx.get_moving_data()
|
||||||
|
gpx_data['moving_time'] = mv.moving_time
|
||||||
|
gpx_data['stop_time'] = mv.stopped_time
|
||||||
|
distance = mv.moving_distance + mv.stopped_distance
|
||||||
|
gpx_data['distance'] = distance/1000
|
||||||
|
|
||||||
|
average_speed = distance / duration
|
||||||
|
gpx_data['average_speed'] = (average_speed / 1000) * 3600
|
||||||
|
|
||||||
|
return gpx_data
|
@ -15,6 +15,7 @@ class BaseConfig:
|
|||||||
current_app.root_path, 'uploads'
|
current_app.root_path, 'uploads'
|
||||||
)
|
)
|
||||||
PICTURE_ALLOWED_EXTENSIONS = {'jpg', 'png', 'gif'}
|
PICTURE_ALLOWED_EXTENSIONS = {'jpg', 'png', 'gif'}
|
||||||
|
ACTIVITY_ALLOWED_EXTENSIONS = {'gpx'}
|
||||||
|
|
||||||
|
|
||||||
class DevelopmentConfig(BaseConfig):
|
class DevelopmentConfig(BaseConfig):
|
||||||
|
@ -1,8 +1,30 @@
|
|||||||
import datetime
|
import datetime
|
||||||
import json
|
import json
|
||||||
|
from io import BytesIO
|
||||||
|
|
||||||
from mpwo_api.tests.utils import add_activity, add_sport, add_user
|
from mpwo_api.tests.utils import add_activity, add_sport, add_user
|
||||||
|
|
||||||
|
gpx_file = (
|
||||||
|
'<?xml version=\'1.0\' encoding=\'UTF-8\'?>'
|
||||||
|
'<gpx xmlns:gpxdata="http://www.cluetrust.com/XML/GPXDATA/1/0" xmlns:gpxtpx="http://www.garmin.com/xmlschemas/TrackPointExtension/v1" xmlns:gpxext="http://www.garmin.com/xmlschemas/GpxExtensions/v3" xmlns="http://www.topografix.com/GPX/1/1">' # noqa
|
||||||
|
' <metadata/>'
|
||||||
|
' <trk>'
|
||||||
|
' <trkseg>'
|
||||||
|
' <trkpt lat="77.2261324" lon="-42.1223054">'
|
||||||
|
' <time>2015-09-20T13:48:44+00:00</time>'
|
||||||
|
' </trkpt>'
|
||||||
|
' <trkpt lat="77.2261324" lon="-42.1223054">'
|
||||||
|
' <time>2015-09-20T13:48:46+00:00</time>'
|
||||||
|
' <ele>223.28399658203125</ele>'
|
||||||
|
' </trkpt>'
|
||||||
|
' <trkpt lat="77.2261324" lon="-42.1223054">'
|
||||||
|
' <time>2015-09-20T13:48:46+00:00</time>'
|
||||||
|
' </trkpt>'
|
||||||
|
' </trkseg>'
|
||||||
|
' </trk>'
|
||||||
|
'</gpx>'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_get_all_activities(app):
|
def test_get_all_activities(app):
|
||||||
add_user('test', 'test@test.com', '12345678')
|
add_user('test', 'test@test.com', '12345678')
|
||||||
@ -54,3 +76,132 @@ def test_get_all_activities(app):
|
|||||||
assert 3600 == data['data']['activities'][0]['duration']
|
assert 3600 == data['data']['activities'][0]['duration']
|
||||||
assert 1024 == data['data']['activities'][1]['duration']
|
assert 1024 == data['data']['activities'][1]['duration']
|
||||||
|
|
||||||
|
|
||||||
|
def test_add_an_activity(app):
|
||||||
|
add_user('test', 'test@test.com', '12345678')
|
||||||
|
add_sport('cycling')
|
||||||
|
|
||||||
|
client = app.test_client()
|
||||||
|
resp_login = client.post(
|
||||||
|
'/api/auth/login',
|
||||||
|
data=json.dumps(dict(
|
||||||
|
email='test@test.com',
|
||||||
|
password='12345678'
|
||||||
|
)),
|
||||||
|
content_type='application/json'
|
||||||
|
)
|
||||||
|
response = client.post(
|
||||||
|
'/api/activities',
|
||||||
|
data=dict(
|
||||||
|
file=(BytesIO(str.encode(gpx_file)), 'example.gpx'),
|
||||||
|
data='{"sport_id": 1}'
|
||||||
|
),
|
||||||
|
headers=dict(
|
||||||
|
content_type='multipart/form-data',
|
||||||
|
Authorization='Bearer ' + json.loads(
|
||||||
|
resp_login.data.decode()
|
||||||
|
)['auth_token']
|
||||||
|
)
|
||||||
|
)
|
||||||
|
data = json.loads(response.data.decode())
|
||||||
|
|
||||||
|
assert response.status_code == 201
|
||||||
|
assert 'created' in data['status']
|
||||||
|
|
||||||
|
|
||||||
|
def test_add_an_activity_invalid_file(app):
|
||||||
|
add_user('test', 'test@test.com', '12345678')
|
||||||
|
add_sport('cycling')
|
||||||
|
|
||||||
|
client = app.test_client()
|
||||||
|
resp_login = client.post(
|
||||||
|
'/api/auth/login',
|
||||||
|
data=json.dumps(dict(
|
||||||
|
email='test@test.com',
|
||||||
|
password='12345678'
|
||||||
|
)),
|
||||||
|
content_type='application/json'
|
||||||
|
)
|
||||||
|
response = client.post(
|
||||||
|
'/api/activities',
|
||||||
|
data=dict(
|
||||||
|
file=(BytesIO(str.encode(gpx_file)), 'example.png'),
|
||||||
|
data='{"sport_id": 1}'
|
||||||
|
),
|
||||||
|
headers=dict(
|
||||||
|
content_type='multipart/form-data',
|
||||||
|
Authorization='Bearer ' + json.loads(
|
||||||
|
resp_login.data.decode()
|
||||||
|
)['auth_token']
|
||||||
|
)
|
||||||
|
)
|
||||||
|
data = json.loads(response.data.decode())
|
||||||
|
|
||||||
|
assert response.status_code == 400
|
||||||
|
assert data['status'] == 'fail'
|
||||||
|
assert data['message'] == 'File extension not allowed.'
|
||||||
|
|
||||||
|
|
||||||
|
def test_add_an_activity_no_sport_id(app):
|
||||||
|
add_user('test', 'test@test.com', '12345678')
|
||||||
|
add_sport('cycling')
|
||||||
|
|
||||||
|
client = app.test_client()
|
||||||
|
resp_login = client.post(
|
||||||
|
'/api/auth/login',
|
||||||
|
data=json.dumps(dict(
|
||||||
|
email='test@test.com',
|
||||||
|
password='12345678'
|
||||||
|
)),
|
||||||
|
content_type='application/json'
|
||||||
|
)
|
||||||
|
response = client.post(
|
||||||
|
'/api/activities',
|
||||||
|
data=dict(
|
||||||
|
file=(BytesIO(str.encode(gpx_file)), 'example.gpx'),
|
||||||
|
data='{}'
|
||||||
|
),
|
||||||
|
headers=dict(
|
||||||
|
content_type='multipart/form-data',
|
||||||
|
Authorization='Bearer ' + json.loads(
|
||||||
|
resp_login.data.decode()
|
||||||
|
)['auth_token']
|
||||||
|
)
|
||||||
|
)
|
||||||
|
data = json.loads(response.data.decode())
|
||||||
|
|
||||||
|
assert response.status_code == 400
|
||||||
|
assert data['status'] == 'error'
|
||||||
|
assert data['message'] == 'Invalid payload.'
|
||||||
|
|
||||||
|
|
||||||
|
def test_add_an_activity_no_file(app):
|
||||||
|
add_user('test', 'test@test.com', '12345678')
|
||||||
|
add_sport('cycling')
|
||||||
|
|
||||||
|
client = app.test_client()
|
||||||
|
resp_login = client.post(
|
||||||
|
'/api/auth/login',
|
||||||
|
data=json.dumps(dict(
|
||||||
|
email='test@test.com',
|
||||||
|
password='12345678'
|
||||||
|
)),
|
||||||
|
content_type='application/json'
|
||||||
|
)
|
||||||
|
response = client.post(
|
||||||
|
'/api/activities',
|
||||||
|
data=dict(
|
||||||
|
data='{}'
|
||||||
|
),
|
||||||
|
headers=dict(
|
||||||
|
content_type='multipart/form-data',
|
||||||
|
Authorization='Bearer ' + json.loads(
|
||||||
|
resp_login.data.decode()
|
||||||
|
)['auth_token']
|
||||||
|
)
|
||||||
|
)
|
||||||
|
data = json.loads(response.data.decode())
|
||||||
|
|
||||||
|
assert response.status_code == 400
|
||||||
|
assert data['status'] == 'fail'
|
||||||
|
assert data['message'] == 'No file part.'
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import datetime
|
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from mpwo_api.tests.utils import add_admin, add_sport, add_user
|
from mpwo_api.tests.utils import add_admin, add_sport, add_user
|
||||||
|
@ -7,7 +7,7 @@ from sqlalchemy import exc, or_
|
|||||||
from werkzeug.utils import secure_filename
|
from werkzeug.utils import secure_filename
|
||||||
|
|
||||||
from .models import User
|
from .models import User
|
||||||
from .utils import allowed_picture, authenticate, register_controls
|
from .utils import authenticate, register_controls, verify_extension
|
||||||
|
|
||||||
auth_blueprint = Blueprint('auth', __name__)
|
auth_blueprint = Blueprint('auth', __name__)
|
||||||
|
|
||||||
@ -236,20 +236,11 @@ def edit_user(user_id):
|
|||||||
@authenticate
|
@authenticate
|
||||||
def edit_picture(user_id):
|
def edit_picture(user_id):
|
||||||
code = 400
|
code = 400
|
||||||
if 'file' not in request.files:
|
response_object = verify_extension('picture', request)
|
||||||
response_object = {'status': 'fail', 'message': 'No file part.'}
|
if response_object['status'] != 'success':
|
||||||
return jsonify(response_object), code
|
|
||||||
file = request.files['file']
|
|
||||||
if file.filename == '':
|
|
||||||
response_object = {'status': 'fail', 'message': 'No selected file.'}
|
|
||||||
return jsonify(response_object), code
|
|
||||||
if not allowed_picture(file.filename):
|
|
||||||
response_object = {
|
|
||||||
'status': 'fail',
|
|
||||||
'message': 'File extension not allowed.'
|
|
||||||
}
|
|
||||||
return jsonify(response_object), code
|
return jsonify(response_object), code
|
||||||
|
|
||||||
|
file = request.files['file']
|
||||||
filename = secure_filename(file.filename)
|
filename = secure_filename(file.filename)
|
||||||
dirpath = os.path.join(
|
dirpath = os.path.join(
|
||||||
current_app.config['UPLOAD_FOLDER'],
|
current_app.config['UPLOAD_FOLDER'],
|
||||||
|
@ -6,12 +6,40 @@ from flask import current_app, jsonify, request
|
|||||||
from .models import User
|
from .models import User
|
||||||
|
|
||||||
|
|
||||||
|
def allowed_activity(filename):
|
||||||
|
return '.' in filename and \
|
||||||
|
filename.rsplit('.', 1)[1].lower() in \
|
||||||
|
current_app.config.get('ACTIVITY_ALLOWED_EXTENSIONS')
|
||||||
|
|
||||||
|
|
||||||
def allowed_picture(filename):
|
def allowed_picture(filename):
|
||||||
return '.' in filename and \
|
return '.' in filename and \
|
||||||
filename.rsplit('.', 1)[1].lower() in \
|
filename.rsplit('.', 1)[1].lower() in \
|
||||||
current_app.config.get('PICTURE_ALLOWED_EXTENSIONS')
|
current_app.config.get('PICTURE_ALLOWED_EXTENSIONS')
|
||||||
|
|
||||||
|
|
||||||
|
def verify_extension(file_type, req):
|
||||||
|
response_object = {'status': 'success'}
|
||||||
|
|
||||||
|
if 'file' not in req.files:
|
||||||
|
response_object = {'status': 'fail', 'message': 'No file part.'}
|
||||||
|
return response_object
|
||||||
|
|
||||||
|
file = req.files['file']
|
||||||
|
if file.filename == '':
|
||||||
|
response_object = {'status': 'fail', 'message': 'No selected file.'}
|
||||||
|
return response_object
|
||||||
|
|
||||||
|
if ((file_type == 'picture' and not allowed_picture(file.filename)) or
|
||||||
|
(file_type == 'activity' and not allowed_activity(file.filename))):
|
||||||
|
response_object = {
|
||||||
|
'status': 'fail',
|
||||||
|
'message': 'File extension not allowed.'
|
||||||
|
}
|
||||||
|
|
||||||
|
return response_object
|
||||||
|
|
||||||
|
|
||||||
def authenticate(f):
|
def authenticate(f):
|
||||||
@wraps(f)
|
@wraps(f)
|
||||||
def decorated_function(*args, **kwargs):
|
def decorated_function(*args, **kwargs):
|
||||||
@ -34,6 +62,7 @@ def authenticate(f):
|
|||||||
if not user:
|
if not user:
|
||||||
return jsonify(response_object), code
|
return jsonify(response_object), code
|
||||||
return f(resp, *args, **kwargs)
|
return f(resp, *args, **kwargs)
|
||||||
|
|
||||||
return decorated_function
|
return decorated_function
|
||||||
|
|
||||||
|
|
||||||
|
@ -13,6 +13,7 @@ Flask-Bcrypt==0.7.1
|
|||||||
Flask-Migrate==2.1.1
|
Flask-Migrate==2.1.1
|
||||||
Flask-SQLAlchemy==2.3.2
|
Flask-SQLAlchemy==2.3.2
|
||||||
Flask-Testing==0.6.2
|
Flask-Testing==0.6.2
|
||||||
|
gpxpy==1.2.0
|
||||||
isort==4.2.15
|
isort==4.2.15
|
||||||
itsdangerous==0.24
|
itsdangerous==0.24
|
||||||
Jinja2==2.10
|
Jinja2==2.10
|
||||||
|
Loading…
Reference in New Issue
Block a user