Merge branch 'dev' into oauth2
This commit is contained in:
@ -21,7 +21,7 @@ from werkzeug.middleware.proxy_fix import ProxyFix
|
||||
from fittrackee.emails.email import EmailService
|
||||
from fittrackee.request import CustomRequest
|
||||
|
||||
VERSION = __version__ = '0.6.6'
|
||||
VERSION = __version__ = '0.6.7'
|
||||
db = SQLAlchemy()
|
||||
bcrypt = Bcrypt()
|
||||
migrate = Migrate()
|
||||
|
@ -48,7 +48,7 @@ def get_application_config() -> Union[Dict, HttpResponse]:
|
||||
"max_users": 0,
|
||||
"max_zip_file_size": 10485760,
|
||||
"map_attribution": "© <a href=http://www.openstreetmap.org/copyright>OpenStreetMap</a> contributors"
|
||||
"version": "0.6.6"
|
||||
"version": "0.6.7"
|
||||
},
|
||||
"status": "success"
|
||||
}
|
||||
@ -98,7 +98,7 @@ def update_application_config(auth_user: User) -> Union[Dict, HttpResponse]:
|
||||
"max_users": 10,
|
||||
"max_zip_file_size": 10485760,
|
||||
"map_attribution": "© <a href=http://www.openstreetmap.org/copyright>OpenStreetMap</a> contributors"
|
||||
"version": "0.6.6"
|
||||
"version": "0.6.7"
|
||||
},
|
||||
"status": "success"
|
||||
}
|
||||
|
2
fittrackee/dist/index.html
vendored
2
fittrackee/dist/index.html
vendored
@ -1 +1 @@
|
||||
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><!--[if IE]><link rel="icon" href="/favicon.ico"><![endif]--><link rel="stylesheet" href="/static/css/fork-awesome.min.css"/><link rel="stylesheet" href="/static/css/leaflet.css"/><title>FitTrackee</title><script defer="defer" src="/static/js/chunk-vendors.e335684a.js"></script><script defer="defer" src="/static/js/app.a9b9439a.js"></script><link href="/static/css/app.e8b7692c.css" rel="stylesheet"><link rel="icon" type="image/png" sizes="32x32" href="/img/icons/favicon-32x32.png"><link rel="icon" type="image/png" sizes="16x16" href="/img/icons/favicon-16x16.png"><link rel="manifest" href="/manifest.json"><meta name="theme-color" content="#4DBA87"><meta name="apple-mobile-web-app-capable" content="no"><meta name="apple-mobile-web-app-status-bar-style" content="default"><meta name="apple-mobile-web-app-title" content="fittrackee_client"><link rel="apple-touch-icon" href="/img/icons/apple-touch-icon-152x152.png"><link rel="mask-icon" href="/img/icons/safari-pinned-tab.svg" color="#4DBA87"><meta name="msapplication-TileImage" content="/img/icons/msapplication-icon-144x144.png"><meta name="msapplication-TileColor" content="#000000"></head><body><noscript><strong>We're sorry but FitTrackee doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div></body></html>
|
||||
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><!--[if IE]><link rel="icon" href="/favicon.ico"><![endif]--><link rel="stylesheet" href="/static/css/fork-awesome.min.css"/><link rel="stylesheet" href="/static/css/leaflet.css"/><title>FitTrackee</title><script defer="defer" src="/static/js/chunk-vendors.e335684a.js"></script><script defer="defer" src="/static/js/app.8517c25d.js"></script><link href="/static/css/app.e8b7692c.css" rel="stylesheet"><link rel="icon" type="image/png" sizes="32x32" href="/img/icons/favicon-32x32.png"><link rel="icon" type="image/png" sizes="16x16" href="/img/icons/favicon-16x16.png"><link rel="manifest" href="/manifest.json"><meta name="theme-color" content="#4DBA87"><meta name="apple-mobile-web-app-capable" content="no"><meta name="apple-mobile-web-app-status-bar-style" content="default"><meta name="apple-mobile-web-app-title" content="fittrackee_client"><link rel="apple-touch-icon" href="/img/icons/apple-touch-icon-152x152.png"><link rel="mask-icon" href="/img/icons/safari-pinned-tab.svg" color="#4DBA87"><meta name="msapplication-TileImage" content="/img/icons/msapplication-icon-144x144.png"><meta name="msapplication-TileColor" content="#000000"></head><body><noscript><strong>We're sorry but FitTrackee doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div></body></html>
|
2
fittrackee/dist/service-worker.js
vendored
2
fittrackee/dist/service-worker.js
vendored
File diff suppressed because one or more lines are too long
2
fittrackee/dist/service-worker.js.map
vendored
2
fittrackee/dist/service-worker.js.map
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
114
fittrackee/tests/fixtures/fixtures_workouts.py
vendored
114
fittrackee/tests/fixtures/fixtures_workouts.py
vendored
@ -452,6 +452,120 @@ def gpx_file_wo_name() -> str:
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def gpx_file_with_offset() -> str:
|
||||
return (
|
||||
'<?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="44.68095" lon="6.07367">'
|
||||
' <ele>998</ele>'
|
||||
' <time>2018-03-13T13:44:45+01:00</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.68091" lon="6.07367">'
|
||||
' <ele>998</ele>'
|
||||
' <time>2018-03-13T13:44:50+01:00</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.6808" lon="6.07364">'
|
||||
' <ele>994</ele>'
|
||||
' <time>2018-03-13T13:45:00+01:00</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.68075" lon="6.07364">'
|
||||
' <ele>994</ele>'
|
||||
' <time>2018-03-13T13:45:05+01:00</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.68071" lon="6.07364">'
|
||||
' <ele>994</ele>'
|
||||
' <time>2018-03-13T13:45:10+01:00</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.68049" lon="6.07361">'
|
||||
' <ele>993</ele>'
|
||||
' <time>2018-03-13T13:45:30+01:00</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.68019" lon="6.07356">'
|
||||
' <ele>992</ele>'
|
||||
' <time>2018-03-13T13:45:55+01:00</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.68014" lon="6.07355">'
|
||||
' <ele>992</ele>'
|
||||
' <time>2018-03-13T13:46:00+01:00</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.67995" lon="6.07358">'
|
||||
' <ele>987</ele>'
|
||||
' <time>2018-03-13T13:46:15+01:00</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.67977" lon="6.07364">'
|
||||
' <ele>987</ele>'
|
||||
' <time>2018-03-13T13:46:30+01:00</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.67972" lon="6.07367">'
|
||||
' <ele>987</ele>'
|
||||
' <time>2018-03-13T13:46:35+01:00</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.67966" lon="6.07368">'
|
||||
' <ele>987</ele>'
|
||||
' <time>2018-03-13T13:46:40+01:00</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.67961" lon="6.0737">'
|
||||
' <ele>986</ele>'
|
||||
' <time>2018-03-13T13:46:45+01:00</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.67938" lon="6.07377">'
|
||||
' <ele>986</ele>'
|
||||
' <time>2018-03-13T13:47:05+01:00</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.67933" lon="6.07381">'
|
||||
' <ele>986</ele>'
|
||||
' <time>2018-03-13T13:47:10+01:00</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.67922" lon="6.07385">'
|
||||
' <ele>985</ele>'
|
||||
' <time>2018-03-13T13:47:20+01:00</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.67911" lon="6.0739">'
|
||||
' <ele>980</ele>'
|
||||
' <time>2018-03-13T13:47:30+01:00</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.679" lon="6.07399">'
|
||||
' <ele>980</ele>'
|
||||
' <time>2018-03-13T13:47:40+01:00</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.67896" lon="6.07402">'
|
||||
' <ele>980</ele>'
|
||||
' <time>2018-03-13T13:47:45+01:00</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.67884" lon="6.07408">'
|
||||
' <ele>979</ele>'
|
||||
' <time>2018-03-13T13:47:55+01:00</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.67863" lon="6.07423">'
|
||||
' <ele>981</ele>'
|
||||
' <time>2018-03-13T13:48:15+01:00</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.67858" lon="6.07425">'
|
||||
' <ele>980</ele>'
|
||||
' <time>2018-03-13T13:48:20+01:00</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.67842" lon="6.07434">'
|
||||
' <ele>979</ele>'
|
||||
' <time>2018-03-13T13:48:35+01:00</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.67837" lon="6.07435">'
|
||||
' <ele>979</ele>'
|
||||
' <time>2018-03-13T13:48:40+01:00</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.67822" lon="6.07442">'
|
||||
' <ele>975</ele>'
|
||||
' <time>2018-03-13T13:48:55+01:00</time>'
|
||||
' </trkpt>'
|
||||
' </trkseg>'
|
||||
' </trk>'
|
||||
'</gpx>'
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def gpx_file_wo_track() -> str:
|
||||
return (
|
||||
|
@ -2,7 +2,7 @@ from unittest.mock import call, patch
|
||||
|
||||
import pytest
|
||||
from flask import Flask
|
||||
from gpxpy.gpx import MovingData
|
||||
from gpxpy.gpx import IGNORE_TOP_SPEED_PERCENTILES, MovingData
|
||||
from werkzeug.datastructures import FileStorage
|
||||
|
||||
from fittrackee.users.models import User, UserSportPreference
|
||||
@ -55,7 +55,12 @@ class TestStoppedSpeedThreshold:
|
||||
assert gpx_track_segment_mock.call_args_list[0] == call(
|
||||
stopped_speed_threshold=expected_threshold
|
||||
)
|
||||
gpx_track_segment_mock.assert_called_with(expected_threshold)
|
||||
gpx_track_segment_mock.assert_called_with(
|
||||
expected_threshold, # stopped_speed_threshold
|
||||
False, # raw
|
||||
IGNORE_TOP_SPEED_PERCENTILES, # speed_extreemes_percentiles
|
||||
True, # ignore_nonstandard_distances
|
||||
)
|
||||
|
||||
def test_it_calls_get_moving_data_with_threshold_depending_from_user_preference( # noqa
|
||||
self,
|
||||
@ -85,4 +90,9 @@ class TestStoppedSpeedThreshold:
|
||||
assert gpx_track_segment_mock.call_args_list[0] == call(
|
||||
stopped_speed_threshold=expected_threshold
|
||||
)
|
||||
gpx_track_segment_mock.assert_called_with(expected_threshold)
|
||||
gpx_track_segment_mock.assert_called_with(
|
||||
expected_threshold, # stopped_speed_threshold
|
||||
False, # raw
|
||||
IGNORE_TOP_SPEED_PERCENTILES, # speed_extreemes_percentiles
|
||||
True, # ignore_nonstandard_distances
|
||||
)
|
||||
|
@ -1,9 +1,27 @@
|
||||
from datetime import datetime
|
||||
from statistics import mean
|
||||
from typing import List
|
||||
from typing import List, Union
|
||||
|
||||
import pytest
|
||||
import pytz
|
||||
from gpxpy.gpxfield import SimpleTZ
|
||||
|
||||
from fittrackee.workouts.utils.workouts import get_average_speed
|
||||
from fittrackee.workouts.utils.workouts import (
|
||||
get_average_speed,
|
||||
get_workout_datetime,
|
||||
)
|
||||
|
||||
utc_datetime = datetime(
|
||||
year=2022, month=6, day=11, hour=10, minute=23, second=00, tzinfo=pytz.utc
|
||||
)
|
||||
input_workout_dates = [
|
||||
utc_datetime,
|
||||
utc_datetime.replace(tzinfo=None),
|
||||
utc_datetime.replace(tzinfo=SimpleTZ('Z')),
|
||||
utc_datetime.astimezone(pytz.timezone('Europe/Paris')),
|
||||
utc_datetime.astimezone(pytz.timezone('America/Toronto')),
|
||||
'2022-06-11 12:23:00',
|
||||
]
|
||||
|
||||
|
||||
class TestWorkoutAverageSpeed:
|
||||
@ -30,3 +48,68 @@ class TestWorkoutAverageSpeed:
|
||||
assert get_average_speed(
|
||||
nb_workouts, total_average_speed, workout_average_speed
|
||||
) == mean(ave_speeds_list)
|
||||
|
||||
|
||||
class TestWorkoutGetWorkoutDatetime:
|
||||
@pytest.mark.parametrize('input_workout_date', input_workout_dates)
|
||||
def test_it_returns_naive_datetime(
|
||||
self, input_workout_date: Union[datetime, str]
|
||||
) -> None:
|
||||
naive_workout_date, _ = get_workout_datetime(
|
||||
workout_date=input_workout_date, user_timezone='Europe/Paris'
|
||||
)
|
||||
|
||||
assert naive_workout_date == datetime(
|
||||
year=2022, month=6, day=11, hour=10, minute=23, second=00
|
||||
)
|
||||
|
||||
def test_it_return_naive_datetime_when_no_user_timezone(self) -> None:
|
||||
naive_workout_date, _ = get_workout_datetime(
|
||||
workout_date='2022-06-11 12:23:00', user_timezone=None
|
||||
)
|
||||
|
||||
assert naive_workout_date == datetime(
|
||||
year=2022, month=6, day=11, hour=12, minute=23, second=00
|
||||
)
|
||||
|
||||
@pytest.mark.parametrize('input_workout_date', input_workout_dates)
|
||||
def test_it_returns_datetime_with_user_timezone(
|
||||
self, input_workout_date: Union[datetime, str]
|
||||
) -> None:
|
||||
timezone = 'Europe/Paris'
|
||||
|
||||
_, workout_date_with_tz = get_workout_datetime(
|
||||
input_workout_date, user_timezone=timezone, with_timezone=True
|
||||
)
|
||||
|
||||
assert workout_date_with_tz == datetime(
|
||||
year=2022,
|
||||
month=6,
|
||||
day=11,
|
||||
hour=10,
|
||||
minute=23,
|
||||
second=00,
|
||||
tzinfo=pytz.utc,
|
||||
).astimezone(pytz.timezone(timezone))
|
||||
|
||||
def test_it_does_not_return_datetime_with_user_timezone_when_no_user_tz(
|
||||
self,
|
||||
) -> None:
|
||||
_, workout_date_with_tz = get_workout_datetime(
|
||||
workout_date='2022-06-11 12:23:00',
|
||||
user_timezone=None,
|
||||
with_timezone=True,
|
||||
)
|
||||
|
||||
assert workout_date_with_tz is None
|
||||
|
||||
def test_it_does_not_return_datetime_with_user_timezone_when_with_timezone_to_false( # noqa
|
||||
self,
|
||||
) -> None:
|
||||
_, workout_date_with_tz = get_workout_datetime(
|
||||
workout_date='2022-06-11 12:23:00',
|
||||
user_timezone='Europe/Paris',
|
||||
with_timezone=False,
|
||||
)
|
||||
|
||||
assert workout_date_with_tz is None
|
||||
|
@ -969,7 +969,7 @@ class TestGetWorkoutsWithFiltersAndPagination(ApiTestCaseMixin):
|
||||
|
||||
|
||||
class TestGetWorkout(ApiTestCaseMixin):
|
||||
def test_it_gets_an_workout(
|
||||
def test_it_gets_a_workout(
|
||||
self,
|
||||
app: Flask,
|
||||
user_1: User,
|
||||
@ -1105,7 +1105,7 @@ class TestGetWorkout(ApiTestCaseMixin):
|
||||
response, f'no gpx file for this workout (id: {workout_short_id})'
|
||||
)
|
||||
|
||||
def test_it_returns_500_on_getting_gpx_if_an_workout_has_invalid_gpx_pathname( # noqa
|
||||
def test_it_returns_500_on_getting_gpx_if_a_workout_has_invalid_gpx_pathname( # noqa
|
||||
self,
|
||||
app: Flask,
|
||||
user_1: User,
|
||||
@ -1125,7 +1125,7 @@ class TestGetWorkout(ApiTestCaseMixin):
|
||||
data = self.assert_500(response)
|
||||
assert 'data' not in data
|
||||
|
||||
def test_it_returns_500_on_getting_chart_data_if_an_workout_has_invalid_gpx_pathname( # noqa
|
||||
def test_it_returns_500_on_getting_chart_data_if_a_workout_has_invalid_gpx_pathname( # noqa
|
||||
self,
|
||||
app: Flask,
|
||||
user_1: User,
|
||||
|
@ -2,7 +2,7 @@ import json
|
||||
import os
|
||||
from datetime import datetime
|
||||
from io import BytesIO
|
||||
from typing import Dict
|
||||
from typing import Dict, Optional
|
||||
from unittest.mock import Mock
|
||||
|
||||
import pytest
|
||||
@ -92,9 +92,7 @@ def assert_workout_data_with_gpx_segments(data: Dict) -> None:
|
||||
assert data['data']['workouts'][0]['descent'] == 23.4
|
||||
assert data['data']['workouts'][0]['distance'] == 0.3
|
||||
assert data['data']['workouts'][0]['max_alt'] == 998.0
|
||||
assert (
|
||||
data['data']['workouts'][0]['max_speed'] is None
|
||||
) # not enough points
|
||||
assert data['data']['workouts'][0]['max_speed'] == 5.25
|
||||
assert data['data']['workouts'][0]['min_alt'] == 975.0
|
||||
assert data['data']['workouts'][0]['moving'] == '0:03:55'
|
||||
assert data['data']['workouts'][0]['pauses'] == '0:00:15'
|
||||
@ -114,7 +112,7 @@ def assert_workout_data_with_gpx_segments(data: Dict) -> None:
|
||||
assert segment['descent'] == 11.0
|
||||
assert segment['distance'] == 0.113
|
||||
assert segment['max_alt'] == 998.0
|
||||
assert segment['max_speed'] is None
|
||||
assert segment['max_speed'] == 5.25
|
||||
assert segment['min_alt'] == 987.0
|
||||
assert segment['moving'] == '0:01:30'
|
||||
assert segment['pauses'] is None
|
||||
@ -128,28 +126,33 @@ def assert_workout_data_with_gpx_segments(data: Dict) -> None:
|
||||
assert segment['descent'] == 12.4
|
||||
assert segment['distance'] == 0.186
|
||||
assert segment['max_alt'] == 987.0
|
||||
assert segment['max_speed'] is None
|
||||
assert segment['max_speed'] == 5.12
|
||||
assert segment['min_alt'] == 975.0
|
||||
assert segment['moving'] == '0:02:25'
|
||||
assert segment['pauses'] is None
|
||||
|
||||
records = data['data']['workouts'][0]['records']
|
||||
assert len(records) == 3
|
||||
assert len(records) == 4
|
||||
assert records[0]['sport_id'] == 1
|
||||
assert records[0]['workout_id'] == data['data']['workouts'][0]['id']
|
||||
assert records[0]['record_type'] == 'LD'
|
||||
assert records[0]['record_type'] == 'MS'
|
||||
assert records[0]['workout_date'] == 'Tue, 13 Mar 2018 12:44:45 GMT'
|
||||
assert records[0]['value'] == '0:03:55'
|
||||
assert records[0]['value'] == 5.25
|
||||
assert records[1]['sport_id'] == 1
|
||||
assert records[1]['workout_id'] == data['data']['workouts'][0]['id']
|
||||
assert records[1]['record_type'] == 'FD'
|
||||
assert records[1]['record_type'] == 'LD'
|
||||
assert records[1]['workout_date'] == 'Tue, 13 Mar 2018 12:44:45 GMT'
|
||||
assert records[1]['value'] == 0.3
|
||||
assert records[1]['value'] == '0:03:55'
|
||||
assert records[2]['sport_id'] == 1
|
||||
assert records[2]['workout_id'] == data['data']['workouts'][0]['id']
|
||||
assert records[2]['record_type'] == 'AS'
|
||||
assert records[2]['record_type'] == 'FD'
|
||||
assert records[2]['workout_date'] == 'Tue, 13 Mar 2018 12:44:45 GMT'
|
||||
assert records[2]['value'] == 4.59
|
||||
assert records[2]['value'] == 0.3
|
||||
assert records[3]['sport_id'] == 1
|
||||
assert records[3]['workout_id'] == data['data']['workouts'][0]['id']
|
||||
assert records[3]['record_type'] == 'AS'
|
||||
assert records[3]['workout_date'] == 'Tue, 13 Mar 2018 12:44:45 GMT'
|
||||
assert records[3]['value'] == 4.59
|
||||
|
||||
|
||||
def assert_workout_data_wo_gpx(data: Dict) -> None:
|
||||
@ -222,7 +225,7 @@ class TestPostWorkoutWithGpx(ApiTestCaseMixin, CallArgsMixin):
|
||||
|
||||
self.assert_401(response)
|
||||
|
||||
def test_it_adds_an_workout_with_gpx_file(
|
||||
def test_it_adds_a_workout_with_gpx_file(
|
||||
self, app: Flask, user_1: User, sport_1_cycling: Sport, gpx_file: str
|
||||
) -> None:
|
||||
client, auth_token = self.get_test_client_and_auth_token(
|
||||
@ -248,7 +251,7 @@ class TestPostWorkoutWithGpx(ApiTestCaseMixin, CallArgsMixin):
|
||||
assert 'just a workout' == data['data']['workouts'][0]['title']
|
||||
assert_workout_data_with_gpx(data)
|
||||
|
||||
def test_it_adds_an_workout_with_gpx_without_name(
|
||||
def test_it_adds_a_workout_with_gpx_without_name(
|
||||
self,
|
||||
app: Flask,
|
||||
user_1: User,
|
||||
@ -281,7 +284,7 @@ class TestPostWorkoutWithGpx(ApiTestCaseMixin, CallArgsMixin):
|
||||
)
|
||||
assert_workout_data_with_gpx(data)
|
||||
|
||||
def test_it_adds_an_workout_with_gpx_without_name_timezone(
|
||||
def test_it_adds_a_workout_with_gpx_without_name_timezone(
|
||||
self,
|
||||
app: Flask,
|
||||
user_1: User,
|
||||
@ -315,6 +318,41 @@ class TestPostWorkoutWithGpx(ApiTestCaseMixin, CallArgsMixin):
|
||||
)
|
||||
assert_workout_data_with_gpx(data)
|
||||
|
||||
@pytest.mark.parametrize('input_user_timezone', [None, 'Europe/Paris'])
|
||||
def test_it_adds_a_workout_with_gpx_with_offset(
|
||||
self,
|
||||
app: Flask,
|
||||
user_1: User,
|
||||
sport_1_cycling: Sport,
|
||||
gpx_file_with_offset: str,
|
||||
input_user_timezone: Optional[str],
|
||||
) -> None:
|
||||
user_1.timezone = input_user_timezone
|
||||
client, auth_token = self.get_test_client_and_auth_token(
|
||||
app, user_1.email
|
||||
)
|
||||
|
||||
response = client.post(
|
||||
'/api/workouts',
|
||||
data=dict(
|
||||
file=(
|
||||
BytesIO(str.encode(gpx_file_with_offset)),
|
||||
'example.gpx',
|
||||
),
|
||||
data='{"sport_id": 1}',
|
||||
),
|
||||
headers=dict(
|
||||
content_type='multipart/form-data',
|
||||
Authorization=f'Bearer {auth_token}',
|
||||
),
|
||||
)
|
||||
|
||||
data = json.loads(response.data.decode())
|
||||
assert response.status_code == 201
|
||||
assert 'created' in data['status']
|
||||
assert len(data['data']['workouts']) == 1
|
||||
assert_workout_data_with_gpx(data)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'input_description,input_notes',
|
||||
[
|
||||
@ -606,7 +644,7 @@ class TestPostWorkoutWithoutGpx(ApiTestCaseMixin):
|
||||
|
||||
self.assert_401(response)
|
||||
|
||||
def test_it_adds_an_workout_without_gpx(
|
||||
def test_it_adds_a_workout_without_gpx(
|
||||
self, app: Flask, user_1: User, sport_1_cycling: Sport
|
||||
) -> None:
|
||||
client, auth_token = self.get_test_client_and_auth_token(
|
||||
@ -953,12 +991,12 @@ class TestPostAndGetWorkoutWithGpx(ApiTestCaseMixin):
|
||||
|
||||
self.assert_500(response)
|
||||
|
||||
def test_it_gets_an_workout_created_with_gpx(
|
||||
def test_it_gets_a_workout_created_with_gpx(
|
||||
self, app: Flask, user_1: User, sport_1_cycling: Sport, gpx_file: str
|
||||
) -> None:
|
||||
return self.workout_assertion(app, user_1, gpx_file, False)
|
||||
|
||||
def test_it_gets_an_workout_created_with_gpx_with_segments(
|
||||
def test_it_gets_a_workout_created_with_gpx_with_segments(
|
||||
self,
|
||||
app: Flask,
|
||||
user_1: User,
|
||||
@ -969,7 +1007,7 @@ class TestPostAndGetWorkoutWithGpx(ApiTestCaseMixin):
|
||||
app, user_1, gpx_file_with_segments, True
|
||||
)
|
||||
|
||||
def test_it_gets_chart_data_for_an_workout_created_with_gpx(
|
||||
def test_it_gets_chart_data_for_a_workout_created_with_gpx(
|
||||
self, app: Flask, user_1: User, sport_1_cycling: Sport, gpx_file: str
|
||||
) -> None:
|
||||
client, auth_token = self.get_test_client_and_auth_token(
|
||||
@ -1000,7 +1038,7 @@ class TestPostAndGetWorkoutWithGpx(ApiTestCaseMixin):
|
||||
assert data['message'] == ''
|
||||
assert data['data']['chart_data'] != ''
|
||||
|
||||
def test_it_gets_segment_chart_data_for_an_workout_created_with_gpx(
|
||||
def test_it_gets_segment_chart_data_for_a_workout_created_with_gpx(
|
||||
self, app: Flask, user_1: User, sport_1_cycling: Sport, gpx_file: str
|
||||
) -> None:
|
||||
client, auth_token = self.get_test_client_and_auth_token(
|
||||
@ -1125,7 +1163,7 @@ class TestPostAndGetWorkoutWithGpx(ApiTestCaseMixin):
|
||||
|
||||
|
||||
class TestPostAndGetWorkoutWithoutGpx(ApiTestCaseMixin):
|
||||
def test_it_add_and_gets_an_workout_wo_gpx(
|
||||
def test_it_add_and_gets_a_workout_wo_gpx(
|
||||
self, app: Flask, user_1: User, sport_1_cycling: Sport
|
||||
) -> None:
|
||||
client, auth_token = self.get_test_client_and_auth_token(
|
||||
@ -1158,7 +1196,7 @@ class TestPostAndGetWorkoutWithoutGpx(ApiTestCaseMixin):
|
||||
assert len(data['data']['workouts']) == 1
|
||||
assert_workout_data_wo_gpx(data)
|
||||
|
||||
def test_it_adds_and_gets_an_workout_wo_gpx_notes(
|
||||
def test_it_adds_and_gets_a_workout_wo_gpx_notes(
|
||||
self, app: Flask, user_1: User, sport_1_cycling: Sport
|
||||
) -> None:
|
||||
client, auth_token = self.get_test_client_and_auth_token(
|
||||
@ -1194,7 +1232,7 @@ class TestPostAndGetWorkoutWithoutGpx(ApiTestCaseMixin):
|
||||
|
||||
|
||||
class TestPostAndGetWorkoutUsingTimezones(ApiTestCaseMixin):
|
||||
def test_it_add_and_gets_an_workout_wo_gpx_with_timezone(
|
||||
def test_it_add_and_gets_a_workout_wo_gpx_with_timezone(
|
||||
self, app: Flask, user_1: User, sport_1_cycling: Sport
|
||||
) -> None:
|
||||
user_1.timezone = 'Europe/Paris'
|
||||
|
@ -9,7 +9,7 @@ from fittrackee.users.models import User
|
||||
from fittrackee.workouts.models import Sport, Workout
|
||||
|
||||
from ..mixins import ApiTestCaseMixin
|
||||
from .utils import get_random_short_id, post_an_workout
|
||||
from .utils import get_random_short_id, post_a_workout
|
||||
|
||||
|
||||
def assert_workout_data_with_gpx(data: Dict, sport_id: int) -> None:
|
||||
@ -56,7 +56,7 @@ def assert_workout_data_with_gpx(data: Dict, sport_id: int) -> None:
|
||||
|
||||
|
||||
class TestEditWorkoutWithGpx(ApiTestCaseMixin):
|
||||
def test_it_updates_title_for_an_workout_with_gpx(
|
||||
def test_it_updates_title_for_a_workout_with_gpx(
|
||||
self,
|
||||
app: Flask,
|
||||
user_1: User,
|
||||
@ -64,7 +64,7 @@ class TestEditWorkoutWithGpx(ApiTestCaseMixin):
|
||||
sport_2_running: Sport,
|
||||
gpx_file: str,
|
||||
) -> None:
|
||||
token, workout_short_id = post_an_workout(app, gpx_file)
|
||||
token, workout_short_id = post_a_workout(app, gpx_file)
|
||||
client = app.test_client()
|
||||
|
||||
response = client.patch(
|
||||
@ -100,7 +100,7 @@ class TestEditWorkoutWithGpx(ApiTestCaseMixin):
|
||||
sport_2_running: Sport,
|
||||
gpx_file: str,
|
||||
) -> None:
|
||||
token, workout_short_id = post_an_workout(app, gpx_file)
|
||||
token, workout_short_id = post_a_workout(app, gpx_file)
|
||||
client = app.test_client()
|
||||
|
||||
response = client.patch(
|
||||
@ -124,7 +124,7 @@ class TestEditWorkoutWithGpx(ApiTestCaseMixin):
|
||||
sport_2_running: Sport,
|
||||
gpx_file: str,
|
||||
) -> None:
|
||||
token, workout_short_id = post_an_workout(
|
||||
token, workout_short_id = post_a_workout(
|
||||
app, gpx_file, notes=uuid4().hex
|
||||
)
|
||||
client = app.test_client()
|
||||
@ -142,7 +142,7 @@ class TestEditWorkoutWithGpx(ApiTestCaseMixin):
|
||||
assert len(data['data']['workouts']) == 1
|
||||
assert data['data']['workouts'][0]['notes'] == ''
|
||||
|
||||
def test_it_raises_403_when_editing_an_workout_from_different_user(
|
||||
def test_it_raises_403_when_editing_a_workout_from_different_user(
|
||||
self,
|
||||
app: Flask,
|
||||
user_1: User,
|
||||
@ -151,7 +151,7 @@ class TestEditWorkoutWithGpx(ApiTestCaseMixin):
|
||||
sport_2_running: Sport,
|
||||
gpx_file: str,
|
||||
) -> None:
|
||||
_, workout_short_id = post_an_workout(app, gpx_file)
|
||||
_, workout_short_id = post_a_workout(app, gpx_file)
|
||||
client, auth_token = self.get_test_client_and_auth_token(
|
||||
app, user_2.email
|
||||
)
|
||||
@ -173,7 +173,7 @@ class TestEditWorkoutWithGpx(ApiTestCaseMixin):
|
||||
sport_2_running: Sport,
|
||||
gpx_file: str,
|
||||
) -> None:
|
||||
token, workout_short_id = post_an_workout(app, gpx_file)
|
||||
token, workout_short_id = post_a_workout(app, gpx_file)
|
||||
client = app.test_client()
|
||||
|
||||
response = client.patch(
|
||||
@ -194,7 +194,7 @@ class TestEditWorkoutWithGpx(ApiTestCaseMixin):
|
||||
def test_it_returns_400_if_payload_is_empty(
|
||||
self, app: Flask, user_1: User, sport_1_cycling: Sport, gpx_file: str
|
||||
) -> None:
|
||||
token, workout_short_id = post_an_workout(app, gpx_file)
|
||||
token, workout_short_id = post_a_workout(app, gpx_file)
|
||||
client = app.test_client()
|
||||
|
||||
response = client.patch(
|
||||
@ -209,7 +209,7 @@ class TestEditWorkoutWithGpx(ApiTestCaseMixin):
|
||||
def test_it_raises_500_if_sport_does_not_exists(
|
||||
self, app: Flask, user_1: User, sport_1_cycling: Sport, gpx_file: str
|
||||
) -> None:
|
||||
token, workout_short_id = post_an_workout(app, gpx_file)
|
||||
token, workout_short_id = post_a_workout(app, gpx_file)
|
||||
client = app.test_client()
|
||||
|
||||
response = client.patch(
|
||||
@ -223,7 +223,7 @@ class TestEditWorkoutWithGpx(ApiTestCaseMixin):
|
||||
|
||||
|
||||
class TestEditWorkoutWithoutGpx(ApiTestCaseMixin):
|
||||
def test_it_updates_an_workout_wo_gpx(
|
||||
def test_it_updates_a_workout_wo_gpx(
|
||||
self,
|
||||
app: Flask,
|
||||
user_1: User,
|
||||
@ -364,7 +364,7 @@ class TestEditWorkoutWithoutGpx(ApiTestCaseMixin):
|
||||
assert len(data['data']['workouts']) == 1
|
||||
assert data['data']['workouts'][0]['notes'] == ''
|
||||
|
||||
def test_returns_403_when_editing_an_workout_wo_gpx_from_different_user(
|
||||
def test_returns_403_when_editing_a_workout_wo_gpx_from_different_user(
|
||||
self,
|
||||
app: Flask,
|
||||
user_1: User,
|
||||
@ -393,7 +393,7 @@ class TestEditWorkoutWithoutGpx(ApiTestCaseMixin):
|
||||
|
||||
self.assert_403(response)
|
||||
|
||||
def test_it_updates_an_workout_wo_gpx_with_timezone(
|
||||
def test_it_updates_a_workout_wo_gpx_with_timezone(
|
||||
self,
|
||||
app: Flask,
|
||||
user_1_paris: User,
|
||||
@ -468,7 +468,7 @@ class TestEditWorkoutWithoutGpx(ApiTestCaseMixin):
|
||||
assert records[3]['workout_date'] == 'Tue, 15 May 2018 13:05:00 GMT'
|
||||
assert records[3]['value'] == 8.0
|
||||
|
||||
def test_it_updates_only_sport_and_distance_an_workout_wo_gpx(
|
||||
def test_it_updates_only_sport_and_distance_a_workout_wo_gpx(
|
||||
self,
|
||||
app: Flask,
|
||||
user_1: User,
|
||||
|
@ -7,7 +7,7 @@ from fittrackee.users.models import User
|
||||
from fittrackee.workouts.models import Sport, Workout
|
||||
|
||||
from ..mixins import ApiTestCaseMixin
|
||||
from .utils import get_random_short_id, post_an_workout
|
||||
from .utils import get_random_short_id, post_a_workout
|
||||
|
||||
|
||||
def get_gpx_filepath(workout_id: int) -> str:
|
||||
@ -16,10 +16,10 @@ def get_gpx_filepath(workout_id: int) -> str:
|
||||
|
||||
|
||||
class TestDeleteWorkoutWithGpx(ApiTestCaseMixin):
|
||||
def test_it_deletes_an_workout_with_gpx(
|
||||
def test_it_deletes_a_workout_with_gpx(
|
||||
self, app: Flask, user_1: User, sport_1_cycling: Sport, gpx_file: str
|
||||
) -> None:
|
||||
token, workout_short_id = post_an_workout(app, gpx_file)
|
||||
token, workout_short_id = post_a_workout(app, gpx_file)
|
||||
client = app.test_client()
|
||||
|
||||
response = client.delete(
|
||||
@ -29,7 +29,7 @@ class TestDeleteWorkoutWithGpx(ApiTestCaseMixin):
|
||||
|
||||
assert response.status_code == 204
|
||||
|
||||
def test_it_returns_403_when_deleting_an_workout_from_different_user(
|
||||
def test_it_returns_403_when_deleting_a_workout_from_different_user(
|
||||
self,
|
||||
app: Flask,
|
||||
user_1: User,
|
||||
@ -37,7 +37,7 @@ class TestDeleteWorkoutWithGpx(ApiTestCaseMixin):
|
||||
sport_1_cycling: Sport,
|
||||
gpx_file: str,
|
||||
) -> None:
|
||||
_, workout_short_id = post_an_workout(app, gpx_file)
|
||||
_, workout_short_id = post_a_workout(app, gpx_file)
|
||||
client, auth_token = self.get_test_client_and_auth_token(
|
||||
app, user_2.email
|
||||
)
|
||||
@ -64,10 +64,10 @@ class TestDeleteWorkoutWithGpx(ApiTestCaseMixin):
|
||||
data = self.assert_404(response)
|
||||
assert 'not found' in data['status']
|
||||
|
||||
def test_it_returns_500_when_deleting_an_workout_with_gpx_invalid_file(
|
||||
def test_it_returns_500_when_deleting_a_workout_with_gpx_invalid_file(
|
||||
self, app: Flask, user_1: User, sport_1_cycling: Sport, gpx_file: str
|
||||
) -> None:
|
||||
token, workout_short_id = post_an_workout(app, gpx_file)
|
||||
token, workout_short_id = post_a_workout(app, gpx_file)
|
||||
client = app.test_client()
|
||||
gpx_filepath = get_gpx_filepath(1)
|
||||
gpx_filepath = get_absolute_file_path(gpx_filepath)
|
||||
@ -82,7 +82,7 @@ class TestDeleteWorkoutWithGpx(ApiTestCaseMixin):
|
||||
|
||||
|
||||
class TestDeleteWorkoutWithoutGpx(ApiTestCaseMixin):
|
||||
def test_it_deletes_an_workout_wo_gpx(
|
||||
def test_it_deletes_a_workout_wo_gpx(
|
||||
self,
|
||||
app: Flask,
|
||||
user_1: User,
|
||||
@ -98,7 +98,7 @@ class TestDeleteWorkoutWithoutGpx(ApiTestCaseMixin):
|
||||
)
|
||||
assert response.status_code == 204
|
||||
|
||||
def test_it_returns_403_when_deleting_an_workout_from_different_user(
|
||||
def test_it_returns_403_when_deleting_a_workout_from_different_user(
|
||||
self,
|
||||
app: Flask,
|
||||
user_1: User,
|
||||
|
@ -12,7 +12,7 @@ def get_random_short_id() -> str:
|
||||
return encode_uuid(uuid4())
|
||||
|
||||
|
||||
def post_an_workout(
|
||||
def post_a_workout(
|
||||
app: Flask, gpx_file: str, notes: Optional[str] = None
|
||||
) -> Tuple[str, str]:
|
||||
client = app.test_client()
|
||||
|
@ -1,5 +1,5 @@
|
||||
from datetime import timedelta
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Any, Dict, List, Optional, Tuple, Union
|
||||
|
||||
import gpxpy.gpx
|
||||
|
||||
@ -16,9 +16,9 @@ def open_gpx_file(gpx_file: str) -> Optional[gpxpy.gpx.GPX]:
|
||||
|
||||
|
||||
def get_gpx_data(
|
||||
parsed_gpx: gpxpy.gpx,
|
||||
parsed_gpx: Union[gpxpy.gpx.GPX, gpxpy.gpx.GPXTrackSegment],
|
||||
max_speed: float,
|
||||
start: int,
|
||||
start: Union[datetime, None],
|
||||
stopped_time_between_seg: timedelta,
|
||||
stopped_speed_threshold: float,
|
||||
) -> Dict:
|
||||
@ -32,7 +32,8 @@ def get_gpx_data(
|
||||
|
||||
duration = parsed_gpx.get_duration()
|
||||
gpx_data['duration'] = (
|
||||
timedelta(seconds=duration) + stopped_time_between_seg
|
||||
timedelta(seconds=duration if duration else 0)
|
||||
+ stopped_time_between_seg
|
||||
)
|
||||
|
||||
ele = parsed_gpx.get_elevation_extremes()
|
||||
@ -43,18 +44,24 @@ def get_gpx_data(
|
||||
gpx_data['uphill'] = hill.uphill
|
||||
gpx_data['downhill'] = hill.downhill
|
||||
|
||||
mv = parsed_gpx.get_moving_data(
|
||||
moving_data = parsed_gpx.get_moving_data(
|
||||
stopped_speed_threshold=stopped_speed_threshold
|
||||
)
|
||||
gpx_data['moving_time'] = timedelta(seconds=mv.moving_time)
|
||||
gpx_data['stop_time'] = (
|
||||
timedelta(seconds=mv.stopped_time) + stopped_time_between_seg
|
||||
)
|
||||
distance = mv.moving_distance + mv.stopped_distance
|
||||
gpx_data['distance'] = distance / 1000
|
||||
if moving_data:
|
||||
gpx_data['moving_time'] = timedelta(seconds=moving_data.moving_time)
|
||||
gpx_data['stop_time'] = (
|
||||
timedelta(seconds=moving_data.stopped_time)
|
||||
+ stopped_time_between_seg
|
||||
)
|
||||
distance = moving_data.moving_distance + moving_data.stopped_distance
|
||||
gpx_data['distance'] = distance / 1000
|
||||
|
||||
average_speed = distance / mv.moving_time if mv.moving_time > 0 else 0
|
||||
gpx_data['average_speed'] = (average_speed / 1000) * 3600
|
||||
average_speed = (
|
||||
distance / moving_data.moving_time
|
||||
if moving_data.moving_time > 0
|
||||
else 0
|
||||
)
|
||||
gpx_data['average_speed'] = (average_speed / 1000) * 3600
|
||||
|
||||
return gpx_data
|
||||
|
||||
@ -72,9 +79,9 @@ def get_gpx_info(
|
||||
if gpx is None:
|
||||
raise WorkoutGPXException('not found', 'No gpx file')
|
||||
|
||||
gpx_data = {'name': gpx.tracks[0].name, 'segments': []}
|
||||
max_speed = 0
|
||||
start = 0
|
||||
gpx_data: Dict = {'name': gpx.tracks[0].name, 'segments': []}
|
||||
max_speed = 0.0
|
||||
start: Optional[datetime] = None
|
||||
map_data = []
|
||||
weather_data = []
|
||||
segments_nb = len(gpx.tracks[0].segments)
|
||||
@ -83,14 +90,15 @@ def get_gpx_info(
|
||||
stopped_time_between_seg = no_stopped_time
|
||||
|
||||
for segment_idx, segment in enumerate(gpx.tracks[0].segments):
|
||||
segment_start = 0
|
||||
segment_start: Optional[datetime] = None
|
||||
segment_points_nb = len(segment.points)
|
||||
for point_idx, point in enumerate(segment.points):
|
||||
if point_idx == 0:
|
||||
segment_start = point.time
|
||||
# first gpx point => get weather
|
||||
if start == 0:
|
||||
if start is None:
|
||||
start = point.time
|
||||
if update_weather_data:
|
||||
if point.time and update_weather_data:
|
||||
weather_data.append(get_weather(point))
|
||||
|
||||
# if a previous segment exists, calculate stopped time between
|
||||
@ -108,13 +116,19 @@ def get_gpx_info(
|
||||
|
||||
if update_map_data:
|
||||
map_data.append([point.longitude, point.latitude])
|
||||
calculated_max_speed = segment.get_moving_data(
|
||||
moving_data = segment.get_moving_data(
|
||||
stopped_speed_threshold=stopped_speed_threshold
|
||||
).max_speed
|
||||
segment_max_speed = calculated_max_speed if calculated_max_speed else 0
|
||||
)
|
||||
if moving_data:
|
||||
calculated_max_speed = moving_data.max_speed
|
||||
segment_max_speed = (
|
||||
calculated_max_speed if calculated_max_speed else 0
|
||||
)
|
||||
|
||||
if segment_max_speed > max_speed:
|
||||
max_speed = segment_max_speed
|
||||
if segment_max_speed > max_speed:
|
||||
max_speed = segment_max_speed
|
||||
else:
|
||||
segment_max_speed = 0.0
|
||||
|
||||
segment_data = get_gpx_data(
|
||||
segment,
|
||||
@ -137,12 +151,16 @@ def get_gpx_info(
|
||||
|
||||
if update_map_data:
|
||||
bounds = gpx.get_bounds()
|
||||
gpx_data['bounds'] = [
|
||||
bounds.min_latitude,
|
||||
bounds.min_longitude,
|
||||
bounds.max_latitude,
|
||||
bounds.max_longitude,
|
||||
]
|
||||
gpx_data['bounds'] = (
|
||||
[
|
||||
bounds.min_latitude,
|
||||
bounds.min_longitude,
|
||||
bounds.max_latitude,
|
||||
bounds.max_longitude,
|
||||
]
|
||||
if bounds
|
||||
else []
|
||||
)
|
||||
|
||||
return gpx_data, map_data, weather_data
|
||||
|
||||
@ -222,7 +240,11 @@ def get_chart_data(
|
||||
'latitude': point.latitude,
|
||||
'longitude': point.longitude,
|
||||
'speed': speed,
|
||||
'time': point.time,
|
||||
# workaround
|
||||
# https://github.com/tkrajina/gpxpy/issues/209
|
||||
'time': point.time.replace(
|
||||
tzinfo=timezone(point.time.utcoffset())
|
||||
),
|
||||
}
|
||||
)
|
||||
previous_point = point
|
||||
|
@ -3,18 +3,22 @@ from typing import Dict, Optional
|
||||
|
||||
import forecastio
|
||||
import pytz
|
||||
from gpxpy.gpx import GPXRoutePoint
|
||||
from gpxpy.gpx import GPXTrackPoint
|
||||
|
||||
from fittrackee import appLog
|
||||
|
||||
API_KEY = os.getenv('WEATHER_API_KEY')
|
||||
|
||||
|
||||
def get_weather(point: GPXRoutePoint) -> Optional[Dict]:
|
||||
if not API_KEY or API_KEY == '':
|
||||
def get_weather(point: GPXTrackPoint) -> Optional[Dict]:
|
||||
if not API_KEY or not point.time:
|
||||
return None
|
||||
try:
|
||||
point_time = pytz.utc.localize(point.time)
|
||||
point_time = (
|
||||
pytz.utc.localize(point.time)
|
||||
if point.time.tzinfo is None
|
||||
else point.time.astimezone(pytz.utc)
|
||||
)
|
||||
forecast = forecastio.load_forecast(
|
||||
API_KEY,
|
||||
point.latitude,
|
||||
|
@ -22,31 +22,42 @@ from .gpx import get_gpx_info
|
||||
from .maps import generate_map, get_map_hash
|
||||
|
||||
|
||||
def get_datetime_with_tz(
|
||||
timezone: str, workout_date: datetime, gpx_data: Optional[Dict] = None
|
||||
) -> Tuple[Optional[datetime], datetime]:
|
||||
def get_workout_datetime(
|
||||
workout_date: Union[datetime, str],
|
||||
user_timezone: Optional[str],
|
||||
date_str_format: Optional[str] = None,
|
||||
with_timezone: bool = False,
|
||||
) -> Tuple[datetime, Optional[datetime]]:
|
||||
"""
|
||||
Return naive datetime and datetime with user timezone
|
||||
Return naive datetime and datetime with user timezone if with_timezone
|
||||
"""
|
||||
workout_date_tz = None
|
||||
if timezone:
|
||||
user_tz = pytz.timezone(timezone)
|
||||
utc_tz = pytz.utc
|
||||
if gpx_data:
|
||||
# workout date in gpx is in UTC, but in naive datetime
|
||||
fmt = '%Y-%m-%d %H:%M:%S'
|
||||
workout_date_string = workout_date.strftime(fmt)
|
||||
workout_date_tmp = utc_tz.localize(
|
||||
datetime.strptime(workout_date_string, fmt)
|
||||
)
|
||||
workout_date_tz = workout_date_tmp.astimezone(user_tz)
|
||||
else:
|
||||
workout_date_tz = user_tz.localize(workout_date)
|
||||
workout_date = workout_date_tz.astimezone(utc_tz)
|
||||
# make datetime 'naive' like in gpx file
|
||||
workout_date = workout_date.replace(tzinfo=None)
|
||||
workout_date_with_user_tz = None
|
||||
|
||||
return workout_date_tz, workout_date
|
||||
# workout w/o gpx
|
||||
if isinstance(workout_date, str):
|
||||
if not date_str_format:
|
||||
date_str_format = '%Y-%m-%d %H:%M:%S'
|
||||
workout_date = datetime.strptime(workout_date, date_str_format)
|
||||
if user_timezone:
|
||||
workout_date = pytz.timezone(user_timezone).localize(workout_date)
|
||||
|
||||
if workout_date.tzinfo is None:
|
||||
naive_workout_date = workout_date
|
||||
if user_timezone and with_timezone:
|
||||
pytz.utc.localize(naive_workout_date)
|
||||
workout_date_with_user_tz = pytz.utc.localize(
|
||||
naive_workout_date
|
||||
).astimezone(pytz.timezone(user_timezone))
|
||||
else:
|
||||
naive_workout_date = workout_date.astimezone(pytz.utc).replace(
|
||||
tzinfo=None
|
||||
)
|
||||
if user_timezone and with_timezone:
|
||||
workout_date_with_user_tz = workout_date.astimezone(
|
||||
pytz.timezone(user_timezone)
|
||||
)
|
||||
|
||||
return naive_workout_date, workout_date_with_user_tz
|
||||
|
||||
|
||||
def get_datetime_from_request_args(
|
||||
@ -57,25 +68,32 @@ def get_datetime_from_request_args(
|
||||
|
||||
date_from_str = params.get('from')
|
||||
if date_from_str:
|
||||
date_from = datetime.strptime(date_from_str, '%Y-%m-%d')
|
||||
_, date_from = get_datetime_with_tz(user.timezone, date_from)
|
||||
date_from, _ = get_workout_datetime(
|
||||
workout_date=date_from_str,
|
||||
user_timezone=user.timezone,
|
||||
date_str_format='%Y-%m-%d',
|
||||
)
|
||||
date_to_str = params.get('to')
|
||||
if date_to_str:
|
||||
date_to = datetime.strptime(
|
||||
f'{date_to_str} 23:59:59', '%Y-%m-%d %H:%M:%S'
|
||||
date_to, _ = get_workout_datetime(
|
||||
workout_date=f'{date_to_str} 23:59:59',
|
||||
user_timezone=user.timezone,
|
||||
)
|
||||
_, date_to = get_datetime_with_tz(user.timezone, date_to)
|
||||
return date_from, date_to
|
||||
|
||||
|
||||
def _remove_microseconds(delta: timedelta) -> timedelta:
|
||||
return delta - timedelta(microseconds=delta.microseconds)
|
||||
|
||||
|
||||
def update_workout_data(
|
||||
workout: Union[Workout, WorkoutSegment], gpx_data: Dict
|
||||
) -> Union[Workout, WorkoutSegment]:
|
||||
"""
|
||||
Update workout or workout segment with data from gpx file
|
||||
"""
|
||||
workout.pauses = gpx_data['stop_time']
|
||||
workout.moving = gpx_data['moving_time']
|
||||
workout.pauses = _remove_microseconds(gpx_data['stop_time'])
|
||||
workout.moving = _remove_microseconds(gpx_data['moving_time'])
|
||||
workout.min_alt = gpx_data['elevation_min']
|
||||
workout.max_alt = gpx_data['elevation_max']
|
||||
workout.descent = gpx_data['downhill']
|
||||
@ -92,17 +110,17 @@ def create_workout(
|
||||
Create Workout from data entered by user and from gpx if a gpx file is
|
||||
provided
|
||||
"""
|
||||
workout_date = (
|
||||
gpx_data['start']
|
||||
workout_date, workout_date_tz = get_workout_datetime(
|
||||
workout_date=gpx_data['start']
|
||||
if gpx_data
|
||||
else datetime.strptime(workout_data['workout_date'], '%Y-%m-%d %H:%M')
|
||||
)
|
||||
workout_date_tz, workout_date = get_datetime_with_tz(
|
||||
user.timezone, workout_date, gpx_data
|
||||
else workout_data['workout_date'],
|
||||
date_str_format=None if gpx_data else '%Y-%m-%d %H:%M',
|
||||
user_timezone=user.timezone,
|
||||
with_timezone=True,
|
||||
)
|
||||
|
||||
duration = (
|
||||
gpx_data['duration']
|
||||
_remove_microseconds(gpx_data['duration'])
|
||||
if gpx_data
|
||||
else timedelta(seconds=workout_data['duration'])
|
||||
)
|
||||
@ -202,11 +220,10 @@ def edit_workout(
|
||||
workout.notes = workout_data.get('notes')
|
||||
if not workout.gpx:
|
||||
if workout_data.get('workout_date'):
|
||||
workout_date = datetime.strptime(
|
||||
workout_data['workout_date'], '%Y-%m-%d %H:%M'
|
||||
)
|
||||
_, workout.workout_date = get_datetime_with_tz(
|
||||
auth_user.timezone, workout_date
|
||||
workout.workout_date, _ = get_workout_datetime(
|
||||
workout_date=workout_data.get('workout_date', ''),
|
||||
date_str_format='%Y-%m-%d %H:%M',
|
||||
user_timezone=auth_user.timezone,
|
||||
)
|
||||
|
||||
if workout_data.get('duration'):
|
||||
|
@ -303,7 +303,7 @@ def get_workout(
|
||||
auth_user: User, workout_short_id: str
|
||||
) -> Union[Dict, HttpResponse]:
|
||||
"""
|
||||
Get an workout
|
||||
Get a workout
|
||||
|
||||
**Example request**:
|
||||
|
||||
@ -405,7 +405,7 @@ def get_workout_data(
|
||||
data_type: str,
|
||||
segment_id: Optional[int] = None,
|
||||
) -> Union[Dict, HttpResponse]:
|
||||
"""Get data from an workout gpx file"""
|
||||
"""Get data from a workout gpx file"""
|
||||
workout_uuid = decode_short_id(workout_short_id)
|
||||
workout = Workout.query.filter_by(uuid=workout_uuid).first()
|
||||
if not workout:
|
||||
@ -467,7 +467,7 @@ def get_workout_gpx(
|
||||
auth_user: User, workout_short_id: str
|
||||
) -> Union[Dict, HttpResponse]:
|
||||
"""
|
||||
Get gpx file for an workout displayed on map with Leaflet
|
||||
Get gpx file for a workout displayed on map with Leaflet
|
||||
|
||||
**Example request**:
|
||||
|
||||
@ -517,7 +517,7 @@ def get_workout_chart_data(
|
||||
auth_user: User, workout_short_id: str
|
||||
) -> Union[Dict, HttpResponse]:
|
||||
"""
|
||||
Get chart data from an workout gpx file, to display it with Recharts
|
||||
Get chart data from a workout gpx file, to display it with Recharts
|
||||
|
||||
**Example request**:
|
||||
|
||||
@ -587,7 +587,7 @@ def get_segment_gpx(
|
||||
auth_user: User, workout_short_id: str, segment_id: int
|
||||
) -> Union[Dict, HttpResponse]:
|
||||
"""
|
||||
Get gpx file for an workout segment displayed on map with Leaflet
|
||||
Get gpx file for a workout segment displayed on map with Leaflet
|
||||
|
||||
**Example request**:
|
||||
|
||||
@ -639,7 +639,7 @@ def get_segment_chart_data(
|
||||
auth_user: User, workout_short_id: str, segment_id: int
|
||||
) -> Union[Dict, HttpResponse]:
|
||||
"""
|
||||
Get chart data from an workout gpx file, to display it with Recharts
|
||||
Get chart data from a workout gpx file, to display it with Recharts
|
||||
|
||||
**Example request**:
|
||||
|
||||
@ -851,7 +851,7 @@ def get_map_tile(s: str, z: str, x: str, y: str) -> Tuple[Response, int]:
|
||||
@require_auth(scopes='write')
|
||||
def post_workout(auth_user: User) -> Union[Tuple[Dict, int], HttpResponse]:
|
||||
"""
|
||||
Post an workout with a gpx file
|
||||
Post a workout with a gpx file
|
||||
|
||||
**Example request**:
|
||||
|
||||
@ -1021,7 +1021,7 @@ def post_workout_no_gpx(
|
||||
auth_user: User,
|
||||
) -> Union[Tuple[Dict, int], HttpResponse]:
|
||||
"""
|
||||
Post an workout without gpx file
|
||||
Post a workout without gpx file
|
||||
|
||||
**Example request**:
|
||||
|
||||
@ -1111,7 +1111,8 @@ def post_workout_no_gpx(
|
||||
"status": "success"
|
||||
}
|
||||
|
||||
:<json string workout_date: workout date (format: ``%Y-%m-%d %H:%M``)
|
||||
:<json string workout_date: workout date, in user timezone
|
||||
(format: ``%Y-%m-%d %H:%M``)
|
||||
:<json float distance: workout distance in km
|
||||
:<json integer duration: workout duration in seconds
|
||||
:<json string notes: notes (not mandatory)
|
||||
@ -1169,7 +1170,7 @@ def update_workout(
|
||||
auth_user: User, workout_short_id: str
|
||||
) -> Union[Dict, HttpResponse]:
|
||||
"""
|
||||
Update an workout
|
||||
Update a workout
|
||||
|
||||
**Example request**:
|
||||
|
||||
@ -1261,7 +1262,8 @@ def update_workout(
|
||||
|
||||
:param string workout_short_id: workout short id
|
||||
|
||||
:<json string workout_date: workout date (format: ``%Y-%m-%d %H:%M``)
|
||||
:<json string workout_date: workout date in user timezone
|
||||
(format: ``%Y-%m-%d %H:%M``)
|
||||
(only for workout without gpx)
|
||||
:<json float distance: workout distance in km
|
||||
(only for workout without gpx)
|
||||
@ -1316,7 +1318,7 @@ def delete_workout(
|
||||
auth_user: User, workout_short_id: str
|
||||
) -> Union[Tuple[Dict, int], HttpResponse]:
|
||||
"""
|
||||
Delete an workout
|
||||
Delete a workout
|
||||
|
||||
**Example request**:
|
||||
|
||||
|
Reference in New Issue
Block a user