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/.#runkeeperstuff.py b/rowers/.#runkeeperstuff.py new file mode 100644 index 00000000..8cbb2c1f --- /dev/null +++ b/rowers/.#runkeeperstuff.py @@ -0,0 +1 @@ +E408191@CZ27LT9RCGN72.9372:1490257958 \ No newline at end of file diff --git a/rowers/models.py b/rowers/models.py index c6603e37..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 = ( @@ -400,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..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/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 @@
Concept2 icon -
-{% endif %} + + {% endif %} -{% if workout.uploadedtostrava == 0 %} -{% if user.rower.stravatoken == None or user.rower.stravatoken == '' %} -
- - Strava icon -
-{% else %} -
- Strava icon -
-{% endif %} -{% else %} -
- - Concept2 icon -
-{% endif %} -{% if workout.uploadedtosporttracks == 0 %} -{% if user.rower.sporttrackstoken == None or user.rower.sporttrackstoken == '' %} -
- - SportTracks icon -
-{% else %} -
- - SportTracks icon -
-{% endif %} -{% else %} -
- - Concept2 icon -
-{% endif %} -
- - TCX Export -
+ {% if workout.uploadedtostrava == 0 %} + {% if user.rower.stravatoken == None or user.rower.stravatoken == '' %} +
+ + Strava icon +
+ {% else %} +
+ Strava icon +
+ {% endif %} + {% else %} +
+ + Concept2 icon +
+ {% endif %} + {% if workout.uploadedtosporttracks == 0 %} + {% if user.rower.sporttrackstoken == None or user.rower.sporttrackstoken == '' %} +
+ + SportTracks icon +
+ {% else %} +
+ + SportTracks icon +
+ {% endif %} + {% else %} +
+ + Concept2 icon +
+ {% endif %} +
+ + TCX Export +
+ +
+ + CSV Export +
-
- - CSV Export -
+
+ {% if workout.uploadedtorunkeeper == 0 %} + {% if user.rower.runkeepertoken == None or user.rower.runkeepertoken == '' %} +
+ + Runkeeper icon +
+ {% else %} +
+ Runkeeper icon +
+ {% endif %} + {% else %} +
+ + Runkeeper icon +
+ {% endif %} +
+
-

Connect

+

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.

+ +
+

connect with strava

+ +
+
+

connect with Concept2

+
+ -
-

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.

- -
-

connect with strava

- -
-
-

connect with Concept2

-
- - -
-

connect with SportTracks

-
- -
+
+

connect with SportTracks

+
+ +
+
+
+

connect with RunKeeper

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

