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 5ff287b3..900f46aa 100644
--- a/rowers/dataprep.py
+++ b/rowers/dataprep.py
@@ -22,7 +22,7 @@ from rowingdata import (
BoatCoachParser,RowPerfectParser,BoatCoachAdvancedParser,
MysteryParser,
painsledDesktopParser,speedcoachParser,ErgStickParser,
- SpeedCoach2Parser,FITParser,fitsummarydata,
+ SpeedCoach2Parser,FITParser,FitSummaryData,
make_cumvalues,
summarydata,get_file_type,
)
@@ -541,7 +541,7 @@ def handle_nonpainsled(f2,fileformat,summary=''):
# handle FIT
if (fileformat == 'fit'):
row = FITParser(f2)
- s = fitsummarydata(f2)
+ s = FitSummaryData(f2)
s.setsummary()
summary = s.summarytext
diff --git a/rowers/dataprepnodjango.py b/rowers/dataprepnodjango.py
index 0cd3fc55..6b776831 100644
--- a/rowers/dataprepnodjango.py
+++ b/rowers/dataprepnodjango.py
@@ -296,7 +296,7 @@ def handle_nonpainsled(f2,fileformat,summary=''):
# handle FIT
if (fileformat == 'fit'):
row = FITParser(f2)
- s = fitsummarydata(f2)
+ s = FitSummaryData(f2)
s.setsummary()
summary = s.summarytext
diff --git a/rowers/mailprocessing.py b/rowers/mailprocessing.py
index cdf7f38c..835798cd 100644
--- a/rowers/mailprocessing.py
+++ b/rowers/mailprocessing.py
@@ -14,7 +14,7 @@ from rowingdata import rowingdata as rrdata
from rowingdata import TCXParser,RowProParser,ErgDataParser,TCXParserNoHR
from rowingdata import MysteryParser,BoatCoachParser
from rowingdata import painsledDesktopParser,speedcoachParser,ErgStickParser
-from rowingdata import SpeedCoach2Parser,FITParser,fitsummarydata
+from rowingdata import SpeedCoach2Parser,FITParser,FitSummaryData
from rowingdata import make_cumvalues
from rowingdata import summarydata,get_file_type
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/templates/export.html b/rowers/templates/export.html
index d207cf0b..696b9709 100644
--- a/rowers/templates/export.html
+++ b/rowers/templates/export.html
@@ -128,6 +128,24 @@
{% 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/urls.py b/rowers/urls.py
index 190424dc..219baa58 100644
--- a/rowers/urls.py
+++ b/rowers/urls.py
@@ -239,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),
@@ -274,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 eea09e00..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
@@ -1075,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)
@@ -1190,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()
@@ -1319,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))
@@ -1404,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))
@@ -1470,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))
@@ -1533,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))
@@ -1575,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)
@@ -1593,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)
@@ -1611,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)
@@ -1630,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)
@@ -1653,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):
@@ -1702,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):
@@ -1863,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):
@@ -5720,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..b02cd593 100644
--- a/rowsandall_app/settings.py
+++ b/rowsandall_app/settings.py
@@ -242,6 +242,12 @@ 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://localhost:8000/tp_callback"
+TP_CLIENT_KEY = TP_CLIENT_ID
+
# RQ stuff
RQ_QUEUES = {
diff --git a/rowsandall_app/urls.py b/rowsandall_app/urls.py
index eea73c39..4f4c2c9c 100644
--- a/rowsandall_app/urls.py
+++ b/rowsandall_app/urls.py
@@ -67,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/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