Private
Public Access
1
0

first attempt on c2

This commit is contained in:
Sander Roosendaal
2023-02-10 16:55:47 +01:00
parent d661f861c4
commit 2981c59a5d
19 changed files with 873 additions and 1129 deletions

View File

@@ -101,923 +101,3 @@ def getagegrouprecord(age, sex='male', weightcategory='hwt',
return power
oauth_data = {
'client_id': C2_CLIENT_ID,
'client_secret': C2_CLIENT_SECRET,
'redirect_uri': C2_REDIRECT_URI,
'autorization_uri': "https://log.concept2.com/oauth/authorize",
'content_type': 'application/x-www-form-urlencoded',
'tokenname': 'c2token',
'refreshtokenname': 'c2refreshtoken',
'expirydatename': 'tokenexpirydate',
'bearer_auth': True,
'base_url': "https://log.concept2.com/oauth/access_token",
'scope': 'write',
}
# Checks if user has Concept2 tokens, resets tokens if they are
# expired.
def c2_open(user):
r = Rower.objects.get(user=user)
if (r.c2token == '') or (r.c2token is None):
raise NoTokenError("User has no token")
else:
if (timezone.now() > r.tokenexpirydate):
res = rower_c2_token_refresh(user)
if res is None: # pragma: no cover
raise NoTokenError("User has no token")
if res[0] is not None:
thetoken = res[0]
else: # pragma: no cover
raise NoTokenError("User has no token")
else:
thetoken = r.c2token
return thetoken
def get_c2_workouts(rower, page=1, do_async=True):
try:
_ = c2_open(rower.user)
except NoTokenError: # pragma: no cover
return 0
res = get_c2_workout_list(rower.user, page=page)
if (res.status_code != 200): # pragma: no cover
return 0
else:
c2ids = [item['id'] for item in res.json()['data']]
alldata = {}
for item in res.json()['data']:
alldata[item['id']] = item
knownc2ids = [
w.uploadedtoc2 for w in Workout.objects.filter(user=rower)
]
tombstones = [
t.uploadedtoc2 for t in TombStone.objects.filter(user=rower)
]
# get "blocked" c2ids
parkedids = []
try:
with open('c2blocked.json', 'r') as c2blocked:
jsondata = json.load(c2blocked)
parkedids = jsondata['ids']
except FileNotFoundError: # pragma: no cover
pass
knownc2ids = uniqify(knownc2ids+tombstones+parkedids)
newids = [c2id for c2id in c2ids if c2id not in knownc2ids]
if settings.TESTING:
newids = c2ids
newparkedids = uniqify(newids+parkedids)
with open('c2blocked.json', 'wt') as c2blocked:
data = {'ids': newparkedids}
json.dump(data, c2blocked)
counter = 0
for c2id in newids:
if do_async: # pragma: no cover
_ = myqueue(queuehigh,
handle_c2_async_workout,
alldata,
rower.user.id,
rower.c2token,
c2id,
counter,
rower.defaulttimezone)
counter = counter+1
else:
_ = create_async_workout(alldata, rower.user, c2id)
return 1
# get workout metrics, then relay stroke data to an asynchronous task
def create_async_workout(alldata, user, c2id):
data = alldata[c2id]
splitdata = None
c2id = data['id']
workouttype = data['type']
startdatetime = iso8601.parse_date(data['date'])
try:
title = data['name']
except KeyError:
title = ""
try:
t = data['comments'].split('\n', 1)[0]
title += t[:40]
except:
title = ''
# Create CSV file name and save data to CSV file
csvfilename = 'media/Import_'+str(c2id)+'.csv.gz'
r = Rower.objects.get(user=user)
authorizationstring = str('Bearer ' + r.c2token)
headers = {'Authorization': authorizationstring,
'user-agent': 'sanderroosendaal',
'Content-Type': 'application/json'}
# url2 = "https://log.concept2.com/api/users/me/results"+str(c2id)
url = "https://log.concept2.com/api/users/me/results/"+str(c2id)+"/strokes"
try:
s = requests.get(url, headers=headers)
except ConnectionError: # pragma: no cover
return 0
if s.status_code != 200: # pragma: no cover
return 0
strokedata = pd.DataFrame.from_dict(s.json()['data'])
res = make_cumvalues(0.1*strokedata['t'])
cum_time = res[0]
lapidx = res[1]
starttimeunix = arrow.get(startdatetime).timestamp()
starttimeunix = starttimeunix-cum_time.max()
unixtime = cum_time+starttimeunix
# unixtime[0] = starttimeunix
seconds = 0.1*strokedata.loc[:, 't']
nr_rows = len(unixtime)
try: # pragma: no cover
latcoord = strokedata.loc[:, 'lat']
loncoord = strokedata.loc[:, 'lon']
except:
latcoord = np.zeros(nr_rows)
loncoord = np.zeros(nr_rows)
try:
strokelength = strokedata.loc[:, 'strokelength']
except:
strokelength = np.zeros(nr_rows)
dist2 = 0.1*strokedata.loc[:, 'd']
try:
spm = strokedata.loc[:, 'spm']
except KeyError: # pragma: no cover
spm = 0*dist2
try:
hr = strokedata.loc[:, 'hr']
except KeyError: # pragma: no cover
hr = 0*spm
pace = strokedata.loc[:, 'p']/10.
pace = np.clip(pace, 0, 1e4)
pace = pace.replace(0, 300)
velo = 500./pace
power = 2.8*velo**3
if workouttype == 'bike': # pragma: no cover
velo = 1000./pace
df = pd.DataFrame({'TimeStamp (sec)': unixtime,
' Horizontal (meters)': dist2,
' Cadence (stokes/min)': spm,
' HRCur (bpm)': hr,
' longitude': loncoord,
' latitude': latcoord,
' Stroke500mPace (sec/500m)': pace,
' Power (watts)': power,
' DragFactor': np.zeros(nr_rows),
' DriveLength (meters)': np.zeros(nr_rows),
' StrokeDistance (meters)': strokelength,
' DriveTime (ms)': np.zeros(nr_rows),
' StrokeRecoveryTime (ms)': np.zeros(nr_rows),
' AverageDriveForce (lbs)': np.zeros(nr_rows),
' PeakDriveForce (lbs)': np.zeros(nr_rows),
' lapIdx': lapidx,
' WorkoutState': 4,
' ElapsedTime (sec)': seconds,
'cum_dist': dist2
})
df.sort_values(by='TimeStamp (sec)', ascending=True)
res = df.to_csv(csvfilename, index_label='index',
compression='gzip')
userid = r.user.id
uploadoptions = {
'secret': UPLOAD_SERVICE_SECRET,
'user': userid,
'file': csvfilename,
'title': title,
'workouttype': workouttype,
'boattype': '1x',
'c2id': c2id,
}
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: # pragma: no cover
return 0
try:
workoutid = response.json()['id']
except KeyError: # pragma: no cover
workoutid = 1
newc2id = Workout.objects.get(id=workoutid).uploadedtoc2
parkedids = []
with open('c2blocked.json', 'r') as c2blocked:
jsondata = json.load(c2blocked)
parkedids = jsondata['ids']
newparkedids = [id for id in parkedids if id != newc2id]
with open('c2blocked.json', 'wt') as c2blocked:
data = {'ids': newparkedids}
c2blocked.seek(0)
json.dump(data, c2blocked)
# summary
if 'workout' in data: # pragma: no cover
if 'splits' in data['workout']:
splitdata = data['workout']['splits']
elif 'intervals' in data['workout']:
splitdata = data['workout']['intervals']
else:
splitdata = False
else:
splitdata = False
if splitdata: # pragma: no cover
summary, sa, results = c2stuff.summaryfromsplitdata(
splitdata, data, csvfilename, workouttype=workouttype)
w = Workout.objects.get(id=workoutid)
w.summary = summary
w.save()
from rowingdata.trainingparser import getlist
if sa:
values = getlist(sa)
units = getlist(sa, sel='unit')
types = getlist(sa, sel='type')
rowdata = rdata(w.csvfilename)
if rowdata:
rowdata.updateintervaldata(values,
units, types, results)
rowdata.write_csv(w.csvfilename, gzip=True)
dataprep.update_strokedata(w.id, rowdata.df)
return workoutid
# convert datetime object to seconds
def makeseconds(t):
seconds = t.hour*3600.+t.minute*60.+t.second+0.1*int(t.microsecond/1.e5)
return seconds
# convert our weight class code to Concept2 weight class code
def c2wc(weightclass):
if (weightclass == "lwt"): # pragma: no cover
res = "L"
else:
res = "H"
return res
# Concept2 logbook sends over split data for each interval
# We use it here to generate a custom summary
# Some users complained about small differences
def summaryfromsplitdata(splitdata, data, filename, sep='|', workouttype='rower'):
workouttype = workouttype.lower()
totaldist = data['distance']
totaltime = data['time']/10.
try:
spm = data['stroke_rate']
except KeyError: # pragma: no cover
spm = 0
try:
resttime = data['rest_time']/10.
except KeyError: # pragma: no cover
resttime = 0
try:
restdistance = data['rest_distance']
except KeyError: # pragma: no cover
restdistance = 0
try:
avghr = data['heart_rate']['average']
except KeyError: # pragma: no cover
avghr = 0
try:
maxhr = data['heart_rate']['max']
except KeyError: # pragma: no cover
maxhr = 0
try:
avgpace = 500.*totaltime/totaldist
except (ZeroDivisionError, OverflowError): # pragma: no cover
avgpace = 0.
try:
restpace = 500.*resttime/restdistance
except (ZeroDivisionError, OverflowError): # pragma: no cover
restpace = 0.
velo = totaldist/totaltime
avgpower = 2.8*velo**(3.0)
if workouttype in ['bike', 'bikeerg']: # pragma: no cover
velo = velo/2.
avgpower = 2.8*velo**(3.0)
velo = velo*2
try:
restvelo = restdistance/resttime
except (ZeroDivisionError, OverflowError): # pragma: no cover
restvelo = 0
restpower = 2.8*restvelo**(3.0)
if workouttype in ['bike', 'bikeerg']: # pragma: no cover
restvelo = restvelo/2.
restpower = 2.8*restvelo**(3.0)
restvelo = restvelo*2
try:
avgdps = totaldist/data['stroke_count']
except (ZeroDivisionError, OverflowError, KeyError): # pragma: no cover
avgdps = 0
from rowingdata import summarystring, workstring, interval_string
sums = summarystring(totaldist, totaltime, avgpace, spm, avghr, maxhr,
avgdps, avgpower, readFile=filename,
separator=sep)
sums += workstring(totaldist, totaltime, avgpace, spm, avghr, maxhr,
avgdps, avgpower, separator=sep, symbol='W')
sums += workstring(restdistance, resttime, restpace, 0, 0, 0, 0, restpower,
separator=sep,
symbol='R')
sums += '\nWorkout Details\n'
sums += '#-{sep}SDist{sep}-Split-{sep}-SPace-{sep}-Pwr-{sep}SPM-{sep}AvgHR{sep}MaxHR{sep}DPS-\n'.format(
sep=sep
)
intervalnr = 0
sa = []
results = []
try:
timebased = data['workout_type'] in [
'FixedTimeSplits', 'FixedTimeInterval']
except KeyError: # pragma: no cover
timebased = False
for interval in splitdata:
try:
idist = interval['distance']
except KeyError: # pragma: no cover
idist = 0
try:
itime = interval['time']/10.
except KeyError: # pragma: no cover
itime = 0
try:
ipace = 500.*itime/idist
except (ZeroDivisionError, OverflowError): # pragma: no cover
ipace = 180.
try:
ispm = interval['stroke_rate']
except KeyError: # pragma: no cover
ispm = 0
try:
irest_time = interval['rest_time']/10.
except KeyError: # pragma: no cover
irest_time = 0
try:
iavghr = interval['heart_rate']['average']
except KeyError: # pragma: no cover
iavghr = 0
try:
imaxhr = interval['heart_rate']['average']
except KeyError: # pragma: no cover
imaxhr = 0
# create interval values
iarr = [idist, 'meters', 'work']
resarr = [itime]
if timebased: # pragma: no cover
iarr = [itime, 'seconds', 'work']
resarr = [idist]
if irest_time > 0:
iarr += [irest_time, 'seconds', 'rest']
try:
resarr += [interval['rest_distance']]
except KeyError: # pragma: no cover
resarr += [np.nan]
sa += iarr
results += resarr
if itime != 0:
ivelo = idist/itime
ipower = 2.8*ivelo**(3.0)
if workouttype in ['bike', 'bikeerg']: # pragma: no cover
ipower = 2.8*(ivelo/2.)**(3.0)
else: # pragma: no cover
ivelo = 0
ipower = 0
sums += interval_string(intervalnr, idist, itime, ipace, ispm,
iavghr, imaxhr, 0, ipower, separator=sep)
intervalnr += 1
return sums, sa, results
# Create the Data object for the stroke data to be sent to Concept2 logbook
# API
def createc2workoutdata(w):
filename = w.csvfilename
try:
row = rowingdata(csvfile=filename)
except IOError: # pragma: no cover
return 0
try:
averagehr = int(row.df[' HRCur (bpm)'].mean())
maxhr = int(row.df[' HRCur (bpm)'].max())
except (ValueError, KeyError): # pragma: no cover
averagehr = 0
maxhr = 0
# Calculate intervalstats
itime, idist, itype = row.intervalstats_values()
try:
lapnames = row.df[' lapIdx'].unique()
except KeyError: # pragma: no cover
lapnames = range(len(itime))
nrintervals = len(itime)
if len(lapnames) != nrintervals:
newlapnames = []
for name in lapnames:
newlapnames += [name, name]
lapnames = newlapnames
intervaldata = []
for i in range(nrintervals):
if itime[i] > 0:
mask = (row.df[' lapIdx'] == lapnames[i]) & (
row.df[' WorkoutState'] == itype[i])
try:
spmav = int(row.df[' Cadence (stokes/min)']
[mask].mean().astype(int))
hrav = int(row.df[' HRCur (bpm)'][mask].mean().astype(int))
except AttributeError: # pragma: no cover
try:
spmav = int(row.df[' Cadence (stokes/min)'][mask].mean())
hrav = int(row.df[' HRCur (bpm)'][mask].mean())
except ValueError:
spmav = 0
try:
hrav = int(row.df[' HRCur (bpm)'][mask].mean())
except ValuError:
hrav = 0
intervaldict = {
'type': 'distance',
'time': int(10*itime[i]),
'distance': int(idist[i]),
'heart_rate': {
'average': hrav,
},
'stroke_rate': spmav,
}
intervaldata.append(intervaldict)
# adding diff, trying to see if this is valid
t = 10*row.df.loc[:, 'TimeStamp (sec)'].values - \
10*row.df.loc[:, 'TimeStamp (sec)'].iloc[0]
try:
t[0] = t[1]
except IndexError: # pragma: no cover
pass
d = 10*row.df.loc[:, ' Horizontal (meters)'].values
try:
d[0] = d[1]
except IndexError: # pragma: no cover
pass
p = abs(10*row.df.loc[:, ' Stroke500mPace (sec/500m)'].values)
p = np.clip(p, 0, 3600)
if w.workouttype == 'bike': # pragma: no cover
p = 2.0*p
t = t.astype(int)
d = d.astype(int)
p = p.astype(int)
spm = row.df[' Cadence (stokes/min)'].astype(int)
try:
spm[0] = spm[1]
except (KeyError, IndexError): # pragma: no cover
spm = 0*t
try:
hr = row.df[' HRCur (bpm)'].astype(int)
except ValueError: # pragma: no cover
hr = 0*d
stroke_data = []
t = t.tolist()
d = d.tolist()
p = p.tolist()
spm = spm.tolist()
hr = hr.tolist()
for i in range(len(t)):
thisrecord = {"t": t[i],
"d": d[i],
"p": p[i],
"spm": spm[i],
"hr": hr[i]}
stroke_data.append(thisrecord)
try:
durationstr = datetime.datetime.strptime(
str(w.duration), "%H:%M:%S.%f")
except ValueError:
durationstr = datetime.datetime.strptime(str(w.duration), "%H:%M:%S")
workouttype = w.workouttype
if workouttype in otwtypes:
workouttype = 'water'
if w.timezone == 'tzutc()': # pragma: no cover
w.timezone = 'UTC'
w.save()
try:
wendtime = w.startdatetime.astimezone(pytz.timezone(
w.timezone))+datetime.timedelta(seconds=makeseconds(durationstr))
except UnknownTimeZoneError:
wendtime = w.startdatetime.astimezone(pytz.utc)+datetime.timedelta(seconds=makeseconds(durationstr))
data = {
"type": mytypes.c2mapping[workouttype],
# w.startdatetime.isoformat(),
"date": wendtime.strftime('%Y-%m-%d %H:%M:%S'),
"stroke_count": int(row.stroke_count),
"timezone": w.timezone,
"distance": int(w.distance),
"time": int(10*makeseconds(durationstr)),
"weight_class": c2wc(w.weightcategory),
"comments": w.notes,
'stroke_rate': int(row.df[' Cadence (stokes/min)'].mean()),
'drag_factor': int(row.dragfactor),
"heart_rate": {
"average": averagehr,
"max": maxhr,
},
"stroke_data": stroke_data,
'workout': {
'splits': intervaldata,
}
}
return data
# Refresh Concept2 authorization token
def do_refresh_token(refreshtoken):
scope = "results:write,user:read"
# client_auth = requests.auth.HTTPBasicAuth(C2_CLIENT_ID, C2_CLIENT_SECRET)
post_data = {"grant_type": "refresh_token",
"client_secret": C2_CLIENT_SECRET,
"client_id": C2_CLIENT_ID,
"refresh_token": refreshtoken,
}
headers = {'user-agent': 'sanderroosendaal'}
url = "https://log.concept2.com/oauth/access_token"
s = Session()
req = Request('POST', url, data=post_data, headers=headers)
prepped = req.prepare()
prepped.body += "&scope="
prepped.body += scope
response = s.send(prepped)
try:
token_json = response.json()
except JSONDecodeError: # pragma: no cover
return [None, None, None]
try:
thetoken = token_json['access_token']
expires_in = token_json['expires_in']
refresh_token = token_json['refresh_token']
except: # pragma: no cover
with open("media/c2errors.log", "a") as errorlog:
errorstring = str(sys.exc_info()[0])
timestr = time.strftime("%Y%m%d-%H%M%S")
errorlog.write(timestr+errorstring+"\r\n")
errorlog.write(str(token_json)+"\r\n")
thetoken = None
expires_in = None
refresh_token = None
return [thetoken, expires_in, refresh_token]
# Exchange authorization code for authorization token
def get_token(code):
messg = ''
scope = "user:read,results:write"
# client_auth = requests.auth.HTTPBasicAuth(C2_CLIENT_ID, C2_CLIENT_SECRET)
post_data = {"grant_type": "authorization_code",
"code": code,
"redirect_uri": C2_REDIRECT_URI,
"client_secret": C2_CLIENT_SECRET,
"client_id": C2_CLIENT_ID,
}
headers = {'user-agent': 'sanderroosendaal'}
url = "https://log.concept2.com/oauth/access_token"
s = Session()
req = Request('POST', url, data=post_data, headers=headers)
prepped = req.prepare()
prepped.body += "&scope="
prepped.body += scope
response = s.send(prepped)
token_json = response.json()
try:
status_code = response.status_code
# status_code = token_json['status_code']
except AttributeError: # pragma: no cover
# except KeyError:
return (0, response.text)
try:
status_code = token_json.status_code
except AttributeError: # pragma: no cover
return (0, 'Attribute Error on c2_get_token')
if status_code == 200:
thetoken = token_json['access_token']
expires_in = token_json['expires_in']
refresh_token = token_json['refresh_token']
else: # pragma: no cover
return (0, token_json['message'])
return (thetoken, expires_in, refresh_token, messg)
# Make URL for authorization and load it
def make_authorization_url(request): # pragma: no cover
# Generate a random string for the state parameter
# Save it for use later to prevent xsrf attacks
from uuid import uuid4
# state = str(uuid4())
scope = "user:read,results:write"
params = {"client_id": C2_CLIENT_ID,
"response_type": "code",
"redirect_uri": C2_REDIRECT_URI}
url = "https://log.concept2.com/oauth/authorize?" + \
urllib.parse.urlencode(params)
url += "&scope="+scope
return HttpResponseRedirect(url)
# Get workout from C2 ID
def get_workout(user, c2id, do_async=True):
r = Rower.objects.get(user=user)
_ = c2_open(user)
_ = myqueue(queuehigh,
handle_c2_getworkout,
user.id,
r.c2token,
c2id,
r.defaulttimezone)
return 1
# Get list of C2 workouts. We load only the first page,
# assuming that users don't want to import their old workouts
def get_c2_workout_list(user, page=1):
r = Rower.objects.get(user=user)
if (r.c2token == '') or (r.c2token is None): # pragma: no cover
s = "Token doesn't exist. Need to authorize"
return custom_exception_handler(401, s)
elif (timezone.now() > r.tokenexpirydate): # pragma: no cover
s = "Token expired. Needs to refresh."
return custom_exception_handler(401, s)
# ready to fetch. Hurray
authorizationstring = str('Bearer ' + r.c2token)
headers = {'Authorization': authorizationstring,
'user-agent': 'sanderroosendaal',
'Content-Type': 'application/json'}
url = "https://log.concept2.com/api/users/me/results"
url += "?page={page}".format(page=page)
s = requests.get(url, headers=headers)
return s
# Get username, having access token.
# Handy for checking if the API access is working
def get_username(access_token): # pragma: no cover
authorizationstring = str('Bearer ' + access_token)
headers = {'Authorization': authorizationstring,
'user-agent': 'sanderroosendaal',
'Content-Type': 'application/json'}
import urllib
url = "https://log.concept2.com/api/users/me"
response = requests.get(url, headers=headers)
me_json = response.json()
try:
res = me_json['data']['username']
_ = me_json['data']['id']
except KeyError:
res = None
return res
# Get user id, having access token
# Handy for checking if the API access is working
def get_userid(access_token):
authorizationstring = str('Bearer ' + access_token)
headers = {'Authorization': authorizationstring,
'user-agent': 'sanderroosendaal',
'Content-Type': 'application/json'}
import urllib
url = "https://log.concept2.com/api/users/me"
try:
response = requests.get(url, headers=headers)
except: # pragma: no cover
return 0
try:
me_json = response.json()
except: # pragma: no cover
return 0
try:
res = me_json['data']['id']
except KeyError: # pragma: no cover
return 0
return res
# For debugging purposes
def process_callback(request): # pragma: no cover
# need error handling
code = request.GET['code']
access_token = get_token(code)
username, id = get_username(access_token)
return HttpResponse("got a user name: %s" % username)
def default(o): # pragma: no cover
if isinstance(o, numpy.int64):
return int(o)
raise TypeError
# Uploading workout
def workout_c2_upload(user, w, asynchron=False):
message = 'trying C2 upload'
try:
if mytypes.c2mapping[w.workouttype] is None: # pragma: no cover
return "This workout type cannot be uploaded to Concept2", 0
except KeyError: # pragma: no cover
return "This workout type cannot be uploaded to Concept2", 0
_ = c2_open(user)
r = Rower.objects.get(user=user)
# ready to upload. Hurray
if (is_workout_user(user, w)):
c2userid = get_userid(r.c2token)
if not c2userid: # pragma: no cover
raise NoTokenError("User has no token")
dologging('debuglog.log',
'Upload to C2 user {userid}'.format(userid=user.id))
data = createc2workoutdata(w)
dologging('debuglog.log', json.dumps(data))
if data == 0: # pragma: no cover
return "Error: No data file. Contact info@rowsandall.com if the problem persists", 0
authorizationstring = str('Bearer ' + r.c2token)
headers = {'Authorization': authorizationstring,
'user-agent': 'sanderroosendaal',
'Content-Type': 'application/json'}
import urllib
url = "https://log.concept2.com/api/users/%s/results" % (c2userid)
if not asynchron: # pragma: no cover
response = requests.post(
url, headers=headers, data=json.dumps(data, default=default))
if (response.status_code == 409): # pragma: no cover
message = "Concept2 Duplicate error"
w.uploadedtoc2 = -1
c2id = -1
w.save()
elif (response.status_code == 201 or response.status_code == 200):
# s= json.loads(response.text)
s = response.json()
c2id = s['data']['id']
w.uploadedtoc2 = c2id
w.save()
message = "Upload to Concept2 was successful"
else: # pragma: no cover
message = "Something went wrong in workout_c2_upload_view." \
" Response code 200/201 but C2 sync failed: "+response.text
c2id = 0
else: # pragma: no cover
_ = myqueue(queue,
handle_c2_sync,
w.id,
url,
headers,
json.dumps(data, default=default))
c2id = 0
return message, c2id
# This is token refresh. Looks for tokens in our database, then refreshes
def rower_c2_token_refresh(user):
r = Rower.objects.get(user=user)
res = do_refresh_token(r.c2refreshtoken)
if res[0]:
access_token = res[0]
expires_in = res[1]
refresh_token = res[2]
expirydatetime = timezone.now()+datetime.timedelta(seconds=expires_in)
r = Rower.objects.get(user=user)
r.c2token = access_token
r.tokenexpirydate = expirydatetime
r.c2refreshtoken = refresh_token
r.save()
return r.c2token
else: # pragma: no cover
return None

View File

@@ -66,7 +66,7 @@ def splitstdata(lijst):
return [np.array(t), np.array(latlong)]
# covered in integrations
def imports_open(user, oauth_data):
r = Rower.objects.get(user=user)
token = getattr(r, oauth_data['tokenname'])
@@ -101,7 +101,7 @@ def imports_open(user, oauth_data):
return token
# covered in integrations
# Refresh token using refresh token
def imports_do_refresh_token(refreshtoken, oauth_data, access_token=''):
# client_auth = requests.auth.HTTPBasicAuth(
@@ -175,7 +175,7 @@ def imports_do_refresh_token(refreshtoken, oauth_data, access_token=''):
# Exchange ST access code for long-lived ST access token
# implemented in integrations
def imports_get_token(
code, oauth_data
):

View File

@@ -0,0 +1 @@
from .c2 import C2Integration

450
rowers/integrations/c2.py Normal file
View File

@@ -0,0 +1,450 @@
from .integrations import SyncIntegration, NoTokenError
from rowers.models import User, Rower, Workout, TombStone
from rowingdata import rowingdata
import numpy as np
import datetime
import json
import urllib
from rowers.utils import dologging, uniqify
from django.utils import timezone
import requests
from pytz.exceptions import UnknownTimeZoneError
import pytz
from rowsandall_app.settings import (
C2_CLIENT_ID, C2_REDIRECT_URI, C2_CLIENT_SECRET,
UPLOAD_SERVICE_URL, UPLOAD_SERVICE_SECRET
)
from rowers.tasks import (
handle_c2_import_stroke_data, handle_c2_sync, handle_c2_async_workout,
handle_c2_getworkout
)
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 requests import Request, Session
import rowers.mytypes as mytypes
from rowers.rower_rules import is_workout_user, ispromember
def default(o): # pragma: no cover
if isinstance(o, numpy.int64):
return int(o)
raise TypeError
# convert datetime object to seconds
def makeseconds(t):
seconds = t.hour*3600.+t.minute*60.+t.second+0.1*int(t.microsecond/1.e5)
return seconds
# convert our weight class code to Concept2 weight class code
def c2wc(weightclass):
if (weightclass == "lwt"): # pragma: no cover
res = "L"
else:
res = "H"
return res
class C2Integration(SyncIntegration):
def __init__(self, *args, **kwargs):
super(C2Integration, self).__init__(self, *args, **kwargs)
self.oauth_data = {
'client_id': C2_CLIENT_ID,
'client_secret': C2_CLIENT_SECRET,
'redirect_uri': C2_REDIRECT_URI,
'autorization_uri': "https://log.concept2.com/oauth/authorize",
'content_type': 'application/x-www-form-urlencoded',
'tokenname': 'c2token',
'refreshtokenname': 'c2refreshtoken',
'expirydatename': 'tokenexpirydate',
'bearer_auth': True,
'base_url': "https://log.concept2.com/oauth/access_token",
'scope': 'write',
}
def get_token(self, code, *args, **kwargs):
messg = ''
scope = "user:read,results:write"
post_data = {"grant_type": "authorization_code",
"code": code,
"redirect_uri": C2_REDIRECT_URI,
"client_secret": C2_CLIENT_SECRET,
"client_id": C2_CLIENT_ID,
}
headers = {'user-agent': 'sanderroosendaal'}
url = "https://log.concept2.com/oauth/access_token"
s = Session()
req = Request('POST', url, data=post_data, headers=headers)
prepped = req.prepare()
prepped.body += "&scope="
prepped.body += scope
response = s.send(prepped)
token_json = response.json()
try:
status_code = response.status_code
except AttributeError: # pragma: no cover
return (0, response.text)
if status_code == 200:
thetoken = token_json['access_token']
expires_in = token_json['expires_in']
refresh_token = token_json['refresh_token']
else: # pragma: no cover
raise NoTokenError("No Token")
return (thetoken, expires_in, refresh_token, messg)
def open(self, *args, **kwargs):
return super(C2Integration, self).open(*args, **kwargs)
def token_refresh(self, *args, **kwargs):
return super(C2Integration, self).open(*args, **kwargs)
def make_authorization_url(self, *args, **kwargs):
scope = "user:read,results:write"
params = {"client_id": C2_CLIENT_ID,
"response_type": "code",
"redirect_uri": C2_REDIRECT_URI}
url = "https://log.concept2.com/oauth/authorize?" + \
urllib.parse.urlencode(params)
url += "&scope="+scope
return url
def get_userid(self, *args, **kwargs):
_ = self.open()
access_token = self.rower.c2token
authorizationstring = str('Bearer ' + access_token)
headers = {'Authorization': authorizationstring,
'user-agent': 'sanderroosendaal',
'Content-Type': 'application/json'}
url = "https://log.concept2.com/api/users/me"
try:
response = requests.get(url, headers=headers)
except: # pragma: no cover
return 0
try:
me_json = response.json()
except: # pragma: no cover
return 0
try:
res = me_json['data']['id']
except KeyError: # pragma: no cover
return 0
return res
def createworkoutdata(self, w, *args, **kwargs) -> dict:
filename = w.csvfilename
try:
row = rowingdata(csvfile=filename)
except IOError: # pragma: no cover
return 0
try:
averagehr = int(row.df[' HRCur (bpm)'].mean())
maxhr = int(row.df[' HRCur (bpm)'].max())
except (ValueError, KeyError): # pragma: no cover
averagehr = 0
maxhr = 0
# Calculate intervalstats
itime, idist, itype = row.intervalstats_values()
lapnames = range(len(itime))
nrintervals = len(itime)
try:
lapnames = row.df[' lapIdx'].unique()
except KeyError: # pragma: no cover
pass
if len(lapnames) != nrintervals:
newlapnames = []
for name in lapnames:
newlapnames += [name, name]
lapnames = newlapnames
intervaldata = []
for i in range(nrintervals):
if itime[i] > 0:
mask = (row.df[' lapIdx'] == lapnames[i]) & (
row.df[' WorkoutState'] == itype[i])
try:
spmav = int(row.df[' Cadence (stokes/min)']
[mask].mean().astype(int))
hrav = int(row.df[' HRCur (bpm)'][mask].mean().astype(int))
except AttributeError: # pragma: no cover
try:
spmav = int(row.df[' Cadence (stokes/min)'][mask].mean())
hrav = int(row.df[' HRCur (bpm)'][mask].mean())
except ValueError:
spmav = 0
try:
hrav = int(row.df[' HRCur (bpm)'][mask].mean())
except ValuError:
hrav = 0
intervaldict = {
'type': 'distance',
'time': int(10*itime[i]),
'distance': int(idist[i]),
'heart_rate': {
'average': hrav,
},
'stroke_rate': spmav,
}
intervaldata.append(intervaldict)
# adding diff, trying to see if this is valid
t = 10*row.df.loc[:, 'TimeStamp (sec)'].values - \
10*row.df.loc[:, 'TimeStamp (sec)'].iloc[0]
try:
t[0] = t[1]
except IndexError: # pragma: no cover
pass
d = 10*row.df.loc[:, ' Horizontal (meters)'].values
try:
d[0] = d[1]
except IndexError: # pragma: no cover
pass
p = abs(10*row.df.loc[:, ' Stroke500mPace (sec/500m)'].values)
p = np.clip(p, 0, 3600)
if w.workouttype == 'bike': # pragma: no cover
p = 2.0*p
t = t.astype(int)
d = d.astype(int)
p = p.astype(int)
spm = row.df[' Cadence (stokes/min)'].astype(int)
try:
spm[0] = spm[1]
except (KeyError, IndexError): # pragma: no cover
spm = 0*t
try:
hr = row.df[' HRCur (bpm)'].astype(int)
except ValueError: # pragma: no cover
hr = 0*d
stroke_data = []
t = t.tolist()
d = d.tolist()
p = p.tolist()
spm = spm.tolist()
hr = hr.tolist()
for i in range(len(t)):
thisrecord = {"t": t[i],
"d": d[i],
"p": p[i],
"spm": spm[i],
"hr": hr[i]}
stroke_data.append(thisrecord)
try:
durationstr = datetime.datetime.strptime(
str(w.duration), "%H:%M:%S.%f")
except ValueError:
durationstr = datetime.datetime.strptime(str(w.duration), "%H:%M:%S")
workouttype = w.workouttype
if workouttype in mytypes.otwtypes:
workouttype = 'water'
if w.timezone == 'tzutc()': # pragma: no cover
w.timezone = 'UTC'
w.save()
try:
wendtime = w.startdatetime.astimezone(pytz.timezone(
w.timezone))+datetime.timedelta(seconds=makeseconds(durationstr))
except UnknownTimeZoneError:
wendtime = w.startdatetime.astimezone(pytz.utc)+datetime.timedelta(seconds=makeseconds(durationstr))
data = {
"type": mytypes.c2mapping[workouttype],
# w.startdatetime.isoformat(),
"date": wendtime.strftime('%Y-%m-%d %H:%M:%S'),
"stroke_count": int(row.stroke_count),
"timezone": w.timezone,
"distance": int(w.distance),
"time": int(10*makeseconds(durationstr)),
"weight_class": c2wc(w.weightcategory),
"comments": w.notes,
'stroke_rate': int(row.df[' Cadence (stokes/min)'].mean()),
'drag_factor': int(row.dragfactor),
"heart_rate": {
"average": averagehr,
"max": maxhr,
},
"stroke_data": stroke_data,
'workout': {
'splits': intervaldata,
}
}
return data
def workout_export(self, workout, *args, **kwargs) -> str:
try:
if mytypes.c2mapping[workout.workouttype] is None: # pragma: no cover
return "0"
except KeyError: # pragma: no cover
return "0"
_ = self.open()
r = self.rower
user = self.user
if (is_workout_user(user, workout)):
c2userid = self.get_userid()
if not c2userid: # pragma: no cover
raise NoTokenError("User has no token")
dologging('debuglog.log',
'Upload to C2 user {userid}'.format(userid=user.id))
data = self.createworkoutdata(workout)
dologging('c2_log.log', json.dumps(data))
if data == 0: # pragma: no cover
return 0
authorizationstring = str('Bearer ' + r.c2token)
headers = {'Authorization': authorizationstring,
'user-agent': 'sanderroosendaal',
'Content-Type': 'application/json'}
url = "https://log.concept2.com/api/users/%s/results" % (c2userid)
_ = myqueue(queue,
handle_c2_sync,
workout.id,
url,
headers,
json.dumps(data, default=default))
c2id = 0
return c2id
def get_workout(self, id):
_ = self.open()
_ = myqueue(queuehigh,
handle_c2_getworkout,
self.user.id,
self.rower.c2token,
id,
self.rower.defaulttimezone)
return 1
def get_workouts(self, *args, **kwargs):
page = kwargs.get('page',1)
try:
_ = self.open()
except NoTokenError:
return 0
# this part to get_workout_list
workouts = self.get_workout_list(page=page)
counter = 0
for workout in workouts:
c2id = workout['id']
if workout['new'] == 'NEW':
self.get_workout(c2id)
return 1
# should be unified to workout list
def get_workout_list(self, *args, **kwargs):
page = kwargs.get('page',1)
r = self.rower
if (r.c2token == '') or (r.c2token is None): # pragma: no cover
s = "Token doesn't exist. Need to authorize"
return custom_exception_handler(401, s)
elif (timezone.now() > r.tokenexpirydate): # pragma: no cover
s = "Token expired. Needs to refresh."
return custom_exception_handler(401, s)
# ready to fetch. Hurray
authorizationstring = str('Bearer ' + r.c2token)
headers = {'Authorization': authorizationstring,
'user-agent': 'sanderroosendaal',
'Content-Type': 'application/json'}
url = "https://log.concept2.com/api/users/me/results"
url += "?page={page}".format(page=page)
res = requests.get(url, headers=headers)
if (res.status_code != 200): # pragma: no cover
return []
workouts = []
c2ids = [item['id'] for item in res.json()['data']]
knownc2ids = uniqify([
w.uploadedtoc2 for w in Workout.objects.filter(user=self.rower)
])
tombstones = [
t.uploadedtoc2 for t in TombStone.objects.filter(user=self.rower)
]
parkedids = []
try:
with open('c2blocked.json', 'r') as c2blocked:
jsondata = json.load(c2blocked)
parkedids = jsondata['ids']
except: # pragma: no cover
pass
knownc2ids = uniqify(knownc2ids+tombstones+parkedids)
for item in res.json()['data']:
d = item['distance']
i = item['id']
ttot = item['time_formatted']
s = item['date']
r = item['type']
s2 = item['source']
c = item['comments']
if i in knownc2ids:
nnn = ''
else: # pragma: no cover
nnn = 'NEW'
keys = ['id', 'distance', 'duration', 'starttime',
'rowtype', 'source', 'comment', 'new']
values = [i, d, ttot, s, r, s2, c, nnn]
ress = dict(zip(keys, values))
workouts.append(ress)
return workouts
# just as a quick test during development
u = User.objects.get(id=1)
c2_integration_1 = C2Integration(u)

View File

@@ -0,0 +1,250 @@
from abc import ABCMeta, ABC, abstractmethod
from importlib import import_module
from rowers.models import Rower, User
from rowers.utils import NoTokenError
import requests
from django.utils import timezone
from datetime import timedelta
import arrow
import urllib
from uuid import uuid4
class SyncIntegration(metaclass=ABCMeta):
oauth_data = {
'tokenname':'token',
'expirydatename':'exp',
'refreshtokenname':'r',
'redirect_uri': 'r',
'client_secret': 's',
'base_uri': 's'
}
user = User()
rower = Rower()
def __init__(self, *args, **kwargs):
user = args[1]
self.user = user
self.rower = user.rower
@classmethod
def __subclasshook__(cls, subclass):
return (hasattr(subclass, 'get_token') and
callable(subclass.get_token) or
NotImplemented)
@abstractmethod
def createworkoutdata(w, *args, **kwargs) -> dict:
return {}
@abstractmethod
def workout_export(workout, *args, **kwargs) -> str:
pass
@abstractmethod
def get_workouts(*args, **kwargs) -> int:
pass
@abstractmethod
def get_workout(id) -> int:
pass
# need to unify workout list
@abstractmethod
def get_workout_list(*args, **kwargs) -> list:
pass
@abstractmethod
def make_authorization_url(self, *args, **kwargs) -> str: # 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": self.oauth_data['client_id'],
"response_type": "code",
"redirect_uri": self.oauth_data['redirect_uri'],
"scope": self.oauth_data['scope'],
"state": state}
url = oauth_data['authorizaton_uri']+urllib.parse.urlencode(params)
return url
@abstractmethod
def get_token(self, code, *args, **kwargs) -> (str, int, str):
redirect_uri = self.oauth_data['redirect_uri']
client_secret = self.oauth_data['client_secret']
client_id = self.oauth_data['client_id']
base_uri = self.oauth_data['base_url']
post_data = {"grant_type": "authorization_code",
"code": code,
"redirect_uri": redirect_uri,
"client_secret": client_secret,
"client_id": client_id,
}
try:
headers = self.oauth_data['headers']
except KeyError:
headers = {'Accept': 'application/json',
'Api-Key': client_id,
'Content-Type': 'application/json',
'user-agent': 'sanderroosendaal'}
if 'grant_type' in self.oauth_data:
if self.oauth_data['grant_type']:
post_data['grant_type'] = self.oauth_data['grant_type']
if 'strava' in self.oauth_data['autorization_uri']:
post_data['grant_type'] = "authorization_code"
if 'json' in self.oauth_data['content_type']:
response = requests.post(
base_uri,
data=json.dumps(post_data),
headers=headers)
else: # pragma: no cover
response = requests.post(
base_uri,
data=post_data,
headers=headers, verify=False)
if response.status_code == 200 or response.status_code == 201:
token_json = response.json()
try:
thetoken = token_json['access_token']
except KeyError: # pragma: no cover
raise NoTokenError("Failed to obtain token")
try:
refresh_token = token_json['refresh_token']
except KeyError: # pragma: no cover
refresh_token = ''
try:
expires_in = token_json['expires_in']
except KeyError: # pragma: no cover
expires_in = 0
try:
expires_in = int(expires_in)
except (ValueError, TypeError): # pragma: no cover
expires_in = 0
else: # pragma: no cover
raise NoTokenError("Failed to obtain token")
return [thetoken, expires_in, refresh_token]
@abstractmethod
def open(self, *args, **kwargs) -> str:
token = getattr(self.rower, self.oauth_data['tokenname'])
try:
tokenexpirydate = getattr(self.rower, self.oauth_data['expirydatename'])
except (TypeError, AttributeError, KeyError): # pragma: no cover
tokenexpirydate = None
if (token == '') or (token is None):
raise NoTokenError("User has no token")
else:
tokenname = self.oauth_data['tokenname']
refreshtokenname = self.oauth_data['refreshtokenname']
expirydatename = self.oauth_data['expirydatename']
if tokenexpirydate and timezone.now()+timedelta(seconds=60) > tokenexpirydate:
token = self.token_refresh()
elif tokenexpirydate is None and expirydatename is not None and 'strava' in expirydatename: # pragma: no cover
token = self.token_refresh()
return token
def do_refresh_token(self, *args, **kwargs) -> (str, int, str):
post_data = {"grant_type": "refresh_token",
"client_secret": self.oauth_data['client_secret'],
"client_id": self.oauth_data['client_id'],
"refresh_token": refreshtoken,
}
headers = {'user-agent': 'sanderroosendaal',
'Accept': 'application/json',
'Content-Type': self.oauth_data['content_type']}
# for Strava
if 'grant_type' in self.oauth_data:
if self.oauth_data['grant_type']:
post_data['grant_type'] = self.oauth_data['grant_type']
if self.oauth_data['bearer_auth']:
headers['authorization'] = 'Bearer %s' % access_token
baseurl = self.oauth_data['base_url']
if 'json' in self.oauth_data['content_type']:
try:
response = requests.post(baseurl,
data=json.dumps(post_data),
headers=headers, verify=False)
except: # pragma: no cover
raise NoTokenError("Failed to get token")
else:
try:
response = requests.post(baseurl,
data=post_data,
headers=headers, verify=False,
)
except: # pragma: no cover
raise NoTokenError("Failed to get token")
if response.status_code == 200 or response.status_code == 201:
token_json = response.json()
else: # pragma: no cover
raise NoTokenError("User has no token")
try:
thetoken = token_json['access_token']
except KeyError: # pragma: no cover
raise NoTokenError("User has no token")
try:
expires_in = token_json['expires_in']
except KeyError:
try:
expires_at = arrow.get(token_json['expires_at']).timestamp()
expires_in = expires_at - arrow.now().timestamp()
except KeyError: # pragma: no cover
expires_in = 0
try:
refresh_token = token_json['refresh_token']
except KeyError: # pragma: no cover
refresh_token = refreshtoken
try:
expires_in = int(expires_in)
except (TypeError, ValueError): # pragma: no cover
expires_in = 0
return [thetoken, expires_in, refresh_token]
@abstractmethod
def token_refresh(self, *args, **kwargs) -> str:
refreshtoken = getattr(self.rower, oauth['refreshtokenname'])
if not refreshtoken:
refreshtoken = getattr(self.rower, oauth['tokenname'])
access_token, expires_in, refresh_token = self.do_refresh_token()
expirydatetime = timezone.now()+timedelta(seconds=expires_in)
setattr(self.rower, tokenname, access_token)
if expirydatename is not None:
setattr(self.rower, expirydatename, expirydatetime)
if refreshtokenname is not None:
setattr(self.rower, refreshtokenname, refresh_token)
self.rower.save()
return access_token

