From 027fa8f69969d7ea2076fb7ef49edc35cead8ea1 Mon Sep 17 00:00:00 2001 From: Sam Date: Mon, 11 Jun 2018 16:44:49 +0200 Subject: [PATCH] API: timezone support for activity creation and edition - #11 --- .../fittrackee_api/activities/activities.py | 2 +- .../fittrackee_api/activities/utils.py | 33 +++- .../tests/test_activities_api_1_post.py | 43 +++++ .../tests/test_activities_api_2_patch.py | 149 +++++++++++++++++- fittrackee_api/fittrackee_api/users/auth.py | 1 + fittrackee_api/requirements.txt | 1 + 6 files changed, 219 insertions(+), 10 deletions(-) diff --git a/fittrackee_api/fittrackee_api/activities/activities.py b/fittrackee_api/fittrackee_api/activities/activities.py index d58e0a2f..5893fbfb 100644 --- a/fittrackee_api/fittrackee_api/activities/activities.py +++ b/fittrackee_api/fittrackee_api/activities/activities.py @@ -300,7 +300,7 @@ def update_activity(auth_user_id, activity_id): try: activity = Activity.query.filter_by(id=activity_id).first() if activity: - activity = edit_activity(activity, activity_data) + activity = edit_activity(activity, activity_data, auth_user_id) db.session.commit() response_object = { 'status': 'success', diff --git a/fittrackee_api/fittrackee_api/activities/utils.py b/fittrackee_api/fittrackee_api/activities/utils.py index d638125f..2ba5977f 100644 --- a/fittrackee_api/fittrackee_api/activities/utils.py +++ b/fittrackee_api/fittrackee_api/activities/utils.py @@ -5,12 +5,14 @@ import zipfile from datetime import datetime, timedelta import gpxpy.gpx +import pytz from fittrackee_api import db from flask import current_app from sqlalchemy import exc from staticmap import Line, StaticMap from werkzeug.utils import secure_filename +from ..users.models import User from .models import Activity, ActivitySegment, Sport @@ -37,8 +39,20 @@ def update_activity_data(activity, gpx_data): def create_activity( auth_user_id, activity_data, gpx_data=None ): + user = User.query.filter_by(id=auth_user_id).first() + activity_date = gpx_data['start'] if gpx_data else datetime.strptime( activity_data.get('activity_date'), '%Y-%m-%d %H:%M') + activity_date_tz = None + # activity date in gpx are directly in UTC + if user.timezone and not gpx_data: + user_tz = pytz.timezone(user.timezone) + activity_date_tz = user_tz.localize(activity_date) + if not gpx_data: + # make datetime 'naive' like in gpx file + activity_date = activity_date_tz.astimezone(pytz.utc) + activity_date = activity_date.replace(tzinfo=None) + duration = gpx_data['duration'] if gpx_data \ else timedelta(seconds=activity_data.get('duration')) distance = gpx_data['distance'] if gpx_data \ @@ -58,7 +72,12 @@ def create_activity( new_activity.title = title else: sport = Sport.query.filter_by(id=new_activity.sport_id).first() - new_activity.title = f'{sport.label} - {new_activity.activity_date}' + fmt = "%Y-%m-%d %H:%M:%S" + activity_datetime = ( + activity_date_tz.strftime(fmt) + if activity_date_tz + else new_activity.activity_date.strftime(fmt)) + new_activity.title = f'{sport.label} - {activity_datetime}' if gpx_data: new_activity.gpx = gpx_data['filename'] @@ -85,15 +104,23 @@ def create_segment(activity_id, segment_data): return new_segment -def edit_activity(activity, activity_data): +def edit_activity(activity, activity_data, auth_user_id): if activity_data.get('sport_id'): activity.sport_id = activity_data.get('sport_id') if activity_data.get('title'): activity.title = activity_data.get('title') if not activity.gpx: if activity_data.get('activity_date'): - activity.activity_date = datetime.strptime( + activity_date = datetime.strptime( activity_data.get('activity_date'), '%Y-%m-%d %H:%M') + user = User.query.filter_by(id=auth_user_id).first() + if user.timezone: + # make datetime 'naive' like in gpx file + user_tz = pytz.timezone(user.timezone) + activity_date_tz = user_tz.localize(activity_date) + activity_date = activity_date_tz.astimezone(pytz.utc) + activity_date = activity_date.replace(tzinfo=None) + activity.activity_date = activity_date if activity_data.get('duration'): activity.duration = timedelta( seconds=activity_data.get('duration')) diff --git a/fittrackee_api/fittrackee_api/tests/test_activities_api_1_post.py b/fittrackee_api/fittrackee_api/tests/test_activities_api_1_post.py index c6bcaa71..928921fb 100644 --- a/fittrackee_api/fittrackee_api/tests/test_activities_api_1_post.py +++ b/fittrackee_api/fittrackee_api/tests/test_activities_api_1_post.py @@ -569,6 +569,49 @@ def test_get_an_activity_wo_gpx(app, user_1, sport_1_cycling): assert_activity_data_wo_gpx(data) +def test_get_an_activity_wo_gpx_with_timezone(app, user_1, sport_1_cycling): + user_1.timezone = 'Europe/Paris' + client = app.test_client() + resp_login = client.post( + '/api/auth/login', + data=json.dumps(dict( + email='test@test.com', + password='12345678' + )), + content_type='application/json' + ) + client.post( + '/api/activities/no_gpx', + content_type='application/json', + data=json.dumps(dict( + sport_id=1, + duration=3600, + activity_date='2018-05-15 14:05', + distance=10 + )), + headers=dict( + Authorization='Bearer ' + json.loads( + resp_login.data.decode() + )['auth_token'] + ) + ) + response = client.get( + '/api/activities/1', + headers=dict( + Authorization='Bearer ' + json.loads( + resp_login.data.decode() + )['auth_token'] + ) + ) + data = json.loads(response.data.decode()) + + assert response.status_code == 200 + assert 'success' in data['status'] + assert len(data['data']['activities']) == 1 + assert data['data']['activities'][0]['activity_date'] == 'Tue, 15 May 2018 12:05:00 GMT' # noqa + assert data['data']['activities'][0]['title'] == 'Cycling - 2018-05-15 14:05:00' # noqa + + def test_add_an_activity_no_gpx_invalid_payload(app, user_1, sport_1_cycling): client = app.test_client() resp_login = client.post( diff --git a/fittrackee_api/fittrackee_api/tests/test_activities_api_2_patch.py b/fittrackee_api/fittrackee_api/tests/test_activities_api_2_patch.py index 4cdb7196..c48ffeb1 100644 --- a/fittrackee_api/fittrackee_api/tests/test_activities_api_2_patch.py +++ b/fittrackee_api/fittrackee_api/tests/test_activities_api_2_patch.py @@ -306,7 +306,7 @@ def test_edit_an_activity_wo_gpx( data=json.dumps(dict( sport_id=2, duration=3600, - activity_date='2018-05-15 14:05', + activity_date='2018-05-15 15:05', distance=8, title='Activity test' )), @@ -322,7 +322,7 @@ def test_edit_an_activity_wo_gpx( assert 'success' in data['status'] assert len(data['data']['activities']) == 1 assert 'creation_date' in data['data']['activities'][0] - assert data['data']['activities'][0]['activity_date'] == 'Tue, 15 May 2018 14:05:00 GMT' # noqa + assert data['data']['activities'][0]['activity_date'] == 'Tue, 15 May 2018 15:05:00 GMT' # noqa assert data['data']['activities'][0]['user_id'] == 1 assert data['data']['activities'][0]['sport_id'] == 2 assert data['data']['activities'][0]['duration'] == '1:00:00' @@ -343,22 +343,159 @@ def test_edit_an_activity_wo_gpx( assert records[0]['sport_id'] == 2 assert records[0]['activity_id'] == 1 assert records[0]['record_type'] == 'MS' - assert records[0]['activity_date'] == 'Tue, 15 May 2018 14:05:00 GMT' + assert records[0]['activity_date'] == 'Tue, 15 May 2018 15:05:00 GMT' assert records[0]['value'] == 8.0 assert records[1]['sport_id'] == 2 assert records[1]['activity_id'] == 1 assert records[1]['record_type'] == 'LD' - assert records[1]['activity_date'] == 'Tue, 15 May 2018 14:05:00 GMT' + assert records[1]['activity_date'] == 'Tue, 15 May 2018 15:05:00 GMT' assert records[1]['value'] == '1:00:00' assert records[2]['sport_id'] == 2 assert records[2]['activity_id'] == 1 assert records[2]['record_type'] == 'FD' - assert records[2]['activity_date'] == 'Tue, 15 May 2018 14:05:00 GMT' + assert records[2]['activity_date'] == 'Tue, 15 May 2018 15:05:00 GMT' assert records[2]['value'] == 8.0 assert records[3]['sport_id'] == 2 assert records[3]['activity_id'] == 1 assert records[3]['record_type'] == 'AS' - assert records[3]['activity_date'] == 'Tue, 15 May 2018 14:05:00 GMT' + assert records[3]['activity_date'] == 'Tue, 15 May 2018 15:05:00 GMT' + assert records[3]['value'] == 8.0 + + +def test_edit_an_activity_wo_gpx_timezone( + app, user_1, sport_1_cycling, sport_2_running +): + client = app.test_client() + user_1.timezone = 'Europe/Paris' + resp_login = client.post( + '/api/auth/login', + data=json.dumps(dict( + email='test@test.com', + password='12345678' + )), + content_type='application/json' + ) + + response = client.post( + '/api/activities/no_gpx', + content_type='application/json', + data=json.dumps(dict( + sport_id=1, + duration=3600, + activity_date='2018-05-14 14:05', + distance=7 + )), + headers=dict( + Authorization='Bearer ' + json.loads( + resp_login.data.decode() + )['auth_token'] + ) + ) + + data = json.loads(response.data.decode()) + + assert response.status_code == 201 + assert 'created' in data['status'] + assert len(data['data']['activities']) == 1 + assert 'creation_date' in data['data']['activities'][0] + assert data['data']['activities'][0]['activity_date'] == 'Mon, 14 May 2018 12:05:00 GMT' # noqa + assert data['data']['activities'][0]['user_id'] == 1 + assert data['data']['activities'][0]['sport_id'] == 1 + assert data['data']['activities'][0]['duration'] == '1:00:00' + assert data['data']['activities'][0]['title'] == 'Cycling - 2018-05-14 14:05:00' # noqa + assert data['data']['activities'][0]['ascent'] is None + assert data['data']['activities'][0]['ave_speed'] == 7.0 + assert data['data']['activities'][0]['descent'] is None + assert data['data']['activities'][0]['distance'] == 7.0 + assert data['data']['activities'][0]['max_alt'] is None + assert data['data']['activities'][0]['max_speed'] == 7.0 + assert data['data']['activities'][0]['min_alt'] is None + assert data['data']['activities'][0]['moving'] == '1:00:00' + assert data['data']['activities'][0]['pauses'] is None + assert data['data']['activities'][0]['with_gpx'] is False + + records = data['data']['activities'][0]['records'] + assert len(records) == 4 + assert records[0]['sport_id'] == 1 + assert records[0]['activity_id'] == 1 + assert records[0]['record_type'] == 'MS' + assert records[0]['activity_date'] == 'Mon, 14 May 2018 12:05:00 GMT' + assert records[0]['value'] == 7.0 + assert records[1]['sport_id'] == 1 + assert records[1]['activity_id'] == 1 + assert records[1]['record_type'] == 'LD' + assert records[1]['activity_date'] == 'Mon, 14 May 2018 12:05:00 GMT' + assert records[1]['value'] == '1:00:00' + assert records[2]['sport_id'] == 1 + assert records[2]['activity_id'] == 1 + assert records[2]['record_type'] == 'FD' + assert records[2]['activity_date'] == 'Mon, 14 May 2018 12:05:00 GMT' + assert records[2]['value'] == 7.0 + assert records[3]['sport_id'] == 1 + assert records[3]['activity_id'] == 1 + assert records[3]['record_type'] == 'AS' + assert records[3]['activity_date'] == 'Mon, 14 May 2018 12:05:00 GMT' + assert records[3]['value'] == 7.0 + + response = client.patch( + '/api/activities/1', + content_type='application/json', + data=json.dumps(dict( + sport_id=2, + duration=3600, + activity_date='2018-05-15 15:05', + distance=8, + title='Activity test' + )), + headers=dict( + Authorization='Bearer ' + json.loads( + resp_login.data.decode() + )['auth_token'] + ) + ) + data = json.loads(response.data.decode()) + + assert response.status_code == 200 + assert 'success' in data['status'] + assert len(data['data']['activities']) == 1 + assert 'creation_date' in data['data']['activities'][0] + assert data['data']['activities'][0]['activity_date'] == 'Tue, 15 May 2018 13:05:00 GMT' # noqa + assert data['data']['activities'][0]['user_id'] == 1 + assert data['data']['activities'][0]['sport_id'] == 2 + assert data['data']['activities'][0]['duration'] == '1:00:00' + assert data['data']['activities'][0]['title'] == 'Activity test' # noqa + assert data['data']['activities'][0]['ascent'] is None + assert data['data']['activities'][0]['ave_speed'] == 8.0 + assert data['data']['activities'][0]['descent'] is None + assert data['data']['activities'][0]['distance'] == 8.0 + assert data['data']['activities'][0]['max_alt'] is None + assert data['data']['activities'][0]['max_speed'] == 8.0 + assert data['data']['activities'][0]['min_alt'] is None + assert data['data']['activities'][0]['moving'] == '1:00:00' + assert data['data']['activities'][0]['pauses'] is None + assert data['data']['activities'][0]['with_gpx'] is False + + records = data['data']['activities'][0]['records'] + assert len(records) == 4 + assert records[0]['sport_id'] == 2 + assert records[0]['activity_id'] == 1 + assert records[0]['record_type'] == 'MS' + assert records[0]['activity_date'] == 'Tue, 15 May 2018 13:05:00 GMT' + assert records[0]['value'] == 8.0 + assert records[1]['sport_id'] == 2 + assert records[1]['activity_id'] == 1 + assert records[1]['record_type'] == 'LD' + assert records[1]['activity_date'] == 'Tue, 15 May 2018 13:05:00 GMT' + assert records[1]['value'] == '1:00:00' + assert records[2]['sport_id'] == 2 + assert records[2]['activity_id'] == 1 + assert records[2]['record_type'] == 'FD' + assert records[2]['activity_date'] == 'Tue, 15 May 2018 13:05:00 GMT' + assert records[2]['value'] == 8.0 + assert records[3]['sport_id'] == 2 + assert records[3]['activity_id'] == 1 + assert records[3]['record_type'] == 'AS' + assert records[3]['activity_date'] == 'Tue, 15 May 2018 13:05:00 GMT' assert records[3]['value'] == 8.0 diff --git a/fittrackee_api/fittrackee_api/users/auth.py b/fittrackee_api/fittrackee_api/users/auth.py index 4c22f672..4418036c 100644 --- a/fittrackee_api/fittrackee_api/users/auth.py +++ b/fittrackee_api/fittrackee_api/users/auth.py @@ -59,6 +59,7 @@ def register_user(): email=email, password=password ) + new_user.timezone = 'Europe/Paris' db.session.add(new_user) db.session.commit() # generate auth token diff --git a/fittrackee_api/requirements.txt b/fittrackee_api/requirements.txt index 5ce7cf55..a2eeca31 100644 --- a/fittrackee_api/requirements.txt +++ b/fittrackee_api/requirements.txt @@ -43,6 +43,7 @@ pytest-isort==0.1.0 pytest-runner==3.0 python-dateutil==2.7.2 python-editor==1.0.3 +pytz==2018.4 requests==2.18.4 six==1.11.0 SQLAlchemy==1.2.7