Private
Public Access
1
0

working on workout import (auth and workout list done)

This commit is contained in:
Sander Roosendaal
2017-03-22 15:55:13 +01:00
parent 01fe1e8807
commit d89219a2df
7 changed files with 350 additions and 4 deletions

View File

@@ -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)

188
rowers/runkeeperstuff.py Normal file
View File

@@ -0,0 +1,188 @@
# 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,
)
# 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
# Generate Workout data for Runkeeper (a TCX file)
def createrunkeeperworkoutdata(w):
filename = w.csvfilename
try:
row = rowingdata(filename)
tcxfilename = filename[:-4]+'.tcx'
row.exporttotcx(tcxfilename,notes=w.notes)
except:
tcxfilename = 0
return tcxfilename
# Upload the TCX file to Runkeeper and set the workout activity type
# to rowing on Runkeeper
def handle_runkeeperexport(f2,workoutname,runkeepertoken,description=''):
# w = Workout.objects.get(id=workoutid)
client = runkeeperlib.Client(access_token=runkeepertoken)
act = client.upload_activity(f2,'tcx',name=workoutname)
try:
res = act.wait(poll_interval=5.0,timeout=30)
message = 'Workout successfully synchronized to Runkeeper'
except:
res = 0
# description doesn't work yet. Have to wait for runkeeperlib to update
if res:
act = client.update_activity(res.id,activity_type='Rowing',description=description)
else:
message = 'Runkeeper upload timed out.'
return (0,message)
return (res.id,message)

View File

@@ -0,0 +1,37 @@
{% extends "base.html" %}
{% load staticfiles %}
{% load rowerfilters %}
{% block title %}Workouts{% endblock %}
{% block content %}
<h1>Available on Runkeeper</h1>
{% if workouts %}
<table width="70%" class="listtable">
<thead>
<tr>
<th> Import </th>
<th> Date/Time </th>
<th> Duration </th>
<th> Total Distance</th>
<th> Type</th>
</tr>
</thead>
<tbody>
{% for workout in workouts %}
<tr>
<td>
<a href="/rowers/workout/runkeeperimport/{{ workout|lookup:'id' }}/">Import</a></td>
<td>{{ workout|lookup:'starttime' }}</td>
<td>{{ workout|lookup:'duration' }} </td>
<td>{{ workout|lookup:'distance' }} m</td>
<td>{{ workout|lookup:'type' }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p> No workouts found </p>
{% endif %}
{% endblock %}

View File

@@ -212,6 +212,8 @@ 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),
@@ -251,6 +253,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),

View File

@@ -49,10 +49,16 @@ from c2stuff import C2NoTokenError
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
@@ -1053,6 +1059,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 +1224,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()
@@ -4425,8 +4463,46 @@ def workout_stravaimport_view(request,message=""):
'message':message,
})
return HttpResponse(res)
return HttpResponse(res)
# 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'])
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=""):
@@ -4582,6 +4658,34 @@ 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)
print res.json()
data = res.json()
return HttpResponse(data)
id,message = add_workout_from_stdata(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()

View File

@@ -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 = {

View File

@@ -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')),
]