diff --git a/rowers/models.py b/rowers/models.py index ee55c678..b573169c 100644 --- a/rowers/models.py +++ b/rowers/models.py @@ -195,6 +195,10 @@ class Rower(models.Model): sporttrackstokenexpirydate = models.DateTimeField(blank=True,null=True) sporttracksrefreshtoken = models.CharField(default='',max_length=200, blank=True,null=True) + underarmourtoken = models.CharField(default='',max_length=200,blank=True,null=True) + underarmourtokenexpirydate = models.DateTimeField(blank=True,null=True) + underarmourrefreshtoken = models.CharField(default='',max_length=200, + blank=True,null=True) stravatoken = models.CharField(default='',max_length=200,blank=True,null=True) runkeepertoken = models.CharField(default='',max_length=200, @@ -360,6 +364,7 @@ class Workout(models.Model): maxhr = models.IntegerField(blank=True,null=True) uploadedtostrava = models.IntegerField(default=0) uploadedtosporttracks = models.IntegerField(default=0) + uploadedtounderarmour = models.IntegerField(default=0) uploadedtorunkeeper = models.IntegerField(default=0) # empower stuff diff --git a/rowers/underarmourstuff.py b/rowers/underarmourstuff.py new file mode 100644 index 00000000..33392481 --- /dev/null +++ b/rowers/underarmourstuff.py @@ -0,0 +1,323 @@ +# All the functionality needed to connect to Runkeeper + +# Python +import oauth2 as oauth +import cgi +import requests +import requests.auth +import json +from django.utils import timezone +from datetime import datetime +import numpy as np +from dateutil import parser +import time +import math +from math import sin,cos,atan2,sqrt +import os,sys + +# 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 + +# Project +# from .models import Profile +from rowingdata import rowingdata +import pandas as pd +from rowers.models import Rower,Workout + +from rowsandall_app.settings import ( + C2_CLIENT_ID, C2_REDIRECT_URI, C2_CLIENT_SECRET, + STRAVA_CLIENT_ID, STRAVA_REDIRECT_URI, STRAVA_CLIENT_SECRET, + UNDERARMOUR_CLIENT_ID, UNDERARMOUR_CLIENT_SECRET, + UNDERARMOUR_REDIRECT_URI,UNDERARMOUR_CLIENT_KEY, + ) + +# Custom error class - to raise a NoTokenError +class UnderArmourNoTokenError(Exception): + def __init__(self,value): + self.value=value + + def __str__(self): + return repr(self.value) + + # Exponentially weighted moving average +# Used for data smoothing of the jagged data obtained by Strava +# See bitbucket issue 72 +def ewmovingaverage(interval,window_size): + # Experimental code using Exponential Weighted moving average + + try: + intervaldf = pd.DataFrame({'v':interval}) + idf_ewma1 = intervaldf.ewm(span=window_size) + idf_ewma2 = intervaldf[::-1].ewm(span=window_size) + + i_ewma1 = idf_ewma1.mean().ix[:,'v'] + i_ewma2 = idf_ewma2.mean().ix[:,'v'] + + interval2 = np.vstack((i_ewma1,i_ewma2[::-1])) + interval2 = np.mean( interval2, axis=0) # average + except ValueError: + interval2 = interval + + return interval2 + +from utils import geo_distance + + +# 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 + +# Refresh ST token using refresh token +def do_refresh_token(refreshtoken): + client_auth = requests.auth.HTTPBasicAuth(UNDERARMOUR_CLIENT_KEY, UNDERARMOUR_CLIENT_SECRET) + post_data = {"grant_type": "refresh_token", + "client_secret": UNDERARMOUR_CLIENT_SECRET, + "client_id":UNDERARMOUR_CLIENT_KEY, + "refresh_token": refreshtoken, + } + headers = {'user-agent': 'sanderroosendaal', + "Api-Key":UNDERARMOUR_CLIENT_KEY, + 'Accept': 'application/json', + 'Content-Type': 'application/json'} + + url = "https://api.ua.com/v7.1/oauth2/access_token" + + response = requests.post(url, + data=json.dumps(post_data), + headers=headers) + + token_json = response.json() + thetoken = token_json['access_token'] + expires_in = token_json['expires_in'] + try: + refresh_token = token_json['refresh_token'] + except KeyError: + refresh_token = refreshtoken + + return [thetoken,expires_in,refresh_token] + +# Exchange access code for long-lived access token +def get_token(code): + client_auth = requests.auth.HTTPBasicAuth(UNDERARMOUR_CLIENT_KEY, UNDERARMOUR_CLIENT_SECRET) + post_data = { + "grant_type": "authorization_code", + "code": code, + "client_secret": UNDERARMOUR_CLIENT_SECRET, + "client_id":UNDERARMOUR_CLIENT_KEY, + } + headers = { + 'user-agent': 'sanderroosendaal', + "Api-Key":UNDERARMOUR_CLIENT_KEY, + } + + response = requests.post("https://api.ua.com/v7.1/oauth2/access_token/", + data=post_data, + headers=headers) + try: + token_json = response.json() + thetoken = token_json['access_token'] + expires_in = token_json['expires_in'] + refresh_token = token_json['refresh_token'] + except KeyError: + thetoken = 0 + + return thetoken,expires_in,refresh_token + +# Make authorization URL including random string +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()) + + params = {"client_id": UNDERARMOUR_CLIENT_KEY, + "response_type": "code", + "redirect_uri": UNDERARMOUR_REDIRECT_URI, + } + import urllib + url = "https://www.mapmyfitness.com/v7.1/oauth2/uacf/authorize/" +urllib.urlencode(params) + + return HttpResponseRedirect(url) + +# Get list of workouts available on Underarmour +def get_underarmour_workout_list(user): + r = Rower.objects.get(user=user) + if (r.underarmourtoken == '') or (r.underarmourtoken is None): + s = "Token doesn't exist. Need to authorize" + return custom_exception_handler(401,s) + else: + # ready to fetch. Hurray + authorizationstring = str('Bearer ' + r.underarmourtoken) + headers = {'Authorization': authorizationstring, + 'Api-Key': UNDERARMOUR_CLIENT_KEY, + 'user-agent': 'sanderroosendaal', + 'Content-Type': 'application/json'} + url = "https://api.underarmour.com/fitnessActivities" + s = requests.get(url,headers=headers) + + return s + +# Get workout summary data by Underarmour ID +def get_underarmour_workout(user,underarmourid): + r = Rower.objects.get(user=user) + if (r.underarmourtoken == '') or (r.underarmourtoken is None): + return custom_exception_handler(401,s) + s = "Token doesn't exist. Need to authorize" + else: + # ready to fetch. Hurray + authorizationstring = str('Bearer ' + r.underarmourtoken) + headers = {'Authorization': authorizationstring, + 'Api-Key': UNDERARMOUR_CLIENT_KEY, + 'user-agent': 'sanderroosendaal', + 'Content-Type': 'application/json'} + url = "https://api.underarmour.com/fitnessActivities/"+str(underarmourid) + s = requests.get(url,headers=headers) + + return s + +# Create Workout Data for upload to Underarmour +def createunderarmourworkoutdata(w): + filename = w.csvfilename + try: + row = rowingdata(filename) + except: + return 0 + + averagehr = int(row.df[' HRCur (bpm)'].mean()) + maxhr = int(row.df[' HRCur (bpm)'].max()) + duration = w.duration.hour*3600 + duration += w.duration.minute*60 + duration += w.duration.second + duration += +1.0e-6*w.duration.microsecond + + # adding diff, trying to see if this is valid + #t = row.df.ix[:,'TimeStamp (sec)'].values-10*row.df.ix[0,'TimeStamp (sec)'] + t = row.df.ix[:,'TimeStamp (sec)'].values-row.df.ix[0,'TimeStamp (sec)'] + t[0] = t[1] + + d = row.df.ix[:,'cum_dist'].values + d[0] = d[1] + t = t.astype(int) + d = d.astype(int) + spm = row.df[' Cadence (stokes/min)'].astype(int) + spm[0] = spm[1] + hr = row.df[' HRCur (bpm)'].astype(int) + + haslatlon=1 + + try: + lat = row.df[' latitude'].values + lon = row.df[' longitude'].values + if not lat.std() and not lon.std(): + haslatlon = 0 + except KeyError: + haslatlon = 0 + + # path data + if haslatlon: + locdata = [] + for e in zip(t,lat,lon): + point = {'timestamp':e[0], + 'latitude':e[1], + 'longitude':e[2], + 'altitude':0, + "type":"gps"} + locdata.append(point) + + hrdata = [] + for e in zip(t,hr): + point = {'timestamp':e[0], + 'heart_rate':e[1] + } + hrdata.append(point) + + distancedata = [] + for e in zip(t,d): + point = {'timestamp':e[0], + 'distance':e[1] + } + distancedata.append(point) + + start_time = w.startdatetime.strftime("%a, %d %b %Y %H:%M:%S") + + if haslatlon: + data = { + "type": "Rowing", + "start_time": start_time, + "total_distance": int(w.distance), + "duration": duration, + "notes": w.notes, + "average_heart_rate": averagehr, + "path": locdata, + "distance": distancedata, + "heart_rate": hrdata, + "post_to_twitter":"false", + "post_to_facebook":"false", + } + else: + data = { + "type": "Rowing", + "start_time": start_time, + "total_distance": int(w.distance), + "duration": duration, + "notes": w.notes, + "avg_heartrate": averagehr, + "distance": distancedata, + "heart_rate": hrdata, + "post_to_twitter":"false", + "post_to_facebook":"false", + } + + + return data + +# Obtain Underarmour Workout ID from the response returned on successful +# upload +def getidfromresponse(response): + uri = response.headers["Location"] + id = uri[len(uri)-9:] + + return int(id) + + +# 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, + 'Api-Key': UNDERARMOUR_CLIENT_KEY, + 'user-agent': 'sanderroosendaal', + 'Content-Type': 'application/json'} + import urllib + url = "https://api.underarmour.com/user" + response = requests.get(url,headers=headers) + + + me_json = response.json() + + try: + res = me_json['userID'] + except KeyError: + res = 0 + + return res diff --git a/rowers/urls.py b/rowers/urls.py index 48d544fe..5a72d091 100644 --- a/rowers/urls.py +++ b/rowers/urls.py @@ -269,6 +269,7 @@ urlpatterns = [ url(r'^me/revokeapp/(\d+)$',views.rower_revokeapp_view), url(r'^me/stravaauthorize/$',views.rower_strava_authorize), url(r'^me/sporttracksauthorize/$',views.rower_sporttracks_authorize), + url(r'^me/underarmourauthorize/$',views.rower_underarmour_authorize), url(r'^me/runkeeperauthorize/$',views.rower_runkeeper_authorize), url(r'^me/sporttracksrefresh/$',views.rower_sporttracks_token_refresh), url(r'^me/c2refresh/$',views.rower_c2_token_refresh), diff --git a/rowers/views.py b/rowers/views.py index 2a04bddd..d80126b9 100644 --- a/rowers/views.py +++ b/rowers/views.py @@ -56,6 +56,7 @@ from sporttracksstuff import SportTracksNoTokenError from iso8601 import ParseError import stravastuff import sporttracksstuff +import underarmourstuff import runkeeperstuff import ownapistuff from ownapistuff import TEST_CLIENT_ID, TEST_CLIENT_SECRET, TEST_REDIRECT_URI @@ -64,6 +65,8 @@ from rowsandall_app.settings import ( STRAVA_CLIENT_ID, STRAVA_REDIRECT_URI, STRAVA_CLIENT_SECRET, SPORTTRACKS_CLIENT_ID, SPORTTRACKS_REDIRECT_URI, SPORTTRACKS_CLIENT_SECRET, + UNDERARMOUR_CLIENT_ID, UNDERARMOUR_REDIRECT_URI, + UNDERARMOUR_CLIENT_SECRET,UNDERARMOUR_CLIENT_KEY, RUNKEEPER_CLIENT_ID,RUNKEEPER_REDIRECT_URI,RUNKEEPER_CLIENT_SECRET, ) @@ -878,6 +881,20 @@ def sporttracks_open(user): return thetoken +# Checks if user has UnderArmour token, renews them if they are expired +def underarmour_open(user): + r = Rower.objects.get(user=user) + if (r.underarmourtoken == '') or (r.underarmourtoken is None): + s = "Token doesn't exist. Need to authorize" + raise UnderarmourNoTokenError("User has no token") + else: + if (timezone.now()>r.underarmourtokenexpirydate): + thetoken = underarmourstuff.rower_underarmour_token_refresh(user) + else: + thetoken = r.underarmourtoken + + return thetoken + # Checks if user has SportTracks token, renews them if they are expired def runkeeper_open(user): r = Rower.objects.get(user=user) @@ -1373,6 +1390,24 @@ def rower_sporttracks_authorize(request): return HttpResponseRedirect(url) +# Underarmour Authorization +@login_required() +def rower_underarmour_authorize(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()) + + redirect_uri = UNDERARMOUR_REDIRECT_URI + redirect_uri = 'http://localhost:8000/underarmour_callback' + + url = 'https://api.mapmyfitness.com/v7.1/oauth2/authorize/?' \ + 'client_id={0}&response_type=code&redirect_uri={1}'.format( + UNDERARMOUR_CLIENT_KEY, redirect_uri + ) + + return HttpResponseRedirect(url) + # Concept2 token refresh. URL for manual refresh. Not visible to users @login_required() def rower_c2_token_refresh(request): @@ -1535,6 +1570,28 @@ def rower_process_sporttrackscallback(request): successmessage = "Tokens stored. Good to go" return imports_view(request,successmessage=successmessage) +# Process Underarmour callback +@login_required() +def rower_process_underarmourcallback(request): + code = request.GET['code'] + res = underarmourstuff.get_token(code) + + + 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=request.user) + r.underarmourtoken = access_token + r.underarmourtokenexpirydate = expirydatetime + r.underarmourrefreshtoken = refresh_token + + r.save() + + successmessage = "Tokens stored. Good to go" + return imports_view(request,successmessage=successmessage) + # Process Own API callback - for API testing purposes @login_required() def rower_process_testcallback(request): diff --git a/rowsandall_app/settings.py b/rowsandall_app/settings.py index c62a27bb..9d48b408 100644 --- a/rowsandall_app/settings.py +++ b/rowsandall_app/settings.py @@ -234,6 +234,14 @@ RUNKEEPER_CLIENT_ID = CFG['runkeeper_client_id'] RUNKEEPER_CLIENT_SECRET = CFG['runkeeper_client_secret'] RUNKEEPER_REDIRECT_URI = "http://rowsandall.com/runkeeper_callback" +# Under Armour + +UNDERARMOUR_CLIENT_ID = CFG['underarmour_client_name'] +UNDERARMOUR_CLIENT_SECRET = CFG['underarmour_client_secret'] +UNDERARMOUR_CLIENT_KEY = CFG['underarmour_client_key'] +#UNDERARMOUR_REDIRECT_URI = "https://rowsandall.com/underarmour_callback" +UNDERARMOUR_REDIRECT_URI = "http://localhost:8000/underarmour_callback" + # RQ stuff RQ_QUEUES = { diff --git a/rowsandall_app/urls.py b/rowsandall_app/urls.py index 08834cb7..26734641 100644 --- a/rowsandall_app/urls.py +++ b/rowsandall_app/urls.py @@ -55,6 +55,7 @@ urlpatterns += [ url(r'^call\_back',rowersviews.rower_process_callback), url(r'^stravacall\_back',rowersviews.rower_process_stravacallback), url(r'^sporttracks\_callback',rowersviews.rower_process_sporttrackscallback), + url(r'^underarmour\_callback',rowersviews.rower_process_underarmourcallback), url(r'^runkeeper\_callback',rowersviews.rower_process_runkeepercallback), url(r'^twitter\_callback',rowersviews.rower_process_twittercallback), url(r'^i18n/', include('django.conf.urls.i18n')),