API: add an activity (w/ gpx file)
This commit is contained in:
		@@ -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 .utils import get_gpx_info
 | 
			
		||||
 | 
			
		||||
activities_blueprint = Blueprint('activities', __name__)
 | 
			
		||||
 | 
			
		||||
@@ -29,3 +37,77 @@ def get_activities(auth_user_id):
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    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'
 | 
			
		||||
    )
 | 
			
		||||
    PICTURE_ALLOWED_EXTENSIONS = {'jpg', 'png', 'gif'}
 | 
			
		||||
    ACTIVITY_ALLOWED_EXTENSIONS = {'gpx'}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class DevelopmentConfig(BaseConfig):
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,30 @@
 | 
			
		||||
import datetime
 | 
			
		||||
import json
 | 
			
		||||
from io import BytesIO
 | 
			
		||||
 | 
			
		||||
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):
 | 
			
		||||
    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 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
 | 
			
		||||
 | 
			
		||||
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 .models import User
 | 
			
		||||
from .utils import allowed_picture, authenticate, register_controls
 | 
			
		||||
from .utils import authenticate, register_controls, verify_extension
 | 
			
		||||
 | 
			
		||||
auth_blueprint = Blueprint('auth', __name__)
 | 
			
		||||
 | 
			
		||||
@@ -236,20 +236,11 @@ def edit_user(user_id):
 | 
			
		||||
@authenticate
 | 
			
		||||
def edit_picture(user_id):
 | 
			
		||||
    code = 400
 | 
			
		||||
    if 'file' not in request.files:
 | 
			
		||||
        response_object = {'status': 'fail', 'message': 'No file part.'}
 | 
			
		||||
        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.'
 | 
			
		||||
        }
 | 
			
		||||
    response_object = verify_extension('picture', request)
 | 
			
		||||
    if response_object['status'] != 'success':
 | 
			
		||||
        return jsonify(response_object), code
 | 
			
		||||
 | 
			
		||||
    file = request.files['file']
 | 
			
		||||
    filename = secure_filename(file.filename)
 | 
			
		||||
    dirpath = os.path.join(
 | 
			
		||||
        current_app.config['UPLOAD_FOLDER'],
 | 
			
		||||
 
 | 
			
		||||
@@ -6,12 +6,40 @@ from flask import current_app, jsonify, request
 | 
			
		||||
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):
 | 
			
		||||
    return '.' in filename and \
 | 
			
		||||
           filename.rsplit('.', 1)[1].lower() in \
 | 
			
		||||
           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):
 | 
			
		||||
    @wraps(f)
 | 
			
		||||
    def decorated_function(*args, **kwargs):
 | 
			
		||||
@@ -34,6 +62,7 @@ def authenticate(f):
 | 
			
		||||
        if not user:
 | 
			
		||||
            return jsonify(response_object), code
 | 
			
		||||
        return f(resp, *args, **kwargs)
 | 
			
		||||
 | 
			
		||||
    return decorated_function
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -13,6 +13,7 @@ Flask-Bcrypt==0.7.1
 | 
			
		||||
Flask-Migrate==2.1.1
 | 
			
		||||
Flask-SQLAlchemy==2.3.2
 | 
			
		||||
Flask-Testing==0.6.2
 | 
			
		||||
gpxpy==1.2.0
 | 
			
		||||
isort==4.2.15
 | 
			
		||||
itsdangerous==0.24
 | 
			
		||||
Jinja2==2.10
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user