Private
Public Access
1
0
Files
rowsandall/rowers/c2stuff.py
2017-11-14 21:49:53 -06:00

631 lines
18 KiB
Python

# The interactions with the Concept2 logbook API
# All C2 related functions should be defined here
# (There is still some stuff defined directly in views.py. Need to
# move that here.)
# Python
import oauth2 as oauth
import cgi
import requests
import requests.auth
import json
from django.utils import timezone
from datetime import datetime
from datetime import timedelta
import time
# Django
from django.shortcuts import render_to_response
from django.http import HttpResponseRedirect, HttpResponse,JsonResponse
from django.conf import settings
from django.contrib.auth import authenticate, login, logout
from django.contrib.auth.models import User
from django.contrib.auth.decorators import login_required
from rowingdata import rowingdata
import pandas as pd
import numpy as np
from rowers.models import Rower,Workout
from rowers.models import checkworkoutuser
import sys
import urllib
from requests import Request, Session
from rowsandall_app.settings import C2_CLIENT_ID, C2_REDIRECT_URI, C2_CLIENT_SECRET
# Custom error class - to raise a NoTokenError
class C2NoTokenError(Exception):
def __init__(self,value):
self.value=value
def __str__(self):
return repr(self.value)
# Custom exception handler, returns a 401 HTTP message
# with exception details in the json data
def custom_exception_handler(exc,message):
response = {
"errors": [
{
"code": str(exc),
"detail": message,
}
]
}
res = HttpResponse(message)
res.status_code = 401
res.json = json.dumps(response)
return res
# 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):
s = "Token doesn't exist. Need to authorize"
raise C2NoTokenError("User has no token")
else:
if (timezone.now()>r.tokenexpirydate):
res = rower_c2_token_refresh(user)
if res == None:
raise C2NoTokenError("User has no token")
if res[0] != None:
thetoken = res[0]
else:
raise C2NoTokenError("User has no token")
else:
thetoken = r.c2token
return thetoken
# 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"):
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='|'):
totaldist = data['distance']
totaltime = data['time']/10.
spm = data['stroke_rate']
try:
resttime = data['rest_time']/10.
except KeyError:
resttime = 0
try:
restdistance = data['rest_distance']
except keyError:
restdistance = 0
try:
avghr = data['heart_rate']['average']
except KeyError:
avghr = 0
try:
maxhr = data['heart_rate']['max']
except KeyError:
maxhr = 0
avgpace = 500.*totaltime/totaldist
restpace = 500.*resttime/restdistance
velo = totaldist/totaltime
avgpower = 2.8*velo**(3.0)
restvelo = restdistance/resttime
restpower = 2.8*restvelo**(3.0)
avgdps = totaldist/data['stroke_count']
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:
timebased = False
for interval in splitdata:
idist = interval['distance']
itime = interval['time']/10.
ipace = 500.*itime/idist
ispm = interval['stroke_rate']
try:
irest_time = interval['rest_time']/10.
except KeyError:
irest_time = 0
try:
iavghr = interval['heart_rate']['average']
except KeyError:
iavghr = 0
try:
imaxhr = interval['heart_rate']['average']
except KeyError:
imaxhr = 0
# create interval values
iarr = [idist,'meters','work']
resarr = [itime]
if timebased:
iarr = [itime,'seconds','work']
resarr = [idist]
if irest_time > 0:
iarr += [irest_time,'seconds','rest']
try:
resarr += [interval['rest_distance']]
except KeyError:
resarr += [np.nan]
sa += iarr
results += resarr
ivelo = idist/itime
ipower = 2.8*ivelo**(3.0)
sums += interval_string(intervalnr,idist,itime,ipace,ispm,
iavghr,imaxhr,0,ipower,separator=sep)
intervalnr+=1
return sums,sa,results
# Not used now. Could be used to add workout split data to Concept2
# logbook but needs to be reviewed.
def createc2workoutdata_as_splits(w):
filename = w.csvfilename
row = rowingdata(filename)
# resize per minute
df = row.df.groupby(lambda x:x/60).mean()
averagehr = int(df[' HRCur (bpm)'].mean())
maxhr = int(df[' HRCur (bpm)'].max())
# adding diff, trying to see if this is valid
t = 10*df.ix[:,' ElapsedTime (sec)'].diff().values
t[0] = t[1]
d = df.ix[:,' Horizontal (meters)'].diff().values
d[0] = d[1]
p = 10*df.ix[:,' Stroke500mPace (sec/500m)'].values
t = t.astype(int)
d = d.astype(int)
p = p.astype(int)
spm = df[' Cadence (stokes/min)'].astype(int)
spm[0] = spm[1]
hr = df[' HRCur (bpm)'].astype(int)
split_data = []
for i in range(len(t)):
thisrecord = {"time":t[i],"distance":d[i],"stroke_rate":spm[i],
"heart_rate":{
"average:":hr[i]
}
}
split_data.append(thisrecord)
try:
durationstr = datetime.strptime(str(w.duration),"%H:%M:%S.%f")
except ValueError:
durationstr = datetime.strptime(str(w.duration),"%H:%M:%S")
try:
newnotes = w.notes+'\n from '+w.workoutsource+' via rowsandall.com'
except TypeError:
newnotes = 'from '+w.workoutsource+' via rowsandall.com'
wtype = w.workouttype
if wtype in ('other','coastal'):
wtype = 'water'
data = {
"type": wtype,
"date": w.startdatetime.isoformat(),
"distance": int(w.distance),
"time": int(10*makeseconds(durationstr)),
"timezone": w.timezone,
"weight_class": c2wc(w.weightcategory),
"comments": newnotes,
"heart_rate": {
"average": averagehr,
"max": maxhr,
},
"splits": split_data,
}
return data
# Create the Data object for the stroke data to be sent to Concept2 logbook
# API
def createc2workoutdata(w):
filename = w.csvfilename
try:
row = rowingdata(filename)
except IOError:
return 0
try:
averagehr = int(row.df[' HRCur (bpm)'].mean())
maxhr = int(row.df[' HRCur (bpm)'].max())
except ValueError:
averagehr = 0
maxhr = 0
# adding diff, trying to see if this is valid
t = 10*row.df.ix[:,'TimeStamp (sec)'].values-10*row.df.ix[0,'TimeStamp (sec)']
t[0] = t[1]
d = 10*row.df.ix[:,' Horizontal (meters)'].values
d[0] = d[1]
p = abs(10*row.df.ix[:,' Stroke500mPace (sec/500m)'].values)
p = np.clip(p,0,3600)
t = t.astype(int)
d = d.astype(int)
p = p.astype(int)
spm = row.df[' Cadence (stokes/min)'].astype(int)
spm[0] = spm[1]
try:
hr = row.df[' HRCur (bpm)'].astype(int)
except ValueError:
hr = 0*d
stroke_data = []
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.strptime(str(w.duration),"%H:%M:%S.%f")
except ValueError:
durationstr = datetime.strptime(str(w.duration),"%H:%M:%S")
workouttype = w.workouttype
if workouttype in ('coastal','other'):
workouttype = 'water'
data = {
"type": workouttype,
"date": w.startdatetime.isoformat(),
"timezone": w.timezone,
"distance": int(w.distance),
"time": int(10*makeseconds(durationstr)),
"weight_class": c2wc(w.weightcategory),
"comments": w.notes,
"heart_rate": {
"average": averagehr,
"max": maxhr,
},
"stroke_data": stroke_data,
}
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)
token_json = response.json()
try:
thetoken = token_json['access_token']
expires_in = token_json['expires_in']
refresh_token = token_json['refresh_token']
except:
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
print prepped.body
response = s.send(prepped)
with open("media/c2authorize.log","a") as f:
try:
f.write(reponse.status_code+"\n")
f.write(reponse.text+"\n")
f.write(response.json+"\n\n")
except:
pass
token_json = response.json()
try:
status_code = response.status_code
# status_code = token_json['status_code']
except AttributeError:
# except KeyError:
return (0,response.text)
try:
status_code = token_json.status_code
except AttributeError:
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:
return (0,token_json['message'])
return (thetoken,expires_in,refresh_token,messg)
# Make URL for authorization and load it
def make_authorization_url(request):
# 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.urlencode(params)
url += "&scope="+scope
return HttpResponseRedirect(url)
# Get workout from C2 ID
def get_c2_workout(user,c2id):
r = Rower.objects.get(user=user)
if (r.c2token == '') or (r.c2token is None):
s = "Token doesn't exist. Need to authorize"
return custom_exception_handler(401,s)
elif (timezone.now()>r.tokenexpirydate):
s = "Token expired. Needs to refresh."
return custom_exception_handler(401,s)
else:
# 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/"+str(c2id)
s = requests.get(url,headers=headers)
return s
# Get stroke data belonging to C2 ID
def get_c2_workout_strokes(user,c2id):
r = Rower.objects.get(user=user)
if (r.c2token == '') or (r.c2token is None):
return custom_exception_handler(401,s)
s = "Token doesn't exist. Need to authorize"
elif (timezone.now()>r.tokenexpirydate):
s = "Token expired. Needs to refresh."
return custom_exception_handler(401,s)
else:
# 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/"+str(c2id)+"/strokes"
s = requests.get(url,headers=headers)
return s
# 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):
s = "Token doesn't exist. Need to authorize"
return custom_exception_handler(401,s)
elif (timezone.now()>r.tokenexpirydate):
s = "Token expired. Needs to refresh."
return custom_exception_handler(401,s)
else:
# 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):
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']
id = 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"
response = requests.get(url,headers=headers)
me_json = response.json()
try:
res = me_json['data']['id']
except KeyError:
res = 0
return res
# For debugging purposes
def process_callback(request):
# 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)
# Uploading workout
def workout_c2_upload(user,w):
message = 'trying C2 upload'
thetoken = c2_open(user)
r = Rower.objects.get(user=user)
# ready to upload. Hurray
if (checkworkoutuser(user,w)):
c2userid = get_userid(r.c2token)
if not c2userid:
raise C2NoTokenError
data = createc2workoutdata(w)
if data == 0:
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)
response = requests.post(url,headers=headers,data=json.dumps(data))
if (response.status_code == 409 ):
message = "Concept2 Duplicate error"
w.uploadedtoc2 = -1
c2id = -1
w.save()
elif (response.status_code == 201 or response.status_code == 200):
try:
s= json.loads(response.text)
c2id = s['data']['id']
w.uploadedtoc2 = c2id
w.save()
message = "Upload to Concept2 was successful"
except:
message = "Something went wrong in workout_c2_upload_view. Response code 200/201 but C2 sync failed: "+response.text
c2id = 0
else:
print response.status_code
message = "Something went wrong in workout_c2_upload_view. Response code 200/201 but C2 sync failed: "+response.text
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()+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:
return None