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 index 7d9b2df9..0d11281f 100644 --- a/rowers/underarmourstuff.py +++ b/rowers/underarmourstuff.py @@ -171,9 +171,9 @@ def get_underarmour_workout_list(user): headers = {'Authorization': authorizationstring, 'Api-Key': UNDERARMOUR_CLIENT_KEY, 'user-agent': 'sanderroosendaal', - 'user':'v7.1/user/'+str(get_userid(r.underarmourtoken))+'/', 'Content-Type': 'application/json'} - url = "https://api.ua.com/v7.1/workout/" + url = "https://api.ua.com/v7.1/workout/?user="+str(get_userid(r.underarmourtoken)) + s = requests.get(url,headers=headers) return s @@ -191,7 +191,7 @@ def get_underarmour_workout(user,underarmourid): 'Api-Key': UNDERARMOUR_CLIENT_KEY, 'user-agent': 'sanderroosendaal', 'Content-Type': 'application/json'} - url = "https://api.ua.com/v7.1/workout/"+str(underarmourid)+"/" + url = "https://api.ua.com/v7.1/workout/"+str(underarmourid)+"/?field_set=time_series" s = requests.get(url,headers=headers) return s @@ -292,13 +292,36 @@ def createunderarmourworkoutdata(w): 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:] +# 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 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 - return int(id) # Get user id, having access token diff --git a/rowers/urls.py b/rowers/urls.py index 5a72d091..3f409e35 100644 --- a/rowers/urls.py +++ b/rowers/urls.py @@ -229,6 +229,8 @@ 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), diff --git a/rowers/views.py b/rowers/views.py index bdf96a25..54975258 100644 --- a/rowers/views.py +++ b/rowers/views.py @@ -214,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 = [] @@ -816,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 @@ -1401,7 +1577,7 @@ def rower_underarmour_authorize(request): redirect_uri = UNDERARMOUR_REDIRECT_URI redirect_uri = 'http://localhost:8000/underarmour_callback' - url = 'https://api.mapmyfitness.com/v7.1/oauth2/authorize/?' \ + url = 'https://www.mapmyfitness.com/v7.1/oauth2/authorize/?' \ 'client_id={0}&response_type=code&redirect_uri={1}'.format( UNDERARMOUR_CLIENT_KEY, redirect_uri ) @@ -4896,6 +5072,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=""): @@ -5078,6 +5298,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/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