Available on Runkeeper

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

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\w+.*)/s/(?P\w+.*)$',views.rower_teams_view), url(r'^me/teams/s/(?P\w+.*)$',views.rower_teams_view), @@ -251,6 +254,7 @@ urlpatterns = [ url(r'^me/revokeapp/(\d+)$',views.rower_revokeapp_view), url(r'^me/stravaauthorize/$',views.rower_strava_authorize), url(r'^me/sporttracksauthorize/$',views.rower_sporttracks_authorize), + url(r'^me/runkeeperauthorize/$',views.rower_runkeeper_authorize), url(r'^me/sporttracksrefresh/$',views.rower_sporttracks_token_refresh), url(r'^me/c2refresh/$',views.rower_c2_token_refresh), url(r'^me/favoritecharts/$',views.rower_favoritecharts_view), diff --git a/rowers/views.py b/rowers/views.py index d1aca901..0f127a9d 100644 --- a/rowers/views.py +++ b/rowers/views.py @@ -1,4 +1,5 @@ import time +import timestring import zipfile import operator import warnings @@ -46,13 +47,21 @@ import datetime import iso8601 import c2stuff from c2stuff import C2NoTokenError +from runkeeperstuff import RunKeeperNoTokenError +from sporttracksstuff import SportTracksNoTokenError from iso8601 import ParseError import stravastuff import sporttracksstuff +import runkeeperstuff import ownapistuff from ownapistuff import TEST_CLIENT_ID, TEST_CLIENT_SECRET, TEST_REDIRECT_URI -from rowsandall_app.settings import C2_CLIENT_ID, C2_REDIRECT_URI, C2_CLIENT_SECRET, STRAVA_CLIENT_ID, STRAVA_REDIRECT_URI, STRAVA_CLIENT_SECRET -from rowsandall_app.settings import SPORTTRACKS_CLIENT_ID, SPORTTRACKS_REDIRECT_URI, SPORTTRACKS_CLIENT_SECRET +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_ID, SPORTTRACKS_REDIRECT_URI, + SPORTTRACKS_CLIENT_SECRET, + RUNKEEPER_CLIENT_ID,RUNKEEPER_REDIRECT_URI,RUNKEEPER_CLIENT_SECRET, + ) import requests import json @@ -183,8 +192,28 @@ def get_time(second): # get the workout ID from the SportTracks URI -def getidfromsturi(uri): - return uri[len(uri)-8:] +def getidfromsturi(uri,length=8): + return uri[len(uri)-length:] + +def splitrunkeeperlatlongdata(lijst,tname,latname,lonname): + t = [] + lat = [] + lon = [] + for d in lijst: + t.append(d[tname]) + lat.append(d[latname]) + lon.append(d[lonname]) + + return [np.array(t),np.array(lat),np.array(lon)] + +def splitrunkeeperdata(lijst,xname,yname): + x = [] + y = [] + for d in lijst: + x.append(d[xname]) + y.append(d[yname]) + + return [np.array(x),np.array(y)] # Splits SportTracks data which is one long sequence of # [t,[lat,lon],t2,[lat2,lon2] ...] @@ -479,6 +508,166 @@ def add_workout_from_strokedata(user,importid,data,strokedata, return id,message +# Create workout from RunKeeper Data +def add_workout_from_runkeeperdata(user,importid,data): + # To Do - add utcoffset to time + workouttype = data['type'] + if workouttype not in [x[0] for x in Workout.workouttypes]: + workouttype = 'water' + try: + comments = data['notes'] + except: + comments = '' + + try: + utcoffset = tz(data['utcoffset']) + except: + utcoffset = 0 + + r = Rower.objects.get(user=user) + + try: + rowdatetime = iso8601.parse_date(data['start_time']) + except iso8601.ParseError: + try: + rowdatetime = datetime.datetime.strptime(data['start_time'],"%Y-%m-%d %H:%M:%S") + rowdatetime = thetimezone.localize(rowdatetime).astimezone(utc) + except: + try: + rowdatetime = dateutil.parser.parse(data['start_time']) + #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()) + startimeunix += utcoffset*3600 + + + try: + title = data['name'] + except: + title = "Imported data" + + + + res = splitrunkeeperdata(data['distance'],'timestamp','distance') + + distance = res[1] + times_distance = res[0] + + try: + l = data['path'] + + res = splitrunkeeperlatlongdata(l,'timestamp','latitude','longitude') + times_location = res[0] + latcoord = res[1] + loncoord = res[2] + + except: + times_location = times_distance + latcoord = np.zeros(len(times_distance)) + loncoord = np.zeros(len(times_distance)) + if workouttype == 'water': + workouttype = 'rower' + + try: + res = splitrunkeeperdata(data['cadence'],'timestamp','cadence') + times_spm = res[0] + spm = res[1] + except KeyError: + times_spm = times_distance + spm = 0*times_distance + + try: + res = splitrunkeeperdata(data['heart_rate'],'timestamp','heart_rate') + 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 + + 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_stdata(user,importid,data): @@ -509,19 +698,15 @@ def add_workout_from_stdata(user,importid,data): except: rowdatetime = datetime.datetime.strptime(data['date'],"%Y-%m-%d %H:%M:%S") rowdatetime = thetimezone.localize(rowdatetime).astimezone(utc) - -# try: -# c2intervaltype = data['workout_type'] - -# except: -# c2intervaltype = '' + starttimeunix = mktime(rowdatetime.utctimetuple()) + try: title = data['name'] except: title = "Imported data" - starttimeunix = mktime(rowdatetime.utctimetuple()) + res = splitstdata(data['distance']) @@ -677,7 +862,18 @@ def sporttracks_open(user): thetoken = r.sporttrackstoken return thetoken - + +# Checks if user has SportTracks token, renews them if they are expired +def runkeeper_open(user): + r = Rower.objects.get(user=user) + if (r.runkeepertoken == '') or (r.runkeepertoken is None): + s = "Token doesn't exist. Need to authorize" + raise RunKeeperNoTokenError("User has no token") + else: + thetoken = r.runkeepertoken + + return thetoken + # Export workout to TCX and send to user's email address @login_required() def workout_tcxemail_view(request,id=0): @@ -959,6 +1155,70 @@ def workout_c2_upload_view(request,id=0): return HttpResponseRedirect(url) +# Upload workout to RunKeeper +@login_required() +def workout_runkeeper_upload_view(request,id=0): + message = "" + try: + thetoken = runkeeper_open(request.user) + except RunKeeperNoTokenError: + return HttpResponseRedirect("/rowers/me/runkeeperauthorize/") + + # ready to upload. Hurray + try: + w = Workout.objects.get(id=id) + except Workout.DoesNotExist: + raise Http404("Workout doesn't exist") + + if (checkworkoutuser(request.user,w)): + data = runkeeperstuff.createrunkeeperworkoutdata(w) + 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, + 'user-agent': 'sanderroosendaal', + 'Content-Type': 'application/vnd.com.runkeeper.NewFitnessActivity+json', + 'Content-Length':'nnn'} + + import urllib + url = "https://api.runkeeper.com/fitnessActivities" + response = requests.post(url,headers=headers,data=json.dumps(data)) + + # check for duplicate error first + if (response.status_code == 409 ): + message = "Duplicate error" + w.uploadedtorunkeeper = -1 + w.save() + elif (response.status_code == 201 or response.status_code==200): + runkeeperid = runkeeperstuff.getidfromresponse(response) + w.uploadedtorunkeeper = runkeeperid + w.save() + url = "/rowers/workout/"+str(w.id)+"/export" + return HttpResponseRedirect(url) + else: + s = response + print dir(s) + print s.text + message = "Something went wrong in workout_runkeeper_upload_view: %s" % s.reason + + 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): @@ -1053,6 +1313,25 @@ def rower_strava_authorize(request): import urllib url = "https://www.strava.com/oauth/authorize?"+ urllib.urlencode(params) + return HttpResponseRedirect(url) + +# Runkeeper authorization +@login_required() +def rower_runkeeper_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()) + + params = {"client_id": RUNKEEPER_CLIENT_ID, + "response_type": "code", + "state": state, + "redirect_uri": RUNKEEPER_REDIRECT_URI} + + import urllib + url = "https://runkeeper.com/apps/authorize?"+ urllib.urlencode(params) + + return HttpResponseRedirect(url) # SportTracks Authorization @@ -1199,6 +1478,19 @@ def rower_process_stravacallback(request): message = "Something went wrong with the Strava authorization" return imports_view(request,message=message) +# Process Runkeeper callback +@login_required() +def rower_process_runkeepercallback(request): + code = request.GET['code'] + access_token = runkeeperstuff.get_token(code) + + r = Rower.objects.get(user=request.user) + r.runkeepertoken = access_token + + r.save() + + successmessage = "Tokens stored. Good to go" + return imports_view(request,successmessage=successmessage) # Process SportTracks callback @login_required() @@ -3717,6 +4009,13 @@ def workout_export_view(request,id=0, message="", successmessage=""): else: c2userid = 0 + rktoken = runkeeper_open(request.user) + if (checkworkoutuser(request.user,row)) and rktoken: + rkuserid = runkeeperstuff.get_userid(rktoken) + else: + rkuserid = 0 + + form = WorkoutForm(instance=row) g = GraphImage.objects.filter(workout=row).order_by("-creationdatetime") # check if user is owner of this workout @@ -3734,6 +4033,7 @@ def workout_export_view(request,id=0, message="", successmessage=""): 'message':message, 'successmessage':successmessage, 'c2userid':c2userid, + 'rkuserid':rkuserid, }) # @@ -4425,8 +4725,46 @@ def workout_stravaimport_view(request,message=""): }) return HttpResponse(res) - -# The page where you select which SportTracks page to import + +# The page where you select which RunKeeper workout to import +@login_required() +def workout_runkeeperimport_view(request,message=""): + res = runkeeperstuff.get_runkeeper_workout_list(request.user) + if (res.status_code != 200): + if (res.status_code == 401): + r = Rower.objects.get(user=request.user) + if (r.runkeepertoken == '') or (r.runkeepertoken is None): + s = "Token doesn't exist. Need to authorize" + return HttpResponseRedirect("/rowers/me/runkeeperauthorize/") + message = "Something went wrong in workout_runkeeperimport_view" + if settings.DEBUG: + return HttpResponse(res) + else: + url = reverse(workouts_view, + kwargs = { + 'message': str(message) + }) + return HttpResponseRedirect(url) + else: + workouts = [] + for item in res.json()['items']: + d = int(float(item['total_distance'])) + i = getidfromsturi(item['uri'],length=9) + ttot = str(datetime.timedelta(seconds=int(float(item['duration'])))) + s = item['start_time'] + r = item['type'] + keys = ['id','distance','duration','starttime','type'] + values = [i,d,ttot,s,r] + res = dict(zip(keys,values)) + workouts.append(res) + return render(request,'runkeeper_list_import.html', + {'workouts':workouts, + 'message':message, + }) + + return HttpResponse(res) + +# The page where you select which SportTracks workout to import @login_required() def workout_sporttracksimport_view(request,message=""): res = sporttracksstuff.get_sporttracks_workout_list(request.user) @@ -4582,6 +4920,31 @@ def workout_getstravaworkout_view(request,stravaid): return HttpResponseRedirect(url) +# Imports a workout from Runkeeper +@login_required() +def workout_getrunkeeperworkout_view(request,runkeeperid): + res = runkeeperstuff.get_runkeeper_workout(request.user,runkeeperid) + data = res.json() + + id,message = add_workout_from_runkeeperdata(request.user,runkeeperid,data) + w = Workout.objects.get(id=id) + w.uploadedtorunkeeper=runkeeperid + 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 @login_required() def workout_getsporttracksworkout_view(request,sporttracksid): diff --git a/rowsandall_app/settings.py b/rowsandall_app/settings.py index 81a166df..0cd505dd 100644 --- a/rowsandall_app/settings.py +++ b/rowsandall_app/settings.py @@ -226,6 +226,12 @@ SPORTTRACKS_CLIENT_ID = CFG['sporttracks_client_id'] SPORTTRACKS_CLIENT_SECRET = CFG['sporttracks_client_secret'] SPORTTRACKS_REDIRECT_URI = "http://rowsandall.com/sporttracks_callback" +# Runkeeper + +RUNKEEPER_CLIENT_ID = CFG['runkeeper_client_id'] +RUNKEEPER_CLIENT_SECRET = CFG['runkeeper_client_secret'] +RUNKEEPER_REDIRECT_URI = "http://rowsandall.com/runkeeper_callback" + # RQ stuff RQ_QUEUES = { diff --git a/rowsandall_app/urls.py b/rowsandall_app/urls.py index 0f1d9fbf..08834cb7 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'^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/img/rk-icon.png b/static/img/rk-icon.png new file mode 100644 index 00000000..0febe0cc Binary files /dev/null and b/static/img/rk-icon.png differ diff --git a/static/img/rk-icon.xcf b/static/img/rk-icon.xcf new file mode 100644 index 00000000..4ccab7d5 Binary files /dev/null and b/static/img/rk-icon.xcf differ diff --git a/static/img/rk-logo.png b/static/img/rk-logo.png new file mode 100644 index 00000000..e33272ef Binary files /dev/null and b/static/img/rk-logo.png differ diff --git a/static/img/rkchecked.png b/static/img/rkchecked.png new file mode 100644 index 00000000..29ce98af Binary files /dev/null and b/static/img/rkchecked.png differ diff --git a/static/img/rkgray.png b/static/img/rkgray.png new file mode 100644 index 00000000..30b246af Binary files /dev/null and b/static/img/rkgray.png differ diff --git a/static/img/rksquare.png b/static/img/rksquare.png new file mode 100644 index 00000000..406f0f35 Binary files /dev/null and b/static/img/rksquare.png differ