sporttracks, not fully tested
This commit is contained in:
@@ -71,7 +71,7 @@ class SyncIntegration(metaclass=ABCMeta):
|
||||
"scope": self.oauth_data['scope'],
|
||||
"state": state}
|
||||
|
||||
url = self.oauth_data['authorizaton_uri']+urllib.parse.urlencode(params)
|
||||
url = self.oauth_data['authorization_uri']+urllib.parse.urlencode(params)
|
||||
|
||||
return url
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
from .integrations import SyncIntegration, NoTokenError
|
||||
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
|
||||
import rowers.mytypes as mytypes
|
||||
from rowsandall_app.settings import (
|
||||
@@ -11,13 +13,38 @@ from rowsandall_app.settings import (
|
||||
|
||||
import re
|
||||
import numpy
|
||||
import pytz
|
||||
import json
|
||||
import requests
|
||||
import datetime
|
||||
import pandas as pd
|
||||
|
||||
import django_rq
|
||||
queue = django_rq.get_queue('default')
|
||||
queuelow = django_rq.get_queue('low')
|
||||
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):
|
||||
def __init__(self, *args, **kwargs):
|
||||
@@ -27,7 +54,7 @@ class SportTracksIntegration(SyncIntegration):
|
||||
'client_id': SPORTTRACKS_CLIENT_ID,
|
||||
'client_secret': SPORTTRACKS_CLIENT_SECRET,
|
||||
'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',
|
||||
'tokenname': 'sporttrackstoken',
|
||||
'refreshtokenname': 'sporttracksrefreshtoken',
|
||||
@@ -42,13 +69,178 @@ class SportTracksIntegration(SyncIntegration):
|
||||
return super(SportTracksIntegration, self).open(*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:
|
||||
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:
|
||||
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
|
||||
|
||||
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(
|
||||
job = myqueue(
|
||||
queue,
|
||||
handle_sporttracks_workout_from_data,
|
||||
self.user,
|
||||
sporttracksid, data,
|
||||
strokedata,
|
||||
id,
|
||||
'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()
|
||||
r = self.rower
|
||||
|
||||
@@ -98,13 +276,18 @@ class SportTracksIntegration(SyncIntegration):
|
||||
s = "Token doesn't exist. Need to authorize"
|
||||
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 = []
|
||||
|
||||
knownstids = uniqify([
|
||||
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']))
|
||||
i = int(getidfromuri(item['uri']))
|
||||
if i in knownstids: # pragma: no cover
|
||||
@@ -127,7 +310,7 @@ class SportTracksIntegration(SyncIntegration):
|
||||
return super(SportTracksIntegration, self).make_authorization_url(*args, **kwargs)
|
||||
|
||||
def get_token(self, code, *args, **kwargs) -> (str, int, str):
|
||||
return ""
|
||||
return super(SportTracksIntegration, self).get_token(code, *args, **kwargs)
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -458,9 +458,24 @@ def splitstdata(lijst):
|
||||
return [np.array(t), np.array(latlong)]
|
||||
|
||||
@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):
|
||||
|
||||
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:
|
||||
workouttype = data['type']
|
||||
except KeyError: # pragma: no cover
|
||||
|
||||
@@ -64,7 +64,7 @@ from mock import Mock, patch
|
||||
#from minimocktest import MockTestCase
|
||||
import pandas as pd
|
||||
import rowers.c2stuff as c2stuff
|
||||
import rowers.sporttracksstuff as sporttracksstuff
|
||||
|
||||
import rowers.rojabo_stuff as rojabo_stuff
|
||||
|
||||
from django.urls import reverse, reverse_lazy
|
||||
|
||||
@@ -1236,7 +1236,7 @@ class STObjects(DjangoTestCase):
|
||||
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):
|
||||
response = self.c.get('/sporttracks_callback?code=dsdoij232s',follow=True)
|
||||
|
||||
@@ -1244,15 +1244,15 @@ class STObjects(DjangoTestCase):
|
||||
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):
|
||||
response = self.c.get('/rowers/me/sporttracksrefresh/',follow=True)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
|
||||
@patch('rowers.sporttracksstuff.requests.post', side_effect=mocked_requests)
|
||||
@patch('rowers.sporttracksstuff.requests.get', side_effect=mocked_requests)
|
||||
@patch('rowers.integrations.sporttracks.requests.post', side_effect=mocked_requests)
|
||||
@patch('rowers.integrations.sporttracks.requests.get', side_effect=mocked_requests)
|
||||
def test_sporttracks_upload(self, mock_get, mock_post):
|
||||
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.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):
|
||||
response = self.c.get('/rowers/workout/sporttracksimport',follow=True)
|
||||
|
||||
@@ -1306,26 +1306,6 @@ class STObjects(DjangoTestCase):
|
||||
|
||||
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
|
||||
|
||||
BIN
rowers/tests/testdata/testdata.tcx.gz
vendored
BIN
rowers/tests/testdata/testdata.tcx.gz
vendored
Binary file not shown.
@@ -2,7 +2,6 @@ from rowers.mytypes import workouttypes, boattypes, otwtypes, workoutsources, wo
|
||||
|
||||
from rowers.rower_rules import is_promember
|
||||
import rowers.tpstuff as tpstuff
|
||||
import rowers.sporttracksstuff as sporttracksstuff
|
||||
|
||||
from rowers.integrations 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):
|
||||
|
||||
do_strava_export = w.user.strava_auto_export
|
||||
try:
|
||||
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))
|
||||
|
||||
do_st_export = w.user.sporttracks_auto_export
|
||||
|
||||
if options['sporttracksid'] != 0 and options['sporttracksid'] != '':
|
||||
w.uploadedtost = options['sporttracksid']
|
||||
w.uploadedtosporttracks = options['sporttracksid']
|
||||
|
||||
w.save()
|
||||
do_st_export = False
|
||||
try: # pragma: no cover
|
||||
@@ -248,9 +248,9 @@ def do_sync(w, options, quick=False):
|
||||
|
||||
if do_st_export: # pragma: no cover
|
||||
try:
|
||||
message, id = sporttracksstuff.workout_sporttracks_upload(
|
||||
w.user.user, w, asynchron=True,
|
||||
)
|
||||
st_integration = SportTracksIntegration(w.user.user)
|
||||
id = st_integration.workout_export(w)
|
||||
|
||||
dologging('st_export.log',
|
||||
'exported workout {wid} for user {uid}'.format(
|
||||
wid = w.id,
|
||||
|
||||
@@ -138,13 +138,13 @@ def workout_c2_upload_view(request, id=0):
|
||||
# Upload workout to SportTracks
|
||||
@permission_required('workout.change_workout', fn=get_workout_by_opaqueid)
|
||||
def workout_sporttracks_upload_view(request, id=0):
|
||||
message = ""
|
||||
st_integration = SportTracksIntegration(request.user)
|
||||
|
||||
# ready to upload. Hurray
|
||||
w = get_workout(id)
|
||||
r = w.user
|
||||
|
||||
message, res = sporttracksstuff.workout_sporttracks_upload(
|
||||
r.user, w, asynchron=True)
|
||||
id = st_integration.workout_export(w)
|
||||
|
||||
messages.info(
|
||||
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
|
||||
# Generate a random string for the state parameter
|
||||
# Save it for use later to prevent xsrf attacks
|
||||
|
||||
state = str(uuid4())
|
||||
|
||||
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)
|
||||
st_integration = SportTracksIntegration(request.user)
|
||||
url = st_integration.make_authorization_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
|
||||
@login_required()
|
||||
def rower_sporttracks_token_refresh(request):
|
||||
r = getrower(request.user)
|
||||
res = sporttracksstuff.do_refresh_token(
|
||||
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)
|
||||
st_integration = SportTracksIntegration(request.user)
|
||||
result = st_integration.token_refresh()
|
||||
|
||||
url = reverse('workouts_view')
|
||||
|
||||
@@ -725,20 +701,8 @@ def rower_process_sporttrackscallback(request):
|
||||
url = reverse('rower_exportsettings_view')
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
res = sporttracksstuff.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()
|
||||
st_integration = SportTracksIntegration(request.user)
|
||||
token = st_integration.get_token(code)
|
||||
|
||||
successmessage = "Tokens stored. Good to go. Please check your import/export settings"
|
||||
messages.info(request, successmessage)
|
||||
@@ -1100,17 +1064,8 @@ def workout_stravaimport_view(request, message="", userid=0):
|
||||
alldata = {}
|
||||
|
||||
for stravaid in stravaids:
|
||||
csvfilename = 'media/{code}_{stravaid}.csv'.format(
|
||||
code=uuid4().hex[:16], stravaid=stravaid)
|
||||
_ = myqueue(
|
||||
queue,
|
||||
fetch_strava_workout,
|
||||
r.stravatoken,
|
||||
strava_integration.oauth_data,
|
||||
stravaid,
|
||||
csvfilename,
|
||||
r.user.id
|
||||
)
|
||||
res = strava_integration.get_workout(id)
|
||||
|
||||
# done, redirect to workouts list
|
||||
messages.info(request,
|
||||
'Your Strava workouts will be imported in the background.'
|
||||
@@ -1416,54 +1371,34 @@ 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_not_freecoach', fn=get_user_by_userid, raise_exception=True)
|
||||
def workout_sporttracksimport_view(request, message="", userid=0):
|
||||
r = getrequestrower(request, userid=userid)
|
||||
if r.user != request.user:
|
||||
messages.error(
|
||||
request, 'You can only access your own workouts on the NK Logbook, not those of your athletes')
|
||||
url = reverse('workout_sporttracksimport_view',
|
||||
kwargs={'userid': request.user.id})
|
||||
return HttpResponseRedirect(url)
|
||||
st_integration = SportTracksIntegration(request.user)
|
||||
try:
|
||||
_ = st_integration.open()
|
||||
except NoTokenError:
|
||||
return HttpResponseRedirect("/rowers/me/sporttracksauthorize/")
|
||||
|
||||
res = sporttracksstuff.get_sporttracks_workout_list(request.user)
|
||||
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)
|
||||
workouts = st_integration.get_workout_list()
|
||||
|
||||
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 = [
|
||||
{
|
||||
@@ -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,
|
||||
'breadcrumbs': breadcrumbs,
|
||||
'active': 'nav-workouts',
|
||||
'rower': r,
|
||||
'teams': get_my_teams(request.user),
|
||||
'integration':'SportTracks',
|
||||
'checknew': checknew,
|
||||
})
|
||||
|
||||
return HttpResponse(res) # pragma: no cover
|
||||
@@ -1630,7 +1570,7 @@ importsources = {
|
||||
'strava': StravaIntegration,
|
||||
'polar': polarstuff,
|
||||
'ownapi': ownapistuff,
|
||||
'sporttracks': sporttracksstuff,
|
||||
'sporttracks': SportTracksIntegration,
|
||||
'trainingpeaks': tpstuff,
|
||||
'nk': NKIntegration,
|
||||
}
|
||||
@@ -1698,27 +1638,9 @@ def workout_getimportview_old(request, externalid, source='c2', do_async=True):
|
||||
# Imports all new workouts from SportTracks
|
||||
@login_required()
|
||||
def workout_getsporttracksworkout_all(request):
|
||||
res = sporttracksstuff.get_sporttracks_workout_list(request.user)
|
||||
if (res.status_code == 200):
|
||||
r = getrower(request.user)
|
||||
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()
|
||||
st_integration = SportTracksIntegration(request.user)
|
||||
st_integration.get_workouts()
|
||||
messages.info(request,"Your SportTracks workouts will be imported in the background")
|
||||
|
||||
url = reverse('workouts_view')
|
||||
return HttpResponseRedirect(url)
|
||||
@@ -1761,27 +1683,9 @@ def workout_getimportview(request, externalid, source='c2', do_async=True):
|
||||
# Imports all new workouts from SportTracks
|
||||
@login_required()
|
||||
def workout_getsporttracksworkout_all(request):
|
||||
res = sporttracksstuff.get_sporttracks_workout_list(request.user)
|
||||
if (res.status_code == 200):
|
||||
r = getrower(request.user)
|
||||
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()
|
||||
st_integration = SportTracksIntegration(request.user)
|
||||
_ = st_integration.get_workouts()
|
||||
messages.info(request,"Your SportTracks workouts will be imported in the background")
|
||||
|
||||
url = reverse('workouts_view')
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -194,7 +194,6 @@ import datetime
|
||||
import iso8601
|
||||
import rowers.rojabo_stuff as rojabo_stuff
|
||||
from rowers.rp3stuff import rp3_open
|
||||
from rowers.sporttracksstuff import sporttracks_open
|
||||
from rowers.tpstuff import tp_open
|
||||
from iso8601 import ParseError
|
||||
|
||||
@@ -203,7 +202,6 @@ import rowers.garmin_stuff as garmin_stuff
|
||||
|
||||
from rowers.rojabo_stuff import rojabo_open
|
||||
import rowers.polarstuff as polarstuff
|
||||
import rowers.sporttracksstuff as sporttracksstuff
|
||||
|
||||
from rowers.integrations import *
|
||||
|
||||
|
||||
@@ -5654,10 +5654,9 @@ def workout_upload_view(request,
|
||||
messages.error(request, message)
|
||||
|
||||
if (upload_to_st): # pragma: no cover
|
||||
st_integration = SportTracksIntegration(request.user)
|
||||
try:
|
||||
message, id = sporttracksstuff.workout_sporttracks_upload(
|
||||
request.user, w
|
||||
)
|
||||
id = st_integration.workout_export(w)
|
||||
except NoTokenError:
|
||||
message = "Please connect to SportTracks first"
|
||||
id = 0
|
||||
|
||||
Reference in New Issue
Block a user