From ee162437b00f2e6f3d31feccae896a4e5922896f Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Tue, 18 Apr 2017 09:00:13 +0200 Subject: [PATCH 01/20] anonymous user cannot save favorite --- rowers/views.py | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/rowers/views.py b/rowers/views.py index 580a672a..57547fd6 100644 --- a/rowers/views.py +++ b/rowers/views.py @@ -260,8 +260,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 @@ -4198,14 +4202,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 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'] From 256d0af28cfa1200a982b96263800eeeb75b8afa Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Tue, 18 Apr 2017 18:07:58 +0200 Subject: [PATCH 02/20] successfully authorizes, obtained and refreshes token --- rowers/models.py | 8 ++ rowers/tpstuff.py | 250 +++++++++++++++++++++++++++++++++++++ rowers/urls.py | 3 + rowers/views.py | 158 +++++++++++++++++++++-- rowsandall_app/settings.py | 6 + rowsandall_app/urls.py | 1 + 6 files changed, 417 insertions(+), 9 deletions(-) create mode 100644 rowers/tpstuff.py 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/tpstuff.py b/rowers/tpstuff.py new file mode 100644 index 00000000..c3ddf58e --- /dev/null +++ b/rowers/tpstuff.py @@ -0,0 +1,250 @@ +# 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,access_token): + 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', + 'authorization': 'Bearer %s' % access_token} + + 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) + + print "Reason" + print response.reason + print response.content + + 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": "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=''): + 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, + "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 8fe5c55d..8454c943 100644 --- a/rowers/urls.py +++ b/rowers/urls.py @@ -238,6 +238,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 +274,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 57547fd6..83e4c269 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,20 @@ 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): + thetoken = tpstuff.rower_tp_token_refresh(user) + 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 +1208,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, + description=w.notes) + 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 +1404,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 +1488,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 +1553,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 +1615,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 +1656,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 +1673,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 +1690,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 +1708,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 +1730,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": "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 +1796,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 +1981,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 +5860,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 26734641..073f0c69 100644 --- a/rowsandall_app/urls.py +++ b/rowsandall_app/urls.py @@ -57,6 +57,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')), ] From cda887fe40f5b00f86d24bf59c72744c9569bfe2 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Tue, 18 Apr 2017 21:58:54 +0200 Subject: [PATCH 03/20] fixed access scope from write to file:write --- rowers/tpstuff.py | 2 +- rowers/views.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/rowers/tpstuff.py b/rowers/tpstuff.py index c3ddf58e..7146a109 100644 --- a/rowers/tpstuff.py +++ b/rowers/tpstuff.py @@ -163,7 +163,7 @@ def make_authorization_url(request): params = {"client_id": TP_CLIENT_KEY, "response_type": "code", "redirect_uri": TP_REDIRECT_URI, - "scope": "write", + "scope": "file:write", } url = "https://oauth.sandbox.trainingpeaks.com/oauth/authorize?" +urllib.urlencode(params) diff --git a/rowers/views.py b/rowers/views.py index 83e4c269..9877f00d 100644 --- a/rowers/views.py +++ b/rowers/views.py @@ -1740,7 +1740,7 @@ def rower_tp_authorize(request): params = {"client_id": TP_CLIENT_KEY, "response_type": "code", "redirect_uri": TP_REDIRECT_URI, - "scope": "write", + "scope": "file:write", } url = "https://oauth.sandbox.trainingpeaks.com/oauth/authorize/?" +urllib.urlencode(params) From 64da35a6f8c03bef8fe72e40f0a24563a6d1ead6 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Wed, 19 Apr 2017 08:11:51 +0200 Subject: [PATCH 04/20] Strava uploads now use gzip --- rowers/stravastuff.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/rowers/stravastuff.py b/rowers/stravastuff.py index 5fd018c4..1323f03b 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' From 88761f36f3c89904ebaa4c82481f2e5ac34feb71 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Wed, 19 Apr 2017 16:23:48 +0200 Subject: [PATCH 05/20] replaced fitsummarydata with FitSummaryData --- rowers/dataprep.py | 2 +- rowers/mailprocessing.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/rowers/dataprep.py b/rowers/dataprep.py index ea252e89..9bf8c2f5 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, ) 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 From 3b6591c6aeab322df3e6401cacfae806b0c18579 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Wed, 19 Apr 2017 16:25:53 +0200 Subject: [PATCH 06/20] replaced fitsummarydata with FitSummaryData --- rowers/dataprep.py | 2 +- rowers/dataprepnodjango.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/rowers/dataprep.py b/rowers/dataprep.py index 9bf8c2f5..07c4be48 100644 --- a/rowers/dataprep.py +++ b/rowers/dataprep.py @@ -540,7 +540,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 From 1d97b2ee881b114838aaf53fa28e252c9a75d23f Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Fri, 21 Apr 2017 08:52:39 +0200 Subject: [PATCH 07/20] added isnan to f determination --- rowers/dataprep.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rowers/dataprep.py b/rowers/dataprep.py index ea252e89..a03c01ee 100644 --- a/rowers/dataprep.py +++ b/rowers/dataprep.py @@ -363,7 +363,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 From 35ce1c70491fd7b883066012b43a76896618974d Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Fri, 21 Apr 2017 15:05:59 +0200 Subject: [PATCH 08/20] added authorization link --- rowers/templates/export.html | 3 +++ rowers/tpstuff.py | 5 ++++- rowers/views.py | 2 +- static/img/TP_logo_horz_2_color.png | Bin 0 -> 7106 bytes 4 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 static/img/TP_logo_horz_2_color.png diff --git a/rowers/templates/export.html b/rowers/templates/export.html index d207cf0b..1ddf4c33 100644 --- a/rowers/templates/export.html +++ b/rowers/templates/export.html @@ -162,6 +162,9 @@

