From 3c0fbc431d5042284a563f17af8a93a377d31af3 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Tue, 14 Feb 2023 20:57:42 +0100 Subject: [PATCH] rp3, untested --- rowers/integrations/__init__.py | 1 + rowers/integrations/rp3.py | 252 ++++++++++++++++++++++++ rowers/rp3stuff.py | 267 -------------------------- rowers/tasks.py | 6 + rowers/tests/test_imports.py | 6 +- rowers/tests/testdata/testdata.tcx.gz | Bin 4000 -> 3999 bytes rowers/uploads.py | 6 +- rowers/views/importviews.py | 101 ++++------ rowers/views/statements.py | 4 +- 9 files changed, 308 insertions(+), 335 deletions(-) create mode 100644 rowers/integrations/rp3.py delete mode 100644 rowers/rp3stuff.py diff --git a/rowers/integrations/__init__.py b/rowers/integrations/__init__.py index 343df09b..fff8ac50 100644 --- a/rowers/integrations/__init__.py +++ b/rowers/integrations/__init__.py @@ -2,3 +2,4 @@ from .c2 import C2Integration from .strava import StravaIntegration from .nk import NKIntegration from .sporttracks import SportTracksIntegration +from .rp3 import RP3Integration diff --git a/rowers/integrations/rp3.py b/rowers/integrations/rp3.py new file mode 100644 index 00000000..2900e6d3 --- /dev/null +++ b/rowers/integrations/rp3.py @@ -0,0 +1,252 @@ +from .integrations import SyncIntegration, NoTokenError +from rowers.models import User, Rower, Workout, TombStone + +from rowers.tasks import handle_rp3_async_workout +from rowsandall_app.settings import ( + RP3_CLIENT_ID, RP3_CLIENT_KEY, RP3_REDIRECT_URI, RP3_CLIENT_SECRET, + UPLOAD_SERVICE_URL, UPLOAD_SERVICE_SECRET +) + +from rowers.utils import myqueue, NoTokenError, dologging, uniqify +from django.utils import timezone +import requests +import pandas as pd +import arrow +import django_rq +queue = django_rq.get_queue('default') +queuelow = django_rq.get_queue('low') +queuehigh = django_rq.get_queue('high') + +from datetime import timedelta + +graphql_url = "https://rp3rowing-app.com/graphql" + + +class RP3Integration(SyncIntegration): + def __init__(self, *args, **kwargs): + super(RP3Integration, self).__init__(*args, **kwargs) + self.oauth_data = { + 'client_id': RP3_CLIENT_ID, + 'client_secret': RP3_CLIENT_SECRET, + 'redirect_uri': RP3_REDIRECT_URI, + 'autorization_uri': "https://rp3rowing-app.com/oauth/authorize?", + 'content_type': 'application/x-www-form-urlencoded', + # 'content_type': 'application/json', + 'tokenname': 'rp3token', + 'refreshtokenname': 'rp3refreshtoken', + 'expirydatename': 'rp3tokenexpirydate', + 'bearer_auth': False, + 'base_url': "https://rp3rowing-app.com/oauth/token", + 'scope': 'read,write', + } + + + def createworkoutdata(self, w, *args, **kwargs): + return None + + + def workout_export(self, workout, *args, **kwargs) -> str: + pass + + + def get_workouts(self, *args, **kwargs) -> int: + auth_token = self.open() + + r = self.rower + workouts_json = self.get_workout_list_json() + + workouts_list = pd.json_normalize(workouts_json['data']['workouts']) + + try: + rp3ids = workouts_list['id'].values + workouts_list.set_index('id',inpace=True) + except (KeyError, IndexError): + return 0 + + knownrp3ids = uniqify([ + w.uploadedtorp3 for w in Workout.objects.filter(user=rower) + ]) + + dologging('rp3_import.log',rp3ids) + + newids = [rp3id for rp3id in rp3ids if rp3id not in knownrp3ids] + + dologging('rp3_import.log',newids) + + for id in newids: + startdatetime = workouts_list.loc[id, 'executed_at_ios8601'] + dologging('rp3_import.log', startdatetime) + + _ = myqueue( + queuehigh, + handle_rp3_async_workout, + self.user.id, + auth_token, + id, + startdatetime, + 20, + {'timezone':self.rower.defaulttimezone} + ) + + return 1 + + def get_workout(self, id, *args, **kwargs) -> int: + startdatetime = kwargs.get('startdatetime', None) + if not startdatetime: + startdatetime = str(timezone.now()) + + auth_token = self.open() + _ = myqueue( + queuehigh, + handle_rp3_async_workout, + self.user.id, + auth_token, + id, + startdatetime, + 20, + timezone = self.rower.defaulttimezone + ) + + def get_workout_schema(self, *args, **kwargs) -> dict: + auth_token = self.open() + headers = {'Authorization': 'Bearer ' + auth_token} + get_schema = """{ + __type(name:"Workout") { + name + fields { + name + description + type { + name + kind + ofType { + name + kind + } + } + } + } + }""" + + response = requests.post( + url = graphql_url, + headers=headers, + json={'query':get_schema} + ) + return response.json() + + def get_workout_list_json(self, *args, **kwargs) -> dict: + auth_token = self.open() + r = self.rower + + headers = {'Authorization': 'Bearer ' + auth_token} + + get_workouts_list = """{ + workouts{ + id + executed_at_iso8601 + } + }""" + + response = requests.post( + url=graphql_url, + headers=headers, + json={'query': get_workouts_list} + ) + + if (response.status_code != 200): # pragma: no cover + raise NoTokenError("Need to authorize") + + return response.json() + + def get_workout_list(self, *args, **kwargs) -> list: + r = self.rower + + workouts_json = self.get_workout_list_json(*args, **kwargs) + + workouts_list = pd.json_normalize(workouts_json['data']['workouts']) + + knownrp3ids = uniqify([ + w.uploadedtorp3 for w in Workout.objects.filter(user=r) + ]) + + workouts = [] + + for key, data in workouts_list.iterrows(): + print(data) + try: + i = data['id'] + except KeyError: # pragma: no cover + i = 0 + if i in knownrp3ids: # pragma: no cover + nnn = '' + else: + nnn = 'NEW' + + try: + s = arrow.get(data['executed_at_iso8601']).isoformat() + except KeyError: # pragma: no cover + s = '' + + keys = ['id', 'distance', 'duration', 'starttime', + 'rowtype', 'source', 'name', 'new'] + values = [i, '', '', s, '', 'rp3', '', nnn] + + res = dict(zip(keys, values)) + + workouts.append(res) + + + return workouts + + + def make_authorization_url(self, *args, **kwargs) -> str: # pragma: no cover + return super(RP3Integration, self).make_authorization_url(*args, **krags) + + def get_token(self, code, *args, **kwargs) -> (str, int, str): + post_data = { + "client_id": RP3_CLIENT_KEY, + "grant_type": "authorization_code", + "code": code, + "redirect_uri": RP3_REDIRECT_URI, + "client_secret": RP3_CLIENT_SECRET, + } + + response = requests.post( + "https://rp3rowing-app.com/oauth/token", + data=post_data, verify=False, + ) + + try: + token_json = response.json() + thetoken = token_json['access_token'] + expires_in = token_json['expires_in'] + refresh_token = token_json['refresh_token'] + except KeyError: + thetoken = "" + expires_in = 0 + refresh_token = "" + + return thetoken, expires_in, refresh_token + + + def open(self, *args, **kwargs) -> str: + tokenexpirydate = self.user.rower.rp3tokenexpirydate + if tokenexpirydate is None: + raise NoTokenError("No Token") + if tokenexpirydate is not None and timezone.now()-timedelta(days=120)>tokenexpirydate: + self.rower.rp3tokenexpirydate = timezone.now()-timedelta(days=1) + self.rower.save() + raise NoTokenError("No Token") + return super(RP3Integration, self).open() + + + def token_refresh(self, *args, **kwargs) -> str: + return super(RP3Integration, self).token_refresh(*args, **kwargs) + + +# just as a quick test during development +u = User.objects.get(id=1) + +integration_1 = RP3Integration(u) + diff --git a/rowers/rp3stuff.py b/rowers/rp3stuff.py deleted file mode 100644 index b4eed2f6..00000000 --- a/rowers/rp3stuff.py +++ /dev/null @@ -1,267 +0,0 @@ -from celery import Celery, app -from rowers.rower_rules import is_workout_user -import time -from django_rq import job -from rowers.tasks import handle_rp3_async_workout -from rowsandall_app.settings import ( - C2_CLIENT_ID, C2_REDIRECT_URI, C2_CLIENT_SECRET, - STRAVA_CLIENT_ID, STRAVA_REDIRECT_URI, STRAVA_CLIENT_SECRET, - RP3_CLIENT_ID, RP3_CLIENT_KEY, RP3_REDIRECT_URI, RP3_CLIENT_SECRET, - UPLOAD_SERVICE_URL, UPLOAD_SERVICE_SECRET -) -from rowers.utils import myqueue, NoTokenError -# All the functionality needed to connect to Runkeeper -from rowers.imports import * - -# Python -import gzip - -from datetime import timedelta - -import base64 -from io import BytesIO - -from rowers.utils import dologging - -import django_rq -queue = django_rq.get_queue('default') -queuelow = django_rq.get_queue('low') -queuehigh = django_rq.get_queue('high') - - -oauth_data = { - 'client_id': RP3_CLIENT_ID, - 'client_secret': RP3_CLIENT_SECRET, - 'redirect_uri': RP3_REDIRECT_URI, - 'autorization_uri': "https://rp3rowing-app.com/oauth/authorize?", - 'content_type': 'application/x-www-form-urlencoded', - # 'content_type': 'application/json', - 'tokenname': 'rp3token', - 'refreshtokenname': 'rp3refreshtoken', - 'expirydatename': 'rp3tokenexpirydate', - 'bearer_auth': False, - 'base_url': "https://rp3rowing-app.com/oauth/token", - 'scope': 'read,write', -} - - -graphql_url = "https://rp3rowing-app.com/graphql" - - -# Checks if user has UnderArmour token, renews them if they are expired -def rp3_open(user): - tokenexpirydate = user.rower.rp3tokenexpirydate - if tokenexpirydate is None: - raise NoTokenError("No Token") - if tokenexpirydate is not None and timezone.now()-timedelta(days=120)>tokenexpirydate: - user.rower.rp3tokenexpirydate = timezone.now()-timedelta(days=1) - user.rower.save() - raise NoTokenError("No Token") - return imports_open(user, oauth_data) - -# Refresh ST token using refresh token - - -def do_refresh_token(refreshtoken): # pragma: no cover - return imports_do_refresh_token(refreshtoken, oauth_data) - -# Exchange access code for long-lived access token - - -def get_token(code): # pragma: no cover - post_data = { - "client_id": RP3_CLIENT_KEY, - "grant_type": "authorization_code", - "code": code, - "redirect_uri": RP3_REDIRECT_URI, - "client_secret": RP3_CLIENT_SECRET, - } - - response = requests.post( - "https://rp3rowing-app.com/oauth/token", - data=post_data, verify=False, - ) - - try: - token_json = response.json() - thetoken = token_json['access_token'] - expires_in = token_json['expires_in'] - refresh_token = token_json['refresh_token'] - except KeyError: - thetoken = 0 - expires_in = 0 - refresh_token = 0 - - return thetoken, expires_in, refresh_token - -# Make authorization URL including random string - - -def make_authorization_url(request): # pragma: no cover - return imports_make_authorization_url(oauth_data) - - -def get_rp3_workout_list(user): - auth_token = rp3_open(user) - - headers = {'Authorization': 'Bearer ' + auth_token} - - get_workouts_list = """{ - workouts{ - id - executed_at - } -}""" - - response = requests.post( - url=graphql_url, - headers=headers, - json={'query': get_workouts_list} - ) - - return response - - -def get_rp3_workouts(rower, do_async=True): # pragma: no cover - try: - auth_token = rp3_open(rower.user) - except NoTokenError: - return 0 - - res = get_rp3_workout_list(rower.user) - - if (res.status_code != 200): - return 0 - - s = '{d}'.format(d=res.json()) - dologging('rp3_import.log', s) - workouts_list = pd.json_normalize(res.json()['data']['workouts']) - try: - rp3ids = workouts_list['id'].values - workouts_list.set_index('id', inplace=True) - except (KeyError, IndexError): - return 0 - - knownrp3ids = uniqify([ - w.uploadedtorp3 for w in Workout.objects.filter(user=rower) - ]) - - dologging('rp3_import.log',rp3ids) - - newids = [rp3id for rp3id in rp3ids if rp3id not in knownrp3ids] - - dologging('rp3_import.log',newids) - - for id in newids: - startdatetime = workouts_list.loc[id, 'executed_at'] - dologging('rp3_import.log', startdatetime) - - _ = myqueue( - queuehigh, - handle_rp3_async_workout, - rower.user.id, - auth_token, - id, - startdatetime, - 20, - ) - - return 1 - - -def download_rp3_file(url, auth_token, filename): # pragma: no cover - headers = {'Authorization': 'Bearer ' + auth_token} - - res = requests.get(url, headers=headers) - - if res.status_code == 200: - with open(filename, 'wb') as f: - f.write(res.content) - - return res.status_code - - -def get_rp3_workout_token(workout_id, auth_token, waittime=3, max_attempts=20): # pragma: no cover - headers = {'Authorization': 'Bearer ' + auth_token} - - get_download_link = """{ - download(workout_id: """ + str(workout_id) + """, type:csv){ - id - status - link - } -}""" - - have_link = False - download_url = '' - counter = 0 - while not have_link: - response = requests.post( - url=graphql_url, - headers=headers, - json={'query': get_download_link} - ) - - if response.status_code != 200: - have_link = True - - workout_download_details = pd.json_normalize( - response.json()['data']['download']) - - if workout_download_details.iat[0, 1] == 'ready': - download_url = workout_download_details.iat[0, 2] - have_link = True - - counter += 1 - - if counter > max_attempts: - have_link = True - - time.sleep(waittime) - - return download_url - - -def get_rp3_workout_link(user, workout_id, waittime=3, max_attempts=20): # pragma: no cover - auth_token = rp3_open(user) - - return get_rp3_workout_token(workout_id, auth_token, waittime=waittime, max_attempts=max_attempts) - - -def get_rp3_workout(user, workout_id, startdatetime=None): # pragma: no cover - url = get_rp3_workout_link(user, workout_id) - filename = 'media/RP3Import_'+str(workout_id)+'.csv' - - auth_token = rp3_open(user) - - if not startdatetime: - startdatetime = str(timezone.now()) - - status_code = download_rp3_file(url, auth_token, filename) - - if status_code != 200: - return 0 - - userid = user.id - - uploadoptions = { - 'secret': UPLOAD_SERVICE_SECRET, - 'user': userid, - 'file': filename, - 'workouttype': 'dynamic', - 'boattype': '1x', - 'rp3id': workout_id, - 'startdatetime': startdatetime, - 'timezone': str(user.rower.defaulttimezone) - } - - session = requests.session() - newHeaders = {'Content-type': 'application/json', 'Accept': 'text/plain'} - session.headers.update(newHeaders) - - response = session.post(UPLOAD_SERVICE_URL, json=uploadoptions) - - if response.status_code != 200: - return 0 - - return response.json()['id'] diff --git a/rowers/tasks.py b/rowers/tasks.py index a327a511..5a311f1f 100644 --- a/rowers/tasks.py +++ b/rowers/tasks.py @@ -3158,6 +3158,9 @@ def handle_update_wps(rid, types, ids, mode, debug=False, **kwargs): @app.task def handle_rp3_async_workout(userid, rp3token, rp3id, startdatetime, max_attempts, debug=False, **kwargs): + + timezone = kwargs.get('timezone', 'UTC') + headers = {'Authorization': 'Bearer ' + rp3token} get_download_link = """{ @@ -3239,8 +3242,11 @@ def handle_rp3_async_workout(userid, rp3token, rp3id, startdatetime, max_attempt 'boattype': '1x', 'rp3id': int(rp3id), 'startdatetime': startdatetime, + 'timezone': timezone, } + print(uploadoptions) + session = requests.session() newHeaders = {'Content-type': 'application/json', 'Accept': 'text/plain'} session.headers.update(newHeaders) diff --git a/rowers/tests/test_imports.py b/rowers/tests/test_imports.py index 6a2f5ee2..0bac3505 100644 --- a/rowers/tests/test_imports.py +++ b/rowers/tests/test_imports.py @@ -971,8 +971,8 @@ class RP3Objects(DjangoTestCase): csvfilename=filename ) - @patch('rowers.rp3stuff.requests.get', side_effect=mocked_requests) - @patch('rowers.rp3stuff.requests.post', side_effect=mocked_requests) + @patch('rowers.integrations.rp3.requests.get', side_effect=mocked_requests) + @patch('rowers.integrations.rp3.requests.post', side_effect=mocked_requests) @patch('rowers.dataprep.getsmallrowdata_db', side_effect=mocked_getsmallrowdata_db) def test_rp3_import(self, mock_get, mockpost, mocked_getsmallrowdata_db): @@ -1002,7 +1002,7 @@ class RP3Objects(DjangoTestCase): res = tasks.handle_rp3_async_workout(userid,rp3token,rp3id,startdatetime,max_attempts) self.assertEqual(res,1) - @patch('rowers.rp3stuff.requests.post', side_effect=mocked_requests) + @patch('rowers.integrations.rp3.requests.post', side_effect=mocked_requests) def notest_rp3_callback(self, mock_post): response = self.c.get('/rp3_callback?code=absdef23&scope=read',follow=True) self.assertEqual(response.status_code, 200) diff --git a/rowers/tests/testdata/testdata.tcx.gz b/rowers/tests/testdata/testdata.tcx.gz index 7fe9f5fa8cb806f06325faf11ecc596079fee673..ef36d3798ef2f5e2a84326924060dec69b447979 100644 GIT binary patch literal 3999 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 F0RVK4I*I@Q literal 4000 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 GfB^uPLKmt4 diff --git a/rowers/uploads.py b/rowers/uploads.py index 3a784473..f99501a4 100644 --- a/rowers/uploads.py +++ b/rowers/uploads.py @@ -235,9 +235,9 @@ def do_sync(w, options, quick=False): do_st_export = w.user.sporttracks_auto_export - if options['sporttracksid'] != 0 and options['sporttracksid'] != '': - w.uploadedtosporttracks = options['sporttracksid'] - + sporttracksid = options.get('sporttracksid','') + if sporttracksid != 0 and sporttracksid != '': + w.uploadedtosporttracks = sporttracksid w.save() do_st_export = False try: # pragma: no cover diff --git a/rowers/views/importviews.py b/rowers/views/importviews.py index 56dfc70b..8fa1bad4 100644 --- a/rowers/views/importviews.py +++ b/rowers/views/importviews.py @@ -725,11 +725,9 @@ def rower_process_rp3callback(request): # pragma: no cover url = reverse('rower_exportsettings_view') return HttpResponseRedirect(url) - res = rp3stuff.get_token(code) + rp3_integration = RP3Integration(request.user) + access_token, expires_in, refresh_token = rp3_integration.get_token(code) - access_token = res[0] - expires_in = res[1] - refresh_token = res[2] expirydatetime = timezone.now()+datetime.timedelta(seconds=expires_in) r = getrower(request.user) @@ -803,56 +801,39 @@ def rower_process_testcallback(request): # pragma: no cover @permission_required('rower.is_coach', fn=get_user_by_userid, raise_exception=True) def workout_rp3import_view(request, userid=0): r = getrequestrower(request, userid=userid) + rp3_integration = RP3Integration(request.user) try: - _ = rp3stuff.rp3_open(request.user) + _ = rp3_integration.open() except NoTokenError: # pragma: no cover url = reverse('rower_rp3_authorize') return HttpResponseRedirect(url) - res = rp3stuff.get_rp3_workout_list(request.user) + workouts = rp3_integration.get_workout_list() + datedict = {} + for workout in workouts: + datedict[workout['id']] = workout['starttime'] - if (res.status_code != 200): # pragma: no cover - if (res.status_code == 401): - r = getrower(request.user) - if (r.stravatoken == '') or (r.stravatoken is None): - s = "Token doesn't exist. Need to authorize" - return HttpResponseRedirect("/rowers/me/stravaauthorize/") - message = "Something went wrong in workout_rp3import_view" - messages.error(request, message) - url = reverse('workouts_view') - return HttpResponseRedirect(url) - workouts_list = pd.json_normalize(res.json()['data']['workouts']) + if request.method == "POST": + try: # pragma: no cover + tdict = dict(request.POST.lists()) + ids = tdict['workoutid'] + rp3ids = [int(id) for id in ids] - knownrp3ids = uniqify([ - w.uploadedtorp3 for w in Workout.objects.filter(user=r) - ]) - - workouts = [] - - for key, data in workouts_list.iterrows(): - try: - i = data['id'] + for rp3id in rp3ids: + rp3_integration.get_workout(rp3id,startdatetime=datedict[rp3id]) + # done, redirect to workouts list + messages.info( + request, + 'Your RP3 workouts will be imported in the background.' + ' It may take a few minutes before they appear.') + url = reverse('workouts_view') + return HttpResponseRedirect(url) except KeyError: # pragma: no cover - i = 0 - if i in knownrp3ids: # pragma: no cover - nnn = '' - else: - nnn = 'NEW' - - try: - s = data['executed_at'] - except KeyError: # pragma: no cover - s = '' - - keys = ['id', 'starttime', 'new'] - values = [i, s, nnn] - - res = dict(zip(keys, values)) - - workouts.append(res) + pass + breadcrumbs = [ { 'url': '/rowers/list-workouts/', @@ -864,13 +845,18 @@ def workout_rp3import_view(request, userid=0): }, ] - return render(request, 'rp3_list_import.html', + checknew = request.GET.get('selectallnew', False) + + + return render(request, 'list_import.html', { 'workouts': workouts, 'rower': r, 'active': 'nav-workouts', 'breadcrumbs': breadcrumbs, - 'teams': get_my_teams(request.user) + 'teams': get_my_teams(request.user), + 'integration': 'RP3', + 'checknew': checknew, }) # The page where you select which Strava workout to import @@ -1461,7 +1447,8 @@ def workout_getrp3workout_all(request): # pragma: no cover r = getrequestrower(request) - result = rp3stuff.get_rp3_workouts(r, do_async=True) + rp3_integration = RP3Integration(request.user) + result = rp3_integration.get_workouts() if result: messages.info( @@ -1582,21 +1569,12 @@ def workout_getrp3importview(request, externalid): r = getrequestrower(request) if r.user != request.user: # pragma: no cover messages.error( - request, 'You can only access your own workouts on the NK Logbook, not those of your athletes') + request, 'You can only access your own workouts on the RP3 Logbook, not those of your athletes') url = reverse('workout_rp3import_view', kwargs={ 'userid': request.user.id}) return HttpResponseRedirect(url) - token = rp3stuff.rp3_open(r.user) - startdatetime = request.GET.get('startdatetime') - - _ = myqueue(queuehigh, - handle_rp3_async_workout, - r.user.id, - token, - externalid, - startdatetime, - 20, - ) + rp3_integration = RP3Integration(request.user) + result = rp3_integration.get_workout(externalid) messages.info(request, 'The workout will be imported in the background') @@ -1684,8 +1662,11 @@ def workout_getimportview(request, externalid, source='c2', do_async=True): @login_required() def workout_getsporttracksworkout_all(request): st_integration = SportTracksIntegration(request.user) - _ = st_integration.get_workouts() - messages.info(request,"Your SportTracks workouts will be imported in the background") + try: + _ = st_integration.get_workouts() + messages.info(request,"Your SportTracks workouts will be imported in the background") + except NoTokenError: + messages.error(request,"You have to connect to SportTracks first") url = reverse('workouts_view') return HttpResponseRedirect(url) diff --git a/rowers/views/statements.py b/rowers/views/statements.py index 7d65e69b..f3f2def6 100644 --- a/rowers/views/statements.py +++ b/rowers/views/statements.py @@ -193,7 +193,7 @@ import sys import datetime import iso8601 import rowers.rojabo_stuff as rojabo_stuff -from rowers.rp3stuff import rp3_open + from rowers.tpstuff import tp_open from iso8601 import ParseError @@ -206,7 +206,7 @@ import rowers.polarstuff as polarstuff from rowers.integrations import * import rowers.tpstuff as tpstuff -import rowers.rp3stuff as rp3stuff + import rowers.ownapistuff as ownapistuff from rowers.ownapistuff import TEST_CLIENT_ID, TEST_CLIENT_SECRET, TEST_REDIRECT_URI from rowsandall_app.settings import (