View File

@@ -28,12 +28,12 @@ from rowingdata import rowingdata as rrdata
import rowers.uploads as uploads
import rowers.polarstuff as polarstuff
import rowers.c2stuff as c2stuff
import rowers.rp3stuff as rp3stuff
import rowers.stravastuff as stravastuff
import rowers.nkstuff as nkstuff
from rowers.opaque import encoder
from rowers.integrations import *
from rowers.rower_rules import user_is_not_basic, user_is_coachee
from rowers.utils import dologging
@@ -91,7 +91,9 @@ class Command(BaseCommand):
rowers = Rower.objects.filter(c2_auto_import=True)
for r in rowers: # pragma: no cover
if user_is_not_basic(r.user) or user_is_coachee(r.user):
c2stuff.get_c2_workouts(r)
c2integration = C2Integration(r.user)
_ = c2integration.get_workouts()
except: # pragma: no cover
exc_type, exc_value, exc_traceback = sys.exc_info()
lines = traceback.format_exception(exc_type, exc_value, exc_traceback)

View File

@@ -15,7 +15,6 @@ import math
from math import sin, cos, atan2, sqrt
import urllib
import rowers.c2stuff as c2stuff
# Django
from django.http import HttpResponseRedirect, HttpResponse, JsonResponse

View File

