Private
Public Access
1
0

sporttracks, not fully tested

This commit is contained in:
Sander Roosendaal
2023-02-13 22:50:01 +01:00
parent 410722a990
commit 91583134d2
11 changed files with 290 additions and 561 deletions

View File

@@ -71,7 +71,7 @@ class SyncIntegration(metaclass=ABCMeta):
"scope": self.oauth_data['scope'], "scope": self.oauth_data['scope'],
"state": state} "state": state}
url = self.oauth_data['authorizaton_uri']+urllib.parse.urlencode(params) url = self.oauth_data['authorization_uri']+urllib.parse.urlencode(params)
return url return url

View File

@@ -1,7 +1,9 @@
from .integrations import SyncIntegration, NoTokenError from .integrations import SyncIntegration, NoTokenError
from rowers.models import User, Rower, Workout, TombStone from rowers.models import User, Rower, Workout, TombStone
from rowers.tasks import handle_sporttracks_sync from rowingdata import rowingdata
from rowers.tasks import handle_sporttracks_sync, handle_sporttracks_workout_from_data
from rowers.rower_rules import is_workout_user from rowers.rower_rules import is_workout_user
import rowers.mytypes as mytypes import rowers.mytypes as mytypes
from rowsandall_app.settings import ( from rowsandall_app.settings import (
@@ -11,13 +13,38 @@ from rowsandall_app.settings import (
import re import re
import numpy import numpy
import pytz
import json
import requests
import datetime
import pandas as pd
import django_rq import django_rq
queue = django_rq.get_queue('default') queue = django_rq.get_queue('default')
queuelow = django_rq.get_queue('low') queuelow = django_rq.get_queue('low')
queuehigh = django_rq.get_queue('high') queuehigh = django_rq.get_queue('high')
from rowers.utils import myqueue, dologging from rowers.utils import myqueue, dologging, uniqify
def getidfromuri(uri): # pragma: no cover
m = re.search('/(\w.*)\/(\d+)', uri)
return m.group(2)
def getidfromresponse(response): # pragma: no cover
t = response.json()
uri = t['uris'][0]
regex = '.*?sporttracks\.mobi\/api\/v2\/fitnessActivities/(\d+)\.json$'
m = re.compile(regex).match(uri).group(1)
id = int(m)
return int(id)
def default(o): # pragma: no cover
if isinstance(o, numpy.int64):
return int(o)
raise TypeError
class SportTracksIntegration(SyncIntegration): class SportTracksIntegration(SyncIntegration):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@@ -27,7 +54,7 @@ class SportTracksIntegration(SyncIntegration):
'client_id': SPORTTRACKS_CLIENT_ID, 'client_id': SPORTTRACKS_CLIENT_ID,
'client_secret': SPORTTRACKS_CLIENT_SECRET, 'client_secret': SPORTTRACKS_CLIENT_SECRET,
'redirect_uri': SPORTTRACKS_REDIRECT_URI, 'redirect_uri': SPORTTRACKS_REDIRECT_URI,
'autorization_uri': "https://api.sporttracks.mobi/oauth2/authorize", 'authorization_uri': "https://api.sporttracks.mobi/oauth2/authorize",
'content_type': 'application/json', 'content_type': 'application/json',
'tokenname': 'sporttrackstoken', 'tokenname': 'sporttrackstoken',
'refreshtokenname': 'sporttracksrefreshtoken', 'refreshtokenname': 'sporttracksrefreshtoken',
@@ -42,13 +69,178 @@ class SportTracksIntegration(SyncIntegration):
return super(SportTracksIntegration, self).open(*args, **kwargs) return super(SportTracksIntegration, self).open(*args, **kwargs)
def createworkoutdata(self, w, *args, **kwargs): def createworkoutdata(self, w, *args, **kwargs):
return None timezone = pytz.timezone(w.timezone)
filename = w.csvfilename
try:
row = rowingdata(csvfile=filename)
except: # pragma: no cover
return {}
try:
averagehr = int(row.df[' HRCur (bpm)'].mean())
maxhr = int(row.df[' HRCur (bpm)'].max())
except KeyError: # pragma: no cover
averagehr = 0
maxhr = 0
try:
duration = w.duration.hour*3600
duration += w.duration.minute*60
duration += w.duration.second
duration += +1.0e-6*w.duration.microsecond
except AttributeError: # pragma: no cover
return {}
t = row.df.loc[:, 'TimeStamp (sec)'].values - \
row.df.loc[:, 'TimeStamp (sec)'].iloc[0]
try:
t[0] = t[1]
except IndexError: # pragma: no cover
return {}
d = row.df.loc[:, 'cum_dist'].values
d[0] = d[1]
t = t.astype(int)
d = d.astype(int)
spm = row.df[' Cadence (stokes/min)'].astype(int).values
spm[0] = spm[1]
hr = row.df[' HRCur (bpm)'].astype(int).values
haslatlon = 1
try:
lat = row.df[' latitude'].values
lon = row.df[' longitude'].values
if not lat.std() and not lon.std(): # pragma: no cover
haslatlon = 0
except KeyError:
haslatlon = 0
haspower = 1
try:
power = row.df[' Power (watts)'].astype(int).values
except KeyError: # pragma: no cover
haspower = 0
locdata = []
hrdata = []
spmdata = []
distancedata = []
powerdata = []
t = t.tolist()
hr = hr.tolist()
d = d.tolist()
spm = spm.tolist()
if haslatlon:
lat = lat.tolist()
lon = lon.tolist()
power = power.tolist()
for i in range(len(t)):
hrdata.append(t[i])
hrdata.append(hr[i])
distancedata.append(t[i])
distancedata.append(d[i])
spmdata.append(t[i])
spmdata.append(spm[i])
if haslatlon:
locdata.append(t[i])
locdata.append([lat[i], lon[i]])
if haspower:
powerdata.append(t[i])
powerdata.append(power[i])
try:
w.notes = w.notes+'\n from '+w.workoutsource+' via rowsandall.com'
except TypeError:
w.notes = 'from '+w.workoutsource+' via rowsandall.com'
st = w.startdatetime.astimezone(timezone)
st = st.replace(microsecond=0)
data = {
"type": "Rowing",
"name": w.name,
"start_time": st.isoformat(),
"total_distance": int(w.distance),
"duration": duration,
"notes": w.notes,
"avg_heartrate": averagehr,
"max_heartrate": maxhr,
"distance": distancedata,
"cadence": spmdata,
"heartrate": hrdata,
}
if haslatlon:
data = {
"type": "Rowing",
"name": w.name,
"start_time": st.isoformat(),
"total_distance": int(w.distance),
"duration": duration,
"notes": w.notes,
"avg_heartrate": averagehr,
"max_heartrate": maxhr,
"location": locdata,
"distance": distancedata,
"cadence": spmdata,
"heartrate": hrdata,
}
if haspower:
data['power'] = powerdata
return data
def workout_export(self, workout, *args, **kwargs) -> str: def workout_export(self, workout, *args, **kwargs) -> str:
pass thetoken = self.open()
stid = "0"
# ready to upload. Hurray
if not(is_workout_user(self.user, workout)):
return "0"
authorizationstring = str('Bearer ' + thetoken)
headers = {'Authorization': authorizationstring,
'user-agent': 'sanderroosendaal',
'Content-Type': 'application/json'}
data = self.createworkoutdata(workout)
if not data:
return "0"
url = "https://api.sporttracks.mobi/api/v2/fitnessActivities.json"
_ = myqueue(
queue,
handle_sporttracks_sync,
workout.id,
url,
headers,
json.dumps(data, default=default))
return 1
def get_workouts(self, *args, **kwargs) -> int: def get_workouts(self, *args, **kwargs) -> int:
pass r = self.rower
workouts_json = self.get_workout_list_json(*args, **kwargs)
stids = [int(getidfromuri(item['uri']))
for item in workouts_json['items']]
knownstids = uniqify([
w.uploadedtosporttracks for w in Workout.objects.filter(user=r)
])
newids = [stid for stid in stids if stid not in knownstids]
for sporttracksid in newids:
id = self.get_workout(sporttracksid)
return 1
@@ -57,33 +249,19 @@ class SportTracksIntegration(SyncIntegration):
r = self.rower r = self.rower
authorizationstring = str('Bearer ' + r.sporttrackstoken)
headers = {'Authorization': authorizationstring,
'user-agent': 'sanderroosendaal',
'Content-Type': 'application/json'}
url = "https://api.sporttracks.mobi/api/v2/fitnessActivities/" + \
str(sporttracksid)
s = requests.get(url, headers=headers)
data = s.json() job = myqueue(
strokedata = pd.DataFrame.from_dict({
key: pd.Series(value, dtype='object') for key, value in data.items()
})
id = myqueue(
queue, queue,
handle_sporttracks_workout_from_data, handle_sporttracks_workout_from_data,
self.user, self.user,
sporttracksid, data, id,
strokedata,
'sporttracks', 'sporttracks',
'sporttracks' 'sporttracks'
) )
return id
return job.id
def get_workout_list(self, *args, **kwargs) -> list: def get_workout_list_json(self, *args, **kwargs) -> dict:
_ = self.open() _ = self.open()
r = self.rower r = self.rower
@@ -98,13 +276,18 @@ class SportTracksIntegration(SyncIntegration):
s = "Token doesn't exist. Need to authorize" s = "Token doesn't exist. Need to authorize"
raise NoTokenError(s) raise NoTokenError(s)
return res.json()
def get_workout_list(self, *args, **kwargs) -> list:
r = self.rower
workouts_json = self.get_workout_list_json(*args, **kwargs)
workouts = [] workouts = []
knownstids = uniqify([ knownstids = uniqify([
w.uploadedtosporttracks for w in Workout.objects.filter(user=r) w.uploadedtosporttracks for w in Workout.objects.filter(user=r)
]) ])
for item in res.json()['items']: for item in workouts_json['items']:
d = int(float(item['total_distance'])) d = int(float(item['total_distance']))
i = int(getidfromuri(item['uri'])) i = int(getidfromuri(item['uri']))
if i in knownstids: # pragma: no cover if i in knownstids: # pragma: no cover
@@ -127,7 +310,7 @@ class SportTracksIntegration(SyncIntegration):
return super(SportTracksIntegration, self).make_authorization_url(*args, **kwargs) return super(SportTracksIntegration, self).make_authorization_url(*args, **kwargs)
def get_token(self, code, *args, **kwargs) -> (str, int, str): def get_token(self, code, *args, **kwargs) -> (str, int, str):
return "" return super(SportTracksIntegration, self).get_token(code, *args, **kwargs)

View File

@@ -1,350 +0,0 @@
from rowers.tasks import handle_sporttracks_sync, handle_sporttracks_workout_from_data
from rowers.rower_rules import is_workout_user
import rowers.mytypes as mytypes
from rowsandall_app.settings import (
C2_CLIENT_ID, C2_REDIRECT_URI, C2_CLIENT_SECRET,
STRAVA_CLIENT_ID, STRAVA_REDIRECT_URI, STRAVA_CLIENT_SECRET,
SPORTTRACKS_CLIENT_SECRET, SPORTTRACKS_CLIENT_ID,
SPORTTRACKS_REDIRECT_URI
)
import re
from rowers.imports import *
from rowers.utils import myqueue
# All the functionality to connect to SportTracks
import numpy
import django_rq
queue = django_rq.get_queue('default')
queuelow = django_rq.get_queue('low')
queuehigh = django_rq.get_queue('low')
oauth_data = {
'client_id': SPORTTRACKS_CLIENT_ID,
'client_secret': SPORTTRACKS_CLIENT_SECRET,
'redirect_uri': SPORTTRACKS_REDIRECT_URI,
'autorization_uri': "https://api.sporttracks.mobi/oauth2/authorize",
'content_type': 'application/json',
'tokenname': 'sporttrackstoken',
'refreshtokenname': 'sporttracksrefreshtoken',
'expirydatename': 'sporttrackstokenexpirydate',
'bearer_auth': False,
'base_url': "https://api.sporttracks.mobi/oauth2/token",
'scope': 'write',
}
# Checks if user has SportTracks token, renews them if they are expired
def sporttracks_open(user):
return imports_open(user, oauth_data)
# Refresh ST token using refresh token
def do_refresh_token(refreshtoken):
return imports_do_refresh_token(refreshtoken, oauth_data)
# Exchange ST access code for long-lived ST access token
def get_token(code):
return imports_get_token(code, oauth_data)
# Make authorization URL including random string
def make_authorization_url(request): # pragma: no cover
return imports_make_authorization_url(oauth_data)
# This is token refresh. Looks for tokens in our database, then refreshes
def rower_sporttracks_token_refresh(user): # pragma: no cover
r = Rower.objects.get(user=user)
res = do_refresh_token(r.sporttracksrefreshtoken)
access_token = res[0]
expires_in = res[1]
refresh_token = res[2]
expirydatetime = timezone.now()+timedelta(seconds=expires_in)
r = Rower.objects.get(user=user)
r.sporttrackstoken = access_token
r.tokenexpirydate = expirydatetime
r.sporttracksrefreshtoken = refresh_token
r.save()
return r.sporttrackstoken
# Get list of workouts available on SportTracks
def get_sporttracks_workout_list(user):
r = Rower.objects.get(user=user)
if (r.sporttrackstoken == '') or (r.sporttrackstoken is None):
s = "Token doesn't exist. Need to authorize"
return custom_exception_handler(401, s)
elif (timezone.now() > r.sporttrackstokenexpirydate): # pragma: no cover
s = "Token expired. Needs to refresh."
return custom_exception_handler(401, s)
else:
# ready to fetch. Hurray
authorizationstring = str('Bearer ' + r.sporttrackstoken)
headers = {'Authorization': authorizationstring,
'user-agent': 'sanderroosendaal',
'Content-Type': 'application/json'}
url = "https://api.sporttracks.mobi/api/v2/fitnessActivities"
s = requests.get(url, headers=headers)
return s
# Get workout summary data by SportTracks ID
def get_workout(user, sporttracksid, do_async=False):
r = Rower.objects.get(user=user)
if (r.sporttrackstoken == '') or (r.sporttrackstoken is None): # pragma: no cover
return custom_exception_handler(401, s)
s = "Token doesn't exist. Need to authorize"
elif (timezone.now() > r.sporttrackstokenexpirydate): # pragma: no cover
s = "Token expired. Needs to refresh."
return custom_exception_handler(401, s)
else:
# ready to fetch. Hurray
authorizationstring = str('Bearer ' + r.sporttrackstoken)
headers = {'Authorization': authorizationstring,
'user-agent': 'sanderroosendaal',
'Content-Type': 'application/json'}
url = "https://api.sporttracks.mobi/api/v2/fitnessActivities/" + \
str(sporttracksid)
s = requests.get(url, headers=headers)
data = s.json()
strokedata = pd.DataFrame.from_dict({
key: pd.Series(value, dtype='object') for key, value in data.items()
})
id= myqueue(
queue,
handle_sporttracks_workout_from_data,
user,
sporttracksid, data,
strokedata,
'sporttracks',
'sporttracks')
return id
# Create Workout Data for upload to SportTracks
def createsporttracksworkoutdata(w):
timezone = pytz.timezone(w.timezone)
filename = w.csvfilename
try:
row = rowingdata(csvfile=filename)
except: # pragma: no cover
return 0
try:
averagehr = int(row.df[' HRCur (bpm)'].mean())
maxhr = int(row.df[' HRCur (bpm)'].max())
except KeyError: # pragma: no cover
averagehr = 0
maxhr = 0
try:
duration = w.duration.hour*3600
duration += w.duration.minute*60
duration += w.duration.second
duration += +1.0e-6*w.duration.microsecond
except AttributeError: # pragma: no cover
return 0
# adding diff, trying to see if this is valid
# t = row.df.loc[:,'TimeStamp (sec)'].values-10*row.df.ix[0,'TimeStamp (sec)']
t = row.df.loc[:, 'TimeStamp (sec)'].values - \
row.df.loc[:, 'TimeStamp (sec)'].iloc[0]
try:
t[0] = t[1]
except IndexError: # pragma: no cover
return 0
d = row.df.loc[:, 'cum_dist'].values
d[0] = d[1]
t = t.astype(int)
d = d.astype(int)
spm = row.df[' Cadence (stokes/min)'].astype(int).values
spm[0] = spm[1]
hr = row.df[' HRCur (bpm)'].astype(int).values
haslatlon = 1
try:
lat = row.df[' latitude'].values
lon = row.df[' longitude'].values
if not lat.std() and not lon.std(): # pragma: no cover
haslatlon = 0
except KeyError:
haslatlon = 0
haspower = 1
try:
power = row.df[' Power (watts)'].astype(int).values
except KeyError: # pragma: no cover
haspower = 0
locdata = []
hrdata = []
spmdata = []
distancedata = []
powerdata = []
t = t.tolist()
hr = hr.tolist()
d = d.tolist()
spm = spm.tolist()
if haslatlon:
lat = lat.tolist()
lon = lon.tolist()
power = power.tolist()
for i in range(len(t)):
hrdata.append(t[i])
hrdata.append(hr[i])
distancedata.append(t[i])
distancedata.append(d[i])
spmdata.append(t[i])
spmdata.append(spm[i])
if haslatlon:
locdata.append(t[i])
locdata.append([lat[i], lon[i]])
if haspower:
powerdata.append(t[i])
powerdata.append(power[i])
try:
w.notes = w.notes+'\n from '+w.workoutsource+' via rowsandall.com'
except TypeError:
w.notes = 'from '+w.workoutsource+' via rowsandall.com'
st = w.startdatetime.astimezone(timezone)
st = st.replace(microsecond=0)
if haslatlon:
data = {
"type": "Rowing",
"name": w.name,
"start_time": st.isoformat(),
"total_distance": int(w.distance),
"duration": duration,
"notes": w.notes,
"avg_heartrate": averagehr,
"max_heartrate": maxhr,
"location": locdata,
"distance": distancedata,
"cadence": spmdata,
"heartrate": hrdata,
}
else:
data = {
"type": "Rowing",
"name": w.name,
"start_time": st.isoformat(),
"total_distance": int(w.distance),
"duration": duration,
"notes": w.notes,
"avg_heartrate": averagehr,
"max_heartrate": maxhr,
"distance": distancedata,
"cadence": spmdata,
"heartrate": hrdata,
}
if haspower:
data['power'] = powerdata
return data
# Obtain SportTracks Workout ID from the response returned on successful
# upload
def getidfromresponse(response): # pragma: no cover
t = response.json()
uri = t['uris'][0]
regex = '.*?sporttracks\.mobi\/api\/v2\/fitnessActivities/(\d+)\.json$'
m = re.compile(regex).match(uri).group(1)
id = int(m)
return int(id)
def default(o): # pragma: no cover
if isinstance(o, numpy.int64):
return int(o)
raise TypeError
def workout_sporttracks_upload(user, w, asynchron=False): # pragma: no cover
message = "Uploading to SportTracks"
stid = 0
# ready to upload. Hurray
thetoken = sporttracks_open(user)
if (is_workout_user(user, w)):
data = createsporttracksworkoutdata(w)
if not data:
message = "Data error"
stid = 0
return message, stid
authorizationstring = str('Bearer ' + thetoken)
headers = {'Authorization': authorizationstring,
'user-agent': 'sanderroosendaal',
'Content-Type': 'application/json'}
url = "https://api.sporttracks.mobi/api/v2/fitnessActivities.json"
if asynchron:
_ = myqueue(queue, handle_sporttracks_sync,
w.id, url, headers, json.dumps(data, default=default))
return "Asynchronous sync", 0
response = requests.post(url, headers=headers,
data=json.dumps(data, default=default))
# check for duplicate error first
if (response.status_code == 409):
message = "Duplicate error"
w.uploadedtosporttracks = -1
stid = -1
w.save()
return message, stid
elif (response.status_code == 201 or response.status_code == 200):
s = response.json()
stid = getidfromresponse(response)
w.uploadedtosporttracks = stid
w.save()
return 'Successfully synced to SportTracks', stid
else:
s = response
message = "Something went wrong in workout_sporttracks_upload_view: %s" % s.reason
stid = 0
return message, stid
else:
message = "You are not authorized to upload this workout"
stid = 0
return message, stid
return message, stid
# Create workout from SportTracks Data, which are slightly different
# than Strava or Concept2 data

View File

@@ -458,9 +458,24 @@ def splitstdata(lijst):
return [np.array(t), np.array(latlong)] return [np.array(t), np.array(latlong)]
@app.task @app.task
def handle_sporttracks_workout_from_data(user, importid, data, strokedata, source, def handle_sporttracks_workout_from_data(user, importid, source,
workoutsource, debug=False, **kwargs): workoutsource, debug=False, **kwargs):
r = user.rower
authorizationstring = str('Bearer ' + r.sporttrackstoken)
headers = {'Authorization': authorizationstring,
'user-agent': 'sanderroosendaal',
'Content-Type': 'application/json'}
url = "https://api.sporttracks.mobi/api/v2/fitnessActivities/" + \
str(importid)
s = requests.get(url, headers=headers)
data = s.json()
strokedata = pd.DataFrame.from_dict({
key: pd.Series(value, dtype='object') for key, value in data.items()
})
try: try:
workouttype = data['type'] workouttype = data['type']
except KeyError: # pragma: no cover except KeyError: # pragma: no cover

View File

@@ -64,7 +64,7 @@ from mock import Mock, patch
#from minimocktest import MockTestCase #from minimocktest import MockTestCase
import pandas as pd import pandas as pd
import rowers.c2stuff as c2stuff import rowers.c2stuff as c2stuff
import rowers.sporttracksstuff as sporttracksstuff
import rowers.rojabo_stuff as rojabo_stuff import rowers.rojabo_stuff as rojabo_stuff
from django.urls import reverse, reverse_lazy from django.urls import reverse, reverse_lazy

View File

@@ -1236,7 +1236,7 @@ class STObjects(DjangoTestCase):
csvfilename=filename csvfilename=filename
) )
@patch('rowers.sporttracksstuff.requests.post', side_effect=mocked_requests) @patch('rowers.integrations.sporttracks.requests.post', side_effect=mocked_requests)
def test_sporttracks_callback(self, mock_post): def test_sporttracks_callback(self, mock_post):
response = self.c.get('/sporttracks_callback?code=dsdoij232s',follow=True) response = self.c.get('/sporttracks_callback?code=dsdoij232s',follow=True)
@@ -1244,15 +1244,15 @@ class STObjects(DjangoTestCase):
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
@patch('rowers.sporttracksstuff.requests.post', side_effect=mocked_requests) @patch('rowers.integrations.sporttracks.requests.post', side_effect=mocked_requests)
def test_sporttracks_token_refresh(self, mock_post): def test_sporttracks_token_refresh(self, mock_post):
response = self.c.get('/rowers/me/sporttracksrefresh/',follow=True) response = self.c.get('/rowers/me/sporttracksrefresh/',follow=True)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
@patch('rowers.sporttracksstuff.requests.post', side_effect=mocked_requests) @patch('rowers.integrations.sporttracks.requests.post', side_effect=mocked_requests)
@patch('rowers.sporttracksstuff.requests.get', side_effect=mocked_requests) @patch('rowers.integrations.sporttracks.requests.get', side_effect=mocked_requests)
def test_sporttracks_upload(self, mock_get, mock_post): def test_sporttracks_upload(self, mock_get, mock_post):
response = self.c.get('/rowers/workout/'+encoded1+'/sporttracksuploadw/') response = self.c.get('/rowers/workout/'+encoded1+'/sporttracksuploadw/')
@@ -1263,7 +1263,7 @@ class STObjects(DjangoTestCase):
self.assertEqual(response.url, '/rowers/workout/'+encoded1+'/edit/') self.assertEqual(response.url, '/rowers/workout/'+encoded1+'/edit/')
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
@patch('rowers.sporttracksstuff.requests.get', side_effect=mocked_requests) @patch('rowers.integrations.sporttracks.requests.get', side_effect=mocked_requests)
def test_sporttracks_list(self, mock_get): def test_sporttracks_list(self, mock_get):
response = self.c.get('/rowers/workout/sporttracksimport',follow=True) response = self.c.get('/rowers/workout/sporttracksimport',follow=True)
@@ -1306,26 +1306,6 @@ class STObjects(DjangoTestCase):
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
@patch('rowers.dataprep.create_engine')
def test_strokedata(self, mocked_sqlalchemy):
with open('rowers/tests/testdata/sporttrackstestdata.txt','r') as infile:
data = json.load(infile)
from rowers.sporttracksstuff import add_workout_from_data
res = add_workout_from_data(self.u,1,data,data)
@patch('rowers.dataprep.create_engine')
def test_strokedatanohr(self, mocked_sqlalchemy):
with open('rowers/tests/testdata/sporttrackstestnohr.txt','r') as infile:
data = json.load(infile)
from rowers.sporttracksstuff import add_workout_from_data
res = add_workout_from_data(self.u,1,data,data)
#@pytest.mark.django_db #@pytest.mark.django_db

Binary file not shown.

View File

@@ -2,7 +2,6 @@ from rowers.mytypes import workouttypes, boattypes, otwtypes, workoutsources, wo
from rowers.rower_rules import is_promember from rowers.rower_rules import is_promember
import rowers.tpstuff as tpstuff import rowers.tpstuff as tpstuff
import rowers.sporttracksstuff as sporttracksstuff
from rowers.integrations import * from rowers.integrations import *
from rowers.utils import ( from rowers.utils import (
@@ -131,7 +130,6 @@ def make_plot(r, w, f1, f2, plottype, title, imagename='', plotnr=0):
def do_sync(w, options, quick=False): def do_sync(w, options, quick=False):
do_strava_export = w.user.strava_auto_export do_strava_export = w.user.strava_auto_export
try: try:
do_strava_export = options['upload_to_Strava'] or do_strava_export do_strava_export = options['upload_to_Strava'] or do_strava_export
@@ -236,8 +234,10 @@ def do_sync(w, options, quick=False):
f.write(str(e)) f.write(str(e))
do_st_export = w.user.sporttracks_auto_export do_st_export = w.user.sporttracks_auto_export
if options['sporttracksid'] != 0 and options['sporttracksid'] != '': if options['sporttracksid'] != 0 and options['sporttracksid'] != '':
w.uploadedtost = options['sporttracksid'] w.uploadedtosporttracks = options['sporttracksid']
w.save() w.save()
do_st_export = False do_st_export = False
try: # pragma: no cover try: # pragma: no cover
@@ -248,9 +248,9 @@ def do_sync(w, options, quick=False):
if do_st_export: # pragma: no cover if do_st_export: # pragma: no cover
try: try:
message, id = sporttracksstuff.workout_sporttracks_upload( st_integration = SportTracksIntegration(w.user.user)
w.user.user, w, asynchron=True, id = st_integration.workout_export(w)
)
dologging('st_export.log', dologging('st_export.log',
'exported workout {wid} for user {uid}'.format( 'exported workout {wid} for user {uid}'.format(
wid = w.id, wid = w.id,

View File

@@ -138,13 +138,13 @@ def workout_c2_upload_view(request, id=0):
# Upload workout to SportTracks # Upload workout to SportTracks
@permission_required('workout.change_workout', fn=get_workout_by_opaqueid) @permission_required('workout.change_workout', fn=get_workout_by_opaqueid)
def workout_sporttracks_upload_view(request, id=0): def workout_sporttracks_upload_view(request, id=0):
message = "" st_integration = SportTracksIntegration(request.user)
# ready to upload. Hurray # ready to upload. Hurray
w = get_workout(id) w = get_workout(id)
r = w.user r = w.user
message, res = sporttracksstuff.workout_sporttracks_upload( id = st_integration.workout_export(w)
r.user, w, asynchron=True)
messages.info( messages.info(
request, 'Your workout will be synchronized with SportTracks in the background') request, 'Your workout will be synchronized with SportTracks in the background')
@@ -240,16 +240,8 @@ def rower_polar_authorize(request): # pragma: no cover
def rower_sporttracks_authorize(request): # pragma: no cover def rower_sporttracks_authorize(request): # pragma: no cover
# Generate a random string for the state parameter # Generate a random string for the state parameter
# Save it for use later to prevent xsrf attacks # Save it for use later to prevent xsrf attacks
st_integration = SportTracksIntegration(request.user)
state = str(uuid4()) url = st_integration.make_authorization_url()
params = {"client_id": SPORTTRACKS_CLIENT_ID,
"response_type": "code",
"state": state,
"redirect_uri": SPORTTRACKS_REDIRECT_URI}
url = "https://api.sporttracks.mobi/oauth2/authorize?" + \
urllib.parse.urlencode(params)
return HttpResponseRedirect(url) return HttpResponseRedirect(url)
@@ -335,24 +327,8 @@ def rower_tp_token_refresh(request):
# SportTracks token refresh. URL for manual refresh. Not visible to users # SportTracks token refresh. URL for manual refresh. Not visible to users
@login_required() @login_required()
def rower_sporttracks_token_refresh(request): def rower_sporttracks_token_refresh(request):
r = getrower(request.user) st_integration = SportTracksIntegration(request.user)
res = sporttracksstuff.do_refresh_token( result = st_integration.token_refresh()
r.sporttracksrefreshtoken,
)
access_token = res[0]
expires_in = res[1]
refresh_token = res[2]
expirydatetime = timezone.now()+datetime.timedelta(seconds=expires_in)
r = getrower(request.user)
r.sporttrackstoken = access_token
r.sporttrackstokenexpirydate = expirydatetime
r.sporttracksrefreshtoken = refresh_token
r.save()
successmessage = "Tokens refreshed. Good to go"
messages.info(request, successmessage)
url = reverse('workouts_view') url = reverse('workouts_view')
@@ -725,20 +701,8 @@ def rower_process_sporttrackscallback(request):
url = reverse('rower_exportsettings_view') url = reverse('rower_exportsettings_view')
return HttpResponseRedirect(url) return HttpResponseRedirect(url)
res = sporttracksstuff.get_token(code) st_integration = SportTracksIntegration(request.user)
token = st_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)
r.sporttrackstoken = access_token
r.sporttrackstokenexpirydate = expirydatetime
r.sporttracksrefreshtoken = refresh_token
r.save()
successmessage = "Tokens stored. Good to go. Please check your import/export settings" successmessage = "Tokens stored. Good to go. Please check your import/export settings"
messages.info(request, successmessage) messages.info(request, successmessage)
@@ -1100,17 +1064,8 @@ def workout_stravaimport_view(request, message="", userid=0):
alldata = {} alldata = {}
for stravaid in stravaids: for stravaid in stravaids:
csvfilename = 'media/{code}_{stravaid}.csv'.format( res = strava_integration.get_workout(id)
code=uuid4().hex[:16], stravaid=stravaid)
_ = myqueue(
queue,
fetch_strava_workout,
r.stravatoken,
strava_integration.oauth_data,
stravaid,
csvfilename,
r.user.id
)
# done, redirect to workouts list # done, redirect to workouts list
messages.info(request, messages.info(request,
'Your Strava workouts will be imported in the background.' 'Your Strava workouts will be imported in the background.'
@@ -1416,55 +1371,35 @@ def workout_polarimport_view(request, userid=0): # pragma: no cover
@permission_required('rower.is_coach', fn=get_user_by_userid, raise_exception=True) @permission_required('rower.is_coach', fn=get_user_by_userid, raise_exception=True)
@permission_required('rower.is_not_freecoach', fn=get_user_by_userid, raise_exception=True) @permission_required('rower.is_not_freecoach', fn=get_user_by_userid, raise_exception=True)
def workout_sporttracksimport_view(request, message="", userid=0): def workout_sporttracksimport_view(request, message="", userid=0):
r = getrequestrower(request, userid=userid) st_integration = SportTracksIntegration(request.user)
if r.user != request.user: try:
messages.error( _ = st_integration.open()
request, 'You can only access your own workouts on the NK Logbook, not those of your athletes') except NoTokenError:
url = reverse('workout_sporttracksimport_view', return HttpResponseRedirect("/rowers/me/sporttracksauthorize/")
kwargs={'userid': request.user.id})
return HttpResponseRedirect(url)
res = sporttracksstuff.get_sporttracks_workout_list(request.user) workouts = st_integration.get_workout_list()
if (res.status_code != 200):
if (res.status_code == 401):
r = getrower(request.user)
if (r.sporttrackstoken == '') or (r.sporttrackstoken is None):
s = "Token doesn't exist. Need to authorize"
return HttpResponseRedirect("/rowers/me/sporttracksauthorize/")
else: # pragma: no cover
return HttpResponseRedirect("/rowers/me/sporttracksrefresh/")
message = "Something went wrong in workout_sporttracksimport_view" # pragma: no cover
messages.error(request, message) # pragma: no cover
if settings.DEBUG: # pragma: no cover
return HttpResponse(res)
else: # pragma: no cover
url = reverse('workouts_view')
return HttpResponseRedirect(url)
workouts = []
knownstids = uniqify([
w.uploadedtosporttracks for w in Workout.objects.filter(user=r)
])
for item in res.json()['items']:
d = int(float(item['total_distance']))
i = int(getidfromuri(item['uri']))
if i in knownstids: # pragma: no cover
nnn = ''
else:
nnn = 'NEW'
n = item['name']
ttot = str(datetime.timedelta(seconds=int(float(item['duration']))))
s = item['start_time']
r = item['type']
keys = ['id', 'distance', 'duration',
'starttime', 'type', 'name', 'new']
values = [i, d, ttot, s, r, n, nnn]
res = dict(zip(keys, values))
workouts.append(res)
r = getrower(request.user) r = getrower(request.user)
if request.method == "POST":
try: # pragma: no cover
tdict = dict(request.POST.lists())
ids = tdict['workoutid']
stids = [int(id) for id in ids]
alldata = {}
for id in stids:
res = st_integration.get_workout(id)
# done, redirect to workouts list
messages.info(request,
'Your SportTracks 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
pass
breadcrumbs = [ breadcrumbs = [
{ {
'url': '/rowers/list-workouts/', 'url': '/rowers/list-workouts/',
@@ -1476,12 +1411,17 @@ def workout_sporttracksimport_view(request, message="", userid=0):
}, },
] ]
return render(request, 'sporttracks_list_import.html', checknew = request.GET.get('selectallnew', False)
return render(request, 'list_import.html',
{'workouts': workouts, {'workouts': workouts,
'breadcrumbs': breadcrumbs, 'breadcrumbs': breadcrumbs,
'active': 'nav-workouts', 'active': 'nav-workouts',
'rower': r, 'rower': r,
'teams': get_my_teams(request.user), 'teams': get_my_teams(request.user),
'integration':'SportTracks',
'checknew': checknew,
}) })
return HttpResponse(res) # pragma: no cover return HttpResponse(res) # pragma: no cover
@@ -1630,7 +1570,7 @@ importsources = {
'strava': StravaIntegration, 'strava': StravaIntegration,
'polar': polarstuff, 'polar': polarstuff,
'ownapi': ownapistuff, 'ownapi': ownapistuff,
'sporttracks': sporttracksstuff, 'sporttracks': SportTracksIntegration,
'trainingpeaks': tpstuff, 'trainingpeaks': tpstuff,
'nk': NKIntegration, 'nk': NKIntegration,
} }
@@ -1698,27 +1638,9 @@ def workout_getimportview_old(request, externalid, source='c2', do_async=True):
# Imports all new workouts from SportTracks # Imports all new workouts from SportTracks
@login_required() @login_required()
def workout_getsporttracksworkout_all(request): def workout_getsporttracksworkout_all(request):
res = sporttracksstuff.get_sporttracks_workout_list(request.user) st_integration = SportTracksIntegration(request.user)
if (res.status_code == 200): st_integration.get_workouts()
r = getrower(request.user) messages.info(request,"Your SportTracks workouts will be imported in the background")
stids = [int(getidfromuri(item['uri']))
for item in res.json()['items']]
knownstids = uniqify([
w.uploadedtosporttracks for w in Workout.objects.filter(user=r)
])
newids = [stid for stid in stids if stid not in knownstids]
for sporttracksid in newids:
id = sporttracksstuff.get_workout(
request.user, sporttracksid)
if id == 0: # pragma: no cover
messages.error(
request, "Something went wrong with workout {id}".format(id=sporttracksid))
else:
w = Workout.objects.get(id=id)
w.uploadedtosporttracks = sporttracksid
w.save()
url = reverse('workouts_view') url = reverse('workouts_view')
return HttpResponseRedirect(url) return HttpResponseRedirect(url)
@@ -1761,27 +1683,9 @@ def workout_getimportview(request, externalid, source='c2', do_async=True):
# Imports all new workouts from SportTracks # Imports all new workouts from SportTracks
@login_required() @login_required()
def workout_getsporttracksworkout_all(request): def workout_getsporttracksworkout_all(request):
res = sporttracksstuff.get_sporttracks_workout_list(request.user) st_integration = SportTracksIntegration(request.user)
if (res.status_code == 200): _ = st_integration.get_workouts()
r = getrower(request.user) messages.info(request,"Your SportTracks workouts will be imported in the background")
stids = [int(getidfromuri(item['uri']))
for item in res.json()['items']]
knownstids = uniqify([
w.uploadedtosporttracks for w in Workout.objects.filter(user=r)
])
newids = [stid for stid in stids if stid not in knownstids]
for sporttracksid in newids:
id = sporttracksstuff.get_workout(
request.user, sporttracksid)
if id == 0: # pragma: no cover
messages.error(
request, "Something went wrong with workout {id}".format(id=sporttracksid))
else:
w = Workout.objects.get(id=id)
w.uploadedtosporttracks = sporttracksid
w.save()
url = reverse('workouts_view') url = reverse('workouts_view')
return HttpResponseRedirect(url) return HttpResponseRedirect(url)

View File

@@ -194,7 +194,6 @@ import datetime
import iso8601 import iso8601
import rowers.rojabo_stuff as rojabo_stuff import rowers.rojabo_stuff as rojabo_stuff
from rowers.rp3stuff import rp3_open from rowers.rp3stuff import rp3_open
from rowers.sporttracksstuff import sporttracks_open
from rowers.tpstuff import tp_open from rowers.tpstuff import tp_open
from iso8601 import ParseError from iso8601 import ParseError
@@ -203,7 +202,6 @@ import rowers.garmin_stuff as garmin_stuff
from rowers.rojabo_stuff import rojabo_open from rowers.rojabo_stuff import rojabo_open
import rowers.polarstuff as polarstuff import rowers.polarstuff as polarstuff
import rowers.sporttracksstuff as sporttracksstuff
from rowers.integrations import * from rowers.integrations import *

View File

@@ -5654,10 +5654,9 @@ def workout_upload_view(request,
messages.error(request, message) messages.error(request, message)
if (upload_to_st): # pragma: no cover if (upload_to_st): # pragma: no cover
st_integration = SportTracksIntegration(request.user)
try: try:
message, id = sporttracksstuff.workout_sporttracks_upload( id = st_integration.workout_export(w)
request.user, w
)
except NoTokenError: except NoTokenError:
message = "Please connect to SportTracks first" message = "Please connect to SportTracks first"
id = 0 id = 0