diff --git a/logos/uasquare.xcf b/logos/uasquare.xcf new file mode 100644 index 00000000..90428f9c Binary files /dev/null and b/logos/uasquare.xcf differ diff --git a/rowers/dataprep.py b/rowers/dataprep.py index c648468d..ea252e89 100644 --- a/rowers/dataprep.py +++ b/rowers/dataprep.py @@ -783,6 +783,8 @@ def rdata(file,rower=rrower()): res = rrdata(csvfile=file+'.gz',rower=rower) except IOError,IndexError: res = 0 + except: + res = 0 return res diff --git a/rowers/management/commands/processemail.py b/rowers/management/commands/processemail.py index c52a233b..e0052064 100644 --- a/rowers/management/commands/processemail.py +++ b/rowers/management/commands/processemail.py @@ -96,6 +96,7 @@ class Command(BaseCommand): donotdelete = 1 try: dd = send_confirm(rr.user,name,link) + time.sleep(10) except: pass 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/templates/export.html b/rowers/templates/export.html index fb62a190..d207cf0b 100644 --- a/rowers/templates/export.html +++ b/rowers/templates/export.html @@ -111,6 +111,24 @@ {% endif %} + {% if workout.uploadedtounderarmour == 0 %} + {% if user.rower.underarmourtoken == None or user.rower.underarmourtoken == '' %} +
+ + Underarmour icon +
+ {% else %} +
+ Underarmour icon +
+ {% endif %} + {% else %} +
+ + Underarmour icon +
+ {% endif %} + @@ -138,8 +156,11 @@
-
-

connect with RunKeeper

+
+

connect with Runkeeper

+
+
+

connect with Under Armour

diff --git a/rowers/templates/imports.html b/rowers/templates/imports.html index 6bfda942..9177b93f 100644 --- a/rowers/templates/imports.html +++ b/rowers/templates/imports.html @@ -43,12 +43,22 @@

Import workouts from RunKeeper

+
+
+

+ Under Armour logo +

+
+
+

Import workouts from MapMyFitness/UnderArmour

+
+

Connect

-
+

Click one of the below logos to connect to the service of your choice. You only need to do this once. After that, the site will have access until you revoke the authorization for the "rowingdata" app.

@@ -67,10 +77,13 @@
-
-
+
+

connect with RunKeeper

+
+

connect with Under Armour

+
diff --git a/rowers/templates/underarmour_list_import.html b/rowers/templates/underarmour_list_import.html new file mode 100644 index 00000000..34fb26a3 --- /dev/null +++ b/rowers/templates/underarmour_list_import.html @@ -0,0 +1,37 @@ +{% extends "base.html" %} +{% load staticfiles %} +{% load rowerfilters %} + +{% block title %}Workouts{% endblock %} + +{% block content %} +

Available on MapMyFitness (UnderArmour)

+ {% if workouts %} + + + + + + + + + + + + {% for workout in workouts %} + + + + + + + + + {% endfor %} + +
Import Date/Time Duration Total Distance Type
+Import{{ workout|ualookup:'starttime' }}{{ workout|ualookup:'duration' }} {{ workout|ualookup:'distance' }} m{{ workout|ualookup:'type' }}
+ {% else %} +

No workouts found. We only list workouts with time data series.

