API - handle gpx files with offset

This commit is contained in:
Sam 2022-06-11 13:10:02 +02:00
parent 5874933643
commit 4288c3c387
10 changed files with 392 additions and 102 deletions

View File

@ -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() @pytest.fixture()
def gpx_file_wo_track() -> str: def gpx_file_wo_track() -> str:
return ( return (

View File

@ -2,7 +2,7 @@ from unittest.mock import call, patch
import pytest import pytest
from flask import Flask from flask import Flask
from gpxpy.gpx import MovingData from gpxpy.gpx import IGNORE_TOP_SPEED_PERCENTILES, MovingData
from werkzeug.datastructures import FileStorage from werkzeug.datastructures import FileStorage
from fittrackee.users.models import User, UserSportPreference from fittrackee.users.models import User, UserSportPreference
@ -55,7 +55,12 @@ class TestStoppedSpeedThreshold:
assert gpx_track_segment_mock.call_args_list[0] == call( assert gpx_track_segment_mock.call_args_list[0] == call(
stopped_speed_threshold=expected_threshold 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 def test_it_calls_get_moving_data_with_threshold_depending_from_user_preference( # noqa
self, self,
@ -85,4 +90,9 @@ class TestStoppedSpeedThreshold:
assert gpx_track_segment_mock.call_args_list[0] == call( assert gpx_track_segment_mock.call_args_list[0] == call(
stopped_speed_threshold=expected_threshold 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
)

View File

@ -1,9 +1,27 @@
from datetime import datetime
from statistics import mean from statistics import mean
from typing import List from typing import List, Union
import pytest 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: class TestWorkoutAverageSpeed:
@ -30,3 +48,68 @@ class TestWorkoutAverageSpeed:
assert get_average_speed( assert get_average_speed(
nb_workouts, total_average_speed, workout_average_speed nb_workouts, total_average_speed, workout_average_speed
) == mean(ave_speeds_list) ) == 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

View File

@ -2,7 +2,7 @@ import json
import os import os
from datetime import datetime from datetime import datetime
from io import BytesIO from io import BytesIO
from typing import Dict from typing import Dict, Optional
from unittest.mock import Mock from unittest.mock import Mock
import pytest 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]['descent'] == 23.4
assert data['data']['workouts'][0]['distance'] == 0.3 assert data['data']['workouts'][0]['distance'] == 0.3
assert data['data']['workouts'][0]['max_alt'] == 998.0 assert data['data']['workouts'][0]['max_alt'] == 998.0
assert ( assert data['data']['workouts'][0]['max_speed'] == 5.25
data['data']['workouts'][0]['max_speed'] is None
) # not enough points
assert data['data']['workouts'][0]['min_alt'] == 975.0 assert data['data']['workouts'][0]['min_alt'] == 975.0
assert data['data']['workouts'][0]['moving'] == '0:03:55' assert data['data']['workouts'][0]['moving'] == '0:03:55'
assert data['data']['workouts'][0]['pauses'] == '0:00:15' 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['descent'] == 11.0
assert segment['distance'] == 0.113 assert segment['distance'] == 0.113
assert segment['max_alt'] == 998.0 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['min_alt'] == 987.0
assert segment['moving'] == '0:01:30' assert segment['moving'] == '0:01:30'
assert segment['pauses'] is None 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['descent'] == 12.4
assert segment['distance'] == 0.186 assert segment['distance'] == 0.186
assert segment['max_alt'] == 987.0 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['min_alt'] == 975.0
assert segment['moving'] == '0:02:25' assert segment['moving'] == '0:02:25'
assert segment['pauses'] is None assert segment['pauses'] is None
records = data['data']['workouts'][0]['records'] records = data['data']['workouts'][0]['records']
assert len(records) == 3 assert len(records) == 4
assert records[0]['sport_id'] == 1 assert records[0]['sport_id'] == 1
assert records[0]['workout_id'] == data['data']['workouts'][0]['id'] 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]['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]['sport_id'] == 1
assert records[1]['workout_id'] == data['data']['workouts'][0]['id'] 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]['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]['sport_id'] == 1
assert records[2]['workout_id'] == data['data']['workouts'][0]['id'] 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]['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: def assert_workout_data_wo_gpx(data: Dict) -> None:
@ -315,6 +318,41 @@ class TestPostWorkoutWithGpx(ApiTestCaseMixin, CallArgsMixin):
) )
assert_workout_data_with_gpx(data) 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( @pytest.mark.parametrize(
'input_description,input_notes', 'input_description,input_notes',
[ [

View File

@ -1,5 +1,5 @@
from datetime import timedelta from datetime import datetime, timedelta, timezone
from typing import Any, Dict, List, Optional, Tuple from typing import Any, Dict, List, Optional, Tuple, Union
import gpxpy.gpx import gpxpy.gpx
@ -16,9 +16,9 @@ def open_gpx_file(gpx_file: str) -> Optional[gpxpy.gpx.GPX]:
def get_gpx_data( def get_gpx_data(
parsed_gpx: gpxpy.gpx, parsed_gpx: Union[gpxpy.gpx.GPX, gpxpy.gpx.GPXTrackSegment],
max_speed: float, max_speed: float,
start: int, start: Union[datetime, None],
stopped_time_between_seg: timedelta, stopped_time_between_seg: timedelta,
stopped_speed_threshold: float, stopped_speed_threshold: float,
) -> Dict: ) -> Dict:
@ -32,7 +32,8 @@ def get_gpx_data(
duration = parsed_gpx.get_duration() duration = parsed_gpx.get_duration()
gpx_data['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() ele = parsed_gpx.get_elevation_extremes()
@ -43,18 +44,24 @@ def get_gpx_data(
gpx_data['uphill'] = hill.uphill gpx_data['uphill'] = hill.uphill
gpx_data['downhill'] = hill.downhill gpx_data['downhill'] = hill.downhill
mv = parsed_gpx.get_moving_data( moving_data = parsed_gpx.get_moving_data(
stopped_speed_threshold=stopped_speed_threshold stopped_speed_threshold=stopped_speed_threshold
) )
gpx_data['moving_time'] = timedelta(seconds=mv.moving_time) if moving_data:
gpx_data['stop_time'] = ( gpx_data['moving_time'] = timedelta(seconds=moving_data.moving_time)
timedelta(seconds=mv.stopped_time) + stopped_time_between_seg gpx_data['stop_time'] = (
) timedelta(seconds=moving_data.stopped_time)
distance = mv.moving_distance + mv.stopped_distance + stopped_time_between_seg
gpx_data['distance'] = distance / 1000 )
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 average_speed = (
gpx_data['average_speed'] = (average_speed / 1000) * 3600 distance / moving_data.moving_time
if moving_data.moving_time > 0
else 0
)
gpx_data['average_speed'] = (average_speed / 1000) * 3600
return gpx_data return gpx_data
@ -72,9 +79,9 @@ def get_gpx_info(
if gpx is None: if gpx is None:
raise WorkoutGPXException('not found', 'No gpx file') raise WorkoutGPXException('not found', 'No gpx file')
gpx_data = {'name': gpx.tracks[0].name, 'segments': []} gpx_data: Dict = {'name': gpx.tracks[0].name, 'segments': []}
max_speed = 0 max_speed = 0.0
start = 0 start: Optional[datetime] = None
map_data = [] map_data = []
weather_data = [] weather_data = []
segments_nb = len(gpx.tracks[0].segments) segments_nb = len(gpx.tracks[0].segments)
@ -83,14 +90,15 @@ def get_gpx_info(
stopped_time_between_seg = no_stopped_time stopped_time_between_seg = no_stopped_time
for segment_idx, segment in enumerate(gpx.tracks[0].segments): for segment_idx, segment in enumerate(gpx.tracks[0].segments):
segment_start = 0 segment_start: Optional[datetime] = None
segment_points_nb = len(segment.points) segment_points_nb = len(segment.points)
for point_idx, point in enumerate(segment.points): for point_idx, point in enumerate(segment.points):
if point_idx == 0: if point_idx == 0:
segment_start = point.time
# first gpx point => get weather # first gpx point => get weather
if start == 0: if start is None:
start = point.time start = point.time
if update_weather_data: if point.time and update_weather_data:
weather_data.append(get_weather(point)) weather_data.append(get_weather(point))
# if a previous segment exists, calculate stopped time between # if a previous segment exists, calculate stopped time between
@ -108,13 +116,19 @@ def get_gpx_info(
if update_map_data: if update_map_data:
map_data.append([point.longitude, point.latitude]) 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 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: if segment_max_speed > max_speed:
max_speed = segment_max_speed max_speed = segment_max_speed
else:
segment_max_speed = 0.0
segment_data = get_gpx_data( segment_data = get_gpx_data(
segment, segment,
@ -137,12 +151,16 @@ def get_gpx_info(
if update_map_data: if update_map_data:
bounds = gpx.get_bounds() bounds = gpx.get_bounds()
gpx_data['bounds'] = [ gpx_data['bounds'] = (
bounds.min_latitude, [
bounds.min_longitude, bounds.min_latitude,
bounds.max_latitude, bounds.min_longitude,
bounds.max_longitude, bounds.max_latitude,
] bounds.max_longitude,
]
if bounds
else []
)
return gpx_data, map_data, weather_data return gpx_data, map_data, weather_data
@ -222,7 +240,11 @@ def get_chart_data(
'latitude': point.latitude, 'latitude': point.latitude,
'longitude': point.longitude, 'longitude': point.longitude,
'speed': speed, '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 previous_point = point

View File

@ -3,18 +3,22 @@ from typing import Dict, Optional
import forecastio import forecastio
import pytz import pytz
from gpxpy.gpx import GPXRoutePoint from gpxpy.gpx import GPXTrackPoint
from fittrackee import appLog from fittrackee import appLog
API_KEY = os.getenv('WEATHER_API_KEY') API_KEY = os.getenv('WEATHER_API_KEY')
def get_weather(point: GPXRoutePoint) -> Optional[Dict]: def get_weather(point: GPXTrackPoint) -> Optional[Dict]:
if not API_KEY or API_KEY == '': if not API_KEY or not point.time:
return None return None
try: 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( forecast = forecastio.load_forecast(
API_KEY, API_KEY,
point.latitude, point.latitude,

View File

@ -22,31 +22,42 @@ from .gpx import get_gpx_info
from .maps import generate_map, get_map_hash from .maps import generate_map, get_map_hash
def get_datetime_with_tz( def get_workout_datetime(
timezone: str, workout_date: datetime, gpx_data: Optional[Dict] = None workout_date: Union[datetime, str],
) -> Tuple[Optional[datetime], datetime]: 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 workout_date_with_user_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)
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( def get_datetime_from_request_args(
@ -57,25 +68,32 @@ def get_datetime_from_request_args(
date_from_str = params.get('from') date_from_str = params.get('from')
if date_from_str: if date_from_str:
date_from = datetime.strptime(date_from_str, '%Y-%m-%d') date_from, _ = get_workout_datetime(
_, date_from = get_datetime_with_tz(user.timezone, date_from) workout_date=date_from_str,
user_timezone=user.timezone,
date_str_format='%Y-%m-%d',
)
date_to_str = params.get('to') date_to_str = params.get('to')
if date_to_str: if date_to_str:
date_to = datetime.strptime( date_to, _ = get_workout_datetime(
f'{date_to_str} 23:59:59', '%Y-%m-%d %H:%M:%S' 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 return date_from, date_to
def _remove_microseconds(delta: timedelta) -> timedelta:
return delta - timedelta(microseconds=delta.microseconds)
def update_workout_data( def update_workout_data(
workout: Union[Workout, WorkoutSegment], gpx_data: Dict workout: Union[Workout, WorkoutSegment], gpx_data: Dict
) -> Union[Workout, WorkoutSegment]: ) -> Union[Workout, WorkoutSegment]:
""" """
Update workout or workout segment with data from gpx file Update workout or workout segment with data from gpx file
""" """
workout.pauses = gpx_data['stop_time'] workout.pauses = _remove_microseconds(gpx_data['stop_time'])
workout.moving = gpx_data['moving_time'] workout.moving = _remove_microseconds(gpx_data['moving_time'])
workout.min_alt = gpx_data['elevation_min'] workout.min_alt = gpx_data['elevation_min']
workout.max_alt = gpx_data['elevation_max'] workout.max_alt = gpx_data['elevation_max']
workout.descent = gpx_data['downhill'] 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 Create Workout from data entered by user and from gpx if a gpx file is
provided provided
""" """
workout_date = ( workout_date, workout_date_tz = get_workout_datetime(
gpx_data['start'] workout_date=gpx_data['start']
if gpx_data if gpx_data
else datetime.strptime(workout_data['workout_date'], '%Y-%m-%d %H:%M') else workout_data['workout_date'],
) date_str_format=None if gpx_data else '%Y-%m-%d %H:%M',
workout_date_tz, workout_date = get_datetime_with_tz( user_timezone=user.timezone,
user.timezone, workout_date, gpx_data with_timezone=True,
) )
duration = ( duration = (
gpx_data['duration'] _remove_microseconds(gpx_data['duration'])
if gpx_data if gpx_data
else timedelta(seconds=workout_data['duration']) else timedelta(seconds=workout_data['duration'])
) )
@ -202,11 +220,10 @@ def edit_workout(
workout.notes = workout_data.get('notes') workout.notes = workout_data.get('notes')
if not workout.gpx: if not workout.gpx:
if workout_data.get('workout_date'): if workout_data.get('workout_date'):
workout_date = datetime.strptime( workout.workout_date, _ = get_workout_datetime(
workout_data['workout_date'], '%Y-%m-%d %H:%M' workout_date=workout_data.get('workout_date', ''),
) date_str_format='%Y-%m-%d %H:%M',
_, workout.workout_date = get_datetime_with_tz( user_timezone=auth_user.timezone,
auth_user.timezone, workout_date
) )
if workout_data.get('duration'): if workout_data.get('duration'):

View File

@ -1111,7 +1111,8 @@ def post_workout_no_gpx(
"status": "success" "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 float distance: workout distance in km
:<json integer duration: workout duration in seconds :<json integer duration: workout duration in seconds
:<json string notes: notes (not mandatory) :<json string notes: notes (not mandatory)
@ -1261,7 +1262,8 @@ def update_workout(
:param string workout_short_id: workout short id :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) (only for workout without gpx)
:<json float distance: workout distance in km :<json float distance: workout distance in km
(only for workout without gpx) (only for workout without gpx)

8
poetry.lock generated
View File

@ -386,11 +386,11 @@ typing-extensions = {version = ">=3.7.4.3", markers = "python_version < \"3.8\""
[[package]] [[package]]
name = "gpxpy" name = "gpxpy"
version = "1.3.4" version = "1.5.0"
description = "GPX file parser and GPS track manipulation library" description = "GPX file parser and GPS track manipulation library"
category = "main" category = "main"
optional = false optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" python-versions = ">=3.6"
[[package]] [[package]]
name = "greenlet" name = "greenlet"
@ -1466,7 +1466,7 @@ testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-
[metadata] [metadata]
lock-version = "1.1" lock-version = "1.1"
python-versions = "^3.7" python-versions = "^3.7"
content-hash = "fa5be5cc59de72ae3ddb18acb2c0291a7d77296de6427492b46c40d794800335" content-hash = "40b8491c8b82a7b29f57a2845209965c6351b527aebb7d504f5f8ec0ace4141e"
[metadata.files] [metadata.files]
alabaster = [ alabaster = [
@ -1727,7 +1727,7 @@ gitpython = [
{file = "GitPython-3.1.27.tar.gz", hash = "sha256:1c885ce809e8ba2d88a29befeb385fcea06338d3640712b59ca623c220bb5704"}, {file = "GitPython-3.1.27.tar.gz", hash = "sha256:1c885ce809e8ba2d88a29befeb385fcea06338d3640712b59ca623c220bb5704"},
] ]
gpxpy = [ gpxpy = [
{file = "gpxpy-1.3.4.tar.gz", hash = "sha256:4a0f072ae5bdf9270c7450e452f93a6c5c91d888114e8d78868a8f163b0dbb15"}, {file = "gpxpy-1.5.0.tar.gz", hash = "sha256:e6993a8945eae07a833cd304b88bbc6c3c132d63b2bf4a9b0a5d9097616b8708"},
] ]
greenlet = [ greenlet = [
{file = "greenlet-1.1.2-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:58df5c2a0e293bf665a51f8a100d3e9956febfbf1d9aaf8c0677cf70218910c6"}, {file = "greenlet-1.1.2-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:58df5c2a0e293bf665a51f8a100d3e9956febfbf1d9aaf8c0677cf70218910c6"},

View File

@ -29,7 +29,7 @@ flask = "^2.1"
flask-bcrypt = "^1.0" flask-bcrypt = "^1.0"
flask-dramatiq = "^0.6.0" flask-dramatiq = "^0.6.0"
flask-migrate = "^3.1" flask-migrate = "^3.1"
gpxpy = "=1.3.4" gpxpy = "=1.5.0"
gunicorn = "^20.1" gunicorn = "^20.1"
humanize = "^4.1" humanize = "^4.1"
psycopg2-binary = "^2.9" psycopg2-binary = "^2.9"