diff --git a/logos/tpchecked.png b/logos/tpchecked.png
new file mode 100644
index 00000000..a1d9e93d
Binary files /dev/null and b/logos/tpchecked.png differ
diff --git a/logos/tpgray.png b/logos/tpgray.png
new file mode 100644
index 00000000..2f7593f9
Binary files /dev/null and b/logos/tpgray.png differ
diff --git a/logos/tpicon.png b/logos/tpicon.png
new file mode 100644
index 00000000..dec832ff
Binary files /dev/null and b/logos/tpicon.png differ
diff --git a/logos/tpicon.xcf b/logos/tpicon.xcf
new file mode 100644
index 00000000..9784e76b
Binary files /dev/null and b/logos/tpicon.xcf differ
diff --git a/rowers/dataprep.py b/rowers/dataprep.py
index ea252e89..64e18712 100644
--- a/rowers/dataprep.py
+++ b/rowers/dataprep.py
@@ -28,6 +28,7 @@ from rowingdata import (
)
from rowers.models import Team
+from rowers.metrics import axes
import os
import zipfile
@@ -363,7 +364,7 @@ def save_workout_database(f2,r,dosmooth=True,workouttype='rower',
velo = 500./pace
f = row.df['TimeStamp (sec)'].diff().mean()
- if f !=0:
+ if f !=0 and not np.isnan(f):
windowsize = 2*(int(10./(f)))+1
else:
windowsize = 1
@@ -899,6 +900,13 @@ def prepmultipledata(ids,verbose=False):
# Read a set of columns for a set of workout ids, returns data as a
# pandas dataframe
def read_cols_df_sql(ids,columns):
+ # drop columns that are not in offical list
+# axx = [ax[0] for ax in axes]
+ axx = StrokeData._meta.get_all_field_names()
+ for c in columns:
+ if not c in axx:
+ columns.remove(c)
+
columns = list(columns)+['distance','spm']
columns = [x for x in columns if x != 'None']
columns = list(set(columns))
diff --git a/rowers/interactiveplots.py b/rowers/interactiveplots.py
index 0fe5bfe6..d3eeefdf 100644
--- a/rowers/interactiveplots.py
+++ b/rowers/interactiveplots.py
@@ -82,6 +82,7 @@ def interactive_forcecurve(theworkouts,workstrokesonly=False):
'workoutstate','driveenergy']
rowdata = dataprep.getsmallrowdata_db(columns,ids=ids)
+
rowdata.dropna(axis=1,how='all',inplace=True)
rowdata.dropna(axis=0,how='any',inplace=True)
@@ -369,6 +370,8 @@ def interactive_histoall(theworkouts):
ids = [int(w.id) for w in theworkouts]
rowdata = dataprep.getsmallrowdata_db(['power'],ids=ids,doclean=True)
+
+
rowdata.dropna(axis=0,how='any',inplace=True)
if rowdata.empty:
@@ -802,6 +805,7 @@ def interactive_chart(id=0,promember=0):
columns = ['time','pace','hr','fpace','ftime']
datadf = dataprep.getsmallrowdata_db(columns,ids=[id])
+
datadf.dropna(axis=0,how='any',inplace=True)
row = Workout.objects.get(id=id)
if datadf.empty:
@@ -883,6 +887,17 @@ def interactive_cum_flex_chart2(theworkouts,promember=0,
columns = [xparam,yparam1,yparam2,'spm','driveenergy','distance']
datadf = dataprep.getsmallrowdata_db(columns,ids=ids,doclean=False)
+ try:
+ tests = rowdata[yparam2]
+ except KeyError:
+ yparam2 = 'None'
+
+ try:
+ tests = rowdata[yparam1]
+ except KeyError:
+ yparam1 = 'None'
+
+
yparamname1 = axlabels[yparam1]
if yparam2 != 'None':
yparamname2 = axlabels[yparam2]
@@ -1167,6 +1182,16 @@ def interactive_flex_chart2(id=0,promember=0,
rowdata = dataprep.getsmallrowdata_db(columns,ids=[id],doclean=True,
workstrokesonly=workstrokesonly)
+ try:
+ tests = rowdata[yparam2]
+ except KeyError:
+ yparam2 = 'None'
+
+ try:
+ tests = rowdata[yparam1]
+ except KeyError:
+ yparam1 = 'None'
+
rowdata.dropna(axis=1,how='all',inplace=True)
rowdata.dropna(axis=0,how='any',inplace=True)
diff --git a/rowers/models.py b/rowers/models.py
index b573169c..49aeba2b 100644
--- a/rowers/models.py
+++ b/rowers/models.py
@@ -199,10 +199,17 @@ class Rower(models.Model):
underarmourtokenexpirydate = models.DateTimeField(blank=True,null=True)
underarmourrefreshtoken = models.CharField(default='',max_length=200,
blank=True,null=True)
+ tptoken = models.CharField(default='',max_length=200,blank=True,null=True)
+ tptokenexpirydate = models.DateTimeField(blank=True,null=True)
+ tprefreshtoken = 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)
@@ -365,6 +372,7 @@ class Workout(models.Model):
uploadedtostrava = models.IntegerField(default=0)
uploadedtosporttracks = models.IntegerField(default=0)
uploadedtounderarmour = models.IntegerField(default=0)
+ uploadedtotp = models.IntegerField(default=0)
uploadedtorunkeeper = models.IntegerField(default=0)
# empower stuff
diff --git a/rowers/stravastuff.py b/rowers/stravastuff.py
index 98add5b2..e03b288b 100644
--- a/rowers/stravastuff.py
+++ b/rowers/stravastuff.py
@@ -14,6 +14,7 @@ import time
import math
from math import sin,cos,atan2,sqrt
import os,sys
+import gzip
# Django
from django.shortcuts import render_to_response
@@ -230,6 +231,13 @@ def createstravaworkoutdata(w):
row = rowingdata(filename)
tcxfilename = filename[:-4]+'.tcx'
row.exporttotcx(tcxfilename,notes=w.notes)
+ gzfilename = tcxfilename+'.gz'
+ with file(tcxfilename,'rb') as inF:
+ s = inF.read()
+ with gzip.GzipFile(gzfilename,'wb') as outF:
+ outF.write(s)
+ os.remove(tcxfilename)
+ return gzfilename
except:
tcxfilename = 0
@@ -241,7 +249,7 @@ def handle_stravaexport(f2,workoutname,stravatoken,description=''):
# w = Workout.objects.get(id=workoutid)
client = stravalib.Client(access_token=stravatoken)
- act = client.upload_activity(f2,'tcx',name=workoutname)
+ act = client.upload_activity(f2,'tcx.gz',name=workoutname)
try:
res = act.wait(poll_interval=5.0,timeout=30)
message = 'Workout successfully synchronized to Strava'
diff --git a/rowers/templates/400.html b/rowers/templates/400.html
index 43d7e090..025dfc60 100644
--- a/rowers/templates/400.html
+++ b/rowers/templates/400.html
@@ -1,4 +1,4 @@
-{% extends "base.html" %}
+{% extends "basenofilters.html" %}
{% load staticfiles %}
{% load rowerfilters %}
diff --git a/rowers/templates/403.html b/rowers/templates/403.html
index 316b4872..3a3d7df7 100644
--- a/rowers/templates/403.html
+++ b/rowers/templates/403.html
@@ -1,4 +1,4 @@
-{% extends "base.html" %}
+{% extends "basenofilters.html" %}
{% load staticfiles %}
{% load rowerfilters %}
diff --git a/rowers/templates/404.html b/rowers/templates/404.html
index 0116bd84..33fd4812 100644
--- a/rowers/templates/404.html
+++ b/rowers/templates/404.html
@@ -1,6 +1,5 @@
-{% extends "base.html" %}
+{% extends "basenofilters.html" %}
{% load staticfiles %}
-{% load rowerfilters %}
{% block title %}Change Workout {% endblock %}
diff --git a/rowers/templates/500.html b/rowers/templates/500.html
index 258d606b..d60793eb 100644
--- a/rowers/templates/500.html
+++ b/rowers/templates/500.html
@@ -1,6 +1,5 @@
-{% extends "base.html" %}
+{% extends "basenofilters.html" %}
{% load staticfiles %}
-{% load rowerfilters %}
{% block title %}Change Workout {% endblock %}
diff --git a/rowers/templates/base.html b/rowers/templates/base.html
index 8e6487a2..77fa6213 100644
--- a/rowers/templates/base.html
+++ b/rowers/templates/base.html
@@ -1,149 +1,9 @@
-{% load cookielaw_tags %}
-{% load analytical %}
-{% load rowerfilters %}
-
-
-
-
- {% analytical_head_top %}
- {% if GOOGLE_ANALYTICS_PROPERTY_ID %}
- {% include "ga.html" %}
- {% endif %}
-
-
+{% extends "basebase.html" %}
+{% block filters %}
+ {% load rowerfilters %}
+{% endblock %}
-
-
-
-
-
-
-
- Rowsandall
-
-
-
-
- {% block meta %} {% endblock %}
- {% analytical_head_bottom %}
-
-
- {% analytical_body_top %}
-
-
-
-
-
-
- {% if user.rower.rowerplan == 'pro' or user.rower.rowerplan == 'coach' %}
-
- {% else %}
-
- {% endif %}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Free Data and Analysis. For Rowers. By Rowers.
-
-
-
-
-
-
- {% if user.is_authenticated %}
-
logout
- {% else %}
-
 
- {% endif %}
-
-
-
- {% if user.rower.rowerplan == 'pro' or user.rower.rowerplan == 'coach' %}
-
Pro Member
- {% else %}
-
- {% endif %}
-
-
-
-
-
-
-
-
-
-
-
-
+{% endblock %}
-
-
-
- {% block message %}
- {% if message %}
-
- {{ message }}
-
- {% endif %}
- {% if successmessage %}
-
- {{ successmessage }}
-
- {% endif %}
- {% endblock %}
-
-
- {% load tz %}
-
- {% block content %}{% endblock %}
-
-
-
-
- {% block footer %}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {% endblock %}
-
- {% cookielaw_banner %}
-
-
- {% analytical_body_bottom %}
-
-
+{% block content %}
+
+{% endblock %}
diff --git a/rowers/templates/basebase.html b/rowers/templates/basebase.html
new file mode 100644
index 00000000..a97bbb60
--- /dev/null
+++ b/rowers/templates/basebase.html
@@ -0,0 +1,219 @@
+{% load cookielaw_tags %}
+{% load analytical %}
+{% block filters %}
+{% endblock %}
+
+
+
+
+ {% analytical_head_top %}
+ {% if GOOGLE_ANALYTICS_PROPERTY_ID %}
+ {% include "ga.html" %}
+ {% endif %}
+
+
+
+
+
+
+
+
+
+
+
Rowsandall
+
+
+
+
+ {% block meta %} {% endblock %}
+ {% analytical_head_bottom %}
+
+
+ {% analytical_body_top %}
+ {% block body_top %}{% endblock %}
+
+
+
+
+
+
+ {% if user.rower.rowerplan == 'pro' or user.rower.rowerplan == 'coach' %}
+
+ {% else %}
+
+ {% endif %}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Free Data and Analysis. For Rowers. By Rowers.
+
+
+
+
+
+
+ {% if user.is_authenticated %}
+
logout
+ {% else %}
+
 
+ {% endif %}
+
+
+
+ {% if user.rower.rowerplan == 'pro' or user.rower.rowerplan == 'coach' %}
+
Pro Member
+ {% else %}
+
+ {% endif %}
+
+
+
+
+
+
+
+
+
+
+
+
+ {% block teams %}
+ {% endblock %}
+
+
+
+
+
+
+ {% block message %}
+ {% if message %}
+
+ {{ message }}
+
+ {% endif %}
+ {% if successmessage %}
+
+ {{ successmessage }}
+
+ {% endif %}
+ {% endblock %}
+
+
+ {% load tz %}
+
+ {% block content %}{% endblock %}
+
+
+
+
+ {% block footer %}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {% endblock %}
+
+ {% cookielaw_banner %}
+
+
+ {% analytical_body_bottom %}
+ {% block body_bottom %}{% endblock %}
+
+
diff --git a/rowers/templates/basefront.html b/rowers/templates/basefront.html
index 15017318..9bf96b9c 100644
--- a/rowers/templates/basefront.html
+++ b/rowers/templates/basefront.html
@@ -1,34 +1,12 @@
-{% load cookielaw_tags %}
-{% load analytical %}
-{% load rowerfilters %}
-
-
-
-
- {% analytical_head_top %}
- {% if GOOGLE_ANALYTICS_PROPERTY_ID %}
- {% include "ga.html" %}
- {% endif %}
-
-
-
-
-
-
-
-
-
-
-
Rowsandall
-
-
-
-
- {% block meta %} {% endblock %}
- {% analytical_head_bottom %}
-
-
+{% extends "basebase.html" %}
+{% block filters %}
+ {% load rowerfilters %}
+{% endblock %}
+{% block meta %}
+{% endblock %}
+
+{% block body_top %}
-
-
- {% analytical_body_top %}
-
-
-
-
-
-
-
- {% if user.rower.rowerplan == 'pro' or user.rower.rowerplan == 'coach' %}
-
- {% else %}
-
- {% endif %}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Free Data and Analysis. For Rowers. By Rowers.
-
-
-
-
-
- {% if user.is_authenticated %}
-
logout
- {% else %}
-
 
- {% endif %}
-
-
-
- {% if user.rower.rowerplan == 'pro' or user.rower.rowerplan == 'coach' %}
-
Pro Member
- {% else %}
-
- {% endif %}
-
-
-
-
-
-
-
-
-
-
-
-
{% endif %}
+
+ {% if workout.uploadedtotp == 0 %}
+ {% if user.rower.tptoken == None or user.rower.tptoken == '' %}
+
+
+
+
+ {% else %}
+
+
+
+ {% endif %}
+ {% else %}
+
+
+
+
+ {% endif %}
@@ -162,6 +180,9 @@
+
+
+
diff --git a/rowers/tpstuff.py b/rowers/tpstuff.py
new file mode 100644
index 00000000..7638f7f2
--- /dev/null
+++ b/rowers/tpstuff.py
@@ -0,0 +1,251 @@
+# 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
+import gzip
+from math import sin,cos,atan2,sqrt
+import os,sys
+import urllib
+import base64
+from io import BytesIO
+
+# 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,
+ TP_CLIENT_ID, TP_CLIENT_SECRET,
+ TP_REDIRECT_URI,TP_CLIENT_KEY,
+ )
+
+tpapilocation = "https://api.sandbox.trainingpeaks.com"
+
+# Custom error class - to raise a NoTokenError
+class TPNoTokenError(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
+
+# Refresh ST token using refresh token
+def do_refresh_token(refreshtoken):
+ client_auth = requests.auth.HTTPBasicAuth(TP_CLIENT_KEY, TP_CLIENT_SECRET)
+ post_data = {"grant_type": "refresh_token",
+ "client_secret": TP_CLIENT_SECRET,
+ "client_id":TP_CLIENT_KEY,
+ "refresh_token": refreshtoken,
+ }
+ headers = {'user-agent': 'sanderroosendaal',
+ 'Accept': 'application/json',
+ 'Content-Type': 'application/x-www-form-urlencoded',
+ }
+
+
+ url = "https://oauth.sandbox.trainingpeaks.com/oauth/token"
+
+ response = requests.post(url,
+ data=post_data,
+ headers=headers)
+
+ token_json = response.json()
+ thetoken = token_json['access_token']
+ expires_in = token_json['expires_in']
+ try:
+ refresh_token = token_json['refresh_token']
+ except KeyError:
+ refresh_token = refreshtoken
+
+ return [thetoken,expires_in,refresh_token]
+
+# Exchange access code for long-lived access token
+def get_token(code):
+ client_auth = requests.auth.HTTPBasicAuth(TP_CLIENT_KEY, TP_CLIENT_SECRET)
+ post_data = {
+ "client_id":TP_CLIENT_KEY,
+ "grant_type": "authorization_code",
+ "code": code,
+ "redirect_uri":TP_REDIRECT_URI,
+ "client_secret": TP_CLIENT_SECRET,
+ }
+ headers = {
+ 'Content-Type': 'application/x-www-form-urlencoded',
+ }
+
+ response = requests.post("https://oauth.sandbox.trainingpeaks.com/oauth/token",
+ data=post_data)
+
+
+ try:
+ token_json = response.json()
+ thetoken = token_json['access_token']
+ expires_in = token_json['expires_in']
+ refresh_token = token_json['refresh_token']
+ except KeyError:
+ thetoken = 0
+
+ return thetoken,expires_in,refresh_token
+
+# 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": TP_CLIENT_KEY,
+ "response_type": "code",
+ "redirect_uri": TP_REDIRECT_URI,
+ "scope": "file:write",
+ }
+ url = "https://oauth.sandbox.trainingpeaks.com/oauth/authorize?" +urllib.urlencode(params)
+
+ return HttpResponseRedirect(url)
+
+
+
+def getidfromresponse(response):
+ t = json.loads(response.text)
+
+ links = t["_links"]
+
+ id = links["self"][0]["id"]
+
+ return int(id)
+
+def createtpworkoutdata(w):
+ filename = w.csvfilename
+ try:
+ row = rowingdata(filename)
+ tcxfilename = filename[:-4]+'.tcx'
+ row.exporttotcx(tcxfilename,notes=w.notes)
+# with file(tcxfilename,'rb') as inF:
+# s = inF.read()
+# with gzip.GzipFile(tcxfilename+'.gz','wb') as outF:
+# outF.write(s)
+ return tcxfilename
+ except:
+ tcxfilename = 0
+
+ return tcxfilename
+
+def tp_check(access_token):
+ headers = {
+ "Content-Type": "application/json",
+ 'Accept': 'application/json',
+ 'authorization': 'Bearer %s' % access_token
+ }
+
+ resp = requests.post(tpapilocation+"/v1/info/version",
+ headers=headers)
+
+ return resp
+
+def uploadactivity(access_token,filename,description='',
+ name='Rowsandall.com workout'):
+ data_gz = BytesIO()
+ with file(filename,'rb') as inF:
+ s = inF.read()
+ with gzip.GzipFile(fileobj=data_gz,mode="w") as gzf:
+ gzf.write(s)
+
+ headers = {
+ "Content-Type": "application/json",
+ 'Accept': 'application/json',
+ 'authorization': 'Bearer %s' % access_token
+ }
+
+
+ data = {
+ "UploadClient": "rowsandall",
+ "Filename": filename,
+ "SetWorkoutPublic": True,
+ "Title":name,
+ "Type": "rowing",
+ "Comment": description,
+ "Data": base64.b64encode(data_gz.getvalue()).decode("ascii")
+ }
+
+ resp = requests.post(tpapilocation+"/v1/file",
+ data = json.dumps(data),
+ headers=headers)
+
+
+ if resp.status_code != 200:
+ print resp.status_code
+ print resp.reason
+ print ""
+ print headers
+ print ""
+ return 0
+ else:
+ return resp.json()[0]["Id"]
+
+ return 0
+
+
diff --git a/rowers/underarmourstuff.py b/rowers/underarmourstuff.py
index 7790552d..ad393b7c 100644
--- a/rowers/underarmourstuff.py
+++ b/rowers/underarmourstuff.py
@@ -142,6 +142,8 @@ def get_token(code):
refresh_token = token_json['refresh_token']
except KeyError:
thetoken = 0
+ expires_in = 30
+ refresh_token = ''
return thetoken,expires_in,refresh_token
diff --git a/rowers/urls.py b/rowers/urls.py
index 8fe5c55d..219baa58 100644
--- a/rowers/urls.py
+++ b/rowers/urls.py
@@ -110,6 +110,7 @@ urlpatterns = [
url(r'^api-docs$', views.schema_view),
url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')),
url(r'^api/workouts/(?P
\d+)/strokedata$',views.strokedatajson),
+ url(r'^500v/$',views.error500_view),
url(r'^500/$', TemplateView.as_view(template_name='500.html'),name='500'),
url(r'^404/$', TemplateView.as_view(template_name='404.html'),name='404'),
url(r'^400/$', TemplateView.as_view(template_name='400.html'),name='400'),
@@ -238,6 +239,7 @@ urlpatterns = [
url(r'^workout/(\d+)/sporttracksuploadw/$',views.workout_sporttracks_upload_view),
url(r'^workout/(\d+)/runkeeperuploadw/$',views.workout_runkeeper_upload_view),
url(r'^workout/(\d+)/underarmouruploadw/$',views.workout_underarmour_upload_view),
+ url(r'^workout/(\d+)/tpuploadw/$',views.workout_tp_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),
@@ -273,9 +275,11 @@ urlpatterns = [
url(r'^me/stravaauthorize/$',views.rower_strava_authorize),
url(r'^me/sporttracksauthorize/$',views.rower_sporttracks_authorize),
url(r'^me/underarmourauthorize/$',views.rower_underarmour_authorize),
+ url(r'^me/tpauthorize/$',views.rower_tp_authorize),
url(r'^me/runkeeperauthorize/$',views.rower_runkeeper_authorize),
url(r'^me/sporttracksrefresh/$',views.rower_sporttracks_token_refresh),
url(r'^me/underarmourrefresh/$',views.rower_underarmour_token_refresh),
+ url(r'^me/tprefresh/$',views.rower_tp_token_refresh),
url(r'^me/c2refresh/$',views.rower_c2_token_refresh),
url(r'^me/favoritecharts/$',views.rower_favoritecharts_view),
url(r'^email/send/$', views.sendmail),
diff --git a/rowers/views.py b/rowers/views.py
index 580a672a..c87b132f 100644
--- a/rowers/views.py
+++ b/rowers/views.py
@@ -3,6 +3,7 @@ import timestring
import zipfile
import operator
import warnings
+import urllib
from numbers import Number
from django.views.generic.base import TemplateView
from django.db.models import Q
@@ -53,10 +54,12 @@ import c2stuff
from c2stuff import C2NoTokenError
from runkeeperstuff import RunKeeperNoTokenError
from sporttracksstuff import SportTracksNoTokenError
+from tpstuff import TPNoTokenError
from iso8601 import ParseError
import stravastuff
import sporttracksstuff
import underarmourstuff
+import tpstuff
import runkeeperstuff
import ownapistuff
from ownapistuff import TEST_CLIENT_ID, TEST_CLIENT_SECRET, TEST_REDIRECT_URI
@@ -68,6 +71,7 @@ from rowsandall_app.settings import (
UNDERARMOUR_CLIENT_ID, UNDERARMOUR_REDIRECT_URI,
UNDERARMOUR_CLIENT_SECRET,UNDERARMOUR_CLIENT_KEY,
RUNKEEPER_CLIENT_ID,RUNKEEPER_REDIRECT_URI,RUNKEEPER_CLIENT_SECRET,
+ TP_CLIENT_ID,TP_REDIRECT_URI,TP_CLIENT_KEY,TP_CLIENT_SECRET,
)
import requests
@@ -260,8 +264,12 @@ from utils import geo_distance,serialize_list,deserialize_list
# Check if a user is a Coach member
def iscoachmember(user):
- r = Rower.objects.get(user=user)
- result = user.is_authenticated() and (r.rowerplan=='coach')
+ if not user.is_anonymous():
+ r = Rower.objects.get(user=user)
+ result = user.is_authenticated() and (r.rowerplan=='coach')
+ else:
+ result = False
+
return result
# Check if a user is a Pro member
@@ -1071,6 +1079,26 @@ def underarmour_open(user):
return thetoken
+# Checks if user has UnderArmour token, renews them if they are expired
+def tp_open(user):
+ r = Rower.objects.get(user=user)
+ if (r.tptoken == '') or (r.tptoken is None):
+ s = "Token doesn't exist. Need to authorize"
+ raise TPNoTokenError("User has no token")
+ else:
+ if (timezone.now()>r.tptokenexpirydate):
+ res = tpstuff.do_refresh_token(r.tprefreshtoken)
+ r.tptoken = res[0]
+ r.tprefreshtoken = res[2]
+ expirydatetime = timezone.now()+datetime.timedelta(seconds=res[1])
+ r.tptokenexpirydate = expirydatetime
+ r.save()
+ thetoken = r.tptoken
+ else:
+ thetoken = r.tptoken
+
+ return thetoken
+
# Checks if user has SportTracks token, renews them if they are expired
def runkeeper_open(user):
r = Rower.objects.get(user=user)
@@ -1186,6 +1214,73 @@ def workout_csvemail_view(request,id=0):
return response
+# Send workout to TP
+@login_required()
+def workout_tp_upload_view(request,id=0):
+ message = ""
+ r = Rower.objects.get(user=request.user)
+ res = -1
+ try:
+ thetoken = tp_open(r.user)
+ except TPNoTokenError:
+ return HttpResponseRedirect("/rowers/me/tpauthorize/")
+
+
+ # ready to upload. Hurray
+ try:
+ w = Workout.objects.get(id=id)
+ r = w.user
+ except Workout.DoesNotExist:
+ raise Http404("Workout doesn't exist")
+ if (checkworkoutuser(request.user,w)):
+ tcxfile = tpstuff.createtpworkoutdata(w)
+ if tcxfile:
+ res = tpstuff.uploadactivity(r.tptoken,tcxfile,
+ name=w.name)
+ if res == 0:
+ message = "Upload to TrainingPeaks failed"
+ w.uploadedtotp = -1
+ w.save()
+ try:
+ os.remove(tcxfile)
+ except WindowsError:
+ pass
+ url = reverse(workout_export_view,
+ kwargs = {
+ 'id':str(w.id),
+ 'message':message,
+ })
+
+ else: # res != 0
+ w.uploadedtotp = res
+ w.save()
+ os.remove(tcxfile)
+ url = reverse(workout_export_view,
+ kwargs = {
+ 'id':str(w.id),
+ 'successmessage':'Uploaded to TP',
+ })
+
+ else: # no tcxfile
+ message = "Upload to TrainingPeaks failed"
+ w.uploadedtotp = -1
+ w.save()
+ url = reverse(workout_export_view,
+ kwargs = {
+ 'id':str(w.id),
+ 'message':message,
+ })
+ else: # not allowed to upload
+ message = "You are not allowed to export this workout to TP"
+ url = reverse(workout_export_view,
+ kwargs = {
+ 'id':str(w.id),
+ 'message':message,
+ })
+
+ return HttpResponseRedirect(url)
+
+
# Send workout to Strava
# abundance of error logging here because there were/are some bugs
@login_required()
@@ -1315,7 +1410,6 @@ def workout_c2_upload_view(request,id=0):
headers = {'Authorization': authorizationstring,
'user-agent': 'sanderroosendaal',
'Content-Type': 'application/json'}
- import urllib
try:
url = "https://log.concept2.com/api/users/%s/results" % (c2userid)
response = requests.post(url,headers=headers,data=json.dumps(data))
@@ -1400,7 +1494,6 @@ def workout_runkeeper_upload_view(request,id=0):
'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))
@@ -1466,7 +1559,6 @@ def workout_underarmour_upload_view(request,id=0):
'Content-Type': 'application/json',
}
- import urllib
url = "https://api.ua.com/v7.1/workout/"
response = requests.post(url,headers=headers,data=json.dumps(data))
@@ -1529,7 +1621,6 @@ def workout_sporttracks_upload_view(request,id=0):
'user-agent': 'sanderroosendaal',
'Content-Type': 'application/json'}
- import urllib
url = "https://api.sporttracks.mobi/api/v2/fitnessActivities.json"
response = requests.post(url,headers=headers,data=json.dumps(data))
@@ -1571,7 +1662,6 @@ def rower_c2_authorize(request):
params = {"client_id": C2_CLIENT_ID,
"response_type": "code",
"redirect_uri": C2_REDIRECT_URI}
- import urllib
url = "http://log.concept2.com/oauth/authorize?"+ urllib.urlencode(params)
url += "&scope="+scope
return HttpResponseRedirect(url)
@@ -1589,7 +1679,6 @@ def rower_strava_authorize(request):
"redirect_uri": STRAVA_REDIRECT_URI,
"scope": "write"}
- import urllib
url = "https://www.strava.com/oauth/authorize?"+ urllib.urlencode(params)
return HttpResponseRedirect(url)
@@ -1607,7 +1696,6 @@ def rower_runkeeper_authorize(request):
"state": state,
"redirect_uri": RUNKEEPER_REDIRECT_URI}
- import urllib
url = "https://runkeeper.com/apps/authorize?"+ urllib.urlencode(params)
@@ -1626,7 +1714,6 @@ def rower_sporttracks_authorize(request):
"state": state,
"redirect_uri": SPORTTRACKS_REDIRECT_URI}
- import urllib
url = "https://api.sporttracks.mobi/oauth2/authorize?"+ urllib.urlencode(params)
@@ -1649,6 +1736,23 @@ def rower_underarmour_authorize(request):
return HttpResponseRedirect(url)
+# Underarmour Authorization
+@login_required()
+def rower_tp_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": TP_CLIENT_KEY,
+ "response_type": "code",
+ "redirect_uri": TP_REDIRECT_URI,
+ "scope": "file:write",
+ }
+ url = "https://oauth.sandbox.trainingpeaks.com/oauth/authorize/?" +urllib.urlencode(params)
+
+ return HttpResponseRedirect(url)
+
+
# Concept2 token refresh. URL for manual refresh. Not visible to users
@login_required()
def rower_c2_token_refresh(request):
@@ -1698,6 +1802,30 @@ def rower_underarmour_token_refresh(request):
return imports_view(request,successmessage=successmessage)
+# TrainingPeaks token refresh. URL for manual refresh. Not visible to users
+@login_required()
+def rower_tp_token_refresh(request):
+ r = Rower.objects.get(user=request.user)
+ res = tpstuff.do_refresh_token(
+ r.tprefreshtoken,
+ r.tptoken
+ )
+ access_token = res[0]
+ expires_in = res[1]
+ refresh_token = res[2]
+ expirydatetime = timezone.now()+datetime.timedelta(seconds=expires_in)
+
+ r = Rower.objects.get(user=request.user)
+ r.tptoken = access_token
+ r.tptokenexpirydate = expirydatetime
+ r.tprefreshtoken = refresh_token
+
+ r.save()
+
+ successmessage = "Tokens refreshed. Good to go"
+ return imports_view(request,successmessage=successmessage)
+
+
# SportTracks token refresh. URL for manual refresh. Not visible to users
@login_required()
def rower_sporttracks_token_refresh(request):
@@ -1859,6 +1987,28 @@ def rower_process_underarmourcallback(request):
successmessage = "Tokens stored. Good to go"
return imports_view(request,successmessage=successmessage)
+# Process TrainingPeaks callback
+@login_required()
+def rower_process_tpcallback(request):
+ code = request.GET['code']
+ res = tpstuff.get_token(code)
+
+
+ access_token = res[0]
+ expires_in = res[1]
+ refresh_token = res[2]
+ expirydatetime = timezone.now()+datetime.timedelta(seconds=expires_in)
+
+ r = Rower.objects.get(user=request.user)
+ r.tptoken = access_token
+ r.tptokenexpirydate = expirydatetime
+ r.tprefreshtoken = refresh_token
+
+ r.save()
+
+ successmessage = "Tokens stored. Good to go"
+ return imports_view(request,successmessage=successmessage)
+
# Process Own API callback - for API testing purposes
@login_required()
def rower_process_testcallback(request):
@@ -4198,14 +4348,15 @@ def workout_flexchart3_view(request,*args,**kwargs):
workstrokesonly = False
if request.method == 'POST' and 'savefavorite' in request.POST:
- workstrokesonly = request.POST['workstrokesonlysave']
- reststrokes = not workstrokesonly
- r = Rower.objects.get(user=request.user)
- f = FavoriteChart(user=r,xparam=xparam,
- yparam1=yparam1,yparam2=yparam2,
- plottype=plottype,workouttype=workouttype,
- reststrokes=reststrokes)
- f.save()
+ if not request.user.is_anonymous():
+ workstrokesonly = request.POST['workstrokesonlysave']
+ reststrokes = not workstrokesonly
+ r = Rower.objects.get(user=request.user)
+ f = FavoriteChart(user=r,xparam=xparam,
+ yparam1=yparam1,yparam2=yparam2,
+ plottype=plottype,workouttype=workouttype,
+ reststrokes=reststrokes)
+ f.save()
if request.method == 'POST' and 'workstrokesonly' in request.POST:
workstrokesonly = request.POST['workstrokesonly']
@@ -5715,7 +5866,7 @@ def workout_upload_view(request,message="",
headers = {'Authorization': authorizationstring,
'user-agent': 'sanderroosendaal',
'Content-Type': 'application/json'}
- import urllib
+
url = "https://log.concept2.com/api/users/%s/results" % (c2userid)
response = requests.post(url,headers=headers,data=json.dumps(data))
diff --git a/rowsandall_app/settings.py b/rowsandall_app/settings.py
index 42ada514..498119b2 100644
--- a/rowsandall_app/settings.py
+++ b/rowsandall_app/settings.py
@@ -242,6 +242,13 @@ UNDERARMOUR_CLIENT_KEY = CFG['underarmour_client_key']
UNDERARMOUR_REDIRECT_URI = "http://rowsandall.com/underarmour_callback"
#UNDERARMOUR_REDIRECT_URI = "http://localhost:8000/underarmour_callback"
+# TrainingPeaks
+TP_CLIENT_ID = CFG["tp_client_id"]
+TP_CLIENT_SECRET = CFG["tp_client_secret"]
+TP_REDIRECT_URI = "http://rowsandall.com/tp_callback"
+#TP_REDIRECT_URI = "http://localhost:8000/tp_callback"
+TP_CLIENT_KEY = TP_CLIENT_ID
+
# RQ stuff
RQ_QUEUES = {
diff --git a/rowsandall_app/settings_dev.py b/rowsandall_app/settings_dev.py
index 8ebb5caf..70354ff5 100644
--- a/rowsandall_app/settings_dev.py
+++ b/rowsandall_app/settings_dev.py
@@ -45,8 +45,9 @@ CELERY_SEND_TASK_SENT_EVENT = True
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
+TEMPLATE_DEBUG = DEBUG
-ALLOWED_HOSTS = []
+ALLOWED_HOSTS = ['localhost']
# Application definition
diff --git a/rowsandall_app/urls.py b/rowsandall_app/urls.py
index 26734641..4f4c2c9c 100644
--- a/rowsandall_app/urls.py
+++ b/rowsandall_app/urls.py
@@ -21,6 +21,16 @@ from rowsandall_app.views import rootview
from django.contrib.auth import views as auth_views
from rowers import views as rowersviews
+from django.conf.urls import (
+ handler400, handler403, handler404, handler500
+)
+
+handler400 = 'rowers.views.error400_view'
+handler403 = 'rowers.views.error403_view'
+handler404 = 'rowers.views.error404_view'
+handler500 = 'rowers.views.error500_view'
+
+
urlpatterns = [
url(r'^password_change_done/$',auth_views.password_change_done,name='password_change_done'),
url(r'^password_change/$',auth_views.password_change),
@@ -57,6 +67,7 @@ urlpatterns += [
url(r'^sporttracks\_callback',rowersviews.rower_process_sporttrackscallback),
url(r'^underarmour\_callback',rowersviews.rower_process_underarmourcallback),
url(r'^runkeeper\_callback',rowersviews.rower_process_runkeepercallback),
+ url(r'^tp\_callback',rowersviews.rower_process_tpcallback),
url(r'^twitter\_callback',rowersviews.rower_process_twittercallback),
url(r'^i18n/', include('django.conf.urls.i18n')),
]
diff --git a/rowsandall_app/views.py b/rowsandall_app/views.py
index 921e05f6..4bcc1b91 100644
--- a/rowsandall_app/views.py
+++ b/rowsandall_app/views.py
@@ -1,4 +1,5 @@
from django.shortcuts import render, redirect, render_to_response
+from django.template import RequestContext
from django.conf import settings
from rowingdata import main as rmain
diff --git a/static/img/TP_logo_horz_2_color.png b/static/img/TP_logo_horz_2_color.png
new file mode 100644
index 00000000..dc6838eb
Binary files /dev/null and b/static/img/TP_logo_horz_2_color.png differ
diff --git a/static/img/tpchecked.png b/static/img/tpchecked.png
new file mode 100644
index 00000000..a1d9e93d
Binary files /dev/null and b/static/img/tpchecked.png differ
diff --git a/static/img/tpgray.png b/static/img/tpgray.png
new file mode 100644
index 00000000..2f7593f9
Binary files /dev/null and b/static/img/tpgray.png differ
diff --git a/static/img/tpicon.png b/static/img/tpicon.png
new file mode 100644
index 00000000..dec832ff
Binary files /dev/null and b/static/img/tpicon.png differ
diff --git a/templates/400.html b/templates/400.html
new file mode 100644
index 00000000..025dfc60
--- /dev/null
+++ b/templates/400.html
@@ -0,0 +1,16 @@
+{% extends "basenofilters.html" %}
+{% load staticfiles %}
+{% load rowerfilters %}
+
+{% block title %}Change Workout {% endblock %}
+
+{% block content %}
+
+
+
Bad Request
+
+HTTP Error 400 Bad Request.
+
+
+
+{% endblock %}
diff --git a/templates/403.html b/templates/403.html
new file mode 100644
index 00000000..3a3d7df7
--- /dev/null
+++ b/templates/403.html
@@ -0,0 +1,17 @@
+{% extends "basenofilters.html" %}
+{% load staticfiles %}
+{% load rowerfilters %}
+
+{% block title %}Change Workout {% endblock %}
+
+{% block content %}
+
+
+
Forbidden
+
+ Access forbidden. You probably tried to access functionality on a workout
+ or chart that is not owned by you.
+
+
+
+{% endblock %}
diff --git a/templates/404.html b/templates/404.html
new file mode 100644
index 00000000..33fd4812
--- /dev/null
+++ b/templates/404.html
@@ -0,0 +1,15 @@
+{% extends "basenofilters.html" %}
+{% load staticfiles %}
+
+{% block title %}Change Workout {% endblock %}
+
+{% block content %}
+
+
+
Error 404 Page not found
+
+We could not find the page on our server.
+
+
+
+{% endblock %}
diff --git a/templates/500.html b/templates/500.html
new file mode 100644
index 00000000..d60793eb
--- /dev/null
+++ b/templates/500.html
@@ -0,0 +1,21 @@
+{% extends "basenofilters.html" %}
+{% load staticfiles %}
+
+{% block title %}Change Workout {% endblock %}
+
+{% block content %}
+
+
+
Error 500 Internal Server Error
+
+ The site reported an internal server error. The site developer has been
+ notified automatically with a full error report. You can help the developer
+ by reporting an issue on Bitbucket using the button below.
+
+
+
+
+
+{% endblock %}
diff --git a/templates/base.html b/templates/base.html
new file mode 100644
index 00000000..77fa6213
--- /dev/null
+++ b/templates/base.html
@@ -0,0 +1,42 @@
+{% extends "basebase.html" %}
+{% block filters %}
+ {% load rowerfilters %}
+{% endblock %}
+
+{% block teams %}
+ {% if user.is_authenticated and user|has_teams %}
+
+ See recent workouts for your team
+ {% elif user.is_authenticated and user.rower.team.all %}
+
+
+ Teams
+
+
+ {% for t in user.rower.team.all %}
+
{{ t.name }}
+ {% endfor %}
+
+
+ See recent workouts for your team
+ {% else %}
+
+ {% endif %}
+{% endblock %}
+
+{% block content %}
+
+{% endblock %}
diff --git a/templates/basebase.html b/templates/basebase.html
new file mode 100644
index 00000000..a97bbb60
--- /dev/null
+++ b/templates/basebase.html
@@ -0,0 +1,219 @@
+{% load cookielaw_tags %}
+{% load analytical %}
+{% block filters %}
+{% endblock %}
+
+
+
+
+ {% analytical_head_top %}
+ {% if GOOGLE_ANALYTICS_PROPERTY_ID %}
+ {% include "ga.html" %}
+ {% endif %}
+
+
+
+
+
+
+
+
+
+
+ Rowsandall
+
+
+
+
+ {% block meta %} {% endblock %}
+ {% analytical_head_bottom %}
+
+
+ {% analytical_body_top %}
+ {% block body_top %}{% endblock %}
+
+
+
+
+
+
+ {% if user.rower.rowerplan == 'pro' or user.rower.rowerplan == 'coach' %}
+
+ {% else %}
+
+ {% endif %}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Free Data and Analysis. For Rowers. By Rowers.
+
+
+
+
+
+
+ {% if user.is_authenticated %}
+
logout
+ {% else %}
+
 
+ {% endif %}
+
+
+
+ {% if user.rower.rowerplan == 'pro' or user.rower.rowerplan == 'coach' %}
+
Pro Member
+ {% else %}
+
+ {% endif %}
+
+
+
+
+
+
+
+
+
+
+
+
+ {% block teams %}
+ {% endblock %}
+
+
+
+
+
+
+ {% block message %}
+ {% if message %}
+
+ {{ message }}
+
+ {% endif %}
+ {% if successmessage %}
+
+ {{ successmessage }}
+
+ {% endif %}
+ {% endblock %}
+
+
+ {% load tz %}
+
+ {% block content %}{% endblock %}
+
+
+
+
+ {% block footer %}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {% endblock %}
+
+ {% cookielaw_banner %}
+
+
+ {% analytical_body_bottom %}
+ {% block body_bottom %}{% endblock %}
+
+
diff --git a/templates/basefront.html b/templates/basefront.html
new file mode 100644
index 00000000..9bf96b9c
--- /dev/null
+++ b/templates/basefront.html
@@ -0,0 +1,80 @@
+{% extends "basebase.html" %}
+{% block filters %}
+ {% load rowerfilters %}
+{% endblock %}
+
+{% block meta %}
+{% endblock %}
+
+{% block body_top %}
+
+
+
+
+
+{% endblock %}
+
+{% block teams %}
+ {% if user.is_authenticated and user|has_teams %}
+
+
See recent workouts for your team
+ {% elif user.is_authenticated and user.rower.team.all %}
+
+
+ Teams
+
+
+ {% for t in user.rower.team.all %}
+
{{ t.name }}
+ {% endfor %}
+
+
+
See recent workouts for your team
+ {% else %}
+
+ {% endif %}
+{% endblock %}
+
+
+{% block body_bottom %}
+
+
+{% endblock %}
+
diff --git a/templates/basenofilters.html b/templates/basenofilters.html
new file mode 100644
index 00000000..04fad305
--- /dev/null
+++ b/templates/basenofilters.html
@@ -0,0 +1,26 @@
+{% extends "basebase.html" %}
+{% block filters %}
+ {% load rowerfilters %}
+{% endblock %}
+
+{% block teams %}
+ {% if user.is_authenticated and user.rower.team.all %}
+
+
+ Teams
+
+
+ {% for t in user.rower.team.all %}
+
{{ t.name }}
+ {% endfor %}
+
+
+
See recent workouts for your team
+ {% else %}
+
+ {% endif %}
+{% endblock %}
+
+{% block content %}
+
+{% endblock %}