+ {% endif %} +{% endblock %} diff --git a/rowers/templatetags/rowerfilters.py b/rowers/templatetags/rowerfilters.py index d3a610fd..31dc8a04 100644 --- a/rowers/templatetags/rowerfilters.py +++ b/rowers/templatetags/rowerfilters.py @@ -1,5 +1,6 @@ from django import template from time import strftime +import dateutil.parser register = template.Library() @@ -27,6 +28,17 @@ def strfdeltah(tdelta): return res +def secondstotimestring(tdelta): + hours, rest = divmod(tdelta,3600) + minutes,seconds = divmod(rest,60) + res = "{hours:0>2}:{minutes:0>2}:{seconds:0>2}".format( + hours=hours, + minutes=minutes, + seconds=seconds, + ) + + return res + @register.filter def durationprint(d,dstring): if (d == None): @@ -57,6 +69,22 @@ def lookup(dict, key): s = s[:22] return s +@register.filter +def ualookup(dict, key): + s = dict.get(key) + + if key=='distance': + s = int(float(s)) + + if key=='duration': + s = secondstotimestring(int(s)) + + + if key=='starttime': + s = dateutil.parser.parse(s) + + return s + @register.filter(name='times') def times(number): return range(number) diff --git a/rowers/underarmourstuff.py b/rowers/underarmourstuff.py new file mode 100644 index 00000000..8f35d692 --- /dev/null +++ b/rowers/underarmourstuff.py @@ -0,0 +1,370 @@ +# 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,access_token): + 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/x-www-form-urlencoded', + 'authorization': 'Bearer %s' % access_token} + + url = "https://api.ua.com/v7.1/oauth2/access_token/" + + response = requests.post(url, + data=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.ua.com/v7.1/workout/?user="+str(get_userid(r.underarmourtoken)) + + 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.ua.com/v7.1/workout/"+str(underarmourid)+"/?field_set=time_series" + 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 + name = w.name + notes = w.notes + + # 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 + + # short for debugging + t = t[:10] + d = d[:10] + lat = lat[:10] + hr = hr[:10] + lon = lon[:10] + spm = spm[:10] + + # path data + if haslatlon: + locdata = [] + for e in zip(t,lat,lon): + point = { + 'lat':e[1], + 'lng':e[2], + 'elevation':0, + } + locdata.append([e[0],point]) + + hrdata = [] + for e in zip(t,hr): + point = [e[0], + e[1] + ] + hrdata.append(point) + + distancedata = [] + for e in zip(t,d): + point = [e[0], + e[1] + ] + distancedata.append(point) + + spmdata = [] + for e in zip(t,spm): + spmdata.append([e[0],e[1]]) + + start_time = w.startdatetime.isoformat() + + timeseries = { + "distance": distancedata, + "heartrate": hrdata, + "cadence": spmdata, + } + + aggregrates = { + "elapsed_time_total": int(duration), + "distance_total": int(max(d)), + "heartrate_avg": averagehr, + "heart_rate_min": int(min(hr)), + "heart_rate_max": int(max(hr)), + } + + +# if haslatlon: +# timeseries["position"] = locdata + + data = { + "name": name, + "start_datetime": start_time, + "time_series": timeseries, + "start_locale_timezone": "Etc/UTC", + "activity_type": "/v7.1/activity_type/128/", + "notes": notes, + } + + return data + +# Obtain Underarmour Workout ID and activity type +def get_idfromuri(user,links): + id = links['self'][0]['id'] + typeid = links['activity_type'][0]['id'] + + + typename = get_typefromid(typeid,user) + + return id,typename + +def getidfromresponse(response): + t = json.loads(response.text) + + links = t["_links"] + + id = links["self"][0]["id"] + + return int(id) + + + +def get_typefromid(typeid,user): + r = Rower.objects.get(user=user) + authorizationstring = str('Bearer ' + r.underarmourtoken) + headers = {'Authorization': authorizationstring, + 'Api-Key': UNDERARMOUR_CLIENT_KEY, + 'user-agent': 'sanderroosendaal', + 'Content-Type': 'application/json'} + import urllib + url = "https://api.ua.com/v7.1/activity_type/"+str(typeid) + response = requests.get(url,headers=headers) + + me_json = response.json() + + try: + res = me_json['name'] + except KeyError: + res = 0 + + 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, + 'Api-Key': UNDERARMOUR_CLIENT_KEY, + 'user-agent': 'sanderroosendaal', + 'Content-Type': 'application/json'} + import urllib + url = "https://api.ua.com/v7.1/user/self" + response = requests.get(url,headers=headers) + + me_json = response.json() + + try: + res = me_json['id'] + except KeyError: + res = 0 + + return res diff --git a/rowers/urls.py b/rowers/urls.py index 48d544fe..8fe5c55d 100644 --- a/rowers/urls.py +++ b/rowers/urls.py @@ -229,12 +229,15 @@ urlpatterns = [ url(r'^workout/sporttracksimport/(\d+)/$',views.workout_getsporttracksworkout_view), url(r'^workout/runkeeperimport/$',views.workout_runkeeperimport_view), url(r'^workout/runkeeperimport/(\d+)/$',views.workout_getrunkeeperworkout_view), + url(r'^workout/underarmourimport/$',views.workout_underarmourimport_view), + url(r'^workout/underarmourimport/(\d+)/$',views.workout_getunderarmourworkout_view), url(r'^workout/(\d+)/deleteconfirm$',views.workout_delete_confirm_view), url(r'^workout/(\d+)/c2uploadw/$',views.workout_c2_upload_view), url(r'^workout/(\d+)/stravauploadw/$',views.workout_strava_upload_view), url(r'^workout/(\d+)/recalcsummary/$',views.workout_recalcsummary_view), url(r'^workout/(\d+)/sporttracksuploadw/$',views.workout_sporttracks_upload_view), url(r'^workout/(\d+)/runkeeperuploadw/$',views.workout_runkeeper_upload_view), + url(r'^workout/(\d+)/underarmouruploadw/$',views.workout_underarmour_upload_view), url(r'^multi-compare$',views.multi_compare_view), url(r'^me/teams/c/(?P\w+.*)/s/(?P\w+.*)$',views.rower_teams_view), url(r'^me/teams/s/(?P\w+.*)$',views.rower_teams_view), @@ -269,8 +272,10 @@ 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/underarmourrefresh/$',views.rower_underarmour_token_refresh), url(r'^me/c2refresh/$',views.rower_c2_token_refresh), url(r'^me/favoritecharts/$',views.rower_favoritecharts_view), url(r'^email/send/$', views.sendmail), diff --git a/rowers/views.py b/rowers/views.py index c2eb66d7..da37dbea 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, ) @@ -211,6 +214,15 @@ def get_time(second): def getidfromsturi(uri,length=8): return uri[len(uri)-length:] +def splituadata(lijst): + t = [] + y = [] + for d in lijst: + t.append(d[0]) + y.append(d[1]) + + return np.array(t),np.array(y) + def splitrunkeeperlatlongdata(lijst,tname,latname,lonname): t = [] lat = [] @@ -813,6 +825,173 @@ def add_workout_from_stdata(user,importid,data): + unixtime = cum_time+starttimeunix + unixtime[0] = starttimeunix + + df['TimeStamp (sec)'] = unixtime + + + dt = np.diff(cum_time).mean() + wsize = round(5./dt) + + velo2 = stravastuff.ewmovingaverage(velo,wsize) + + df[' Stroke500mPace (sec/500m)'] = 500./velo2 + + + df = df.fillna(0) + + df.sort_values(by='TimeStamp (sec)',ascending=True) + + timestr = strftime("%Y%m%d-%H%M%S") + + csvfilename ='media/Import_'+str(importid)+'.csv' + + res = df.to_csv(csvfilename+'.gz',index_label='index', + compression='gzip') + + id,message = dataprep.save_workout_database(csvfilename,r, + workouttype=workouttype, + title=title, + notes=comments) + + return (id,message) + +# Create workout from SportTracks Data, which are slightly different +# than Strava or Concept2 data +def add_workout_from_underarmourdata(user,importid,data): + workouttype = 'water' + + try: + comments = data['notes'] + except: + comments = '' + + try: + thetimezone = tz(data['start_locale_timezone']) + except: + thetimezone = 'UTC' + + r = Rower.objects.get(user=user) + try: + rowdatetime = iso8601.parse_date(data['start_datetime']) + except iso8601.ParseError: + try: + rowdatetime = datetime.datetime.strptime(data['start_datetime'],"%Y-%m-%d %H:%M:%S") + rowdatetime = thetimezone.localize(rowdatetime).astimezone(utc) + except: + try: + rowdatetime = dateutil.parser.parse(data['start_datetime']) + rowdatetime = thetimezone.localize(rowdatetime).astimezone(utc) + except: + rowdatetime = datetime.datetime.strptime(data['date'],"%Y-%m-%d %H:%M:%S") + rowdatetime = thetimezone.localize(rowdatetime).astimezone(utc) + starttimeunix = mktime(rowdatetime.utctimetuple()) + + + try: + title = data['name'] + except: + title = "Imported data" + + timeseries = data['time_series'] + + # position, distance, speed, cadence, power, + + res = splituadata(timeseries['distance']) + + distance = res[1] + + times_distance = res[0] + + print distance[0:5] + print times_distance[0:5] + + try: + l = timeseries['position'] + + res = splituadata(l) + times_location = res[0] + latlong = res[1] + latcoord = [] + loncoord = [] + + for coord in latlong: + lat = coord['lat'] + lon = coord['lng'] + latcoord.append(lat) + loncoord.append(lon) + except: + times_location = times_distance + latcoord = np.zeros(len(times_distance)) + loncoord = np.zeros(len(times_distance)) + if workouttype == 'water': + workouttype = 'rower' + + try: + res = splituadata(timeseries['cadence']) + times_spm = res[0] + spm = res[1] + except KeyError: + times_spm = times_distance + spm = 0*times_distance + + try: + res = splituadata(timeseries['heartrate']) + hr = res[1] + times_hr = res[0] + except KeyError: + times_hr = times_distance + hr = 0*times_distance + + + # create data series and remove duplicates + distseries = pd.Series(distance,index=times_distance) + distseries = distseries.groupby(distseries.index).first() + latseries = pd.Series(latcoord,index=times_location) + latseries = latseries.groupby(latseries.index).first() + lonseries = pd.Series(loncoord,index=times_location) + lonseries = lonseries.groupby(lonseries.index).first() + spmseries = pd.Series(spm,index=times_spm) + spmseries = spmseries.groupby(spmseries.index).first() + hrseries = pd.Series(hr,index=times_hr) + hrseries = hrseries.groupby(hrseries.index).first() + + + # Create dicts and big dataframe + d = { + ' Horizontal (meters)': distseries, + ' latitude': latseries, + ' longitude': lonseries, + ' Cadence (stokes/min)': spmseries, + ' HRCur (bpm)' : hrseries, + } + + + + df = pd.DataFrame(d) + + df = df.groupby(level=0).last() + + cum_time = df.index.values + df[' ElapsedTime (sec)'] = cum_time + + velo = df[' Horizontal (meters)'].diff()/df[' ElapsedTime (sec)'].diff() + + df[' Power (watts)'] = 0.0*velo + + nr_rows = len(velo.values) + + df[' DriveLength (meters)'] = np.zeros(nr_rows) + df[' StrokeDistance (meters)'] = np.zeros(nr_rows) + df[' DriveTime (ms)'] = np.zeros(nr_rows) + df[' StrokeRecoveryTime (ms)'] = np.zeros(nr_rows) + df[' AverageDriveForce (lbs)'] = np.zeros(nr_rows) + df[' PeakDriveForce (lbs)'] = np.zeros(nr_rows) + df[' lapIdx'] = np.zeros(nr_rows) + + + unixtime = cum_time+starttimeunix unixtime[0] = starttimeunix @@ -878,6 +1057,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) @@ -1237,6 +1430,72 @@ def workout_runkeeper_upload_view(request,id=0): return HttpResponseRedirect(url) +# Upload workout to Underarmour +@login_required() +def workout_underarmour_upload_view(request,id=0): + message = "" + try: + w = Workout.objects.get(id=id) + r = w.user + except Workout.DoesNotExist: + raise Http404("Workout doesn't exist") + + try: + thetoken = underarmour_open(r.user) + except UnderarmourNoTokenError: + return HttpResponseRedirect("/rowers/me/underarmourauthorize/") + + # ready to upload. Hurray + + if (checkworkoutuser(request.user,w)): + data = underarmourstuff.createunderarmourworkoutdata(w) +# return HttpResponse(json.dumps(data)) + if not data: + message = "Data error" + url = reverse(workout_export_view, + kwargs = { + 'message':str(message), + 'id':str(w.id), + }) + return HttpResponseRedirect(url) + + authorizationstring = str('Bearer ' + thetoken) + headers = {'Authorization': authorizationstring, + 'Api-Key': UNDERARMOUR_CLIENT_KEY, + 'user-agent': 'sanderroosendaal', + 'Content-Type': 'application/json', + } + + import urllib + url = "https://api.ua.com/v7.1/workout/" + response = requests.post(url,headers=headers,data=json.dumps(data)) + + # check for duplicate error first + if (response.status_code == 409 ): + message = "Duplicate error" + w.uploadedtounderarmour = -1 + w.save() + elif (response.status_code == 201 or response.status_code==200): + underarmourid = underarmourstuff.getidfromresponse(response) + w.uploadedtounderarmour = underarmourid + w.save() + url = "/rowers/workout/"+str(w.id)+"/export" + return HttpResponseRedirect(url) + else: + s = response + message = "Something went wrong in workout_underarmour_upload_view: %s - %s" % (s.reason,s.text) + + else: + message = "You are not authorized to upload this workout" + + url = reverse(workout_export_view, + kwargs = { + 'message':str(message), + 'id':str(w.id), + }) + + return HttpResponseRedirect(url) + # Upload workout to SportTracks @login_required() def workout_sporttracks_upload_view(request,id=0): @@ -1373,6 +1632,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://www.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): @@ -1398,11 +1675,37 @@ def rower_c2_token_refresh(request): return imports_view(request,successmessage=successmessage,message=message) +# Underarmour token refresh. URL for manual refresh. Not visible to users +@login_required() +def rower_underarmour_token_refresh(request): + r = Rower.objects.get(user=request.user) + res = underarmourstuff.do_refresh_token( + r.underarmourrefreshtoken, + r.underarmourtoken + ) + 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 refreshed. Good to go" + return imports_view(request,successmessage=successmessage) + + # SportTracks token refresh. URL for manual refresh. Not visible to users @login_required() def rower_sporttracks_token_refresh(request): r = Rower.objects.get(user=request.user) - res = sporttracksstuff.do_refresh_token(r.sporttracksrefreshtoken) + res = sporttracksstuff.do_refresh_token( + r.sporttracksrefreshtoken, + ) access_token = res[0] expires_in = res[1] refresh_token = res[2] @@ -1535,6 +1838,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): @@ -4839,6 +5164,50 @@ def workout_runkeeperimport_view(request,message=""): return HttpResponse(res) +# The page where you select which RunKeeper workout to import +@login_required() +def workout_underarmourimport_view(request,message=""): + res = underarmourstuff.get_underarmour_workout_list(request.user) + if (res.status_code != 200): + if (res.status_code == 401): + r = Rower.objects.get(user=request.user) + if (r.underarmourtoken == '') or (r.underarmourtoken is None): + s = "Token doesn't exist. Need to authorize" + return HttpResponseRedirect("/rowers/me/underarmourauthorize/") + message = "Something went wrong in workout_underarmourimport_view" + if settings.DEBUG: + return HttpResponse(res) + else: + url = reverse(workouts_view, + kwargs = { + 'message': str(message) + }) + return HttpResponseRedirect(url) + else: + workouts = [] + items = res.json()['_embedded']['workouts'] + for item in items: + if 'has_time_series' in item: + if item['has_time_series']: + s = item['start_datetime'] + i,r = underarmourstuff.get_idfromuri(request.user,item['_links']) + n = item['name'] + d = item['aggregates']['distance_total'] + ttot = item['aggregates']['active_time_total'] + keys = ['id','distance','duration','starttime','type'] + values = [i,d,ttot,s,r] + thedict = dict(zip(keys,values)) + + workouts.append(thedict) + + return render(request,'underarmour_list_import.html', + {'workouts':workouts, + 'teams':get_my_teams(request.user), + 'message':message, + }) + + return HttpResponse(res) + # The page where you select which SportTracks workout to import @login_required() def workout_sporttracksimport_view(request,message=""): @@ -5021,6 +5390,29 @@ def workout_getrunkeeperworkout_view(request,runkeeperid): }) return HttpResponseRedirect(url) +# Imports a workout from Underarmour +@login_required() +def workout_getunderarmourworkout_view(request,underarmourid): + res = underarmourstuff.get_underarmour_workout(request.user,underarmourid) + data = res.json() + + id,message = add_workout_from_underarmourdata(request.user,underarmourid,data) + w = Workout.objects.get(id=id) + w.uploadedtounderarmour=underarmourid + w.save() + if message: + url = reverse(workout_edit_view, + kwargs = { + 'id':id, + 'message':message, + }) + else: + url = reverse(workout_edit_view, + kwargs = { + 'id':id, + }) + return HttpResponseRedirect(url) + # Imports a workout from SportTracks 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')), diff --git a/static/css/rowsandall.css b/static/css/rowsandall.css index ed433213..9d3cd53a 100644 --- a/static/css/rowsandall.css +++ b/static/css/rowsandall.css @@ -19,7 +19,7 @@ } .three { - background-image: url("/static/img/landing1.jpg"); + background-image: url("/static/img/landing2.jpg"); } .four { diff --git a/static/img/UAbtn.png b/static/img/UAbtn.png new file mode 100644 index 00000000..2bee8c39 Binary files /dev/null and b/static/img/UAbtn.png differ diff --git a/static/img/landing3.jpg b/static/img/landing3.jpg index da1c4ec7..c4ab762a 100644 Binary files a/static/img/landing3.jpg and b/static/img/landing3.jpg differ diff --git a/static/img/uachecked.png b/static/img/uachecked.png new file mode 100644 index 00000000..0105ecd8 Binary files /dev/null and b/static/img/uachecked.png differ diff --git a/static/img/uagray.png b/static/img/uagray.png new file mode 100644 index 00000000..b1950760 Binary files /dev/null and b/static/img/uagray.png differ diff --git a/static/img/uasquare.png b/static/img/uasquare.png new file mode 100644 index 00000000..60602e5c Binary files /dev/null and b/static/img/uasquare.png differ