-
+
+
+
+

+
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 %}
+
+
+
+ | Import |
+ Date/Time |
+ Duration |
+ Total Distance |
+ Type |
+
+
+
+ {% for workout in workouts %}
+
+ |
+Import |
+ {{ workout|ualookup:'starttime' }} |
+ {{ workout|ualookup:'duration' }} |
+ {{ workout|ualookup:'distance' }} m |
+ {{ workout|ualookup:'type' }} |
+
+
+ {% endfor %}
+
+
+ {% 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..b1b22ff8 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/landing3.jpg");
}
.four {
diff --git a/static/img/UAbtn.png b/static/img/UAbtn.png
new file mode 100644
index 00000000..7cfdf18d
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..888fb1ee
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..42fe929e
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..6e6cf69c
Binary files /dev/null and b/static/img/uasquare.png differ