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/runkeeperstuff.py b/rowers/runkeeperstuff.py index c29f8f7e..25866bc8 100644 --- a/rowers/runkeeperstuff.py +++ b/rowers/runkeeperstuff.py @@ -260,3 +260,23 @@ def getidfromresponse(response): 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/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/views.py b/rowers/views.py index a69ecc10..0f127a9d 100644 --- a/rowers/views.py +++ b/rowers/views.py @@ -4009,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 @@ -4026,6 +4033,7 @@ def workout_export_view(request,id=0, message="", successmessage=""): 'message':message, 'successmessage':successmessage, 'c2userid':c2userid, + 'rkuserid':rkuserid, }) # 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