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 @@
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/imports.html b/rowers/templates/imports.html index 640cae9a..6bfda942 100644 --- a/rowers/templates/imports.html +++ b/rowers/templates/imports.html @@ -3,62 +3,77 @@ {% block content %}
-

Import Workouts

+

Import Workouts

-
-
-

+

+
+

strava logo -

-
-
-

Import workouts from Strava

-
-
-
-
-

+

+
+
+

Import workouts from Strava

+
+
+
+
+

Concept2 logo -

-
-
-

Import workouts from the Concept2 logbook

-
-
-

+

+
+
+

Import workouts from the Concept2 logbook

+
+
+

SportTracks logo -

-
-
-

Import workouts from SportTracks

-
-
+

+
+
+

Import workouts from SportTracks

+
+
+
+
+

+ Runkeeper logo +

+
+
+

Import workouts from RunKeeper

+
+
-

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.

+
+

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 strava

-
-
-

connect with Concept2

-
+
+
+

connect with Concept2

+
-
-

connect with SportTracks

-
+
+

connect with SportTracks

+
-
+
+
+
+

connect with RunKeeper

+
+
{% endblock content %} - \ No newline at end of file + 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 9e9059c1..4f803ea6 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] ...] @@ -253,7 +282,6 @@ def rower_register_view(request): message += "The first thing you might want to do is check and edit the heart rate band values. After logging in, click the button with your first name.\n" message += "You can also check our videos page at http://rowsandall.com/rowers/videos for some helpful instruction videos.\n\n" message += "User name:"+username+"\n" - message += "Password :"+password+"\n\n" message += "For all your questions, just reply to this email.\n\n" message += "Happy rowing!\n\n\n" message += "Oh, one more thing. The site is currently in beta and is developing fast. Bear with us. Don't hesitate to contact me if anything is broken or doesn't seem to work as advertised." @@ -480,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()) + starttimeunix += 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): @@ -510,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']) @@ -678,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): @@ -960,6 +1155,68 @@ 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 + message = "Something went wrong in workout_runkeeper_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): @@ -1054,6 +1311,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 @@ -1200,6 +1476,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() @@ -3718,6 +4007,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 @@ -3735,6 +4031,7 @@ def workout_export_view(request,id=0, message="", successmessage=""): 'message':message, 'successmessage':successmessage, 'c2userid':c2userid, + 'rkuserid':rkuserid, }) # @@ -4426,8 +4723,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) @@ -4583,6 +4918,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