Merge branch 'release/v1.7'
This commit is contained in:
282
rowers/#runkeeperstuff.py#
Normal file
282
rowers/#runkeeperstuff.py#
Normal file
@@ -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
|
||||
@@ -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)
|
||||
|
||||
284
rowers/runkeeperstuff.py
Normal file
284
rowers/runkeeperstuff.py
Normal file
@@ -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
|
||||
@@ -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):
|
||||
|
||||
@@ -44,78 +44,104 @@
|
||||
<div class="grid_1 alpha">
|
||||
<a href="http://log.concept2.com/profile/{{ c2userid }}/log/{{ workout.uploadedtoc2 }}">
|
||||
<img src="/static/img/c2square_checked.png" alt="Concept2 icon" width="60" height="60"></a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if workout.uploadedtostrava == 0 %}
|
||||
{% if user.rower.stravatoken == None or user.rower.stravatoken == '' %}
|
||||
<div class="grid_1">
|
||||
<a href="/rowers/me/stravaauthorize">
|
||||
<img src="/static/img/stravasquare_gray.png" alt="Strava icon" width="60" height="60"></a>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="grid_1">
|
||||
<a href="/rowers/workout/{{ workout.id }}/stravauploadw"><img src="/static/img/stravasquare.png" alt="Strava icon" width="60" height="60"></a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="grid_1">
|
||||
<a href="https://www.strava.com/activities/{{ workout.uploadedtostrava }}">
|
||||
<img src="/static/img/stravasquare_checked.png" alt="Concept2 icon" width="60" height="60"></a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if workout.uploadedtosporttracks == 0 %}
|
||||
{% if user.rower.sporttrackstoken == None or user.rower.sporttrackstoken == '' %}
|
||||
<div class="grid_1">
|
||||
<a href="/rowers/me/sporttracksauthorize">
|
||||
<img src="/static/img/sporttrackssquare_gray.png" alt="SportTracks icon" width="60" height="60"></a>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="grid_1">
|
||||
<a href="/rowers/workout/{{ workout.id }}/sporttracksuploadw">
|
||||
<img src="/static/img/sporttrackssquare.png" alt="SportTracks icon" width="60" height="60"></a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="grid_1">
|
||||
<a href="https://sporttracks.mobi/activity/{{ workout.uploadedtosporttracks }}">
|
||||
<img src="/static/img/sporttrackssquare_checked.png" alt="Concept2 icon" width="60" height="60"></a>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="grid_1">
|
||||
<a href="/rowers/workout/{{ workout.id }}/emailtcx">
|
||||
<img src="/static/img/export.png" alt="TCX Export" width="60" height="60"></a>
|
||||
</div>
|
||||
{% if workout.uploadedtostrava == 0 %}
|
||||
{% if user.rower.stravatoken == None or user.rower.stravatoken == '' %}
|
||||
<div class="grid_1">
|
||||
<a href="/rowers/me/stravaauthorize">
|
||||
<img src="/static/img/stravasquare_gray.png" alt="Strava icon" width="60" height="60"></a>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="grid_1">
|
||||
<a href="/rowers/workout/{{ workout.id }}/stravauploadw"><img src="/static/img/stravasquare.png" alt="Strava icon" width="60" height="60"></a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="grid_1">
|
||||
<a href="https://www.strava.com/activities/{{ workout.uploadedtostrava }}">
|
||||
<img src="/static/img/stravasquare_checked.png" alt="Concept2 icon" width="60" height="60"></a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if workout.uploadedtosporttracks == 0 %}
|
||||
{% if user.rower.sporttrackstoken == None or user.rower.sporttrackstoken == '' %}
|
||||
<div class="grid_1">
|
||||
<a href="/rowers/me/sporttracksauthorize">
|
||||
<img src="/static/img/sporttrackssquare_gray.png" alt="SportTracks icon" width="60" height="60"></a>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="grid_1">
|
||||
<a href="/rowers/workout/{{ workout.id }}/sporttracksuploadw">
|
||||
<img src="/static/img/sporttrackssquare.png" alt="SportTracks icon" width="60" height="60"></a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="grid_1">
|
||||
<a href="https://sporttracks.mobi/activity/{{ workout.uploadedtosporttracks }}">
|
||||
<img src="/static/img/sporttrackssquare_checked.png" alt="Concept2 icon" width="60" height="60"></a>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="grid_1">
|
||||
<a href="/rowers/workout/{{ workout.id }}/emailtcx">
|
||||
<img src="/static/img/export.png" alt="TCX Export" width="60" height="60"></a>
|
||||
</div>
|
||||
|
||||
<div class="grid_1">
|
||||
<a href="/rowers/workout/{{ workout.id }}/emailcsv">
|
||||
<img src="/static/img/CSVsquare.png" alt="CSV Export" width="60" height="60"></a>
|
||||
</div>
|
||||
|
||||
<div class="grid_1">
|
||||
<a href="/rowers/workout/{{ workout.id }}/emailcsv">
|
||||
<img src="/static/img/CSVsquare.png" alt="CSV Export" width="60" height="60"></a>
|
||||
</div>
|
||||
<div class="grid_6 alpha">
|
||||
{% if workout.uploadedtorunkeeper == 0 %}
|
||||
{% if user.rower.runkeepertoken == None or user.rower.runkeepertoken == '' %}
|
||||
<div class="grid_1 alpha">
|
||||
<a href="/rowers/me/runkeeperauthorize">
|
||||
<img src="/static/img/rkgray.png" alt="Runkeeper icon" width="60" height="60"></a>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="grid_1 alpha">
|
||||
<a href="/rowers/workout/{{ workout.id }}/runkeeperuploadw"><img src="/static/img/rksquare.png" alt="Runkeeper icon" width="60" height="60"></a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="grid_1 alpha">
|
||||
<a href="https://runkeeper.com/fitnessActivity/{{ workout.uploadedtorunkeeper }}">
|
||||
<img src="/static/img/rkchecked.png" alt="Runkeeper icon" width="60" height="60"></a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="grid_6 omega">
|
||||
<h3>Connect</h3>
|
||||
<h3>Connect</h3>
|
||||
|
||||
<div class="grid_6">
|
||||
<p>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.</p>
|
||||
|
||||
<div class="grid_2 alpha">
|
||||
<p><a href="/rowers/me/stravaauthorize/"><img src="/static/img/ConnectWithStrava.png" alt="connect with strava" width="120"></a></p>
|
||||
|
||||
</div>
|
||||
<div class="grid_2">
|
||||
<p><a href="/rowers/me/c2authorize/"><img src="/static/img/blueC2logo.png" alt="connect with Concept2" width="120"></a></p>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="grid_6">
|
||||
<p>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.</p>
|
||||
|
||||
<div class="grid_2 alpha">
|
||||
<p><a href="/rowers/me/stravaauthorize/"><img src="/static/img/ConnectWithStrava.png" alt="connect with strava" width="120"></a></p>
|
||||
|
||||
</div>
|
||||
<div class="grid_2">
|
||||
<p><a href="/rowers/me/c2authorize/"><img src="/static/img/blueC2logo.png" alt="connect with Concept2" width="120"></a></p>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="grid_2 omega">
|
||||
<p><a href="/rowers/me/sporttracksauthorize/"><img src="/static/img/sporttracks-button.png" alt="connect with SportTracks" width="120"></a></p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="grid_2 omega">
|
||||
<p><a href="/rowers/me/sporttracksauthorize/"><img src="/static/img/sporttracks-button.png" alt="connect with SportTracks" width="120"></a></p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="grid_6">
|
||||
<div class="grid_2 alpha suffix_4">
|
||||
<p><a href="/rowers/me/runkeeperauthorize/"><img src="/static/img/rk-logo.png" alt="connect with RunKeeper" width="120"></a></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
@@ -3,62 +3,77 @@
|
||||
{% block content %}
|
||||
|
||||
<div class="grid_6 alpha">
|
||||
<h3>Import Workouts</h3>
|
||||
<h3>Import Workouts</h3>
|
||||
|
||||
<div class="grid_6">
|
||||
<div class="grid_3 alpha">
|
||||
<p>
|
||||
<div class="grid_6">
|
||||
<div class="grid_3 alpha">
|
||||
<p>
|
||||
<a href="/rowers/workout/stravaimport"><img src="/static/img/stravalogo.png" alt="strava logo" width="140"></a>
|
||||
</p>
|
||||
</div>
|
||||
<div class="grid_3 omega">
|
||||
<p>Import workouts from Strava</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid_6">
|
||||
<div class="grid_3 alpha">
|
||||
<p>
|
||||
</p>
|
||||
</div>
|
||||
<div class="grid_3 omega">
|
||||
<p>Import workouts from Strava</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid_6">
|
||||
<div class="grid_3 alpha">
|
||||
<p>
|
||||
<a href="/rowers/workout/c2import"><img src="/static/img/blueC2logo.png" alt="Concept2 logo" width="140"></a>
|
||||
</p>
|
||||
</div>
|
||||
<div class="grid_3 omega">
|
||||
<p>Import workouts from the Concept2 logbook</p>
|
||||
</div>
|
||||
<div class="grid_3 alpha">
|
||||
<p>
|
||||
</p>
|
||||
</div>
|
||||
<div class="grid_3 omega">
|
||||
<p>Import workouts from the Concept2 logbook</p>
|
||||
</div>
|
||||
<div class="grid_3 alpha">
|
||||
<p>
|
||||
<a href="/rowers/workout/sporttracksimport"><img src="/static/img/sporttracks-button.png" alt="SportTracks logo" width="140"></a>
|
||||
</p>
|
||||
</div>
|
||||
<div class="grid_3 omega">
|
||||
<p>Import workouts from SportTracks</p>
|
||||
</div>
|
||||
</div>
|
||||
</p>
|
||||
</div>
|
||||
<div class="grid_3 omega">
|
||||
<p>Import workouts from SportTracks</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid_6">
|
||||
<div class="grid_3 alpha">
|
||||
<p>
|
||||
<a href="/rowers/workout/runkeeperimport"><img src="/static/img/rk-logo.png" alt="Runkeeper logo" width="140"></a>
|
||||
</p>
|
||||
</div>
|
||||
<div class="grid_3 omega">
|
||||
<p>Import workouts from RunKeeper</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid_6 omega">
|
||||
<h3>Connect</h3>
|
||||
<h3>Connect</h3>
|
||||
|
||||
<div class="grid_6">
|
||||
<p>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.</p>
|
||||
<div class="grid_6">
|
||||
<p>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.</p>
|
||||
|
||||
<div class="grid_2 alpha">
|
||||
<p><a href="/rowers/me/stravaauthorize/"><img src="/static/img/ConnectWithStrava.png" alt="connect with strava" width="120"></a></p>
|
||||
<div class="grid_2 alpha">
|
||||
<p><a href="/rowers/me/stravaauthorize/"><img src="/static/img/ConnectWithStrava.png" alt="connect with strava" width="120"></a></p>
|
||||
|
||||
</div>
|
||||
<div class="grid_2">
|
||||
<p><a href="/rowers/me/c2authorize/"><img src="/static/img/blueC2logo.png" alt="connect with Concept2" width="120"></a></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid_2">
|
||||
<p><a href="/rowers/me/c2authorize/"><img src="/static/img/blueC2logo.png" alt="connect with Concept2" width="120"></a></p>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="grid_2 omega">
|
||||
<p><a href="/rowers/me/sporttracksauthorize/"><img src="/static/img/sporttracks-button.png" alt="connect with SportTracks" width="120"></a></p>
|
||||
</div>
|
||||
<div class="grid_2 omega">
|
||||
<p><a href="/rowers/me/sporttracksauthorize/"><img src="/static/img/sporttracks-button.png" alt="connect with SportTracks" width="120"></a></p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid_6">
|
||||
<div class="grid_2 alpha suffix_4">
|
||||
<p><a href="/rowers/me/runkeeperauthorize/"><img src="/static/img/rk-logo.png" alt="connect with RunKeeper" width="120"></a></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{% endblock content %}
|
||||
|
||||
|
||||
|
||||
37
rowers/templates/runkeeper_list_import.html
Normal file
37
rowers/templates/runkeeper_list_import.html
Normal 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 %}
|
||||
@@ -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<message>\w+.*)/s/(?P<successmessage>\w+.*)$',views.rower_teams_view),
|
||||
url(r'^me/teams/s/(?P<successmessage>\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),
|
||||
|
||||
390
rowers/views.py
390
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=""):
|
||||
c2userid = c2stuff.get_userid(thetoken)
|
||||
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")
|
||||
@@ -3735,6 +4031,7 @@ def workout_export_view(request,id=0, message="", successmessage=""):
|
||||
{'workout':row,
|
||||
'message':message,
|
||||
'successmessage':successmessage,
|
||||
'c2userid':c2userid,
|
||||
'rkuserid':rkuserid,
|
||||
})
|
||||
|
||||
@@ -4426,8 +4723,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'],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=""):
|
||||
@@ -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()
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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')),
|
||||
]
|
||||
|
||||
BIN
static/img/rk-icon.png
Normal file
BIN
static/img/rk-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
BIN
static/img/rk-icon.xcf
Normal file
BIN
static/img/rk-icon.xcf
Normal file
Binary file not shown.
BIN
static/img/rk-logo.png
Normal file
BIN
static/img/rk-logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
BIN
static/img/rkchecked.png
Normal file
BIN
static/img/rkchecked.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 58 KiB |
BIN
static/img/rkgray.png
Normal file
BIN
static/img/rkgray.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 29 KiB |
BIN
static/img/rksquare.png
Normal file
BIN
static/img/rksquare.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 57 KiB |
Reference in New Issue
Block a user