# 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[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") data = { "type": w.workouttype, "date": w.startdatetime.isoformat(), "distance": int(w.distance), "time": int(10*makeseconds(durationstr)), "timezone": "Etc/UTC", "weight_class": c2wc(w.weightcategory), "comments": w.notes+'\n from '+w.workoutsource+' via rowsandall.com', "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") data = { "type": w.workouttype, "date": w.startdatetime.isoformat(), "timezone": "Etc/UTC", "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): 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" 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: message = "You are not authorized to upload this workout to Concept2" 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