@@ -0,0 +1,84 @@
{% extends "newbase.html" %}
{% load static %}
{% load rowerfilters %}
{% block title %}Workouts{% endblock %}
{% block main %}
<h1>Available on {{ integration }}</h1>
<ul class="main-content">
{% if workouts %}
<li class="grid_2">
<a href="/rowers/workout/c2import/all/{{ page }}">Import all NEW</a>
<p>This imports all workouts that have not been imported to rowsandall.com.
The action may take a longer time to process, so please be patient. Click on Import in the list below to import an individual workout.
</p>
</li>
<li class="grid_2">
<p>
<span>
{% if page > 1 %}
<a title="Previous" href="/rowers/workout/c2list/{{ page|add:-1 }}">
<i class="fas fa-arrow-alt-left"></i>
</a>
{% endif %}
<a title="Next" href="/rowers/workout/c2list/{{ page|add:1 }}">
<i class="fas fa-arrow-alt-right"></i>
</a>
</span>
</p>
</li>
<li class="grid_4">
<form enctype="multipart/form-data" method="post">
{% csrf_token %}
<input name='workouts' type="submit" value="Import selected workouts">
<a href="/rowers/workout/c2list/?selectallnew=true">Select All New</a>
<table width="70%" class="listtable">
<thead>
<tr>
<th> Import </th>
<th> Date/Time </th>
<th> Duration </th>
<th> Total Distance</th>
<th> Type</th>
<th> Source</th>
<th> Comment</th>
<th> New</th>
</tr>
</thead>
<tbody>
{% for workout in workouts %}
<tr>
<td>
{% if workout|lookup:'new' == 'NEW' and checknew == 'true' %}
<input checked type="checkbox" value={{ workout|lookup:'id' }} name="workoutid">
{% else %}
<input type="checkbox" value={{ workout|lookup:'id' }} name="workoutid">
{% endif %}
</td>
<td>{{ workout|lookup:'starttime' }}</td>
<td>{{ workout|lookup:'duration' }}</td>
<td>{{ workout|lookup:'distance' }}</td>
<td>{{ workout|lookup:'rowtype' }}</td>
<td>{{ workout|lookup:'source' }}</td>
<td>{{ workout|lookup:'comment' }}</td>
<td>
{{ workout|lookup:'new' }}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</form>
</li>
{% else %}
<p> No workouts found </p>
{% endif %}
</ul>
{% endblock %}
{% block sidebar %}
{% include 'menu_workouts.html' %}
{% endblock %}