connect with Under Armour

+
+

connect with TrainingPeaks

+
diff --git a/rowers/tpstuff.py b/rowers/tpstuff.py index 7146a109..0b7cacf1 100644 --- a/rowers/tpstuff.py +++ b/rowers/tpstuff.py @@ -208,7 +208,8 @@ def tp_check(access_token): return resp -def uploadactivity(access_token,filename,description=''): +def uploadactivity(access_token,filename,description='', + name='Rowsandall.com workout'): data_gz = BytesIO() with file(filename,'rb') as inF: s = inF.read() @@ -226,6 +227,8 @@ def uploadactivity(access_token,filename,description=''): "UploadClient": "rowsandall", "Filename": filename, "SetWorkoutPublic": True, + "Title":name, + "Type": "rowing", "Comment": description, "Data": base64.b64encode(data_gz.getvalue()).decode("ascii") } diff --git a/rowers/views.py b/rowers/views.py index 9877f00d..a1dea47a 100644 --- a/rowers/views.py +++ b/rowers/views.py @@ -1230,7 +1230,7 @@ def workout_tp_upload_view(request,id=0): tcxfile = tpstuff.createtpworkoutdata(w) if tcxfile: res = tpstuff.uploadactivity(r.tptoken,tcxfile, - description=w.notes) + name=w.name) if res == 0: message = "Upload to TrainingPeaks failed" w.uploadedtotp = -1 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 0000000000000000000000000000000000000000..dc6838ebebcc15e7e3eb8a7ad27819062d15b03c GIT binary patch literal 7106 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 Cy9v7h literal 0 HcmV?d00001 From 77434f5fd6acb1b722dadd9e24e9ce332dacfe7c Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Sat, 22 Apr 2017 08:41:55 +0200 Subject: [PATCH 09/20] save favorite user check bug fix --- rowers/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rowers/views.py b/rowers/views.py index 57547fd6..eea09e00 100644 --- a/rowers/views.py +++ b/rowers/views.py @@ -4202,7 +4202,7 @@ def workout_flexchart3_view(request,*args,**kwargs): workstrokesonly = False if request.method == 'POST' and 'savefavorite' in request.POST: - if not user.is_anonymous(): + if not request.user.is_anonymous(): workstrokesonly = request.POST['workstrokesonlysave'] reststrokes = not workstrokesonly r = Rower.objects.get(user=request.user) From eae2b8ac4ba0681de21440359c6e126f52e2b5cc Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Sat, 22 Apr 2017 12:32:13 +0200 Subject: [PATCH 10/20] extended templates --- rowers/templates/400.html | 2 +- rowers/templates/403.html | 2 +- rowers/templates/404.html | 3 +- rowers/templates/500.html | 3 +- rowers/templates/base.html | 221 ++------------------------ rowers/templates/basebase.html | 219 +++++++++++++++++++++++++ rowers/templates/basefront.html | 238 +++------------------------- rowers/templates/basenofilters.html | 26 +++ 8 files changed, 279 insertions(+), 435 deletions(-) create mode 100644 rowers/templates/basebase.html create mode 100644 rowers/templates/basenofilters.html 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 %} From 7ad8ffe0aff6995564fce3e16d8eef09c715556d Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Sat, 22 Apr 2017 14:45:07 +0200 Subject: [PATCH 11/20] moved error templates --- rowers/urls.py | 1 + rowsandall_app/settings_dev.py | 5 +- rowsandall_app/urls.py | 10 ++ rowsandall_app/views.py | 1 + templates/400.html | 16 +++ templates/403.html | 17 +++ templates/404.html | 15 +++ templates/500.html | 21 ++++ templates/base.html | 42 +++++++ templates/basebase.html | 219 +++++++++++++++++++++++++++++++++ templates/basefront.html | 80 ++++++++++++ templates/basenofilters.html | 26 ++++ 12 files changed, 451 insertions(+), 2 deletions(-) create mode 100644 templates/400.html create mode 100644 templates/403.html create mode 100644 templates/404.html create mode 100644 templates/500.html create mode 100644 templates/base.html create mode 100644 templates/basebase.html create mode 100644 templates/basefront.html create mode 100644 templates/basenofilters.html diff --git a/rowers/urls.py b/rowers/urls.py index 8fe5c55d..190424dc 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'), diff --git a/rowsandall_app/settings_dev.py b/rowsandall_app/settings_dev.py index 8ebb5caf..d507ce09 100644 --- a/rowsandall_app/settings_dev.py +++ b/rowsandall_app/settings_dev.py @@ -44,9 +44,10 @@ CELERY_SEND_TASK_SENT_EVENT = True # SECURITY WARNING: don't run with debug turned on in production! -DEBUG = True +DEBUG = False +TEMPLATE_DEBUG = DEBUG -ALLOWED_HOSTS = [] +ALLOWED_HOSTS = ['localhost'] # Application definition diff --git a/rowsandall_app/urls.py b/rowsandall_app/urls.py index 26734641..eea73c39 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), 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/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 %} From 9769685253406dda574aee8ab3c50043b9d40b8e Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Sat, 22 Apr 2017 14:47:41 +0200 Subject: [PATCH 12/20] settings_dev --- rowsandall_app/settings_dev.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rowsandall_app/settings_dev.py b/rowsandall_app/settings_dev.py index d507ce09..70354ff5 100644 --- a/rowsandall_app/settings_dev.py +++ b/rowsandall_app/settings_dev.py @@ -44,7 +44,7 @@ CELERY_SEND_TASK_SENT_EVENT = True # SECURITY WARNING: don't run with debug turned on in production! -DEBUG = False +DEBUG = True TEMPLATE_DEBUG = DEBUG ALLOWED_HOSTS = ['localhost'] From 04a3595a0bb4f7ef689fb878536af08b26b8cf08 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Sat, 22 Apr 2017 15:01:47 +0200 Subject: [PATCH 13/20] some error checking for invalid y metrics --- rowers/dataprep.py | 8 ++++++++ rowers/interactiveplots.py | 25 +++++++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/rowers/dataprep.py b/rowers/dataprep.py index a03c01ee..5ff287b3 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 @@ -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] + for c in columns: + if not c in axx: + columns.remove(c) + print c,'aap' + 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) From 32b862c90d01ad8ebe8ca8b1f93605d94c143014 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Sat, 22 Apr 2017 16:26:35 +0200 Subject: [PATCH 14/20] removed some stuff used for debugging --- rowers/tpstuff.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/rowers/tpstuff.py b/rowers/tpstuff.py index 0b7cacf1..89a3d390 100644 --- a/rowers/tpstuff.py +++ b/rowers/tpstuff.py @@ -139,9 +139,6 @@ def get_token(code): response = requests.post("https://oauth.sandbox.trainingpeaks.com/oauth/token", data=post_data) - print "Reason" - print response.reason - print response.content try: token_json = response.json() From 0bd615ab7d60f4ff0ace0e5c83a6976d9cc714aa Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Sat, 22 Apr 2017 16:28:47 +0200 Subject: [PATCH 15/20] underarmour return if not succesful --- rowers/underarmourstuff.py | 2 ++ 1 file changed, 2 insertions(+) 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 From 646894cad28d3bb95c63078e136768b3c1d94648 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Sat, 22 Apr 2017 17:11:01 +0200 Subject: [PATCH 16/20] add TP to export logos --- logos/tpchecked.png | Bin 0 -> 5118 bytes logos/tpgray.png | Bin 0 -> 3730 bytes logos/tpicon.png | Bin 0 -> 5285 bytes logos/tpicon.xcf | Bin 0 -> 9690 bytes rowers/templates/export.html | 18 ++++++++++++++++++ static/img/tpchecked.png | Bin 0 -> 5118 bytes static/img/tpgray.png | Bin 0 -> 3730 bytes static/img/tpicon.png | Bin 0 -> 5285 bytes 8 files changed, 18 insertions(+) create mode 100644 logos/tpchecked.png create mode 100644 logos/tpgray.png create mode 100644 logos/tpicon.png create mode 100644 logos/tpicon.xcf create mode 100644 static/img/tpchecked.png create mode 100644 static/img/tpgray.png create mode 100644 static/img/tpicon.png diff --git a/logos/tpchecked.png b/logos/tpchecked.png new file mode 100644 index 0000000000000000000000000000000000000000..a1d9e93d0e2cf40cc4906b73ad22e7e05d27e012 GIT binary patch literal 5118 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 literal 0 HcmV?d00001 diff --git a/logos/tpgray.png b/logos/tpgray.png new file mode 100644 index 0000000000000000000000000000000000000000..2f7593f914697275ce40f03e39fe873470a3e36e GIT binary patch literal 3730 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 literal 0 HcmV?d00001 diff --git a/logos/tpicon.png b/logos/tpicon.png new file mode 100644 index 0000000000000000000000000000000000000000..dec832ff52f24e02aca5a1db0e040c7828bebe24 GIT binary patch literal 5285 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 literal 0 HcmV?d00001 diff --git a/logos/tpicon.xcf b/logos/tpicon.xcf new file mode 100644 index 0000000000000000000000000000000000000000..9784e76bbc446f575231c72ac7748eeb71dfdf71 GIT binary patch literal 9690 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 YcB_t`fgk;!K2uXa`X6K_rhejo0eDdk!vFvP literal 0 HcmV?d00001 diff --git a/rowers/templates/export.html b/rowers/templates/export.html index 1ddf4c33..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 %}
diff --git a/static/img/tpchecked.png b/static/img/tpchecked.png new file mode 100644 index 0000000000000000000000000000000000000000..a1d9e93d0e2cf40cc4906b73ad22e7e05d27e012 GIT binary patch literal 5118 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 literal 0 HcmV?d00001 diff --git a/static/img/tpgray.png b/static/img/tpgray.png new file mode 100644 index 0000000000000000000000000000000000000000..2f7593f914697275ce40f03e39fe873470a3e36e GIT binary patch literal 3730 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 literal 0 HcmV?d00001 diff --git a/static/img/tpicon.png b/static/img/tpicon.png new file mode 100644 index 0000000000000000000000000000000000000000..dec832ff52f24e02aca5a1db0e040c7828bebe24 GIT binary patch literal 5285 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 literal 0 HcmV?d00001 From b71937d0db836b813cacdf01a0e88bb292551f06 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Sat, 22 Apr 2017 19:40:32 +0200 Subject: [PATCH 17/20] tested refresh TP token --- rowers/tpstuff.py | 5 +++-- rowers/views.py | 8 +++++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/rowers/tpstuff.py b/rowers/tpstuff.py index 89a3d390..7638f7f2 100644 --- a/rowers/tpstuff.py +++ b/rowers/tpstuff.py @@ -94,7 +94,7 @@ def custom_exception_handler(exc,message): return res # Refresh ST token using refresh token -def do_refresh_token(refreshtoken,access_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, @@ -104,7 +104,8 @@ def do_refresh_token(refreshtoken,access_token): headers = {'user-agent': 'sanderroosendaal', 'Accept': 'application/json', 'Content-Type': 'application/x-www-form-urlencoded', - 'authorization': 'Bearer %s' % access_token} + } + url = "https://oauth.sandbox.trainingpeaks.com/oauth/token" diff --git a/rowers/views.py b/rowers/views.py index 6f3f1f61..c87b132f 100644 --- a/rowers/views.py +++ b/rowers/views.py @@ -1087,7 +1087,13 @@ def tp_open(user): raise TPNoTokenError("User has no token") else: if (timezone.now()>r.tptokenexpirydate): - thetoken = tpstuff.rower_tp_token_refresh(user) + 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 From a5a6124c5efd0a3ff6e5da5649f65d4392ebe59e Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Sat, 22 Apr 2017 20:45:17 +0200 Subject: [PATCH 18/20] backwards compatibility --- rowers/dataprep.py | 4 ++-- rowers/dataprepnodjango.py | 2 +- rowers/mailprocessing.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/rowers/dataprep.py b/rowers/dataprep.py index 900f46aa..5ff287b3 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 6b776831..0cd3fc55 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 835798cd..cdf7f38c 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 From 07cac2e2051307ca690e56ed0a0e2622c6370f3b Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Sat, 22 Apr 2017 20:59:37 +0200 Subject: [PATCH 19/20] updated TP callback --- rowsandall_app/settings.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rowsandall_app/settings.py b/rowsandall_app/settings.py index b02cd593..498119b2 100644 --- a/rowsandall_app/settings.py +++ b/rowsandall_app/settings.py @@ -245,7 +245,8 @@ UNDERARMOUR_REDIRECT_URI = "http://rowsandall.com/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_REDIRECT_URI = "http://rowsandall.com/tp_callback" +#TP_REDIRECT_URI = "http://localhost:8000/tp_callback" TP_CLIENT_KEY = TP_CLIENT_ID # RQ stuff From 0874a3109eaa2c973b909f8ca5929e0999955a30 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Sun, 23 Apr 2017 09:50:31 +0200 Subject: [PATCH 20/20] datacolumn check against StrokeData model --- rowers/dataprep.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rowers/dataprep.py b/rowers/dataprep.py index 5ff287b3..64e18712 100644 --- a/rowers/dataprep.py +++ b/rowers/dataprep.py @@ -901,11 +901,11 @@ def prepmultipledata(ids,verbose=False): # 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 = [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) - print c,'aap' columns = list(columns)+['distance','spm'] columns = [x for x in columns if x != 'None']