diff --git a/rowers/#runkeeperstuff.py# b/rowers/#runkeeperstuff.py# new file mode 100644 index 00000000..25866bc8 --- /dev/null +++ b/rowers/#runkeeperstuff.py# @@ -0,0 +1,282 @@ +# 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, + RUNKEEPER_CLIENT_ID, RUNKEEPER_CLIENT_SECRET,RUNKEEPER_REDIRECT_URI, + ) + +# Custom error class - to raise a NoTokenError +class RunKeeperNoTokenError(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 + +# Exchange access code for long-lived access token +def get_token(code): + client_auth = requests.auth.HTTPBasicAuth(RUNKEEPER_CLIENT_ID, RUNKEEPER_CLIENT_SECRET) + post_data = {"grant_type": "authorization_code", + "code": code, + "redirect_uri": RUNKEEPER_REDIRECT_URI, + "client_secret": RUNKEEPER_CLIENT_SECRET, + "client_id":RUNKEEPER_CLIENT_ID, + } + headers = {'user-agent': 'sanderroosendaal'} + response = requests.post("https://runkeeper.com/apps/token", + data=post_data, + headers=headers) + try: + token_json = response.json() + thetoken = token_json['access_token'] + except KeyError: + thetoken = 0 + + return thetoken + +# 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": RUNKEEPER_CLIENT_ID, + "response_type": "code", + "redirect_uri": RUNKEEPER_REDIRECT_URI, + } + import urllib + url = "https://www.runkeeper.com/opps/authorize" +urllib.urlencode(params) + + return HttpResponseRedirect(url) + +# Get list of workouts available on Runkeeper +def get_runkeeper_workout_list(user): + r = Rower.objects.get(user=user) + if (r.runkeepertoken == '') or (r.runkeepertoken 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.runkeepertoken) + headers = {'Authorization': authorizationstring, + 'user-agent': 'sanderroosendaal', + 'Content-Type': 'application/json'} + url = "https://api.runkeeper.com/fitnessActivities" + s = requests.get(url,headers=headers) + + return s + +# Get workout summary data by Runkeeper ID +def get_runkeeper_workout(user,runkeeperid): + r = Rower.objects.get(user=user) + if (r.runkeepertoken == '') or (r.runkeepertoken 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.runkeepertoken) + headers = {'Authorization': authorizationstring, + 'user-agent': 'sanderroosendaal', + 'Content-Type': 'application/json'} + url = "https://api.runkeeper.com/fitnessActivities/"+str(runkeeperid) + s = requests.get(url,headers=headers) + + return s + +# Create Workout Data for upload to SportTracks +def createrunkeeperworkoutdata(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],} + 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, + "heartrate": 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, + "heartrate": hrdata, + "post_to_twitter":"false", + "post_to_facebook":"false", + } + + + return data + +# Obtain Runkeeper 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, + 'user-agent': 'sanderroosendaal', + 'Content-Type': 'application/json'} + import urllib + url = "https://api.runkeeper.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/models.py b/rowers/models.py index 2b8b20f9..6c581794 100644 --- a/rowers/models.py +++ b/rowers/models.py @@ -196,6 +196,12 @@ class Rower(models.Model): sporttracksrefreshtoken = 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, + blank=True,null=True) +# runkeepertokenexpirydate = models.DateTimeField(blank=True,null=True) +# runkeeperrefreshtoken = models.CharField(default='',max_length=200, +# blank=True,null=True) # Plan plans = ( @@ -230,6 +236,13 @@ class Rower(models.Model): def clean_email(self): return self.user.email.lower() +@receiver(models.signals.post_save,sender=Rower) +def auto_delete_teams_on_change(sender, instance, **kwargs): + if instance.rowerplan != 'coach': + teams = Team.objects.filter(manager=instance.user) + for team in teams: + team.delete() + # Saving a chart as a favorite chart class FavoriteChart(models.Model): y1params = ( @@ -393,6 +406,7 @@ class Workout(models.Model): maxhr = models.IntegerField(blank=True,null=True) uploadedtostrava = models.IntegerField(default=0) uploadedtosporttracks = models.IntegerField(default=0) + uploadedtorunkeeper = models.IntegerField(default=0) # empower stuff inboard = models.FloatField(default=0.88) diff --git a/rowers/runkeeperstuff.py b/rowers/runkeeperstuff.py new file mode 100644 index 00000000..b845bf20 --- /dev/null +++ b/rowers/runkeeperstuff.py @@ -0,0 +1,284 @@ +# 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, + RUNKEEPER_CLIENT_ID, RUNKEEPER_CLIENT_SECRET,RUNKEEPER_REDIRECT_URI, + ) + +# Custom error class - to raise a NoTokenError +class RunKeeperNoTokenError(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 + +# Exchange access code for long-lived access token +def get_token(code): + client_auth = requests.auth.HTTPBasicAuth(RUNKEEPER_CLIENT_ID, RUNKEEPER_CLIENT_SECRET) + post_data = {"grant_type": "authorization_code", + "code": code, + "redirect_uri": RUNKEEPER_REDIRECT_URI, + "client_secret": RUNKEEPER_CLIENT_SECRET, + "client_id":RUNKEEPER_CLIENT_ID, + } + headers = {'user-agent': 'sanderroosendaal'} + response = requests.post("https://runkeeper.com/apps/token", + data=post_data, + headers=headers) + try: + token_json = response.json() + thetoken = token_json['access_token'] + except KeyError: + thetoken = 0 + + return thetoken + +# 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": RUNKEEPER_CLIENT_ID, + "response_type": "code", + "redirect_uri": RUNKEEPER_REDIRECT_URI, + } + import urllib + url = "https://www.runkeeper.com/opps/authorize" +urllib.urlencode(params) + + return HttpResponseRedirect(url) + +# Get list of workouts available on Runkeeper +def get_runkeeper_workout_list(user): + r = Rower.objects.get(user=user) + if (r.runkeepertoken == '') or (r.runkeepertoken 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.runkeepertoken) + headers = {'Authorization': authorizationstring, + 'user-agent': 'sanderroosendaal', + 'Content-Type': 'application/json'} + url = "https://api.runkeeper.com/fitnessActivities" + s = requests.get(url,headers=headers) + + return s + +# Get workout summary data by Runkeeper ID +def get_runkeeper_workout(user,runkeeperid): + r = Rower.objects.get(user=user) + if (r.runkeepertoken == '') or (r.runkeepertoken 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.runkeepertoken) + headers = {'Authorization': authorizationstring, + 'user-agent': 'sanderroosendaal', + 'Content-Type': 'application/json'} + url = "https://api.runkeeper.com/fitnessActivities/"+str(runkeeperid) + s = requests.get(url,headers=headers) + + return s + +# Create Workout Data for upload to SportTracks +def createrunkeeperworkoutdata(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 Runkeeper 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, + 'user-agent': 'sanderroosendaal', + 'Content-Type': 'application/json'} + import urllib + url = "https://api.runkeeper.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/sporttracksstuff.py b/rowers/sporttracksstuff.py index 15447944..0e15aca6 100644 --- a/rowers/sporttracksstuff.py +++ b/rowers/sporttracksstuff.py @@ -33,6 +33,15 @@ 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, SPORTTRACKS_CLIENT_SECRET, SPORTTRACKS_CLIENT_ID, SPORTTRACKS_REDIRECT_URI +# Custom error class - to raise a NoTokenError +class SportTracksNoTokenError(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): diff --git a/rowers/templates/export.html b/rowers/templates/export.html index b2cf83e9..fb62a190 100644 --- a/rowers/templates/export.html +++ b/rowers/templates/export.html @@ -44,78 +44,104 @@
-{% endif %} + + {% endif %} -{% if workout.uploadedtostrava == 0 %} -{% if user.rower.stravatoken == None or user.rower.stravatoken == '' %} -
-
-
-
-
-
-
+
+
+
+
+
+
+
-
+
+ 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.
+ + + + -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.
- - - - - - - -+
-+
+Import workouts from Strava
+Import workouts from SportTracks
+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.
+ +| Import | +Date/Time | +Duration | +Total Distance | +Type | +
|---|---|---|---|---|
| +Import | +{{ workout|lookup:'starttime' }} | +{{ workout|lookup:'duration' }} | +{{ workout|lookup:'distance' }} m | +{{ workout|lookup:'type' }} | +
No workouts found
+ {% endif %} +{% endblock %} diff --git a/rowers/urls.py b/rowers/urls.py index 6a524940..ae58dcad 100644 --- a/rowers/urls.py +++ b/rowers/urls.py @@ -212,11 +212,14 @@ urlpatterns = [ url(r'^workout/stravaimport/(\d+)/$',views.workout_getstravaworkout_view), url(r'^workout/sporttracksimport/$',views.workout_sporttracksimport_view), 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/(\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'^multi-compare$',views.multi_compare_view), url(r'^me/teams/c/(?P