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 %} -
-
-   -
-
- -
-
-
- -
- -
- -
-
-

Free Data and Analysis. For Rowers. By Rowers.

-
-
- -
-
- {% if user.is_authenticated %} -

- {{ user.first_name }} -

- Edit user account, e.g. heart rate zones, power zones, email, teams - - {% else %} -

login

- {% endif %} -
-
- {% if user.is_authenticated %} -

logout

- {% else %} -

 

- {% endif %} -
-
-
- {% if user.rower.rowerplan == 'pro' or user.rower.rowerplan == 'coach' %} -
Pro Member
- {% else %} - - {% endif %} -
-
-
- - -
-
- {% if user.is_authenticated %} -

Upload

- Upload CSV, TCX, FIT data files to rowsandall.com - {% else %} -

Register (free)

- {% endif %} -
-
- {% if user.is_authenticated %} -

- Import -

- Import workouts from Strava, SportTracks, and C2 logbook - {% else %} -

 

- {% endif %} -
-
- {% if user.is_authenticated %} -

- Workouts -

- See your list of workouts - {% else %} -

 

- {% endif %} -
-
- {% if user.is_authenticated %} -

- Graphs -

- See your most recent charts - {% else %} -

 

- {% endif %} -
-
- {% if user.is_authenticated %} -

- Analysis -

- Analysis of workouts over a period of time - {% else %} -

 

- {% endif %} -
-
+{% block teams %} {% if user.is_authenticated and user|has_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 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 %} +
+
+   +
+
+ +
+
+
+ +
+ +
+ +
+
+

Free Data and Analysis. For Rowers. By Rowers.

+
+
+ +
+
+ {% if user.is_authenticated %} +

+ {{ user.first_name }} +

+ Edit user account, e.g. heart rate zones, power zones, email, teams + + {% else %} +

login

+ {% endif %} +
+
+ {% if user.is_authenticated %} +

logout

+ {% else %} +

 

+ {% endif %} +
+
+
+ {% if user.rower.rowerplan == 'pro' or user.rower.rowerplan == 'coach' %} +
Pro Member
+ {% else %} + + {% endif %} +
+
+
+ + +
+
+ {% if user.is_authenticated %} +

Upload

+ Upload CSV, TCX, FIT data files to rowsandall.com + {% else %} +

Register (free)

+ {% endif %} +
+
+ {% if user.is_authenticated %} +

+ Import +

+ Import workouts from Strava, SportTracks, and C2 logbook + {% else %} +

 

+ {% endif %} +
+
+ {% if user.is_authenticated %} +

+ Workouts +

+ See your list of workouts + {% else %} +

 

+ {% endif %} +
+
+ {% if user.is_authenticated %} +

+ Graphs +

+ See your most recent charts + {% else %} +

 

+ {% endif %} +
+
+ {% if user.is_authenticated %} +

+ Analysis +

+ Analysis of workouts over a period of time + {% 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 %} -
-
-
-   -
-
- -
-
-
- -
- -
- -
-
-

Free Data and Analysis. For Rowers. By Rowers.

-
-
-
-
- {% if user.is_authenticated %} -

- {{ user.first_name }} -

- Edit user account, e.g. heart rate zones, power zones, email, teams - - {% else %} -

login

- {% endif %} -
-
- {% if user.is_authenticated %} -

logout

- {% else %} -

 

- {% endif %} -
-
-
- {% if user.rower.rowerplan == 'pro' or user.rower.rowerplan == 'coach' %} -
Pro Member
- {% else %} - - {% endif %} -
-
-
- - -
-
- {% if user.is_authenticated %} -

Upload

- Upload CSV, TCX, FIT data files to rowsandall.com - {% else %} -

Register (free)

- {% endif %} -
-
- {% if user.is_authenticated %} -

- Import -

- Import workouts from Strava, SportTracks, and C2 logbook - {% else %} -

 

- {% endif %} -
-
- {% if user.is_authenticated %} -

- Workouts -

- See your list of workouts - {% else %} -

 

- {% endif %} -
-
- {% if user.is_authenticated %} -

- Graphs -

- See your most recent charts - {% else %} -

 

- {% endif %} -
-
- {% if user.is_authenticated %} -

- Analysis -

- Analysis of workouts over a period of time - {% else %} -

 

- {% endif %} -
-
- {% if user.is_authenticated and user|user_teams %} +
+ +
+{% endblock %} + +{% block teams %} + {% if user.is_authenticated and user|has_teams %} +{% endblock %} - -
-
- {% block message %} - {% if message %} -

- {{ message }} -

- {% endif %} - {% if successmessage %} -

- {{ successmessage }} -

- {% endif %} - {% endblock %} + +{% block body_bottom %}
-
- {% load tz %} - - {% block content %}{% endblock %} -
-
- -
- {% block footer %} - - -
- -
-
- -
-
- -
-
- -
-
- -
- - - {% endblock %} -
- {% cookielaw_banner %} -
-
- - {% analytical_body_bottom %} - - +{% endblock %} + diff --git a/rowers/templates/basenofilters.html b/rowers/templates/basenofilters.html new file mode 100644 index 00000000..04fad305 --- /dev/null +++ b/rowers/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 %} + + See recent workouts for your team + {% else %} +

 

+ {% endif %} +{% endblock %} + +{% block content %} + +{% endblock %} 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 @@ Underarmour icon
{% endif %} + + {% if workout.uploadedtotp == 0 %} + {% if user.rower.tptoken == None or user.rower.tptoken == '' %} +
+ + TrainingPeaks icon +
+ {% else %} +
+ Tp icon +
+ {% endif %} + {% else %} +
+ + TrainingPeaks icon +
+ {% endif %}
@@ -162,6 +180,9 @@

connect with Under Armour

+
+

connect with TrainingPeaks

+
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 %} + + 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 %} +
+
+   +
+
+ +
+
+
+ +
+ +
+ +
+
+

Free Data and Analysis. For Rowers. By Rowers.

+
+
+ +
+
+ {% if user.is_authenticated %} +

+ {{ user.first_name }} +

+ Edit user account, e.g. heart rate zones, power zones, email, teams + + {% else %} +

login

+ {% endif %} +
+
+ {% if user.is_authenticated %} +

logout

+ {% else %} +

 

+ {% endif %} +
+
+
+ {% if user.rower.rowerplan == 'pro' or user.rower.rowerplan == 'coach' %} +
Pro Member
+ {% else %} + + {% endif %} +
+
+
+ + +
+
+ {% if user.is_authenticated %} +

Upload

+ Upload CSV, TCX, FIT data files to rowsandall.com + {% else %} +

Register (free)

+ {% endif %} +
+
+ {% if user.is_authenticated %} +

+ Import +

+ Import workouts from Strava, SportTracks, and C2 logbook + {% else %} +

 

+ {% endif %} +
+
+ {% if user.is_authenticated %} +

+ Workouts +

+ See your list of workouts + {% else %} +

 

+ {% endif %} +
+
+ {% if user.is_authenticated %} +

+ Graphs +

+ See your most recent charts + {% else %} +

 

+ {% endif %} +
+
+ {% if user.is_authenticated %} +

+ Analysis +

+ Analysis of workouts over a period of time + {% 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 %} + + 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 %} + + See recent workouts for your team + {% else %} +

 

+ {% endif %} +{% endblock %} + +{% block content %} + +{% endblock %}