From e50c3d3799075b1f6a037aa4f46c8f312eadd798 Mon Sep 17 00:00:00 2001 From: Sam Date: Mon, 29 Nov 2021 16:54:24 +0100 Subject: [PATCH] [API] add endpoint to download gpx file --- .../tests/workouts/test_workouts_api_0_get.py | 86 +++++++++++++++++++ fittrackee/workouts/workouts.py | 68 ++++++++++++++- 2 files changed, 153 insertions(+), 1 deletion(-) diff --git a/fittrackee/tests/workouts/test_workouts_api_0_get.py b/fittrackee/tests/workouts/test_workouts_api_0_get.py index eb6e0b8d..67968074 100644 --- a/fittrackee/tests/workouts/test_workouts_api_0_get.py +++ b/fittrackee/tests/workouts/test_workouts_api_0_get.py @@ -1139,3 +1139,89 @@ class TestGetWorkout(ApiTestCaseMixin): assert response.status_code == 404 assert 'not found' in data['status'] assert 'Map does not exist' in data['message'] + + +class TestDownloadWorkoutGpx(ApiTestCaseMixin): + def test_it_returns_404_if_workout_does_not_exist( + self, + app: Flask, + user_1: User, + ) -> None: + client, auth_token = self.get_test_client_and_auth_token(app) + + response = client.get( + f'/api/workouts/{get_random_short_id()}/gpx/download', + headers=dict(Authorization=f'Bearer {auth_token}'), + ) + + data = json.loads(response.data.decode()) + assert response.status_code == 404 + assert 'not found' in data['status'] + assert 'workout not found' in data['message'] + + def test_it_returns_404_if_workout_does_not_have_gpx( + self, + app: Flask, + user_1: User, + sport_1_cycling: Sport, + workout_cycling_user_1: Workout, + ) -> None: + client, auth_token = self.get_test_client_and_auth_token(app) + + response = client.get( + f'/api/workouts/{workout_cycling_user_1.short_id}/gpx/download', + headers=dict(Authorization=f'Bearer {auth_token}'), + ) + + data = json.loads(response.data.decode()) + assert response.status_code == 404 + assert 'not found' in data['status'] + assert 'no gpx file for workout' in data['message'] + + def test_it_returns_404_if_workout_belongs_to_a_different_user( + self, + app: Flask, + user_1: User, + user_2: User, + sport_1_cycling: Sport, + workout_cycling_user_2: Workout, + ) -> None: + client, auth_token = self.get_test_client_and_auth_token(app) + + response = client.get( + f'/api/workouts/{workout_cycling_user_2.short_id}/gpx/download', + headers=dict(Authorization=f'Bearer {auth_token}'), + ) + + data = json.loads(response.data.decode()) + assert response.status_code == 404 + assert 'not found' in data['status'] + assert 'workout not found' in data['message'] + + def test_it_calls_send_from_directory_if_workout_has_gpx( + self, + app: Flask, + user_1: User, + sport_1_cycling: Sport, + workout_cycling_user_1: Workout, + ) -> None: + gpx_file_path = 'file.gpx' + workout_cycling_user_1.gpx = gpx_file_path + with patch('fittrackee.workouts.workouts.send_from_directory') as mock: + mock.return_value = 'file' + client, auth_token = self.get_test_client_and_auth_token(app) + + client.get( + ( + f'/api/workouts/{workout_cycling_user_1.short_id}/' + 'gpx/download' + ), + headers=dict(Authorization=f'Bearer {auth_token}'), + ) + + mock.assert_called_once_with( + app.config['UPLOAD_FOLDER'], + gpx_file_path, + mimetype='application/gpx+xml', + as_attachment=True, + ) diff --git a/fittrackee/workouts/workouts.py b/fittrackee/workouts/workouts.py index 652009c4..3c32ba03 100644 --- a/fittrackee/workouts/workouts.py +++ b/fittrackee/workouts/workouts.py @@ -5,7 +5,14 @@ from datetime import timedelta from typing import Any, Dict, List, Optional, Tuple, Union import requests -from flask import Blueprint, Response, current_app, request, send_file +from flask import ( + Blueprint, + Response, + current_app, + request, + send_file, + send_from_directory, +) from sqlalchemy import exc from werkzeug.exceptions import RequestEntityTooLarge @@ -703,6 +710,65 @@ def get_segment_chart_data( ) +@workouts_blueprint.route( + '/workouts//gpx/download', methods=['GET'] +) +@authenticate +def download_workout_gpx( + auth_user_id: int, workout_short_id: str +) -> Union[HttpResponse, Response]: + """ + Download gpx file + + **Example request**: + + .. sourcecode:: http + + GET /api/workouts/kjxavSTUrJvoAh2wvCeGEF/gpx/download HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/gpx+xml + + :param integer auth_user_id: authenticate user id (from JSON Web Token) + :param string workout_short_id: workout short id + + :statuscode 200: success + :statuscode 401: + - provide a valid auth token + - signature expired, please log in again + - invalid token, please log in again + :statuscode 404: + - workout not found + - no gpx file for workout + """ + workout_uuid = decode_short_id(workout_short_id) + workout = Workout.query.filter_by( + uuid=workout_uuid, user_id=auth_user_id + ).first() + if not workout: + return DataNotFoundErrorResponse( + data_type='workout', + message=f'workout not found (id: {workout_short_id})', + ) + + if workout.gpx is None: + return DataNotFoundErrorResponse( + data_type='gpx', + message=f'no gpx file for workout (id: {workout_short_id})', + ) + + return send_from_directory( + current_app.config['UPLOAD_FOLDER'], + workout.gpx, + mimetype='application/gpx+xml', + as_attachment=True, + ) + + @workouts_blueprint.route('/workouts/map/', methods=['GET']) def get_map(map_id: int) -> Any: """