View File

@@ -24,8 +24,7 @@ from rowers.models import PlannedSession
from rowers.teams import coach_getcoachees
from rowers import credits
from rowers import c2stuff
from rowers.c2stuff import c2_open
from rowers.integrations import C2Integration
from rowers.rower_rules import *
from rowers.mytypes import (
otwtypes, adaptivetypes, sexcategories, weightcategories, workouttypes,
@@ -521,12 +520,9 @@ def deltatimeprint(d): # pragma: no cover
@register.filter
def c2userid(user): # pragma: no cover
try:
thetoken = c2_open(user)
except NoTokenError: # pragma: no cover
return 0
c2integration = C2Integration(user)
c2userid = c2stuff.get_userid(thetoken)
c2userid = c2integration.get_userid(thetoken)
return c2userid

View File

@@ -18,7 +18,6 @@ from django.test import TestCase, Client,override_settings
from django.core.management import call_command
from django.test.client import RequestFactory
from rowers.views import c2_open
from rowers.models import Workout, User, Rower, WorkoutForm,RowerForm,GraphImage
from rowers.forms import DocumentsForm,CNsummaryForm,RegistrationFormUniqueEmail
import rowers.plots as plots
@@ -39,7 +38,7 @@ from nose.tools import assert_true
from mock import Mock, patch
#from minimocktest import MockTestCase
import pandas as pd
import rowers.c2stuff as c2stuff
import arrow
from django.http import HttpResponseRedirect

View File

@@ -37,7 +37,6 @@ from django.core.files.uploadedfile import SimpleUploadedFile
from io import StringIO
from django.test.client import RequestFactory
from rowers.views import c2_open
from rowers.forms import (

View File

@@ -14,7 +14,7 @@ import numpy as np
import rowers
from rowers import dataprep
from rowers import tasks
from rowers import c2stuff
from rowers import stravastuff
from rowers import polarstuff
import urllib
@@ -22,7 +22,7 @@ import json
import rowers.utils as utils
import rowers.rojabo_stuff as rojabo_stuff
from rowers.integrations import *
from django.db import transaction
import rowers.garmin_stuff as gs
@@ -312,7 +312,9 @@ class C2Objects(DjangoTestCase):
)
def test_timezone_c2(self):
data = c2stuff.createc2workoutdata(self.w)
c2integration = C2Integration(self.u)
data = c2integration.createworkoutdata(self.w)
wenddtime = self.w.startdatetime+datetime.timedelta(seconds=self.totaltime)
t1 = arrow.get(wenddtime).timestamp()
t2 = arrow.get(data['date']).timestamp()
@@ -328,35 +330,34 @@ class C2Objects(DjangoTestCase):
#self.assertEqual(data['date'],wenddtime.strftime('%Y-%m-%d %H:%M:%S'))
@patch('rowers.c2stuff.Session', side_effect=mocked_requests)
@patch('rowers.integrations.c2.Session', side_effect=mocked_requests)
def test_c2_callback(self, mock_Session):
response = self.c.get('/call_back?code=dsdoij232s',follow=True)
self.assertEqual(response.status_code, 200)
@patch('rowers.c2stuff.Session', side_effect=mocked_requests)
@patch('rowers.integrations.c2.Session', side_effect=mocked_requests)
def test_c2_token_refresh(self, mock_Session):
response = self.c.get('/rowers/me/c2refresh/',follow=True)
self.assertEqual(response.status_code, 200)
@patch('rowers.c2stuff.requests.post', side_effect=mocked_requests)
@patch('rowers.c2stuff.requests.get', side_effect=mocked_requests)
@patch('rowers.c2stuff.requests.session', side_effect=mocked_requests)
@patch('rowers.integrations.c2.requests.post', side_effect=mocked_requests)
@patch('rowers.integrations.c2.requests.get', side_effect=mocked_requests)
@patch('rowers.integrations.c2.requests.session', side_effect=mocked_requests)
def test_c2_auto_import(self, mock_get, mock_post,MockSession):
self.r.sporttracks_auto_export = True
self.r.save()
res = c2stuff.get_c2_workouts(self.r)
self.assertEqual(res,1)
c2integration = C2Integration(self.u)
res = c2integration.get_workouts()
res = c2stuff.get_c2_workouts(self.r,do_async=False)
self.assertEqual(res,1)
@patch('rowers.c2stuff.requests.post', side_effect=mocked_requests)
@patch('rowers.c2stuff.requests.get', side_effect=mocked_requests)
@patch('rowers.integrations.c2.requests.post', side_effect=mocked_requests)
@patch('rowers.integrations.c2.requests.get', side_effect=mocked_requests)
def test_c2_upload(self, mock_get, mock_post):
response = self.c.get('/rowers/workout/'+encoded1+'/c2uploadw/')
@@ -367,14 +368,14 @@ class C2Objects(DjangoTestCase):
self.assertEqual(response.url, '/rowers/workout/'+encoded1+'/edit/')
self.assertEqual(response.status_code, 302)
@patch('rowers.c2stuff.requests.post', side_effect=mocked_requests)
@patch('rowers.c2stuff.requests.get', side_effect=mocked_requests)
@patch('rowers.integrations.c2.requests.post', side_effect=mocked_requests)
@patch('rowers.integrations.c2.requests.get', side_effect=mocked_requests)
def test_c2_list(self, mock_get, mock_post):
response = self.c.get('/rowers/workout/c2list',follow=True)
self.assertEqual(response.status_code,200)
@patch('rowers.c2stuff.requests.get', side_effect=mocked_requests)
@patch('rowers.integrations.c2.requests.get', side_effect=mocked_requests)
@patch('rowers.dataprep.create_engine')
def test_c2_import(self, mock_get, mocked_sqlalchemy):
@@ -506,7 +507,7 @@ class C2Objects(DjangoTestCase):
self.assertEqual(got, want)
@patch('rowers.c2stuff.requests.get', side_effect=mocked_requests)
@patch('rowers.integrations.c2.requests.get', side_effect=mocked_requests)
@patch('rowers.dataprep.create_engine')
@patch('rowers.tasks.requests.session', side_effect=mocked_session)
def test_c2_import_tz(self, mock_get, mocked_sqlalchemy, mock_session):
@@ -526,7 +527,7 @@ class C2Objects(DjangoTestCase):
self.assertEqual(timezone,'Europe/Prague')
@patch('rowers.c2stuff.requests.get', side_effect=mocked_requests)
@patch('rowers.integrations.c2.requests.get', side_effect=mocked_requests)
@patch('rowers.dataprep.create_engine')
def test_c2_import_tz3(self, mock_get, mocked_sqlalchemy):
@@ -547,7 +548,7 @@ class C2Objects(DjangoTestCase):
@patch('rowers.tasks.requests.get', side_effect=mocked_requests)
@patch('rowers.dataprep.create_engine')
@patch('rowers.c2stuff.requests.session', side_effect=mocked_requests)
@patch('rowers.integrations.c2.requests.session', side_effect=mocked_requests)
def test_c2_import_tz2(self, mock_get, mocked_sqlalchemy, MockSession):
response = self.c.get('/rowers/workout/c2import/31/',follow=True)
@@ -643,15 +644,15 @@ class C2ObjectsTokenExpired(DjangoTestCase):
@patch('rowers.c2stuff.requests.post', side_effect=mocked_requests)
@patch('rowers.c2stuff.requests.get', side_effect=mocked_requests)
@patch('rowers.c2stuff.Session',side_effect=mocked_requests)
@patch('rowers.integrations.c2.requests.post', side_effect=mocked_requests)
@patch('rowers.integrations.c2.requests.get', side_effect=mocked_requests)
@patch('rowers.integrations.c2.Session',side_effect=mocked_requests)
def test_c2_list(self, mock_get, mock_post, mock_Session):
response = self.c.get('/rowers/workout/c2list',follow=True)
self.assertEqual(response.status_code,200)
@patch('rowers.c2stuff.requests.get', side_effect=mocked_requests)
@patch('rowers.integrations.c2.requests.get', side_effect=mocked_requests)
@patch('rowers.dataprep.create_engine')
def test_c2_import(self, mock_get, mocked_sqlalchemy):

View File

@@ -214,8 +214,8 @@ class PermissionsViewTests(TestCase):
# Test access for anonymous users
@parameterized.expand(viewstotest)
@patch('rowers.c2stuff.Session', side_effect=mocked_requests)
@patch('rowers.c2stuff.c2_open')
@patch('rowers.integrations.c2.Session', side_effect=mocked_requests)
@patch('rowers.integrations.c2.C2Integration.open')
@patch('rowers.dataprep.create_engine')
@patch('rowers.dataprep.read_df_sql')
@patch('rowers.dataprep.getsmallrowdata_db')
@@ -249,8 +249,8 @@ class PermissionsViewTests(TestCase):
# Test access for logged in users - accessing own objects
@parameterized.expand(viewstotest)
@patch('rowers.c2stuff.Session', side_effect=mocked_requests)
@patch('rowers.c2stuff.c2_open')
@patch('rowers.integrations.c2.Session', side_effect=mocked_requests)
@patch('rowers.integrations.c2.C2Integration.open')
@patch('rowers.dataprep.create_engine')
@patch('rowers.dataprep.read_df_sql')
@patch('rowers.dataprep.getsmallrowdata_db')
@@ -331,8 +331,8 @@ class PermissionsViewTests(TestCase):
# Test access for logged in users - accessing team member objects
@parameterized.expand(viewstotest)
@patch('rowers.c2stuff.Session', side_effect=mocked_requests)
@patch('rowers.c2stuff.c2_open')
@patch('rowers.integrations.c2.Session', side_effect=mocked_requests)
@patch('rowers.integrations.c2.C2Integration.open')
@patch('rowers.dataprep.create_engine')
@patch('rowers.dataprep.read_df_sql')
@patch('rowers.dataprep.getsmallrowdata_db')
@@ -414,8 +414,8 @@ class PermissionsViewTests(TestCase):
# Test access for logged in users - accessing coachee
@parameterized.expand(viewstotest)
@patch('rowers.c2stuff.Session', side_effect=mocked_requests)
@patch('rowers.c2stuff.c2_open')
@patch('rowers.integrations.c2.Session', side_effect=mocked_requests)
@patch('rowers.integrations.c2.C2Integration.open')
@patch('rowers.dataprep.create_engine')
@patch('rowers.dataprep.read_df_sql')
@patch('rowers.dataprep.getsmallrowdata_db')

View File

@@ -17,12 +17,13 @@ import pytz
# interactive plots
from rowers import interactiveplots
from rowers import dataprep
from rowers import tasks
from rowers import plannedsessions
from rowers.views.workoutviews import get_video_id
from rowers import stravastuff
import rowingdata
from rowers.c2stuff import getagegrouprecord
class OtherUnitTests(TestCase):
def setUp(self):
@@ -120,7 +121,7 @@ class OtherUnitTests(TestCase):
s = f.read()
data = json.loads(s)
splitdata = data['workout']['intervals']
summary = c2stuff.summaryfromsplitdata(splitdata,data,'aap.txt')
summary = tasks.summaryfromsplitdata(splitdata,data,'aap.txt')
self.assertEqual(len(summary),3)
sums = summary[0]
@@ -592,7 +593,7 @@ class DataPrepTests(TestCase):
def test_getagegrouprecord(self):
records = C2WorldClassAgePerformance.objects.filter(distance=2000,sex=self.r.sex,weightcategory=self.r.weightcategory)
result = c2stuff.getagegrouprecord(25)
result = getagegrouprecord(25)
self.assertEqual(int(result),590)
@patch('rowers.dataprep.getsmallrowdata_db',side_effect=mocked_getsmallrowdata_uh)

View File

@@ -1,9 +1,10 @@
from rowers.mytypes import workouttypes, boattypes, otwtypes, workoutsources, workouttypes_ordered
import rowers.c2stuff as c2stuff
from rowers.rower_rules import is_promember
import rowers.tpstuff as tpstuff
import rowers.sporttracksstuff as sporttracksstuff
import rowers.stravastuff as stravastuff
from rowers.integrations import *
from rowers.utils import (
geo_distance, serialize_list, deserialize_list, uniqify,
str2bool, range_to_color_hex, absolute, myqueue, NoTokenError
@@ -199,9 +200,9 @@ def do_sync(w, options, quick=False):
if do_c2_export: # pragma: no cover
dologging('c2_log.log','Exporting workout to C2 for user {user}'.format(user=w.user.user.id))
c2_integration = C2Integration(w.user.user)
try:
message, id = c2stuff.workout_c2_upload(
w.user.user, w, asynchron=True)
id = c2_integration.workout_export(w)
dologging('c2_log.log','C2 upload succeeded')
except NoTokenError:
id = 0

View File

@@ -1169,8 +1169,3 @@ urlpatterns = [
name="braintree_webhook_view"),
]
if settings.DEBUG: # pragma: no cover
urlpatterns += [
re_path(r'^c2listug/(?P<page>\d+)/$', views.c2listdebug_view),
re_path(r'^c2listug/$', views.c2listdebug_view),
]

View File

@@ -7,6 +7,8 @@ from rowers.views.statements import *
from rowers.plannedsessions import get_dates_timeperiod
from rowers.tasks import fetch_strava_workout
from rowers.integrations.c2 import C2Integration
import numpy
@@ -109,9 +111,10 @@ def workout_c2_upload_view(request, id=0):
)
dologging('c2_log.log', s)
c2_integration = C2Integration(request.user)
try:
message, c2id = c2stuff.workout_c2_upload(
request.user, w, asynchron=True)
c2id = c2_integration.workout_export(w)
except NoTokenError: # pragma: no cover
return HttpResponseRedirect("/rowers/me/c2authorize/")
@@ -187,17 +190,8 @@ def rower_nk_authorize(request): # pragma: no cover
# Concept2 authorization
@login_required()
def rower_c2_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())
scope = "user:read,results:write"
params = {"client_id": C2_CLIENT_ID,
"response_type": "code",
"redirect_uri": C2_REDIRECT_URI}
url = "http://log.concept2.com/oauth/authorize?" + \
urllib.parse.urlencode(params)
url += "&scope="+scope
c2_integration = C2Integration(request.user)
url = c2_integration.make_authorization_url()
return HttpResponseRedirect(url)
@@ -312,26 +306,12 @@ def rower_tp_authorize(request): # pragma: no cover
# Concept2 token refresh. URL for manual refresh. Not visible to users
@login_required()
def rower_c2_token_refresh(request):
r = getrower(request.user)
res = c2stuff.do_refresh_token(r.c2refreshtoken)
if res[0] is not None:
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.c2token = access_token
r.tokenexpirydate = expirydatetime
r.c2refreshtoken = refresh_token
r.save()
successmessage = "Tokens refreshed. Good to go"
messages.info(request, successmessage)
else: # pragma: no cover
message = "Something went wrong (refreshing tokens). Please reauthorize:"
messages.error(request, message)
c2_integration = C2Integration(request.user)
try:
token = c2_integration.token_refresh()
messages.info(request, "Tokens refreshed. Good to go")
except NoTokenError:
messages.error(request, "Something went wrong refreshing C2 tokens. Please reauthorize")
url = reverse('workouts_view')
@@ -395,10 +375,11 @@ def rower_sporttracks_token_refresh(request):
# Concept2 Callback
@login_required()
def rower_process_callback(request):
c2_integration = C2Integration(request.user)
try:
code = request.GET['code']
res = c2stuff.get_token(code)
except MultiValueDictKeyError: # pragma: no cover
res = c2_integration.get_token(code)
except (MultiValueDictKeyError, NoTokenError): # pragma: no cover
message = "The resource owner or authorization server denied the request"
messages.error(request, message)
@@ -1677,61 +1658,18 @@ def workout_sporttracksimport_view(request, message="", userid=0):
# List of workouts on Concept2 logbook. This view only used for debugging
@login_required()
def c2listdebug_view(request, page=1, message=""): # pragma: no cover
try:
_ = c2_open(request.user)
except NoTokenError: # pragma: no cover
return HttpResponseRedirect("/rowers/me/c2authorize/")
r = getrower(request.user)
res = c2stuff.get_c2_workout_list(request.user, page=page)
if (res.status_code != 200):
message = "Something went wrong in workout_c2import_view (C2 token renewal)"
messages.error(request, message)
if settings.DEBUG:
return HttpResponse(res)
else:
url = reverse('workouts_view')
return HttpResponseRedirect(url)
else:
workouts = []
for item in res.json()['data']:
d = item['distance']
i = item['id']
ttot = item['time_formatted']
s = item['date']
r = item['type']
s2 = item['source']
c = item['comments']
keys = ['id', 'distance', 'duration',
'starttime', 'rowtype', 'source', 'comment']
values = [i, d, ttot, s, r, s2, c]
res = dict(zip(keys, values))
workouts.append(res)
return render(request,
'c2_list_import2.html',
{'workouts': workouts,
'teams': get_my_teams(request.user),
})
# Import all unknown workouts available on Concept2 logbook
@login_required()
def workout_getc2workout_all(request, page=1, message=""): # pragma: no cover
r = getrequestrower(request)
c2_integration = C2Integration(request.user)
try:
_ = c2_open(request.user)
_ = c2_integration.open()
except NoTokenError: # pragma: no cover
return HttpResponseRedirect("/rowers/me/c2authorize/")
r = getrequestrower(request)
result = c2stuff.get_c2_workouts(r, page=page, do_async=True)
result = c2_integration.get_workouts(page=page)
if result:
messages.info(
@@ -1777,76 +1715,23 @@ def workout_c2import_view(request, page=1, userid=0, message=""):
'userid': request.user.id})
return HttpResponseRedirect(url)
c2_integration = C2Integration(request.user)
try:
_ = c2_open(request.user)
_ = c2_integration.open()
except NoTokenError: # pragma: no cover
return HttpResponseRedirect("/rowers/me/c2authorize/")
res = c2stuff.get_c2_workout_list(request.user, page=page)
workouts = c2_integration.get_workout_list(page=1)
if (res.status_code != 200): # pragma: no cover
message = "Something went wrong in workout_c2import_view (C2 token refresh)"
messages.error(request, message)
url = reverse('workouts_view')
return HttpResponseRedirect(url)
workouts = []
c2ids = [item['id'] for item in res.json()['data']]
knownc2ids = uniqify([
w.uploadedtoc2 for w in Workout.objects.filter(user=rower)
])
tombstones = [
t.uploadedtoc2 for t in TombStone.objects.filter(user=rower)
]
parkedids = []
try:
with open('c2blocked.json', 'r') as c2blocked:
jsondata = json.load(c2blocked)
parkedids = jsondata['ids']
except: # pragma: no cover
pass
knownc2ids = uniqify(knownc2ids+tombstones+parkedids)
for item in res.json()['data']:
d = item['distance']
i = item['id']
ttot = item['time_formatted']
s = item['date']
r = item['type']
s2 = item['source']
c = item['comments']
if i in knownc2ids:
nnn = ''
else: # pragma: no cover
nnn = 'NEW'
keys = ['id', 'distance', 'duration', 'starttime',
'rowtype', 'source', 'comment', 'new']
values = [i, d, ttot, s, r, s2, c, nnn]
ress = dict(zip(keys, values))
workouts.append(ress)
if request.method == "POST":
try: # pragma: no cover
tdict = dict(request.POST.lists())
ids = tdict['workoutid']
c2ids = [int(id) for id in ids]
alldata = {}
for item in res.json()['data']:
alldata[item['id']] = item
counter = 0
for c2id in c2ids:
_ = myqueue(
queue,
handle_c2_async_workout,
alldata,
rower.user.id,
rower.c2token,
c2id,
counter,
rower.defaulttimezone
)
counter = counter+1
c2_integration.get_workout(c2id)
# done, redirect to workouts list
messages.info(
request,
@@ -1877,7 +1762,7 @@ def workout_c2import_view(request, page=1, userid=0, message=""):
checknew = request.GET.get('selectallnew', False)
return render(request,
'c2_list_import2.html',
'list_import.html',
{'workouts': workouts,
'rower': rower,
'active': 'nav-workouts',
@@ -1885,6 +1770,7 @@ def workout_c2import_view(request, page=1, userid=0, message=""):
'teams': get_my_teams(request.user),
'page': page,
'checknew': checknew,
'integration': 'C2 Logbook'
})
@@ -1909,7 +1795,7 @@ importauthorizeviews = {
}
importsources = {
'c2': c2stuff,
'c2': C2Integration,
'strava': stravastuff,
'polar': polarstuff,
'ownapi': ownapistuff,
@@ -1953,19 +1839,18 @@ def workout_getimportview(request, externalid, source='c2', do_async=True):
startdate = request.session.get('startdate')
enddate = request.session.get('enddate')
integration = importsources[source](request.user)
try:
result = importsources[source].get_workout(request.user, externalid, do_async=do_async,
startdate=startdate, enddate=enddate)
result = integration.get_workout(externalid, startdate=startdate, enddate=enddate)
except NoTokenError:
return HttpResponseRedirect(reverse(importauthorizeviews[source]))
url = reverse(importlistviews[source])
return HttpResponseRedirect(url)
try:
result = importsources[source].get_workout(request.user, externalid,
do_async=do_async)
result = integration.get_workout(externalid)
except NoTokenError:
return HttpResponseRedirect(reverse(importauthorizeviews[source]))
if result: # pragma: no cover

View File

@@ -1,5 +1,6 @@
import rowers.teams as teams
from rowers.serializers import RowerSerializer, WorkoutSerializer
from rowers.integrations import integrations
from rq import Queue, cancel_job
from redis import StrictRedis, Redis
from rowers.models import C2WorldClassAgePerformance
@@ -191,10 +192,8 @@ import os
import sys
import datetime
import iso8601
import rowers.c2stuff as c2stuff
import rowers.nkstuff as nkstuff
import rowers.rojabo_stuff as rojabo_stuff
from rowers.c2stuff import c2_open
from rowers.nkstuff import nk_open
from rowers.rp3stuff import rp3_open
from rowers.sporttracksstuff import sporttracks_open
@@ -208,6 +207,7 @@ from rowers.rojabo_stuff import rojabo_open
import rowers.polarstuff as polarstuff
import rowers.sporttracksstuff as sporttracksstuff
from rowers.integrations import *
import rowers.tpstuff as tpstuff
import rowers.rp3stuff as rp3stuff

View File

@@ -5630,7 +5630,8 @@ def workout_upload_view(request,
# upload to C2
if (upload_to_c2): # pragma: no cover
try:
message, id = c2stuff.workout_c2_upload(request.user, w)
c2integration = C2Integration(request.user)
id = c2integration.workout_export(w)
except NoTokenError:
id = 0
message = "Something went wrong with the Concept2 sync"