2018-05-01 17:51:38 +02:00
|
|
|
import json
|
2018-05-09 18:54:30 +02:00
|
|
|
import os
|
2018-05-29 19:06:33 +02:00
|
|
|
import shutil
|
2018-06-07 22:44:52 +02:00
|
|
|
from datetime import datetime, timedelta
|
2021-01-02 19:28:03 +01:00
|
|
|
from typing import Any, Dict, List, Optional, Tuple, Union
|
2018-01-21 17:43:13 +01:00
|
|
|
|
2020-09-16 11:47:20 +02:00
|
|
|
import requests
|
2020-09-16 15:41:02 +02:00
|
|
|
from fittrackee import appLog, db
|
2021-01-01 16:39:25 +01:00
|
|
|
from fittrackee.responses import (
|
|
|
|
DataInvalidPayloadErrorResponse,
|
|
|
|
DataNotFoundErrorResponse,
|
2021-01-02 19:28:03 +01:00
|
|
|
HttpResponse,
|
2021-01-01 16:39:25 +01:00
|
|
|
InternalServerErrorResponse,
|
|
|
|
InvalidPayloadErrorResponse,
|
|
|
|
NotFoundErrorResponse,
|
|
|
|
handle_error_and_return_response,
|
|
|
|
)
|
|
|
|
from flask import Blueprint, Response, current_app, request, send_file
|
2018-05-01 17:51:38 +02:00
|
|
|
from sqlalchemy import exc
|
|
|
|
|
2018-06-15 10:50:37 +02:00
|
|
|
from ..users.utils import (
|
2019-08-28 13:25:39 +02:00
|
|
|
User,
|
|
|
|
authenticate,
|
|
|
|
can_view_activity,
|
2019-08-31 14:11:00 +02:00
|
|
|
verify_extension_and_size,
|
2018-06-15 10:50:37 +02:00
|
|
|
)
|
2018-05-29 12:53:13 +02:00
|
|
|
from .models import Activity
|
2018-05-09 18:23:17 +02:00
|
|
|
from .utils import (
|
2019-08-28 13:25:39 +02:00
|
|
|
ActivityException,
|
|
|
|
create_activity,
|
|
|
|
edit_activity,
|
|
|
|
get_absolute_file_path,
|
|
|
|
get_datetime_with_tz,
|
|
|
|
process_files,
|
2018-05-09 18:23:17 +02:00
|
|
|
)
|
2018-06-07 21:01:46 +02:00
|
|
|
from .utils_format import convert_in_duration
|
2019-08-25 18:54:33 +02:00
|
|
|
from .utils_gpx import (
|
2019-08-28 13:25:39 +02:00
|
|
|
ActivityGPXException,
|
|
|
|
extract_segment_from_gpx_file,
|
|
|
|
get_chart_data,
|
2019-08-25 18:54:33 +02:00
|
|
|
)
|
2020-12-30 22:07:43 +01:00
|
|
|
from .utils_id import decode_short_id
|
2018-01-21 17:43:13 +01:00
|
|
|
|
|
|
|
activities_blueprint = Blueprint('activities', __name__)
|
|
|
|
|
2020-05-02 18:00:17 +02:00
|
|
|
ACTIVITIES_PER_PAGE = 5
|
|
|
|
|
2018-01-21 17:43:13 +01:00
|
|
|
|
|
|
|
@activities_blueprint.route('/activities', methods=['GET'])
|
|
|
|
@authenticate
|
2021-01-02 19:28:03 +01:00
|
|
|
def get_activities(auth_user_id: int) -> Union[Dict, HttpResponse]:
|
2019-07-14 16:57:16 +02:00
|
|
|
"""
|
|
|
|
Get activities for the authenticated user.
|
|
|
|
|
|
|
|
**Example requests**:
|
|
|
|
|
2019-07-14 21:36:51 +02:00
|
|
|
- without parameters
|
2019-07-14 16:57:16 +02:00
|
|
|
|
|
|
|
.. sourcecode:: http
|
|
|
|
|
|
|
|
GET /api/activities/ HTTP/1.1
|
|
|
|
|
|
|
|
- with some query parameters
|
|
|
|
|
|
|
|
.. sourcecode:: http
|
|
|
|
|
2019-07-14 18:18:41 +02:00
|
|
|
GET /api/activities?from=2019-07-02&to=2019-07-31&sport_id=1 HTTP/1.1
|
2019-07-14 16:57:16 +02:00
|
|
|
|
|
|
|
**Example responses**:
|
|
|
|
|
|
|
|
- returning at least one activity
|
|
|
|
|
|
|
|
.. sourcecode:: http
|
|
|
|
|
|
|
|
HTTP/1.1 200 OK
|
|
|
|
Content-Type: application/json
|
|
|
|
|
|
|
|
{
|
|
|
|
"data": {
|
|
|
|
"activities": [
|
|
|
|
{
|
|
|
|
"activity_date": "Mon, 01 Jan 2018 00:00:00 GMT",
|
|
|
|
"ascent": null,
|
|
|
|
"ave_speed": 10.0,
|
|
|
|
"bounds": [],
|
|
|
|
"creation_date": "Sun, 14 Jul 2019 13:51:01 GMT",
|
|
|
|
"descent": null,
|
|
|
|
"distance": 10.0,
|
|
|
|
"duration": "0:17:04",
|
2020-12-30 22:07:43 +01:00
|
|
|
"id": "kjxavSTUrJvoAh2wvCeGEF",
|
2019-07-14 16:57:16 +02:00
|
|
|
"map": null,
|
|
|
|
"max_alt": null,
|
|
|
|
"max_speed": 10.0,
|
|
|
|
"min_alt": null,
|
|
|
|
"modification_date": null,
|
|
|
|
"moving": "0:17:04",
|
|
|
|
"next_activity": 3,
|
|
|
|
"notes": null,
|
|
|
|
"pauses": null,
|
|
|
|
"previous_activity": null,
|
|
|
|
"records": [
|
|
|
|
{
|
|
|
|
"activity_date": "Mon, 01 Jan 2018 00:00:00 GMT",
|
2020-12-30 22:07:43 +01:00
|
|
|
"activity_id": "kjxavSTUrJvoAh2wvCeGEF",
|
2019-07-14 16:57:16 +02:00
|
|
|
"id": 4,
|
|
|
|
"record_type": "MS",
|
|
|
|
"sport_id": 1,
|
2020-02-08 15:17:07 +01:00
|
|
|
"user": "admin",
|
2019-07-14 16:57:16 +02:00
|
|
|
"value": 10.0
|
|
|
|
},
|
|
|
|
{
|
|
|
|
"activity_date": "Mon, 01 Jan 2018 00:00:00 GMT",
|
2020-12-30 22:07:43 +01:00
|
|
|
"activity_id": "kjxavSTUrJvoAh2wvCeGEF",
|
2019-07-14 16:57:16 +02:00
|
|
|
"id": 3,
|
|
|
|
"record_type": "LD",
|
|
|
|
"sport_id": 1,
|
2020-02-08 15:17:07 +01:00
|
|
|
"user": "admin",
|
2019-07-14 16:57:16 +02:00
|
|
|
"value": "0:17:04"
|
|
|
|
},
|
|
|
|
{
|
|
|
|
"activity_date": "Mon, 01 Jan 2018 00:00:00 GMT",
|
2020-12-30 22:07:43 +01:00
|
|
|
"activity_id": "kjxavSTUrJvoAh2wvCeGEF",
|
2019-07-14 16:57:16 +02:00
|
|
|
"id": 2,
|
|
|
|
"record_type": "FD",
|
|
|
|
"sport_id": 1,
|
2020-02-08 15:17:07 +01:00
|
|
|
"user": "admin",
|
2019-07-14 16:57:16 +02:00
|
|
|
"value": 10.0
|
|
|
|
},
|
|
|
|
{
|
|
|
|
"activity_date": "Mon, 01 Jan 2018 00:00:00 GMT",
|
2020-12-30 22:07:43 +01:00
|
|
|
"activity_id": "kjxavSTUrJvoAh2wvCeGEF",
|
2019-07-14 16:57:16 +02:00
|
|
|
"id": 1,
|
|
|
|
"record_type": "AS",
|
|
|
|
"sport_id": 1,
|
2020-02-08 15:17:07 +01:00
|
|
|
"user": "admin",
|
2019-07-14 16:57:16 +02:00
|
|
|
"value": 10.0
|
|
|
|
}
|
|
|
|
],
|
|
|
|
"segments": [],
|
|
|
|
"sport_id": 1,
|
|
|
|
"title": null,
|
2020-02-08 15:17:07 +01:00
|
|
|
"user": "admin",
|
2019-07-14 16:57:16 +02:00
|
|
|
"weather_end": null,
|
|
|
|
"weather_start": null,
|
|
|
|
"with_gpx": false
|
|
|
|
}
|
|
|
|
]
|
|
|
|
},
|
|
|
|
"status": "success"
|
|
|
|
}
|
|
|
|
|
|
|
|
- returning no activities
|
|
|
|
|
|
|
|
.. sourcecode:: http
|
|
|
|
|
|
|
|
HTTP/1.1 200 OK
|
|
|
|
Content-Type: application/json
|
|
|
|
|
|
|
|
{
|
|
|
|
"data": {
|
|
|
|
"activities": []
|
|
|
|
},
|
|
|
|
"status": "success"
|
|
|
|
}
|
|
|
|
|
2019-07-20 21:57:35 +02:00
|
|
|
:param integer auth_user_id: authenticate user id (from JSON Web Token)
|
2019-07-14 16:57:16 +02:00
|
|
|
|
|
|
|
:query integer page: page if using pagination (default: 1)
|
2020-05-02 18:00:17 +02:00
|
|
|
:query integer per_page: number of activities per page
|
|
|
|
(default: 5, max: 50)
|
2019-07-14 16:57:16 +02:00
|
|
|
:query integer sport_id: sport id
|
2019-07-14 21:36:51 +02:00
|
|
|
:query string from: start date (format: ``%Y-%m-%d``)
|
|
|
|
:query string to: end date (format: ``%Y-%m-%d``)
|
2019-07-14 16:57:16 +02:00
|
|
|
:query float distance_from: minimal distance
|
|
|
|
:query float distance_to: maximal distance
|
2019-07-14 21:36:51 +02:00
|
|
|
:query string duration_from: minimal duration (format: ``%H:%M``)
|
|
|
|
:query string duration_to: maximal distance (format: ``%H:%M``)
|
2019-07-14 16:57:16 +02:00
|
|
|
:query float ave_speed_from: minimal average speed
|
|
|
|
:query float ave_speed_to: maximal average speed
|
|
|
|
:query float max_speed_from: minimal max. speed
|
|
|
|
:query float max_speed_to: maximal max. speed
|
2019-07-14 21:36:51 +02:00
|
|
|
:query string order: sorting order (default: ``desc``)
|
2019-07-14 16:57:16 +02:00
|
|
|
|
|
|
|
:reqheader Authorization: OAuth 2.0 Bearer Token
|
|
|
|
|
2019-07-19 11:56:48 +02:00
|
|
|
:statuscode 200: success
|
2019-07-20 21:57:35 +02:00
|
|
|
:statuscode 401:
|
|
|
|
- Provide a valid auth token.
|
|
|
|
- Signature expired. Please log in again.
|
|
|
|
- Invalid token. Please log in again.
|
2019-07-14 16:57:16 +02:00
|
|
|
:statuscode 500:
|
|
|
|
|
|
|
|
"""
|
2018-05-10 23:39:59 +02:00
|
|
|
try:
|
2018-06-11 19:38:20 +02:00
|
|
|
user = User.query.filter_by(id=auth_user_id).first()
|
2018-05-10 23:39:59 +02:00
|
|
|
params = request.args.copy()
|
2018-06-04 11:06:52 +02:00
|
|
|
page = 1 if 'page' not in params.keys() else int(params.get('page'))
|
|
|
|
date_from = params.get('from')
|
2018-06-11 18:11:37 +02:00
|
|
|
if date_from:
|
|
|
|
date_from = datetime.strptime(date_from, '%Y-%m-%d')
|
2018-06-11 19:38:20 +02:00
|
|
|
_, date_from = get_datetime_with_tz(user.timezone, date_from)
|
2018-06-04 11:06:52 +02:00
|
|
|
date_to = params.get('to')
|
2018-06-11 18:11:37 +02:00
|
|
|
if date_to:
|
2019-08-28 13:25:39 +02:00
|
|
|
date_to = datetime.strptime(
|
|
|
|
f'{date_to} 23:59:59', '%Y-%m-%d %H:%M:%S'
|
|
|
|
)
|
2018-06-11 19:38:20 +02:00
|
|
|
_, date_to = get_datetime_with_tz(user.timezone, date_to)
|
2018-06-07 19:28:06 +02:00
|
|
|
distance_from = params.get('distance_from')
|
|
|
|
distance_to = params.get('distance_to')
|
|
|
|
duration_from = params.get('duration_from')
|
|
|
|
duration_to = params.get('duration_to')
|
|
|
|
ave_speed_from = params.get('ave_speed_from')
|
|
|
|
ave_speed_to = params.get('ave_speed_to')
|
2018-06-11 22:42:04 +02:00
|
|
|
max_speed_from = params.get('max_speed_from')
|
|
|
|
max_speed_to = params.get('max_speed_to')
|
2018-06-04 14:38:48 +02:00
|
|
|
order = params.get('order')
|
2018-06-07 20:31:44 +02:00
|
|
|
sport_id = params.get('sport_id')
|
2020-05-02 18:00:17 +02:00
|
|
|
per_page = (
|
|
|
|
int(params.get('per_page'))
|
|
|
|
if params.get('per_page')
|
|
|
|
else ACTIVITIES_PER_PAGE
|
|
|
|
)
|
|
|
|
if per_page > 50:
|
|
|
|
per_page = 50
|
2019-08-28 13:25:39 +02:00
|
|
|
activities = (
|
|
|
|
Activity.query.filter(
|
|
|
|
Activity.user_id == auth_user_id,
|
|
|
|
Activity.sport_id == sport_id if sport_id else True,
|
|
|
|
Activity.activity_date >= date_from if date_from else True,
|
|
|
|
Activity.activity_date < date_to + timedelta(seconds=1)
|
|
|
|
if date_to
|
|
|
|
else True,
|
|
|
|
Activity.distance >= int(distance_from)
|
|
|
|
if distance_from
|
|
|
|
else True,
|
|
|
|
Activity.distance <= int(distance_to) if distance_to else True,
|
|
|
|
Activity.moving >= convert_in_duration(duration_from)
|
|
|
|
if duration_from
|
|
|
|
else True,
|
|
|
|
Activity.moving <= convert_in_duration(duration_to)
|
|
|
|
if duration_to
|
|
|
|
else True,
|
|
|
|
Activity.ave_speed >= float(ave_speed_from)
|
|
|
|
if ave_speed_from
|
|
|
|
else True,
|
|
|
|
Activity.ave_speed <= float(ave_speed_to)
|
|
|
|
if ave_speed_to
|
|
|
|
else True,
|
|
|
|
Activity.max_speed >= float(max_speed_from)
|
|
|
|
if max_speed_from
|
|
|
|
else True,
|
|
|
|
Activity.max_speed <= float(max_speed_to)
|
|
|
|
if max_speed_to
|
|
|
|
else True,
|
|
|
|
)
|
|
|
|
.order_by(
|
|
|
|
Activity.activity_date.asc()
|
|
|
|
if order == 'asc'
|
|
|
|
else Activity.activity_date.desc()
|
|
|
|
)
|
|
|
|
.paginate(page, per_page, False)
|
|
|
|
.items
|
|
|
|
)
|
2021-01-01 16:39:25 +01:00
|
|
|
return {
|
2018-05-10 23:39:59 +02:00
|
|
|
'status': 'success',
|
|
|
|
'data': {
|
2019-08-28 13:25:39 +02:00
|
|
|
'activities': [
|
|
|
|
activity.serialize(params) for activity in activities
|
|
|
|
]
|
|
|
|
},
|
2018-01-21 17:43:13 +01:00
|
|
|
}
|
2018-05-10 23:39:59 +02:00
|
|
|
except Exception as e:
|
2021-01-01 16:39:25 +01:00
|
|
|
return handle_error_and_return_response(e)
|
2018-05-01 17:51:38 +02:00
|
|
|
|
|
|
|
|
2020-12-30 19:37:59 +01:00
|
|
|
@activities_blueprint.route(
|
2020-12-30 22:07:43 +01:00
|
|
|
'/activities/<string:activity_short_id>', methods=['GET']
|
2020-12-30 19:37:59 +01:00
|
|
|
)
|
2018-05-01 21:26:17 +02:00
|
|
|
@authenticate
|
2021-01-02 19:28:03 +01:00
|
|
|
def get_activity(
|
|
|
|
auth_user_id: int, activity_short_id: str
|
|
|
|
) -> Union[Dict, HttpResponse]:
|
2019-07-14 21:36:51 +02:00
|
|
|
"""
|
|
|
|
Get an activity
|
|
|
|
|
|
|
|
**Example request**:
|
|
|
|
|
|
|
|
.. sourcecode:: http
|
|
|
|
|
2020-12-30 22:07:43 +01:00
|
|
|
GET /api/activities/kjxavSTUrJvoAh2wvCeGEF HTTP/1.1
|
2019-07-14 21:36:51 +02:00
|
|
|
|
2019-07-19 11:56:48 +02:00
|
|
|
**Example responses**:
|
|
|
|
|
|
|
|
- success
|
2019-07-14 21:36:51 +02:00
|
|
|
|
|
|
|
.. sourcecode:: http
|
|
|
|
|
|
|
|
HTTP/1.1 200 OK
|
|
|
|
Content-Type: application/json
|
|
|
|
|
|
|
|
{
|
|
|
|
"data": {
|
|
|
|
"activities": [
|
|
|
|
{
|
|
|
|
"activity_date": "Sun, 07 Jul 2019 07:00:00 GMT",
|
|
|
|
"ascent": null,
|
|
|
|
"ave_speed": 16,
|
|
|
|
"bounds": [],
|
|
|
|
"creation_date": "Sun, 14 Jul 2019 18:57:14 GMT",
|
|
|
|
"descent": null,
|
|
|
|
"distance": 12,
|
|
|
|
"duration": "0:45:00",
|
2020-12-30 22:07:43 +01:00
|
|
|
"id": "kjxavSTUrJvoAh2wvCeGEF",
|
2019-07-14 21:36:51 +02:00
|
|
|
"map": null,
|
|
|
|
"max_alt": null,
|
|
|
|
"max_speed": 16,
|
|
|
|
"min_alt": null,
|
|
|
|
"modification_date": "Sun, 14 Jul 2019 18:57:22 GMT",
|
|
|
|
"moving": "0:45:00",
|
|
|
|
"next_activity": 4,
|
|
|
|
"notes": "activity without gpx",
|
|
|
|
"pauses": null,
|
|
|
|
"previous_activity": 3,
|
|
|
|
"records": [],
|
|
|
|
"segments": [],
|
|
|
|
"sport_id": 1,
|
|
|
|
"title": "biking on sunday morning",
|
2020-02-08 15:17:07 +01:00
|
|
|
"user": "admin",
|
2019-07-14 21:36:51 +02:00
|
|
|
"weather_end": null,
|
|
|
|
"weather_start": null,
|
|
|
|
"with_gpx": false
|
|
|
|
}
|
|
|
|
]
|
|
|
|
},
|
|
|
|
"status": "success"
|
|
|
|
}
|
|
|
|
|
2019-07-19 11:56:48 +02:00
|
|
|
- acitivity not found:
|
|
|
|
|
|
|
|
.. sourcecode:: http
|
|
|
|
|
|
|
|
HTTP/1.1 404 NOT FOUND
|
|
|
|
Content-Type: application/json
|
|
|
|
|
|
|
|
{
|
|
|
|
"data": {
|
|
|
|
"activities": []
|
|
|
|
},
|
|
|
|
"status": "not found"
|
|
|
|
}
|
|
|
|
|
2019-07-20 21:57:35 +02:00
|
|
|
:param integer auth_user_id: authenticate user id (from JSON Web Token)
|
2020-12-30 22:07:43 +01:00
|
|
|
:param string activity_short_id: activity short id
|
2019-07-14 21:36:51 +02:00
|
|
|
|
|
|
|
:reqheader Authorization: OAuth 2.0 Bearer Token
|
|
|
|
|
2019-07-19 11:56:48 +02:00
|
|
|
:statuscode 200: success
|
2019-07-20 21:57:35 +02:00
|
|
|
:statuscode 401:
|
|
|
|
- Provide a valid auth token.
|
|
|
|
- Signature expired. Please log in again.
|
|
|
|
- Invalid token. Please log in again.
|
2019-09-16 17:54:21 +02:00
|
|
|
:statuscode 403: You do not have permissions.
|
2019-07-19 11:56:48 +02:00
|
|
|
:statuscode 404: activity not found
|
2019-07-14 21:36:51 +02:00
|
|
|
|
|
|
|
"""
|
2020-12-30 22:07:43 +01:00
|
|
|
activity_uuid = decode_short_id(activity_short_id)
|
2020-12-30 19:37:59 +01:00
|
|
|
activity = Activity.query.filter_by(uuid=activity_uuid).first()
|
2021-01-01 16:39:25 +01:00
|
|
|
if not activity:
|
|
|
|
return DataNotFoundErrorResponse('activities')
|
2018-05-01 21:26:17 +02:00
|
|
|
|
2021-01-01 16:39:25 +01:00
|
|
|
error_response = can_view_activity(auth_user_id, activity.user_id)
|
|
|
|
if error_response:
|
|
|
|
return error_response
|
|
|
|
|
|
|
|
return {
|
|
|
|
'status': 'success',
|
|
|
|
'data': {'activities': [activity.serialize()]},
|
2018-05-02 17:01:31 +02:00
|
|
|
}
|
2018-05-01 21:26:17 +02:00
|
|
|
|
|
|
|
|
2020-12-30 22:07:43 +01:00
|
|
|
def get_activity_data(
|
2021-01-02 19:28:03 +01:00
|
|
|
auth_user_id: int,
|
|
|
|
activity_short_id: str,
|
|
|
|
data_type: str,
|
|
|
|
segment_id: Optional[int] = None,
|
|
|
|
) -> Union[Dict, HttpResponse]:
|
2019-07-19 11:56:48 +02:00
|
|
|
"""Get data from an activity gpx file"""
|
2020-12-30 22:07:43 +01:00
|
|
|
activity_uuid = decode_short_id(activity_short_id)
|
2020-12-30 19:37:59 +01:00
|
|
|
activity = Activity.query.filter_by(uuid=activity_uuid).first()
|
2021-01-01 16:39:25 +01:00
|
|
|
if not activity:
|
|
|
|
return DataNotFoundErrorResponse(
|
|
|
|
data_type=data_type,
|
|
|
|
message=f'Activity not found (id: {activity_short_id})',
|
2019-08-28 13:25:39 +02:00
|
|
|
)
|
2021-01-01 16:39:25 +01:00
|
|
|
|
|
|
|
error_response = can_view_activity(auth_user_id, activity.user_id)
|
|
|
|
if error_response:
|
|
|
|
return error_response
|
|
|
|
if not activity.gpx or activity.gpx == '':
|
|
|
|
return NotFoundErrorResponse(
|
|
|
|
f'No gpx file for this activity (id: {activity_short_id})'
|
|
|
|
)
|
|
|
|
|
|
|
|
try:
|
|
|
|
absolute_gpx_filepath = get_absolute_file_path(activity.gpx)
|
2021-01-02 19:28:03 +01:00
|
|
|
chart_data_content: Optional[List] = []
|
2021-01-01 16:39:25 +01:00
|
|
|
if data_type == 'chart_data':
|
2021-01-02 19:28:03 +01:00
|
|
|
chart_data_content = get_chart_data(
|
|
|
|
absolute_gpx_filepath, segment_id
|
|
|
|
)
|
2021-01-01 16:39:25 +01:00
|
|
|
else: # data_type == 'gpx'
|
|
|
|
with open(absolute_gpx_filepath, encoding='utf-8') as f:
|
2021-01-02 19:28:03 +01:00
|
|
|
gpx_content = f.read()
|
2021-01-01 16:39:25 +01:00
|
|
|
if segment_id is not None:
|
2021-01-02 19:28:03 +01:00
|
|
|
gpx_segment_content = extract_segment_from_gpx_file(
|
|
|
|
gpx_content, segment_id
|
2021-01-01 16:39:25 +01:00
|
|
|
)
|
|
|
|
except ActivityGPXException as e:
|
|
|
|
appLog.error(e.message)
|
|
|
|
if e.status == 'not found':
|
|
|
|
return NotFoundErrorResponse(e.message)
|
|
|
|
return InternalServerErrorResponse(e.message)
|
|
|
|
except Exception as e:
|
|
|
|
return handle_error_and_return_response(e)
|
|
|
|
|
|
|
|
return {
|
|
|
|
'status': 'success',
|
|
|
|
'message': '',
|
2021-01-02 19:28:03 +01:00
|
|
|
'data': (
|
|
|
|
{
|
|
|
|
data_type: chart_data_content
|
|
|
|
if data_type == 'chart_data'
|
|
|
|
else gpx_content
|
|
|
|
if segment_id is None
|
|
|
|
else gpx_segment_content
|
|
|
|
}
|
|
|
|
),
|
2019-08-28 13:25:39 +02:00
|
|
|
}
|
2018-05-28 14:38:32 +02:00
|
|
|
|
|
|
|
|
2018-05-28 14:57:41 +02:00
|
|
|
@activities_blueprint.route(
|
2020-12-30 22:07:43 +01:00
|
|
|
'/activities/<string:activity_short_id>/gpx', methods=['GET']
|
2018-05-28 14:57:41 +02:00
|
|
|
)
|
|
|
|
@authenticate
|
2021-01-02 19:28:03 +01:00
|
|
|
def get_activity_gpx(
|
|
|
|
auth_user_id: int, activity_short_id: str
|
|
|
|
) -> Union[Dict, HttpResponse]:
|
2019-07-19 11:56:48 +02:00
|
|
|
"""
|
|
|
|
Get gpx file for an activity displayed on map with Leaflet
|
|
|
|
|
|
|
|
**Example request**:
|
|
|
|
|
|
|
|
.. sourcecode:: http
|
|
|
|
|
2020-12-30 22:07:43 +01:00
|
|
|
GET /api/activities/kjxavSTUrJvoAh2wvCeGEF/gpx HTTP/1.1
|
2019-07-19 11:56:48 +02:00
|
|
|
Content-Type: application/json
|
|
|
|
|
|
|
|
**Example response**:
|
|
|
|
|
|
|
|
.. sourcecode:: http
|
|
|
|
|
|
|
|
HTTP/1.1 200 OK
|
|
|
|
Content-Type: application/json
|
|
|
|
|
|
|
|
{
|
|
|
|
"data": {
|
|
|
|
"gpx": "gpx file content"
|
|
|
|
},
|
|
|
|
"message": "",
|
|
|
|
"status": "success"
|
|
|
|
}
|
|
|
|
|
2019-07-20 21:57:35 +02:00
|
|
|
:param integer auth_user_id: authenticate user id (from JSON Web Token)
|
2020-12-30 22:07:43 +01:00
|
|
|
:param string activity_short_id: activity short id
|
2019-07-19 11:56:48 +02:00
|
|
|
|
|
|
|
:reqheader Authorization: OAuth 2.0 Bearer Token
|
|
|
|
|
|
|
|
:statuscode 200: success
|
2019-07-20 21:57:35 +02:00
|
|
|
:statuscode 401:
|
|
|
|
- Provide a valid auth token.
|
|
|
|
- Signature expired. Please log in again.
|
|
|
|
- Invalid token. Please log in again.
|
2020-05-10 15:55:56 +02:00
|
|
|
:statuscode 404:
|
|
|
|
- activity not found
|
|
|
|
- no gpx file for this activity
|
2019-07-19 11:56:48 +02:00
|
|
|
:statuscode 500:
|
|
|
|
|
|
|
|
"""
|
2020-12-30 22:07:43 +01:00
|
|
|
return get_activity_data(auth_user_id, activity_short_id, 'gpx')
|
2018-05-28 14:57:41 +02:00
|
|
|
|
|
|
|
|
2018-05-28 14:38:32 +02:00
|
|
|
@activities_blueprint.route(
|
2020-12-30 22:07:43 +01:00
|
|
|
'/activities/<string:activity_short_id>/chart_data', methods=['GET']
|
2018-05-28 14:38:32 +02:00
|
|
|
)
|
|
|
|
@authenticate
|
2021-01-02 19:28:03 +01:00
|
|
|
def get_activity_chart_data(
|
|
|
|
auth_user_id: int, activity_short_id: str
|
|
|
|
) -> Union[Dict, HttpResponse]:
|
2019-07-19 11:56:48 +02:00
|
|
|
"""
|
|
|
|
Get chart data from an activity gpx file, to display it with Recharts
|
|
|
|
|
|
|
|
**Example request**:
|
|
|
|
|
|
|
|
.. sourcecode:: http
|
|
|
|
|
2020-12-30 22:07:43 +01:00
|
|
|
GET /api/activities/kjxavSTUrJvoAh2wvCeGEF/chart HTTP/1.1
|
2019-07-19 11:56:48 +02:00
|
|
|
Content-Type: application/json
|
|
|
|
|
|
|
|
**Example response**:
|
|
|
|
|
|
|
|
.. sourcecode:: http
|
|
|
|
|
|
|
|
HTTP/1.1 200 OK
|
|
|
|
Content-Type: application/json
|
|
|
|
|
|
|
|
{
|
|
|
|
"data": {
|
|
|
|
"chart_data": [
|
|
|
|
{
|
|
|
|
"distance": 0,
|
|
|
|
"duration": 0,
|
|
|
|
"elevation": 279.4,
|
2019-08-25 15:05:10 +02:00
|
|
|
"latitude": 51.5078118,
|
|
|
|
"longitude": -0.1232004,
|
2019-07-19 11:56:48 +02:00
|
|
|
"speed": 8.63,
|
|
|
|
"time": "Fri, 14 Jul 2017 13:44:03 GMT"
|
|
|
|
},
|
|
|
|
{
|
|
|
|
"distance": 7.5,
|
|
|
|
"duration": 7380,
|
|
|
|
"elevation": 280,
|
2019-08-25 15:05:10 +02:00
|
|
|
"latitude": 51.5079733,
|
|
|
|
"longitude": -0.1234538,
|
2019-07-19 11:56:48 +02:00
|
|
|
"speed": 6.39,
|
|
|
|
"time": "Fri, 14 Jul 2017 15:47:03 GMT"
|
|
|
|
}
|
|
|
|
]
|
|
|
|
},
|
|
|
|
"message": "",
|
|
|
|
"status": "success"
|
|
|
|
}
|
|
|
|
|
2019-07-20 21:57:35 +02:00
|
|
|
:param integer auth_user_id: authenticate user id (from JSON Web Token)
|
2020-12-30 22:07:43 +01:00
|
|
|
:param string activity_short_id: activity short id
|
2019-07-19 11:56:48 +02:00
|
|
|
|
|
|
|
:reqheader Authorization: OAuth 2.0 Bearer Token
|
|
|
|
|
|
|
|
:statuscode 200: success
|
2019-07-20 21:57:35 +02:00
|
|
|
:statuscode 401:
|
|
|
|
- Provide a valid auth token.
|
|
|
|
- Signature expired. Please log in again.
|
|
|
|
- Invalid token. Please log in again.
|
2020-05-10 15:55:56 +02:00
|
|
|
:statuscode 404:
|
|
|
|
- activity not found
|
|
|
|
- no gpx file for this activity
|
2019-07-19 11:56:48 +02:00
|
|
|
:statuscode 500:
|
|
|
|
|
|
|
|
"""
|
2021-01-01 16:39:25 +01:00
|
|
|
return get_activity_data(auth_user_id, activity_short_id, 'chart_data')
|
2018-05-03 21:42:54 +02:00
|
|
|
|
|
|
|
|
2019-08-25 18:54:33 +02:00
|
|
|
@activities_blueprint.route(
|
2020-12-30 22:07:43 +01:00
|
|
|
'/activities/<string:activity_short_id>/gpx/segment/<int:segment_id>',
|
2019-08-28 13:25:39 +02:00
|
|
|
methods=['GET'],
|
2019-08-25 18:54:33 +02:00
|
|
|
)
|
|
|
|
@authenticate
|
2021-01-02 19:28:03 +01:00
|
|
|
def get_segment_gpx(
|
|
|
|
auth_user_id: int, activity_short_id: str, segment_id: int
|
|
|
|
) -> Union[Dict, HttpResponse]:
|
2019-08-25 18:54:33 +02:00
|
|
|
"""
|
|
|
|
Get gpx file for an activity segment displayed on map with Leaflet
|
|
|
|
|
|
|
|
**Example request**:
|
|
|
|
|
|
|
|
.. sourcecode:: http
|
|
|
|
|
2020-12-30 22:07:43 +01:00
|
|
|
GET /api/activities/kjxavSTUrJvoAh2wvCeGEF/gpx/segment/0 HTTP/1.1
|
2019-08-25 18:54:33 +02:00
|
|
|
Content-Type: application/json
|
|
|
|
|
|
|
|
**Example response**:
|
|
|
|
|
|
|
|
.. sourcecode:: http
|
|
|
|
|
|
|
|
HTTP/1.1 200 OK
|
|
|
|
Content-Type: application/json
|
|
|
|
|
|
|
|
{
|
|
|
|
"data": {
|
|
|
|
"gpx": "gpx file content"
|
|
|
|
},
|
|
|
|
"message": "",
|
|
|
|
"status": "success"
|
|
|
|
}
|
|
|
|
|
|
|
|
:param integer auth_user_id: authenticate user id (from JSON Web Token)
|
2020-12-30 22:07:43 +01:00
|
|
|
:param string activity_short_id: activity short id
|
2019-08-25 18:54:33 +02:00
|
|
|
:param integer segment_id: segment id
|
|
|
|
|
|
|
|
:reqheader Authorization: OAuth 2.0 Bearer Token
|
|
|
|
|
|
|
|
:statuscode 200: success
|
|
|
|
:statuscode 400: no gpx file for this activity
|
|
|
|
:statuscode 401:
|
|
|
|
- Provide a valid auth token.
|
|
|
|
- Signature expired. Please log in again.
|
|
|
|
- Invalid token. Please log in again.
|
|
|
|
:statuscode 404: activity not found
|
|
|
|
:statuscode 500:
|
|
|
|
|
|
|
|
"""
|
2020-12-30 22:07:43 +01:00
|
|
|
return get_activity_data(
|
|
|
|
auth_user_id, activity_short_id, 'gpx', segment_id
|
|
|
|
)
|
2019-08-25 18:54:33 +02:00
|
|
|
|
|
|
|
|
|
|
|
@activities_blueprint.route(
|
2020-12-30 22:07:43 +01:00
|
|
|
'/activities/<string:activity_short_id>/chart_data/segment/'
|
|
|
|
'<int:segment_id>',
|
2019-08-28 13:25:39 +02:00
|
|
|
methods=['GET'],
|
2019-08-25 18:54:33 +02:00
|
|
|
)
|
|
|
|
@authenticate
|
2021-01-02 19:28:03 +01:00
|
|
|
def get_segment_chart_data(
|
|
|
|
auth_user_id: int, activity_short_id: str, segment_id: int
|
|
|
|
) -> Union[Dict, HttpResponse]:
|
2019-08-25 18:54:33 +02:00
|
|
|
"""
|
|
|
|
Get chart data from an activity gpx file, to display it with Recharts
|
|
|
|
|
|
|
|
**Example request**:
|
|
|
|
|
|
|
|
.. sourcecode:: http
|
|
|
|
|
2020-12-30 22:07:43 +01:00
|
|
|
GET /api/activities/kjxavSTUrJvoAh2wvCeGEF/chart/segment/0 HTTP/1.1
|
2019-08-25 18:54:33 +02:00
|
|
|
Content-Type: application/json
|
|
|
|
|
|
|
|
**Example response**:
|
|
|
|
|
|
|
|
.. sourcecode:: http
|
|
|
|
|
|
|
|
HTTP/1.1 200 OK
|
|
|
|
Content-Type: application/json
|
|
|
|
|
|
|
|
{
|
|
|
|
"data": {
|
|
|
|
"chart_data": [
|
|
|
|
{
|
|
|
|
"distance": 0,
|
|
|
|
"duration": 0,
|
|
|
|
"elevation": 279.4,
|
|
|
|
"latitude": 51.5078118,
|
|
|
|
"longitude": -0.1232004,
|
|
|
|
"speed": 8.63,
|
|
|
|
"time": "Fri, 14 Jul 2017 13:44:03 GMT"
|
|
|
|
},
|
|
|
|
{
|
|
|
|
"distance": 7.5,
|
|
|
|
"duration": 7380,
|
|
|
|
"elevation": 280,
|
|
|
|
"latitude": 51.5079733,
|
|
|
|
"longitude": -0.1234538,
|
|
|
|
"speed": 6.39,
|
|
|
|
"time": "Fri, 14 Jul 2017 15:47:03 GMT"
|
|
|
|
}
|
|
|
|
]
|
|
|
|
},
|
|
|
|
"message": "",
|
|
|
|
"status": "success"
|
|
|
|
}
|
|
|
|
|
|
|
|
:param integer auth_user_id: authenticate user id (from JSON Web Token)
|
2020-12-30 22:07:43 +01:00
|
|
|
:param string activity_short_id: activity short id
|
2019-08-25 18:54:33 +02:00
|
|
|
:param integer segment_id: segment id
|
|
|
|
|
|
|
|
:reqheader Authorization: OAuth 2.0 Bearer Token
|
|
|
|
|
|
|
|
:statuscode 200: success
|
|
|
|
:statuscode 400: no gpx file for this activity
|
|
|
|
:statuscode 401:
|
|
|
|
- Provide a valid auth token.
|
|
|
|
- Signature expired. Please log in again.
|
|
|
|
- Invalid token. Please log in again.
|
|
|
|
:statuscode 404: activity not found
|
|
|
|
:statuscode 500:
|
|
|
|
|
|
|
|
"""
|
2020-12-30 22:07:43 +01:00
|
|
|
return get_activity_data(
|
2021-01-01 16:39:25 +01:00
|
|
|
auth_user_id, activity_short_id, 'chart_data', segment_id
|
2020-12-30 22:07:43 +01:00
|
|
|
)
|
2019-08-25 18:54:33 +02:00
|
|
|
|
|
|
|
|
2018-05-30 13:35:27 +02:00
|
|
|
@activities_blueprint.route('/activities/map/<map_id>', methods=['GET'])
|
2021-01-02 19:28:03 +01:00
|
|
|
def get_map(map_id: int) -> Any:
|
2019-07-19 11:56:48 +02:00
|
|
|
"""
|
|
|
|
Get map image for activities with gpx
|
|
|
|
|
|
|
|
**Example request**:
|
|
|
|
|
|
|
|
.. sourcecode:: http
|
|
|
|
|
|
|
|
GET /api/activities/map/fa33f4d996844a5c73ecd1ae24456ab8?1563529507772
|
|
|
|
HTTP/1.1
|
|
|
|
|
|
|
|
**Example response**:
|
|
|
|
|
|
|
|
.. sourcecode:: http
|
|
|
|
|
|
|
|
HTTP/1.1 200 OK
|
|
|
|
Content-Type: image/png
|
|
|
|
|
|
|
|
:param string map_id: activity map id
|
|
|
|
|
|
|
|
:statuscode 200: success
|
2019-07-20 21:57:35 +02:00
|
|
|
:statuscode 401:
|
|
|
|
- Provide a valid auth token.
|
|
|
|
- Signature expired. Please log in again.
|
|
|
|
- Invalid token. Please log in again.
|
2019-07-19 11:56:48 +02:00
|
|
|
:statuscode 404: map does not exist
|
|
|
|
:statuscode 500:
|
|
|
|
|
|
|
|
"""
|
2018-05-30 13:35:27 +02:00
|
|
|
try:
|
|
|
|
activity = Activity.query.filter_by(map_id=map_id).first()
|
|
|
|
if not activity:
|
2021-01-01 16:39:25 +01:00
|
|
|
return NotFoundErrorResponse('Map does not exist.')
|
|
|
|
absolute_map_filepath = get_absolute_file_path(activity.map)
|
|
|
|
return send_file(absolute_map_filepath)
|
2018-05-30 13:35:27 +02:00
|
|
|
except Exception as e:
|
2021-01-01 16:39:25 +01:00
|
|
|
return handle_error_and_return_response(e)
|
2018-05-30 13:35:27 +02:00
|
|
|
|
|
|
|
|
2020-09-16 11:47:20 +02:00
|
|
|
@activities_blueprint.route(
|
|
|
|
'/activities/map_tile/<s>/<z>/<x>/<y>.png', methods=['GET']
|
|
|
|
)
|
2021-01-02 19:28:03 +01:00
|
|
|
def get_map_tile(s: str, z: str, x: str, y: str) -> Tuple[Response, int]:
|
2020-09-16 11:47:20 +02:00
|
|
|
"""
|
|
|
|
Get map tile from tile server.
|
|
|
|
|
|
|
|
**Example request**:
|
|
|
|
|
|
|
|
.. sourcecode:: http
|
|
|
|
|
|
|
|
GET /api/activities/map_tile/c/13/4109/2930.png HTTP/1.1
|
|
|
|
|
|
|
|
**Example response**:
|
|
|
|
|
|
|
|
.. sourcecode:: http
|
|
|
|
|
|
|
|
HTTP/1.1 200 OK
|
|
|
|
Content-Type: image/png
|
|
|
|
|
|
|
|
:param string s: subdomain
|
|
|
|
:param string z: zoom
|
|
|
|
:param string x: index of the tile along the map's x axis
|
|
|
|
:param string y: index of the tile along the map's y axis
|
|
|
|
|
|
|
|
Status codes are status codes returned by tile server
|
|
|
|
|
|
|
|
"""
|
2020-09-16 13:01:15 +02:00
|
|
|
url = current_app.config['TILE_SERVER']['URL'].format(s=s, z=z, x=x, y=y)
|
2020-09-16 11:47:20 +02:00
|
|
|
headers = {'User-Agent': 'Mozilla/5.0'}
|
|
|
|
response = requests.get(url, headers=headers)
|
|
|
|
return (
|
|
|
|
Response(
|
|
|
|
response.content,
|
|
|
|
content_type=response.headers['content-type'],
|
|
|
|
),
|
|
|
|
response.status_code,
|
|
|
|
)
|
|
|
|
|
|
|
|
|
2018-05-01 17:51:38 +02:00
|
|
|
@activities_blueprint.route('/activities', methods=['POST'])
|
|
|
|
@authenticate
|
2021-01-02 19:28:03 +01:00
|
|
|
def post_activity(auth_user_id: int) -> Union[Tuple[Dict, int], HttpResponse]:
|
2019-07-14 21:36:51 +02:00
|
|
|
"""
|
|
|
|
Post an activity with a gpx file
|
|
|
|
|
|
|
|
**Example request**:
|
|
|
|
|
|
|
|
.. sourcecode:: http
|
|
|
|
|
|
|
|
POST /api/activities/ HTTP/1.1
|
|
|
|
Content-Type: multipart/form-data
|
|
|
|
|
|
|
|
**Example response**:
|
|
|
|
|
|
|
|
.. sourcecode:: http
|
|
|
|
|
|
|
|
HTTP/1.1 201 CREATED
|
|
|
|
Content-Type: application/json
|
|
|
|
|
|
|
|
{
|
|
|
|
"data": {
|
|
|
|
"activities": [
|
|
|
|
{
|
|
|
|
"activity_date": "Mon, 01 Jan 2018 00:00:00 GMT",
|
|
|
|
"ascent": null,
|
|
|
|
"ave_speed": 10.0,
|
|
|
|
"bounds": [],
|
|
|
|
"creation_date": "Sun, 14 Jul 2019 13:51:01 GMT",
|
|
|
|
"descent": null,
|
|
|
|
"distance": 10.0,
|
|
|
|
"duration": "0:17:04",
|
2020-12-30 22:07:43 +01:00
|
|
|
"id": "kjxavSTUrJvoAh2wvCeGEF",
|
2019-07-14 21:36:51 +02:00
|
|
|
"map": null,
|
|
|
|
"max_alt": null,
|
|
|
|
"max_speed": 10.0,
|
|
|
|
"min_alt": null,
|
|
|
|
"modification_date": null,
|
|
|
|
"moving": "0:17:04",
|
|
|
|
"next_activity": 3,
|
|
|
|
"notes": null,
|
|
|
|
"pauses": null,
|
|
|
|
"previous_activity": null,
|
|
|
|
"records": [
|
|
|
|
{
|
|
|
|
"activity_date": "Mon, 01 Jan 2018 00:00:00 GMT",
|
2020-12-30 22:07:43 +01:00
|
|
|
"activity_id": "kjxavSTUrJvoAh2wvCeGEF",
|
2019-07-14 21:36:51 +02:00
|
|
|
"id": 4,
|
|
|
|
"record_type": "MS",
|
|
|
|
"sport_id": 1,
|
2020-02-08 15:17:07 +01:00
|
|
|
"user": "admin",
|
2019-07-14 21:36:51 +02:00
|
|
|
"value": 10.0
|
|
|
|
},
|
|
|
|
{
|
|
|
|
"activity_date": "Mon, 01 Jan 2018 00:00:00 GMT",
|
2020-12-30 22:07:43 +01:00
|
|
|
"activity_id": "kjxavSTUrJvoAh2wvCeGEF",
|
2019-07-14 21:36:51 +02:00
|
|
|
"id": 3,
|
|
|
|
"record_type": "LD",
|
|
|
|
"sport_id": 1,
|
2020-02-08 15:17:07 +01:00
|
|
|
"user": "admin",
|
2019-07-14 21:36:51 +02:00
|
|
|
"value": "0:17:04"
|
|
|
|
},
|
|
|
|
{
|
|
|
|
"activity_date": "Mon, 01 Jan 2018 00:00:00 GMT",
|
2020-12-30 22:07:43 +01:00
|
|
|
"activity_id": "kjxavSTUrJvoAh2wvCeGEF",
|
2019-07-14 21:36:51 +02:00
|
|
|
"id": 2,
|
|
|
|
"record_type": "FD",
|
|
|
|
"sport_id": 1,
|
2020-02-08 15:17:07 +01:00
|
|
|
"user": "admin",
|
2019-07-14 21:36:51 +02:00
|
|
|
"value": 10.0
|
|
|
|
},
|
|
|
|
{
|
|
|
|
"activity_date": "Mon, 01 Jan 2018 00:00:00 GMT",
|
2020-12-30 22:07:43 +01:00
|
|
|
"activity_id": "kjxavSTUrJvoAh2wvCeGEF",
|
2019-07-14 21:36:51 +02:00
|
|
|
"id": 1,
|
|
|
|
"record_type": "AS",
|
|
|
|
"sport_id": 1,
|
2020-02-08 15:17:07 +01:00
|
|
|
"user": "admin",
|
2019-07-14 21:36:51 +02:00
|
|
|
"value": 10.0
|
|
|
|
}
|
|
|
|
],
|
|
|
|
"segments": [],
|
|
|
|
"sport_id": 1,
|
|
|
|
"title": null,
|
2020-02-08 15:17:07 +01:00
|
|
|
"user": "admin",
|
2019-07-14 21:36:51 +02:00
|
|
|
"weather_end": null,
|
|
|
|
"weather_start": null,
|
|
|
|
"with_gpx": false
|
|
|
|
}
|
|
|
|
]
|
|
|
|
},
|
|
|
|
"status": "success"
|
|
|
|
}
|
|
|
|
|
2019-07-20 21:57:35 +02:00
|
|
|
:param integer auth_user_id: authenticate user id (from JSON Web Token)
|
2019-07-14 21:36:51 +02:00
|
|
|
|
2019-07-20 14:27:05 +02:00
|
|
|
:form file: gpx file (allowed extensions: .gpx, .zip)
|
2019-07-14 21:36:51 +02:00
|
|
|
:form data: sport id and notes (example: ``{"sport_id": 1, "notes": ""}``)
|
|
|
|
|
|
|
|
:reqheader Authorization: OAuth 2.0 Bearer Token
|
|
|
|
|
|
|
|
:statuscode 201: activity created
|
2019-07-20 14:27:05 +02:00
|
|
|
:statuscode 400:
|
|
|
|
- Invalid payload.
|
|
|
|
- No file part.
|
|
|
|
- No selected file.
|
|
|
|
- File extension not allowed.
|
2019-07-20 21:57:35 +02:00
|
|
|
:statuscode 401:
|
|
|
|
- Provide a valid auth token.
|
|
|
|
- Signature expired. Please log in again.
|
|
|
|
- Invalid token. Please log in again.
|
2019-08-31 14:11:00 +02:00
|
|
|
:statuscode 413: Error during picture update: file size exceeds 1.0MB.
|
2019-07-14 21:36:51 +02:00
|
|
|
:statuscode 500:
|
|
|
|
|
|
|
|
"""
|
2021-01-01 16:39:25 +01:00
|
|
|
error_response = verify_extension_and_size('activity', request)
|
|
|
|
if error_response:
|
|
|
|
return error_response
|
2018-05-01 17:51:38 +02:00
|
|
|
|
2020-12-30 22:07:43 +01:00
|
|
|
activity_data = json.loads(request.form['data'])
|
2018-05-01 17:51:38 +02:00
|
|
|
if not activity_data or activity_data.get('sport_id') is None:
|
2021-01-01 16:39:25 +01:00
|
|
|
return InvalidPayloadErrorResponse()
|
2018-05-01 17:51:38 +02:00
|
|
|
|
|
|
|
activity_file = request.files['file']
|
2018-05-29 19:06:33 +02:00
|
|
|
upload_dir = os.path.join(
|
2019-08-28 13:25:39 +02:00
|
|
|
current_app.config['UPLOAD_FOLDER'], 'activities', str(auth_user_id)
|
|
|
|
)
|
2018-05-29 19:06:33 +02:00
|
|
|
folders = {
|
|
|
|
'extract_dir': os.path.join(upload_dir, 'extract'),
|
|
|
|
'tmp_dir': os.path.join(upload_dir, 'tmp'),
|
|
|
|
}
|
2018-05-01 17:51:38 +02:00
|
|
|
|
|
|
|
try:
|
2018-05-29 16:28:59 +02:00
|
|
|
new_activities = process_files(
|
2018-05-29 19:06:33 +02:00
|
|
|
auth_user_id, activity_data, activity_file, folders
|
2018-05-11 13:45:54 +02:00
|
|
|
)
|
2018-05-29 16:28:59 +02:00
|
|
|
if len(new_activities) > 0:
|
|
|
|
response_object = {
|
|
|
|
'status': 'created',
|
|
|
|
'data': {
|
2019-08-28 13:25:39 +02:00
|
|
|
'activities': [
|
|
|
|
new_activity.serialize()
|
|
|
|
for new_activity in new_activities
|
|
|
|
]
|
|
|
|
},
|
2018-05-02 17:01:31 +02:00
|
|
|
}
|
2018-05-29 16:28:59 +02:00
|
|
|
else:
|
2021-01-01 16:39:25 +01:00
|
|
|
return DataInvalidPayloadErrorResponse('activities', 'fail')
|
2018-05-29 12:53:13 +02:00
|
|
|
except ActivityException as e:
|
2018-05-01 17:51:38 +02:00
|
|
|
db.session.rollback()
|
2018-05-29 12:53:13 +02:00
|
|
|
if e.e:
|
|
|
|
appLog.error(e.e)
|
2021-01-01 16:39:25 +01:00
|
|
|
if e.status == 'error':
|
|
|
|
return InternalServerErrorResponse(e.message)
|
|
|
|
return InvalidPayloadErrorResponse(e.message)
|
2018-05-29 19:06:33 +02:00
|
|
|
|
|
|
|
shutil.rmtree(folders['extract_dir'], ignore_errors=True)
|
|
|
|
shutil.rmtree(folders['tmp_dir'], ignore_errors=True)
|
2021-01-01 16:39:25 +01:00
|
|
|
return response_object, 201
|
2018-05-08 15:46:54 +02:00
|
|
|
|
|
|
|
|
|
|
|
@activities_blueprint.route('/activities/no_gpx', methods=['POST'])
|
|
|
|
@authenticate
|
2021-01-02 19:28:03 +01:00
|
|
|
def post_activity_no_gpx(
|
|
|
|
auth_user_id: int,
|
|
|
|
) -> Union[Tuple[Dict, int], HttpResponse]:
|
2019-07-14 21:36:51 +02:00
|
|
|
"""
|
|
|
|
Post an activity without gpx file
|
|
|
|
|
|
|
|
**Example request**:
|
|
|
|
|
|
|
|
.. sourcecode:: http
|
|
|
|
|
2019-07-20 21:57:35 +02:00
|
|
|
POST /api/activities/no_gpx HTTP/1.1
|
2019-07-14 21:36:51 +02:00
|
|
|
Content-Type: application/json
|
|
|
|
|
|
|
|
**Example response**:
|
|
|
|
|
|
|
|
.. sourcecode:: http
|
|
|
|
|
|
|
|
HTTP/1.1 201 CREATED
|
|
|
|
Content-Type: application/json
|
|
|
|
|
|
|
|
{
|
|
|
|
"data": {
|
|
|
|
"activities": [
|
|
|
|
{
|
|
|
|
"activity_date": "Mon, 01 Jan 2018 00:00:00 GMT",
|
|
|
|
"ascent": null,
|
|
|
|
"ave_speed": 10.0,
|
|
|
|
"bounds": [],
|
|
|
|
"creation_date": "Sun, 14 Jul 2019 13:51:01 GMT",
|
|
|
|
"descent": null,
|
|
|
|
"distance": 10.0,
|
|
|
|
"duration": "0:17:04",
|
|
|
|
"map": null,
|
|
|
|
"max_alt": null,
|
|
|
|
"max_speed": 10.0,
|
|
|
|
"min_alt": null,
|
|
|
|
"modification_date": null,
|
|
|
|
"moving": "0:17:04",
|
|
|
|
"next_activity": 3,
|
|
|
|
"notes": null,
|
|
|
|
"pauses": null,
|
|
|
|
"previous_activity": null,
|
|
|
|
"records": [
|
|
|
|
{
|
|
|
|
"activity_date": "Mon, 01 Jan 2018 00:00:00 GMT",
|
2020-12-30 22:07:43 +01:00
|
|
|
"activity_id": "kjxavSTUrJvoAh2wvCeGEF",
|
2019-07-14 21:36:51 +02:00
|
|
|
"id": 4,
|
|
|
|
"record_type": "MS",
|
|
|
|
"sport_id": 1,
|
2020-02-08 15:17:07 +01:00
|
|
|
"user": "admin",
|
2019-07-14 21:36:51 +02:00
|
|
|
"value": 10.0
|
|
|
|
},
|
|
|
|
{
|
|
|
|
"activity_date": "Mon, 01 Jan 2018 00:00:00 GMT",
|
2020-12-30 22:07:43 +01:00
|
|
|
"activity_id": "kjxavSTUrJvoAh2wvCeGEF",
|
2019-07-14 21:36:51 +02:00
|
|
|
"id": 3,
|
|
|
|
"record_type": "LD",
|
|
|
|
"sport_id": 1,
|
2020-02-08 15:17:07 +01:00
|
|
|
"user": "admin",
|
2019-07-14 21:36:51 +02:00
|
|
|
"value": "0:17:04"
|
|
|
|
},
|
|
|
|
{
|
|
|
|
"activity_date": "Mon, 01 Jan 2018 00:00:00 GMT",
|
2020-12-30 22:07:43 +01:00
|
|
|
"activity_id": "kjxavSTUrJvoAh2wvCeGEF",
|
2019-07-14 21:36:51 +02:00
|
|
|
"id": 2,
|
|
|
|
"record_type": "FD",
|
|
|
|
"sport_id": 1,
|
2020-02-08 15:17:07 +01:00
|
|
|
"user": "admin",
|
2019-07-14 21:36:51 +02:00
|
|
|
"value": 10.0
|
|
|
|
},
|
|
|
|
{
|
|
|
|
"activity_date": "Mon, 01 Jan 2018 00:00:00 GMT",
|
2020-12-30 22:07:43 +01:00
|
|
|
"activity_id": "kjxavSTUrJvoAh2wvCeGEF",
|
2019-07-14 21:36:51 +02:00
|
|
|
"id": 1,
|
|
|
|
"record_type": "AS",
|
|
|
|
"sport_id": 1,
|
2020-02-08 15:17:07 +01:00
|
|
|
"user": "admin",
|
2019-07-14 21:36:51 +02:00
|
|
|
"value": 10.0
|
|
|
|
}
|
|
|
|
],
|
|
|
|
"segments": [],
|
|
|
|
"sport_id": 1,
|
|
|
|
"title": null,
|
2020-02-08 15:17:07 +01:00
|
|
|
"user": "admin",
|
2020-12-30 22:07:43 +01:00
|
|
|
"uuid": "kjxavSTUrJvoAh2wvCeGEF"
|
2019-07-14 21:36:51 +02:00
|
|
|
"weather_end": null,
|
|
|
|
"weather_start": null,
|
|
|
|
"with_gpx": false
|
|
|
|
}
|
|
|
|
]
|
|
|
|
},
|
|
|
|
"status": "success"
|
|
|
|
}
|
|
|
|
|
2019-07-20 21:57:35 +02:00
|
|
|
:param integer auth_user_id: authenticate user id (from JSON Web Token)
|
2019-07-14 21:36:51 +02:00
|
|
|
|
|
|
|
:<json string activity_date: activity date (format: ``%Y-%m-%d %H:%M``)
|
|
|
|
:<json float distance: activity distance in km
|
|
|
|
:<json integer duration: activity duration in seconds
|
2019-07-19 11:56:48 +02:00
|
|
|
:<json string notes: notes (not mandatory)
|
2019-07-14 21:36:51 +02:00
|
|
|
:<json integer sport_id: activity sport id
|
|
|
|
:<json string title: activity title
|
|
|
|
|
|
|
|
:reqheader Authorization: OAuth 2.0 Bearer Token
|
|
|
|
|
|
|
|
:statuscode 201: activity created
|
|
|
|
:statuscode 400: invalid payload
|
2019-07-20 21:57:35 +02:00
|
|
|
:statuscode 401:
|
|
|
|
- Provide a valid auth token.
|
|
|
|
- Signature expired. Please log in again.
|
|
|
|
- Invalid token. Please log in again.
|
2019-07-14 21:36:51 +02:00
|
|
|
:statuscode 500:
|
|
|
|
|
|
|
|
"""
|
2018-05-08 15:46:54 +02:00
|
|
|
activity_data = request.get_json()
|
2019-08-28 13:25:39 +02:00
|
|
|
if (
|
|
|
|
not activity_data
|
|
|
|
or activity_data.get('sport_id') is None
|
|
|
|
or activity_data.get('duration') is None
|
|
|
|
or activity_data.get('distance') is None
|
|
|
|
or activity_data.get('activity_date') is None
|
|
|
|
):
|
2021-01-01 16:39:25 +01:00
|
|
|
return InvalidPayloadErrorResponse()
|
2018-05-08 15:46:54 +02:00
|
|
|
|
|
|
|
try:
|
2018-06-11 19:38:20 +02:00
|
|
|
user = User.query.filter_by(id=auth_user_id).first()
|
|
|
|
new_activity = create_activity(user, activity_data)
|
2018-05-08 15:46:54 +02:00
|
|
|
db.session.add(new_activity)
|
|
|
|
db.session.commit()
|
|
|
|
|
2021-01-01 16:39:25 +01:00
|
|
|
return (
|
|
|
|
{
|
|
|
|
'status': 'created',
|
|
|
|
'data': {'activities': [new_activity.serialize()]},
|
|
|
|
},
|
|
|
|
201,
|
|
|
|
)
|
2018-05-08 15:46:54 +02:00
|
|
|
|
|
|
|
except (exc.IntegrityError, ValueError) as e:
|
2021-01-01 16:39:25 +01:00
|
|
|
return handle_error_and_return_response(
|
|
|
|
error=e,
|
|
|
|
message='Error during activity save.',
|
|
|
|
status='fail',
|
|
|
|
db=db,
|
|
|
|
)
|
2018-05-09 18:54:30 +02:00
|
|
|
|
|
|
|
|
2020-12-30 19:37:59 +01:00
|
|
|
@activities_blueprint.route(
|
2020-12-30 22:07:43 +01:00
|
|
|
'/activities/<string:activity_short_id>', methods=['PATCH']
|
2020-12-30 19:37:59 +01:00
|
|
|
)
|
2018-05-09 20:45:01 +02:00
|
|
|
@authenticate
|
2021-01-02 19:28:03 +01:00
|
|
|
def update_activity(
|
|
|
|
auth_user_id: int, activity_short_id: str
|
|
|
|
) -> Union[Dict, HttpResponse]:
|
2019-07-19 11:56:48 +02:00
|
|
|
"""
|
|
|
|
Update an activity
|
|
|
|
|
|
|
|
**Example request**:
|
|
|
|
|
|
|
|
.. sourcecode:: http
|
|
|
|
|
|
|
|
PATCH /api/activities/1 HTTP/1.1
|
|
|
|
Content-Type: application/json
|
|
|
|
|
|
|
|
**Example response**:
|
|
|
|
|
|
|
|
.. sourcecode:: http
|
|
|
|
|
|
|
|
HTTP/1.1 200 OK
|
|
|
|
Content-Type: application/json
|
|
|
|
|
|
|
|
{
|
|
|
|
"data": {
|
|
|
|
"activities": [
|
|
|
|
{
|
|
|
|
"activity_date": "Mon, 01 Jan 2018 00:00:00 GMT",
|
|
|
|
"ascent": null,
|
|
|
|
"ave_speed": 10.0,
|
|
|
|
"bounds": [],
|
|
|
|
"creation_date": "Sun, 14 Jul 2019 13:51:01 GMT",
|
|
|
|
"descent": null,
|
|
|
|
"distance": 10.0,
|
|
|
|
"duration": "0:17:04",
|
|
|
|
"map": null,
|
|
|
|
"max_alt": null,
|
|
|
|
"max_speed": 10.0,
|
|
|
|
"min_alt": null,
|
|
|
|
"modification_date": null,
|
|
|
|
"moving": "0:17:04",
|
|
|
|
"next_activity": 3,
|
|
|
|
"notes": null,
|
|
|
|
"pauses": null,
|
|
|
|
"previous_activity": null,
|
|
|
|
"records": [
|
|
|
|
{
|
|
|
|
"activity_date": "Mon, 01 Jan 2018 00:00:00 GMT",
|
2020-12-30 22:07:43 +01:00
|
|
|
"activity_id": "kjxavSTUrJvoAh2wvCeGEF",
|
2019-07-19 11:56:48 +02:00
|
|
|
"id": 4,
|
|
|
|
"record_type": "MS",
|
|
|
|
"sport_id": 1,
|
2020-02-08 15:17:07 +01:00
|
|
|
"user": "admin",
|
2019-07-19 11:56:48 +02:00
|
|
|
"value": 10.0
|
|
|
|
},
|
|
|
|
{
|
|
|
|
"activity_date": "Mon, 01 Jan 2018 00:00:00 GMT",
|
2020-12-30 22:07:43 +01:00
|
|
|
"activity_id": "kjxavSTUrJvoAh2wvCeGEF",
|
2019-07-19 11:56:48 +02:00
|
|
|
"id": 3,
|
|
|
|
"record_type": "LD",
|
|
|
|
"sport_id": 1,
|
2020-02-08 15:17:07 +01:00
|
|
|
"user": "admin",
|
2019-07-19 11:56:48 +02:00
|
|
|
"value": "0:17:04"
|
|
|
|
},
|
|
|
|
{
|
|
|
|
"activity_date": "Mon, 01 Jan 2018 00:00:00 GMT",
|
2020-12-30 22:07:43 +01:00
|
|
|
"activity_id": "kjxavSTUrJvoAh2wvCeGEF",
|
2019-07-19 11:56:48 +02:00
|
|
|
"id": 2,
|
|
|
|
"record_type": "FD",
|
|
|
|
"sport_id": 1,
|
2020-02-08 15:17:07 +01:00
|
|
|
"user": "admin",
|
2019-07-19 11:56:48 +02:00
|
|
|
"value": 10.0
|
|
|
|
},
|
|
|
|
{
|
|
|
|
"activity_date": "Mon, 01 Jan 2018 00:00:00 GMT",
|
2020-12-30 22:07:43 +01:00
|
|
|
"activity_id": "kjxavSTUrJvoAh2wvCeGEF",
|
2019-07-19 11:56:48 +02:00
|
|
|
"id": 1,
|
|
|
|
"record_type": "AS",
|
|
|
|
"sport_id": 1,
|
2020-02-08 15:17:07 +01:00
|
|
|
"user": "admin",
|
2019-07-19 11:56:48 +02:00
|
|
|
"value": 10.0
|
|
|
|
}
|
|
|
|
],
|
|
|
|
"segments": [],
|
|
|
|
"sport_id": 1,
|
|
|
|
"title": null,
|
2020-02-08 15:17:07 +01:00
|
|
|
"user": "admin",
|
2020-12-30 22:07:43 +01:00
|
|
|
"uuid": "kjxavSTUrJvoAh2wvCeGEF"
|
2019-07-19 11:56:48 +02:00
|
|
|
"weather_end": null,
|
|
|
|
"weather_start": null,
|
|
|
|
"with_gpx": false
|
|
|
|
}
|
|
|
|
]
|
|
|
|
},
|
|
|
|
"status": "success"
|
|
|
|
}
|
|
|
|
|
2019-07-20 21:57:35 +02:00
|
|
|
:param integer auth_user_id: authenticate user id (from JSON Web Token)
|
2020-12-30 22:07:43 +01:00
|
|
|
:param string activity_short_id: activity short id
|
2019-07-19 11:56:48 +02:00
|
|
|
|
|
|
|
:<json string activity_date: activity date (format: ``%Y-%m-%d %H:%M``)
|
|
|
|
(only for activity without gpx)
|
|
|
|
:<json float distance: activity distance in km
|
|
|
|
(only for activity without gpx)
|
|
|
|
:<json integer duration: activity duration in seconds
|
|
|
|
(only for activity without gpx)
|
|
|
|
:<json string notes: notes
|
|
|
|
:<json integer sport_id: activity sport id
|
|
|
|
:<json string title: activity title
|
|
|
|
|
|
|
|
:reqheader Authorization: OAuth 2.0 Bearer Token
|
|
|
|
|
|
|
|
:statuscode 200: activity updated
|
|
|
|
:statuscode 400: invalid payload
|
2019-07-20 21:57:35 +02:00
|
|
|
:statuscode 401:
|
|
|
|
- Provide a valid auth token.
|
|
|
|
- Signature expired. Please log in again.
|
|
|
|
- Invalid token. Please log in again.
|
2019-07-19 11:56:48 +02:00
|
|
|
:statuscode 404: activity not found
|
|
|
|
:statuscode 500:
|
|
|
|
|
|
|
|
"""
|
2018-05-09 20:45:01 +02:00
|
|
|
activity_data = request.get_json()
|
2018-05-11 17:55:46 +02:00
|
|
|
if not activity_data:
|
2021-01-01 16:39:25 +01:00
|
|
|
return InvalidPayloadErrorResponse()
|
2018-05-09 20:45:01 +02:00
|
|
|
|
|
|
|
try:
|
2020-12-30 22:07:43 +01:00
|
|
|
activity_uuid = decode_short_id(activity_short_id)
|
2020-12-30 19:37:59 +01:00
|
|
|
activity = Activity.query.filter_by(uuid=activity_uuid).first()
|
2021-01-01 16:39:25 +01:00
|
|
|
if not activity:
|
|
|
|
return DataNotFoundErrorResponse('activities')
|
2018-06-15 10:50:37 +02:00
|
|
|
|
2021-01-01 16:39:25 +01:00
|
|
|
response_object = can_view_activity(auth_user_id, activity.user_id)
|
|
|
|
if response_object:
|
|
|
|
return response_object
|
|
|
|
|
|
|
|
activity = edit_activity(activity, activity_data, auth_user_id)
|
|
|
|
db.session.commit()
|
|
|
|
return {
|
|
|
|
'status': 'success',
|
|
|
|
'data': {'activities': [activity.serialize()]},
|
2018-05-09 20:45:01 +02:00
|
|
|
}
|
2021-01-01 16:39:25 +01:00
|
|
|
|
|
|
|
except (exc.IntegrityError, exc.OperationalError, ValueError) as e:
|
|
|
|
return handle_error_and_return_response(e)
|
2018-05-09 20:45:01 +02:00
|
|
|
|
|
|
|
|
2018-05-09 18:54:30 +02:00
|
|
|
@activities_blueprint.route(
|
2020-12-30 22:07:43 +01:00
|
|
|
'/activities/<string:activity_short_id>', methods=['DELETE']
|
2018-05-09 18:54:30 +02:00
|
|
|
)
|
|
|
|
@authenticate
|
2021-01-02 19:28:03 +01:00
|
|
|
def delete_activity(
|
|
|
|
auth_user_id: int, activity_short_id: str
|
|
|
|
) -> Union[Tuple[Dict, int], HttpResponse]:
|
2019-07-19 11:56:48 +02:00
|
|
|
"""
|
|
|
|
Delete an activity
|
|
|
|
|
|
|
|
**Example request**:
|
|
|
|
|
|
|
|
.. sourcecode:: http
|
|
|
|
|
2020-12-30 22:07:43 +01:00
|
|
|
DELETE /api/activities/kjxavSTUrJvoAh2wvCeGEF HTTP/1.1
|
2019-07-19 11:56:48 +02:00
|
|
|
Content-Type: application/json
|
|
|
|
|
|
|
|
**Example response**:
|
|
|
|
|
|
|
|
.. sourcecode:: http
|
|
|
|
|
|
|
|
HTTP/1.1 204 NO CONTENT
|
|
|
|
Content-Type: application/json
|
|
|
|
|
2019-07-20 21:57:35 +02:00
|
|
|
:param integer auth_user_id: authenticate user id (from JSON Web Token)
|
2020-12-30 22:07:43 +01:00
|
|
|
:param string activity_short_id: activity short id
|
2019-07-19 11:56:48 +02:00
|
|
|
|
|
|
|
:reqheader Authorization: OAuth 2.0 Bearer Token
|
|
|
|
|
|
|
|
:statuscode 204: activity deleted
|
2019-07-20 21:57:35 +02:00
|
|
|
:statuscode 401:
|
|
|
|
- Provide a valid auth token.
|
|
|
|
- Signature expired. Please log in again.
|
|
|
|
- Invalid token. Please log in again.
|
2019-07-19 11:56:48 +02:00
|
|
|
:statuscode 404: activity not found
|
2019-07-20 14:27:05 +02:00
|
|
|
:statuscode 500: Error. Please try again or contact the administrator.
|
2019-07-19 11:56:48 +02:00
|
|
|
|
|
|
|
"""
|
|
|
|
|
2018-05-09 18:54:30 +02:00
|
|
|
try:
|
2020-12-30 22:07:43 +01:00
|
|
|
activity_uuid = decode_short_id(activity_short_id)
|
2020-12-30 19:37:59 +01:00
|
|
|
activity = Activity.query.filter_by(uuid=activity_uuid).first()
|
2021-01-01 16:39:25 +01:00
|
|
|
if not activity:
|
|
|
|
return DataNotFoundErrorResponse('activities')
|
|
|
|
error_response = can_view_activity(auth_user_id, activity.user_id)
|
|
|
|
if error_response:
|
|
|
|
return error_response
|
2018-06-15 10:50:37 +02:00
|
|
|
|
2021-01-01 16:39:25 +01:00
|
|
|
db.session.delete(activity)
|
|
|
|
db.session.commit()
|
|
|
|
return {'status': 'no content'}, 204
|
2019-08-28 13:25:39 +02:00
|
|
|
except (
|
|
|
|
exc.IntegrityError,
|
|
|
|
exc.OperationalError,
|
|
|
|
ValueError,
|
|
|
|
OSError,
|
|
|
|
) as e:
|
2021-01-01 16:39:25 +01:00
|
|
|
return handle_error_and_return_response(e, db=db)
|