diff --git a/mpwo_api/mpwo_api/activities/activities.py b/mpwo_api/mpwo_api/activities/activities.py index d5fe4a63..f112e070 100644 --- a/mpwo_api/mpwo_api/activities/activities.py +++ b/mpwo_api/mpwo_api/activities/activities.py @@ -9,7 +9,7 @@ from ..users.utils import authenticate, verify_extension from .models import Activity from .utils import ( ActivityException, create_activity, edit_activity, get_chart_data, - handle_one_activity + process_files ) activities_blueprint = Blueprint('activities', __name__) @@ -146,16 +146,27 @@ def post_activity(auth_user_id): activity_file = request.files['file'] try: - new_activity = handle_one_activity( - auth_user_id, activity_file, activity_data + new_activities = process_files( + auth_user_id, activity_data, activity_file ) - response_object = { - 'status': 'created', - 'data': { - 'activities': [new_activity.serialize()] + if len(new_activities) > 0: + response_object = { + 'status': 'created', + 'data': { + 'activities': [new_activity.serialize() + for new_activity in new_activities] + } } - } - return jsonify(response_object), 201 + code = 201 + else: + response_object = { + 'status': 'fail', + 'data': { + 'activities': [] + } + } + code = 400 + return jsonify(response_object), code except ActivityException as e: db.session.rollback() if e.e: diff --git a/mpwo_api/mpwo_api/activities/utils.py b/mpwo_api/mpwo_api/activities/utils.py index c1687769..a5a52e21 100644 --- a/mpwo_api/mpwo_api/activities/utils.py +++ b/mpwo_api/mpwo_api/activities/utils.py @@ -1,5 +1,6 @@ import os import tempfile +import zipfile from datetime import datetime, timedelta import gpxpy.gpx @@ -248,12 +249,8 @@ def get_new_file_path(auth_user_id, activity_date, old_filename, sport): return file_path -def handle_one_activity(auth_user_id, activity_file, activity_data): - filename = secure_filename(activity_file.filename) - file_path = get_file_path(auth_user_id, filename) - +def process_one_gpx_file(auth_user_id, activity_data, file_path, filename): try: - activity_file.save(file_path) gpx_data = get_gpx_info(file_path) sport = Sport.query.filter_by(id=activity_data.get('sport_id')).first() @@ -285,3 +282,43 @@ def handle_one_activity(auth_user_id, activity_file, activity_data): raise ActivityException( 'fail', 'Error during activity save.', e ) + + +def process_zip_archive(auth_user_id, activity_data, zip_path): + extract_dir = os.path.join( + current_app.config['UPLOAD_FOLDER'], + 'activities', + str(auth_user_id), + 'extract') + with zipfile.ZipFile(zip_path, "r") as zip_ref: + zip_ref.extractall(extract_dir) + + new_activities = [] + + for gpx_file in os.listdir(extract_dir): + if ('.' in gpx_file and gpx_file.rsplit('.', 1)[1].lower() + in current_app.config.get('ACTIVITY_ALLOWED_EXTENSIONS')): + file_path = os.path.join(extract_dir, gpx_file) + new_activity = process_one_gpx_file(auth_user_id, activity_data, + file_path, gpx_file) + new_activities.append(new_activity) + + return new_activities + + +def process_files(auth_user_id, activity_data, activity_file): + filename = secure_filename(activity_file.filename) + extension = f".{filename.rsplit('.', 1)[1].lower()}" + file_path = get_file_path(auth_user_id, filename) + + try: + activity_file.save(file_path) + except Exception as e: + raise ActivityException('error', 'Error during activity file save.', e) + + if extension == ".gpx": + return [process_one_gpx_file( + auth_user_id, activity_data, file_path, filename + )] + else: + return process_zip_archive(auth_user_id, activity_data, file_path) diff --git a/mpwo_api/mpwo_api/config.py b/mpwo_api/mpwo_api/config.py index e5279767..11657453 100644 --- a/mpwo_api/mpwo_api/config.py +++ b/mpwo_api/mpwo_api/config.py @@ -15,7 +15,7 @@ class BaseConfig: current_app.root_path, 'uploads' ) PICTURE_ALLOWED_EXTENSIONS = {'jpg', 'png', 'gif'} - ACTIVITY_ALLOWED_EXTENSIONS = {'gpx'} + ACTIVITY_ALLOWED_EXTENSIONS = {'gpx', 'zip'} class DevelopmentConfig(BaseConfig): diff --git a/mpwo_api/mpwo_api/tests/files/gpx_test.zip b/mpwo_api/mpwo_api/tests/files/gpx_test.zip new file mode 100644 index 00000000..29e1e649 Binary files /dev/null and b/mpwo_api/mpwo_api/tests/files/gpx_test.zip differ diff --git a/mpwo_api/mpwo_api/tests/files/gpx_test_folder.zip b/mpwo_api/mpwo_api/tests/files/gpx_test_folder.zip new file mode 100644 index 00000000..8760cf8a Binary files /dev/null and b/mpwo_api/mpwo_api/tests/files/gpx_test_folder.zip differ diff --git a/mpwo_api/mpwo_api/tests/files/gpx_test_incorrect.zip b/mpwo_api/mpwo_api/tests/files/gpx_test_incorrect.zip new file mode 100644 index 00000000..bd7a8dac Binary files /dev/null and b/mpwo_api/mpwo_api/tests/files/gpx_test_incorrect.zip differ diff --git a/mpwo_api/mpwo_api/tests/test_activities_api_1_post.py b/mpwo_api/mpwo_api/tests/test_activities_api_1_post.py index 4bcb2ed0..06af8f8d 100644 --- a/mpwo_api/mpwo_api/tests/test_activities_api_1_post.py +++ b/mpwo_api/mpwo_api/tests/test_activities_api_1_post.py @@ -1,4 +1,5 @@ import json +import os from io import BytesIO @@ -645,3 +646,109 @@ def test_add_activity_zero_value( assert len(data['data']['activities'][0]['segments']) == 0 assert len(data['data']['activities'][0]['records']) == 0 + + +def test_get_an_activity_with_zip(app, user_1, sport_1_cycling): + file_path = os.path.join(app.root_path, 'tests/files/gpx_test.zip') + # 'gpx_test.zip' contains 3 gpx files (same data) and 1 non-gpx file + + with open(file_path, 'rb') as zip_file: + 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=(zip_file, 'gpx_test.zip'), + 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'] + assert len(data['data']['activities']) == 3 + assert 'just an activity' == data['data']['activities'][0]['title'] + assert_activity_data_with_gpx(data) + + +def test_get_an_activity_with_zip_folder(app, user_1, sport_1_cycling): + file_path = os.path.join(app.root_path, 'tests/files/gpx_test_folder.zip') + # 'gpx_test_folder.zip' contains 3 gpx files (same data) and 1 non-gpx file + # in a folder + + with open(file_path, 'rb') as zip_file: + 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=(zip_file, 'gpx_test_folder.zip'), + 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 'fail' in data['status'] + assert len(data['data']['activities']) == 0 + + +def test_get_an_activity_with_zip_incorrect_file(app, user_1, sport_1_cycling): + file_path = os.path.join(app.root_path, 'tests/files/gpx_test_incorrect.zip') # noqa + # 'gpx_test_incorrect.zip' contains 2 gpx files, one is incorrect + + with open(file_path, 'rb') as zip_file: + 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=(zip_file, 'gpx_test_incorrect.zip'), + 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 == 500 + assert 'error' in data['status'] + assert 'Error during gpx file parsing.' in data['message'] + assert 'data' not in data