Private
Public Access
1
0

Merge branch 'release/v2.5'

This commit is contained in:
Sander Roosendaal
2017-05-09 21:21:31 +02:00
66 changed files with 2478 additions and 777 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 250 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

BIN
View File
Binary file not shown.
@@ -0,0 +1,38 @@
/**
* @fileOverview CSS for jquery-autocomplete, the jQuery Autocompleter
* @author <a href="mailto:dylan@dyve.net">Dylan Verheul</a>
* @license MIT | GPL | Apache 2.0, see LICENSE.txt
* @see https://github.com/dyve/jquery-autocomplete
*/
.acResults {
padding: 0px;
border: 1px solid WindowFrame;
background-color: Window;
overflow: hidden;
}
.acResults ul {
margin: 0px;
padding: 0px;
list-style-position: outside;
list-style: none;
}
.acResults ul li {
margin: 0px;
padding: 2px 5px;
cursor: pointer;
display: block;
font: menu;
font-size: 12px;
overflow: hidden;
}
.acLoading {
background : url('../img/indicator.gif') right center no-repeat;
}
.acSelect {
background-color: Highlight;
color: HighlightText;
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

+119
View File
@@ -0,0 +1,119 @@
/**
* Ajax Queue Plugin
*
* Homepage: http://jquery.com/plugins/project/ajaxqueue
* Documentation: http://docs.jquery.com/AjaxQueue
*/
/**
<script>
$(function(){
jQuery.ajaxQueue({
url: "test.php",
success: function(html){ jQuery("ul").append(html); }
});
jQuery.ajaxQueue({
url: "test.php",
success: function(html){ jQuery("ul").append(html); }
});
jQuery.ajaxSync({
url: "test.php",
success: function(html){ jQuery("ul").append("<b>"+html+"</b>"); }
});
jQuery.ajaxSync({
url: "test.php",
success: function(html){ jQuery("ul").append("<b>"+html+"</b>"); }
});
});
</script>
<ul style="position: absolute; top: 5px; right: 5px;"></ul>
*/
/*
* Queued Ajax requests.
* A new Ajax request won't be started until the previous queued
* request has finished.
*/
/*
* Synced Ajax requests.
* The Ajax request will happen as soon as you call this method, but
* the callbacks (success/error/complete) won't fire until all previous
* synced requests have been completed.
*/
(function(jQuery) {
var ajax = jQuery.ajax;
var pendingRequests = {};
var synced = [];
var syncedData = [];
jQuery.ajax = function(settings) {
// create settings for compatibility with ajaxSetup
settings = jQuery.extend(settings, jQuery.extend({}, jQuery.ajaxSettings, settings));
var port = settings.port;
switch(settings.mode) {
case "abort":
if ( pendingRequests[port] ) {
pendingRequests[port].abort();
}
return pendingRequests[port] = ajax.apply(this, arguments);
case "queue":
var _old = settings.complete;
settings.complete = function(){
if ( _old )
_old.apply( this, arguments );
jQuery([ajax]).dequeue("ajax" + port );;
};
jQuery([ ajax ]).queue("ajax" + port, function(){
ajax( settings );
});
return;
case "sync":
var pos = synced.length;
synced[ pos ] = {
error: settings.error,
success: settings.success,
complete: settings.complete,
done: false
};
syncedData[ pos ] = {
error: [],
success: [],
complete: []
};
settings.error = function(){ syncedData[ pos ].error = arguments; };
settings.success = function(){ syncedData[ pos ].success = arguments; };
settings.complete = function(){
syncedData[ pos ].complete = arguments;
synced[ pos ].done = true;
if ( pos == 0 || !synced[ pos-1 ] )
for ( var i = pos; i < synced.length && synced[i].done; i++ ) {
if ( synced[i].error ) synced[i].error.apply( jQuery, syncedData[i].error );
if ( synced[i].success ) synced[i].success.apply( jQuery, syncedData[i].success );
if ( synced[i].complete ) synced[i].complete.apply( jQuery, syncedData[i].complete );
synced[i] = null;
syncedData[i] = null;
}
};
}
return ajax.apply(this, arguments);
};
})((typeof window.jQuery == 'undefined' && typeof window.django != 'undefined')
? django.jQuery
: jQuery
);
File diff suppressed because it is too large Load Diff
+39
View File
@@ -0,0 +1,39 @@
/*! Copyright (c) 2010 Brandon Aaron (http://brandonaaron.net)
* Licensed under the MIT License (LICENSE.txt).
*
* Version 2.1.2
*/
(function($){
$.fn.bgiframe = ($.browser.msie && /msie 6\.0/i.test(navigator.userAgent) ? function(s) {
s = $.extend({
top : 'auto', // auto == .currentStyle.borderTopWidth
left : 'auto', // auto == .currentStyle.borderLeftWidth
width : 'auto', // auto == offsetWidth
height : 'auto', // auto == offsetHeight
opacity : true,
src : 'javascript:false;'
}, s);
var html = '<iframe class="bgiframe"frameborder="0"tabindex="-1"src="'+s.src+'"'+
'style="display:block;position:absolute;z-index:-1;'+
(s.opacity !== false?'filter:Alpha(Opacity=\'0\');':'')+
'top:'+(s.top=='auto'?'expression(((parseInt(this.parentNode.currentStyle.borderTopWidth)||0)*-1)+\'px\')':prop(s.top))+';'+
'left:'+(s.left=='auto'?'expression(((parseInt(this.parentNode.currentStyle.borderLeftWidth)||0)*-1)+\'px\')':prop(s.left))+';'+
'width:'+(s.width=='auto'?'expression(this.parentNode.offsetWidth+\'px\')':prop(s.width))+';'+
'height:'+(s.height=='auto'?'expression(this.parentNode.offsetHeight+\'px\')':prop(s.height))+';'+
'"/>';
return this.each(function() {
if ( $(this).children('iframe.bgiframe').length === 0 )
this.insertBefore( document.createElement(html), this.firstChild );
});
} : function() { return this; });
// old alias
$.fn.bgIframe = $.fn.bgiframe;
function prop(n) {
return n && n.constructor === Number ? n + 'px' : n;
}
})((typeof window.jQuery == 'undefined' && typeof window.django != 'undefined') ? django.jQuery : jQuery);
-282
View File
@@ -1,282 +0,0 @@
# All the functionality needed to connect to Runkeeper
# Python
import oauth2 as oauth
import cgi
import requests
import requests.auth
import json
from django.utils import timezone
from datetime import datetime
import numpy as np
from dateutil import parser
import time
import math
from math import sin,cos,atan2,sqrt
import os,sys
# Django
from django.shortcuts import render_to_response
from django.http import HttpResponseRedirect, HttpResponse,JsonResponse
from django.conf import settings
from django.contrib.auth import authenticate, login, logout
from django.contrib.auth.models import User
from django.contrib.auth.decorators import login_required
# Project
# from .models import Profile
from rowingdata import rowingdata
import pandas as pd
from rowers.models import Rower,Workout
from rowsandall_app.settings import (
C2_CLIENT_ID, C2_REDIRECT_URI, C2_CLIENT_SECRET,
STRAVA_CLIENT_ID, STRAVA_REDIRECT_URI, STRAVA_CLIENT_SECRET,
RUNKEEPER_CLIENT_ID, RUNKEEPER_CLIENT_SECRET,RUNKEEPER_REDIRECT_URI,
)
# Custom error class - to raise a NoTokenError
class RunKeeperNoTokenError(Exception):
def __init__(self,value):
self.value=value
def __str__(self):
return repr(self.value)
# Exponentially weighted moving average
# Used for data smoothing of the jagged data obtained by Strava
# See bitbucket issue 72
def ewmovingaverage(interval,window_size):
# Experimental code using Exponential Weighted moving average
try:
intervaldf = pd.DataFrame({'v':interval})
idf_ewma1 = intervaldf.ewm(span=window_size)
idf_ewma2 = intervaldf[::-1].ewm(span=window_size)
i_ewma1 = idf_ewma1.mean().ix[:,'v']
i_ewma2 = idf_ewma2.mean().ix[:,'v']
interval2 = np.vstack((i_ewma1,i_ewma2[::-1]))
interval2 = np.mean( interval2, axis=0) # average
except ValueError:
interval2 = interval
return interval2
from utils import geo_distance
# Custom exception handler, returns a 401 HTTP message
# with exception details in the json data
def custom_exception_handler(exc,message):
response = {
"errors": [
{
"code": str(exc),
"detail": message,
}
]
}
res = HttpResponse(message)
res.status_code = 401
res.json = json.dumps(response)
return res
# Exchange access code for long-lived access token
def get_token(code):
client_auth = requests.auth.HTTPBasicAuth(RUNKEEPER_CLIENT_ID, RUNKEEPER_CLIENT_SECRET)
post_data = {"grant_type": "authorization_code",
"code": code,
"redirect_uri": RUNKEEPER_REDIRECT_URI,
"client_secret": RUNKEEPER_CLIENT_SECRET,
"client_id":RUNKEEPER_CLIENT_ID,
}
headers = {'user-agent': 'sanderroosendaal'}
response = requests.post("https://runkeeper.com/apps/token",
data=post_data,
headers=headers)
try:
token_json = response.json()
thetoken = token_json['access_token']
except KeyError:
thetoken = 0
return thetoken
# Make authorization URL including random string
def make_authorization_url(request):
# Generate a random string for the state parameter
# Save it for use later to prevent xsrf attacks
from uuid import uuid4
state = str(uuid4())
params = {"client_id": RUNKEEPER_CLIENT_ID,
"response_type": "code",
"redirect_uri": RUNKEEPER_REDIRECT_URI,
}
import urllib
url = "https://www.runkeeper.com/opps/authorize" +urllib.urlencode(params)
return HttpResponseRedirect(url)
# Get list of workouts available on Runkeeper
def get_runkeeper_workout_list(user):
r = Rower.objects.get(user=user)
if (r.runkeepertoken == '') or (r.runkeepertoken is None):
s = "Token doesn't exist. Need to authorize"
return custom_exception_handler(401,s)
else:
# ready to fetch. Hurray
authorizationstring = str('Bearer ' + r.runkeepertoken)
headers = {'Authorization': authorizationstring,
'user-agent': 'sanderroosendaal',
'Content-Type': 'application/json'}
url = "https://api.runkeeper.com/fitnessActivities"
s = requests.get(url,headers=headers)
return s
# Get workout summary data by Runkeeper ID
def get_runkeeper_workout(user,runkeeperid):
r = Rower.objects.get(user=user)
if (r.runkeepertoken == '') or (r.runkeepertoken is None):
return custom_exception_handler(401,s)
s = "Token doesn't exist. Need to authorize"
else:
# ready to fetch. Hurray
authorizationstring = str('Bearer ' + r.runkeepertoken)
headers = {'Authorization': authorizationstring,
'user-agent': 'sanderroosendaal',
'Content-Type': 'application/json'}
url = "https://api.runkeeper.com/fitnessActivities/"+str(runkeeperid)
s = requests.get(url,headers=headers)
return s
# Create Workout Data for upload to SportTracks
def createrunkeeperworkoutdata(w):
filename = w.csvfilename
try:
row = rowingdata(filename)
except:
return 0
averagehr = int(row.df[' HRCur (bpm)'].mean())
maxhr = int(row.df[' HRCur (bpm)'].max())
duration = w.duration.hour*3600
duration += w.duration.minute*60
duration += w.duration.second
duration += +1.0e-6*w.duration.microsecond
# adding diff, trying to see if this is valid
#t = row.df.ix[:,'TimeStamp (sec)'].values-10*row.df.ix[0,'TimeStamp (sec)']
t = row.df.ix[:,'TimeStamp (sec)'].values-row.df.ix[0,'TimeStamp (sec)']
t[0] = t[1]
d = row.df.ix[:,'cum_dist'].values
d[0] = d[1]
t = t.astype(int)
d = d.astype(int)
spm = row.df[' Cadence (stokes/min)'].astype(int)
spm[0] = spm[1]
hr = row.df[' HRCur (bpm)'].astype(int)
haslatlon=1
try:
lat = row.df[' latitude'].values
lon = row.df[' longitude'].values
if not lat.std() and not lon.std():
haslatlon = 0
except KeyError:
haslatlon = 0
# path data
if haslatlon:
locdata = []
for e in zip(t,lat,lon):
point = {'timestamp':e[0],
'latitude':e[1],
'longitude':e[2],}
locdata.append(point)
hrdata = []
for e in zip(t,hr):
point = {'timestamp':e[0],
'heart_rate':e[1]
}
hrdata.append(point)
distancedata = []
for e in zip(t,d):
point = {'timestamp':e[0],
'distance':e[1]
}
distancedata.append(point)
start_time = w.startdatetime.strftime("%a, %d %b %Y %H:%M:%S")
if haslatlon:
data = {
"type": "Rowing",
"start_time": start_time,
"total_distance": int(w.distance),
"duration": duration,
"notes": w.notes,
"average_heart_rate": averagehr,
"path": locdata,
"distance": distancedata,
"heartrate": hrdata,
"post_to_twitter":"false",
"post_to_facebook":"false",
}
else:
data = {
"type": "Rowing",
"start_time": start_time,
"total_distance": int(w.distance),
"duration": duration,
"notes": w.notes,
"avg_heartrate": averagehr,
"distance": distancedata,
"heartrate": hrdata,
"post_to_twitter":"false",
"post_to_facebook":"false",
}
return data
# Obtain Runkeeper Workout ID from the response returned on successful
# upload
def getidfromresponse(response):
uri = response.headers["Location"]
id = uri[len(uri)-9:]
return int(id)
# Get user id, having access token
# Handy for checking if the API access is working
def get_userid(access_token):
authorizationstring = str('Bearer ' + access_token)
headers = {'Authorization': authorizationstring,
'user-agent': 'sanderroosendaal',
'Content-Type': 'application/json'}
import urllib
url = "https://api.runkeeper.com/user"
response = requests.get(url,headers=headers)
me_json = response.json()
try:
res = me_json['userID']
except KeyError:
res = 0
return res
+73 -37
View File
@@ -27,6 +27,7 @@ from rowingdata import rowingdata
import pandas as pd import pandas as pd
import numpy as np import numpy as np
from rowers.models import Rower,Workout from rowers.models import Rower,Workout
from rowers.models import checkworkoutuser
import sys import sys
import urllib import urllib
from requests import Request, Session from requests import Request, Session
@@ -60,13 +61,26 @@ def custom_exception_handler(exc,message):
return res return res
# Check if workout is owned by this user # Checks if user has Concept2 tokens, resets tokens if they are
def checkworkoutuser(user,workout): # expired.
try: def c2_open(user):
r = Rower.objects.get(user=user) r = Rower.objects.get(user=user)
return (workout.user == r) if (r.c2token == '') or (r.c2token is None):
except Rower.DoesNotExist: s = "Token doesn't exist. Need to authorize"
return(False) raise C2NoTokenError("User has no token")
else:
if (timezone.now()>r.tokenexpirydate):
res = rower_c2_token_refresh(user)
if res[0] != None:
thetoken = res[0]
else:
raise C2NoTokenError("User has no token")
else:
thetoken = r.c2token
return thetoken
# convert datetime object to seconds # convert datetime object to seconds
def makeseconds(t): def makeseconds(t):
@@ -250,8 +264,12 @@ def createc2workoutdata(w):
except IOError: except IOError:
return 0 return 0
averagehr = int(row.df[' HRCur (bpm)'].mean()) try:
maxhr = int(row.df[' HRCur (bpm)'].max()) averagehr = int(row.df[' HRCur (bpm)'].mean())
maxhr = int(row.df[' HRCur (bpm)'].max())
except ValueError:
averagehr = 0
maxhr = 0
# adding diff, trying to see if this is valid # adding diff, trying to see if this is valid
t = 10*row.df.ix[:,'TimeStamp (sec)'].values-10*row.df.ix[0,'TimeStamp (sec)'] t = 10*row.df.ix[:,'TimeStamp (sec)'].values-10*row.df.ix[0,'TimeStamp (sec)']
@@ -265,7 +283,10 @@ def createc2workoutdata(w):
p = p.astype(int) p = p.astype(int)
spm = row.df[' Cadence (stokes/min)'].astype(int) spm = row.df[' Cadence (stokes/min)'].astype(int)
spm[0] = spm[1] spm[0] = spm[1]
hr = row.df[' HRCur (bpm)'].astype(int) try:
hr = row.df[' HRCur (bpm)'].astype(int)
except ValueError:
hr = 0*d
stroke_data = [] stroke_data = []
for i in range(len(t)): for i in range(len(t)):
thisrecord = {"t":t[i],"d":d[i],"p":p[i],"spm":spm[i],"hr":hr[i]} thisrecord = {"t":t[i],"d":d[i],"p":p[i],"spm":spm[i],"hr":hr[i]}
@@ -521,34 +542,49 @@ def process_callback(request):
# Uploading workout # Uploading workout
def workout_c2_upload(user,w): def workout_c2_upload(user,w):
response = 'trying C2 upload' response = 'trying C2 upload'
r = Rower.objects.get(user=user) thetoken = c2_open(user)
if (r.c2token == '') or (r.c2token is None):
s = "Token doesn't exist. Need to authorize"
return custom_exception_handler(401,s)
elif (timezone.now()>r.tokenexpirydate):
s = "Token expired. Needs to refresh."
return custom_exception_handler(401,s)
else:
# ready to upload. Hurray
if (checkworkoutuser(user,w)):
c2userid = get_userid(r.c2token)
data = createc2workoutdata(w)
authorizationstring = str('Bearer ' + r.c2token)
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))
if (response.status_code == 201):
s= json.loads(response.text)
c2id = s['data']['id']
w.uploadedtoc2 = c2id
w.save()
else:
response = "You are not authorized to upload this workout"
return response r = Rower.objects.get(user=user)
# ready to upload. Hurray
if (checkworkoutuser(user,w)):
c2userid = get_userid(r.c2token)
if not c2userid:
raise C2NoTokenError
data = createc2workoutdata(w)
if data == 0:
return "Error: No data file. Contact info@rowsandall.com if the problem persists",0
authorizationstring = str('Bearer ' + r.c2token)
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))
if (response.status_code == 409 ):
message = "Duplicate error"
w.uploadedtoc2 = -1
c2id = -1
w.save()
elif (response.status_code == 201 or response.status_code == 200):
try:
s= json.loads(response.text)
c2id = s['data']['id']
w.uploadedtoc2 = c2id
w.save()
message = ""
except:
message = "Something went wrong in workout_c2_upload_view. Response code 200/201 but C2 sync failed: "+response.text
c2id = 0
else:
message = "You are not authorized to upload this workout"
c2id = 0
return message,c2id
# This is token refresh. Looks for tokens in our database, then refreshes # This is token refresh. Looks for tokens in our database, then refreshes
def rower_c2_token_refresh(user): def rower_c2_token_refresh(user):
+10 -1
View File
@@ -338,7 +338,7 @@ def nicepaceformat(values):
# Convert seconds to a Time Delta value, replacing NaN with a 5:50 pace # Convert seconds to a Time Delta value, replacing NaN with a 5:50 pace
def timedeltaconv(x): def timedeltaconv(x):
if not np.isnan(x) and x != 0: if np.isfinite(x) and x != 0:
dt = datetime.timedelta(seconds=x) dt = datetime.timedelta(seconds=x)
else: else:
dt = datetime.timedelta(seconds=350.) dt = datetime.timedelta(seconds=350.)
@@ -471,6 +471,12 @@ def save_workout_database(f2,r,dosmooth=True,workouttype='rower',
if (len(ws) != 0): if (len(ws) != 0):
message = "Warning: This workout probably already exists in the database" message = "Warning: This workout probably already exists in the database"
# checking for inf values
totaldist = np.nan_to_num(totaldist)
maxhr = np.nan_to_num(maxhr)
averagehr = np.nan_to_num(averagehr)
w = Workout(user=r,name=title,date=workoutdate, w = Workout(user=r,name=title,date=workoutdate,
workouttype=workouttype, workouttype=workouttype,
@@ -1069,6 +1075,9 @@ def datafusion(id1,id2,columns,offset):
# Takes a rowingdata object's DataFrame as input # Takes a rowingdata object's DataFrame as input
def dataprep(rowdatadf,id=0,bands=True,barchart=True,otwpower=True, def dataprep(rowdatadf,id=0,bands=True,barchart=True,otwpower=True,
empower=True,inboard=0.88): empower=True,inboard=0.88):
if rowdatadf.empty:
return 0
rowdatadf.set_index([range(len(rowdatadf))],inplace=True) rowdatadf.set_index([range(len(rowdatadf))],inplace=True)
t = rowdatadf.ix[:,'TimeStamp (sec)'] t = rowdatadf.ix[:,'TimeStamp (sec)']
t = pd.Series(t-rowdatadf.ix[0,'TimeStamp (sec)']) t = pd.Series(t-rowdatadf.ix[0,'TimeStamp (sec)'])
+15 -1
View File
@@ -83,7 +83,21 @@ class UploadOptionsForm(forms.Form):
choices=plotchoices, choices=plotchoices,
initial='timeplot', initial='timeplot',
label='Plot Type') label='Plot Type')
upload_to_C2 = forms.BooleanField(initial=False,required=False) upload_to_C2 = forms.BooleanField(initial=False,required=False,
label='Upload to Concept2 logbook')
upload_to_Strava = forms.BooleanField(initial=False,required=False,
label='Upload to Strava')
upload_to_SportTracks = forms.BooleanField(initial=False,required=False,
label='Upload to SportTracks')
upload_to_RunKeeper = forms.BooleanField(initial=False,required=False,
label='Upload to RunKeeper')
upload_to_MapMyFitness = forms.BooleanField(initial=False,
required=False,
label='Upload to MapMyFitness')
upload_to_TrainingPeaks = forms.BooleanField(initial=False,
required=False,
label='Upload to TrainingPeaks')
# do_physics = forms.BooleanField(initial=False,required=False,label='Power Estimate (OTW)')
makeprivate = forms.BooleanField(initial=False,required=False, makeprivate = forms.BooleanField(initial=False,required=False,
label='Make Workout Private') label='Make Workout Private')
+179 -6
View File
@@ -25,6 +25,8 @@ from bokeh.models import (
SaveTool, ResizeTool, ResetTool, TapTool,CrosshairTool,BoxZoomTool, SaveTool, ResizeTool, ResetTool, TapTool,CrosshairTool,BoxZoomTool,
Span, Label Span, Label
) )
from bokeh.models.glyphs import ImageURL
#from bokeh.models.widgets import Slider, Select, TextInput #from bokeh.models.widgets import Slider, Select, TextInput
from bokeh.core.properties import value from bokeh.core.properties import value
@@ -54,6 +56,19 @@ from rowers.metrics import axes,axlabels,yaxminima,yaxmaxima
from utils import lbstoN from utils import lbstoN
watermarkurl = "/static/img/logo7.png"
watermarksource = ColumnDataSource(dict(
url = [watermarkurl],))
watermarkrange = Range1d(start=0,end=1)
watermarkalpha = 0.6
watermarkx = 0.99
watermarky = 0.01
watermarkw = 184
watermarkh = 35
watermarkanchor = 'bottom_right'
def tailwind(bearing,vwind,winddir): def tailwind(bearing,vwind,winddir):
""" Calculates head-on head/tailwind in direction of rowing """ Calculates head-on head/tailwind in direction of rowing
@@ -161,6 +176,21 @@ def interactive_forcecurve(theworkouts,workstrokesonly=False):
plot = Figure(tools=TOOLS, plot = Figure(tools=TOOLS,
toolbar_sticky=False) toolbar_sticky=False)
# add watermark
plot.extra_y_ranges = {"watermark": watermarkrange}
plot.extra_x_ranges = {"watermark": watermarkrange}
plot.image_url([watermarkurl],watermarkx,watermarky,
watermarkw,watermarkh,
global_alpha=watermarkalpha,
w_units='screen',
h_units='screen',
anchor=watermarkanchor,
dilate=True,
x_range_name = "watermark",
y_range_name = "watermark",
)
avf = Span(location=averageforceav,dimension='width',line_color='blue', avf = Span(location=averageforceav,dimension='width',line_color='blue',
line_dash=[6,6],line_width=2) line_dash=[6,6],line_width=2)
@@ -400,6 +430,21 @@ def interactive_histoall(theworkouts):
toolbar_location="above" toolbar_location="above"
) )
# add watermark
plot.extra_y_ranges = {"watermark": watermarkrange}
plot.extra_x_ranges = {"watermark": watermarkrange}
plot.image_url([watermarkurl],watermarkx,watermarky,
watermarkw,watermarkh,
global_alpha=watermarkalpha,
w_units='screen',
h_units='screen',
anchor=watermarkanchor,
dilate=True,
x_range_name = "watermark",
y_range_name = "watermark",
)
hist,edges = np.histogram(histopwr,bins=150) hist,edges = np.histogram(histopwr,bins=150)
histsum = np.cumsum(hist) histsum = np.cumsum(hist)
@@ -434,7 +479,7 @@ def interactive_histoall(theworkouts):
hover.mode = 'mouse' hover.mode = 'mouse'
plot.extra_y_ranges = {"fraction": Range1d(start=0,end=105)} plot.extra_y_ranges["fraction"] = Range1d(start=0,end=105)
plot.line('right','histsum',source=source,color="red", plot.line('right','histsum',source=source,color="red",
y_range_name="fraction") y_range_name="fraction")
plot.add_layout(LinearAxis(y_range_name="fraction", plot.add_layout(LinearAxis(y_range_name="fraction",
@@ -444,6 +489,8 @@ def interactive_histoall(theworkouts):
return [script,div] return [script,div]
def googlemap_chart(lat,lon,name=""): def googlemap_chart(lat,lon,name=""):
if lat.empty or lon.empty:
return [0,"invalid coordinate data"]
# plot tools # plot tools
TOOLS = 'save,pan,box_zoom,wheel_zoom,reset,tap,resize' TOOLS = 'save,pan,box_zoom,wheel_zoom,reset,tap,resize'
@@ -595,6 +642,20 @@ def interactive_cpchart(thedistances,thesecs,theavpower,
plot_width=900, plot_width=900,
toolbar_location="above", toolbar_location="above",
toolbar_sticky=False) toolbar_sticky=False)
# add watermark
plot.extra_y_ranges = {"watermark": watermarkrange}
plot.image_url([watermarkurl],1.8*max(thesecs),watermarky,
watermarkw,watermarkh,
global_alpha=watermarkalpha,
w_units='screen',
h_units='screen',
anchor=watermarkanchor,
dilate=True,
y_range_name = "watermark",
)
plot.circle('duration','power',source=source,fill_color='red',size=15, plot.circle('duration','power',source=source,fill_color='red',size=15,
legend='Power') legend='Power')
plot.xaxis.axis_label = "Duration (seconds)" plot.xaxis.axis_label = "Duration (seconds)"
@@ -665,6 +726,7 @@ def interactive_windchart(id=0,promember=0):
# create interactive plot # create interactive plot
plot = Figure(plot_width=400,plot_height=300) plot = Figure(plot_width=400,plot_height=300)
# get user # get user
# u = User.objects.get(id=row.user.id) # u = User.objects.get(id=row.user.id)
r = row.user r = row.user
@@ -836,6 +898,21 @@ def interactive_chart(id=0,promember=0):
toolbar_sticky=False, toolbar_sticky=False,
tools=TOOLS) tools=TOOLS)
# add watermark
plot.extra_y_ranges = {"watermark": watermarkrange}
plot.extra_x_ranges = {"watermark": watermarkrange}
plot.image_url([watermarkurl],0.01,0.99,
0.5*watermarkw,0.5*watermarkh,
global_alpha=watermarkalpha,
w_units='screen',
h_units='screen',
anchor='top_left',
dilate=True,
x_range_name = "watermark",
y_range_name = "watermark",
)
plot.line('time','pace',source=source,legend="Pace") plot.line('time','pace',source=source,legend="Pace")
plot.title.text = row.name plot.title.text = row.name
plot.title.text_font_size=value("1.0em") plot.title.text_font_size=value("1.0em")
@@ -875,7 +952,7 @@ def interactive_chart(id=0,promember=0):
hover.mode = 'mouse' hover.mode = 'mouse'
plot.extra_y_ranges = {"hrax": Range1d(start=100,end=200)} plot.extra_y_ranges["hrax"] = Range1d(start=100,end=200)
plot.line('time','hr',source=source,color="red", plot.line('time','hr',source=source,color="red",
y_range_name="hrax", legend="Heart Rate") y_range_name="hrax", legend="Heart Rate")
plot.add_layout(LinearAxis(y_range_name="hrax",axis_label="HR"),'right') plot.add_layout(LinearAxis(y_range_name="hrax",axis_label="HR"),'right')
@@ -981,6 +1058,21 @@ def interactive_cum_flex_chart2(theworkouts,promember=0,
toolbar_location="above", toolbar_location="above",
toolbar_sticky=False) toolbar_sticky=False)
# add watermark
plot.extra_y_ranges = {"watermark": watermarkrange}
plot.extra_x_ranges = {"watermark": watermarkrange}
plot.image_url([watermarkurl],watermarkx,watermarky,
watermarkw,watermarkh,
global_alpha=watermarkalpha,
w_units='screen',
h_units='screen',
anchor=watermarkanchor,
dilate=True,
x_range_name = "watermark",
y_range_name = "watermark",
)
x1means = Span(location=x1mean,dimension='height',line_color='green', x1means = Span(location=x1mean,dimension='height',line_color='green',
line_dash=[6,6], line_width=2) line_dash=[6,6], line_width=2)
@@ -1034,7 +1126,7 @@ def interactive_cum_flex_chart2(theworkouts,promember=0,
if yparam2 != 'None': if yparam2 != 'None':
yrange2 = Range1d(start=yaxminima[yparam2],end=yaxmaxima[yparam2]) yrange2 = Range1d(start=yaxminima[yparam2],end=yaxmaxima[yparam2])
plot.extra_y_ranges = {"yax2": yrange2} plot.extra_y_ranges["yax2"] = yrange2
plot.circle('x1','y2',color="red",y_range_name="yax2", plot.circle('x1','y2',color="red",y_range_name="yax2",
legend=yparamname2, legend=yparamname2,
@@ -1324,6 +1416,23 @@ def interactive_flex_chart2(id=0,promember=0,
toolbar_sticky=False toolbar_sticky=False
) )
# add watermark
plot.extra_y_ranges = {"watermark": watermarkrange}
plot.extra_x_ranges = {"watermark": watermarkrange}
plot.image_url([watermarkurl],watermarkx,watermarky,
watermarkw,watermarkh,
global_alpha=watermarkalpha,
w_units='screen',
h_units='screen',
anchor=watermarkanchor,
dilate=True,
x_range_name = "watermark",
y_range_name = "watermark",
)
x1means = Span(location=x1mean,dimension='height',line_color='green', x1means = Span(location=x1mean,dimension='height',line_color='green',
line_dash=[6,6], line_width=2) line_dash=[6,6], line_width=2)
@@ -1409,9 +1518,11 @@ def interactive_flex_chart2(id=0,promember=0,
minutes = ["%M"] minutes = ["%M"]
) )
if yparam2 != 'None': if yparam2 != 'None':
yrange2 = Range1d(start=yaxminima[yparam2],end=yaxmaxima[yparam2]) yrange2 = Range1d(start=yaxminima[yparam2],end=yaxmaxima[yparam2])
plot.extra_y_ranges = {"yax2": yrange2} plot.extra_y_ranges["yax2"] = yrange2
#= {"yax2": yrange2}
if plottype=='line': if plottype=='line':
plot.line('x1','y2',color="red",y_range_name="yax2", plot.line('x1','y2',color="red",y_range_name="yax2",
@@ -1461,7 +1572,8 @@ def interactive_flex_chart2(id=0,promember=0,
y2label=y2label, y2label=y2label,
xlabel=xlabel, xlabel=xlabel,
annolabel=annolabel, annolabel=annolabel,
y2means=y2means), code=""" y2means=y2means,
), code="""
var data = source.data var data = source.data
var data2 = source2.data var data2 = source2.data
var x1 = data['x1'] var x1 = data['x1']
@@ -1622,6 +1734,22 @@ def interactive_bar_chart(id=0,promember=0):
toolbar_sticky=False, toolbar_sticky=False,
plot_width=920, plot_width=920,
tools=TOOLS) tools=TOOLS)
# add watermark
plot.extra_y_ranges = {"watermark": watermarkrange}
plot.extra_x_ranges = {"watermark": watermarkrange}
plot.image_url([watermarkurl],0.01,0.99,
watermarkw,watermarkh,
global_alpha=watermarkalpha,
w_units='screen',
h_units='screen',
anchor='top_left',
dilate=True,
x_range_name = "watermark",
y_range_name = "watermark",
)
plot.title.text = row.name plot.title.text = row.name
plot.title.text_font_size=value("1.0em") plot.title.text_font_size=value("1.0em")
plot.xaxis.axis_label = "Time" plot.xaxis.axis_label = "Time"
@@ -1662,7 +1790,7 @@ def interactive_bar_chart(id=0,promember=0):
hover.mode = 'mouse' hover.mode = 'mouse'
plot.extra_y_ranges = {"hr": Range1d(start=100,end=200)} plot.extra_y_ranges["hr"] = Range1d(start=100,end=200)
plot.quad(left='time',top='hr_ut2',bottom='hr_bottom', plot.quad(left='time',top='hr_ut2',bottom='hr_bottom',
right='x_right',source=source,color="gray", right='x_right',source=source,color="gray",
y_range_name="hr", legend="<UT2") y_range_name="hr", legend="<UT2")
@@ -1765,6 +1893,21 @@ def interactive_multiple_compare_chart(ids,xparam,yparam,plottype='line',
plot_width=920, plot_width=920,
toolbar_sticky=False) toolbar_sticky=False)
# add watermark
plot.extra_y_ranges = {"watermark": watermarkrange}
plot.extra_x_ranges = {"watermark": watermarkrange}
plot.image_url([watermarkurl],0.05,0.9,
watermarkw,watermarkh,
global_alpha=watermarkalpha,
w_units='screen',
h_units='screen',
anchor='top_left',
dilate=True,
x_range_name = "watermark",
y_range_name = "watermark",
)
colors = itertools.cycle(palette) colors = itertools.cycle(palette)
cntr = 0 cntr = 0
@@ -1980,6 +2123,21 @@ def interactive_comparison_chart(id1=0,id2=0,xparam='distance',yparam='spm',
plot_width=920, plot_width=920,
toolbar_sticky=False) toolbar_sticky=False)
# add watermark
plot.extra_y_ranges = {"watermark": watermarkrange}
plot.extra_x_ranges = {"watermark": watermarkrange}
plot.image_url([watermarkurl],0.05,watermarky,
watermarkw,watermarkh,
global_alpha=watermarkalpha,
w_units='screen',
h_units='screen',
anchor='bottom_left',
dilate=True,
x_range_name = "watermark",
y_range_name = "watermark",
)
TIPS = OrderedDict([ TIPS = OrderedDict([
('time','@ftime1'), ('time','@ftime1'),
('pace','@fpace1'), ('pace','@fpace1'),
@@ -2076,6 +2234,21 @@ def interactive_otw_advanced_pace_chart(id=0,promember=0):
plot_width=920, plot_width=920,
toolbar_sticky=False) toolbar_sticky=False)
# add watermark
plot.extra_y_ranges = {"watermark": watermarkrange}
plot.extra_x_ranges = {"watermark": watermarkrange}
plot.image_url([watermarkurl],watermarkx,watermarky,
watermarkw,watermarkh,
global_alpha=watermarkalpha,
w_units='screen',
h_units='screen',
anchor=watermarkanchor,
dilate=True,
x_range_name = "watermark",
y_range_name = "watermark",
)
plot.title.text = row.name plot.title.text = row.name
plot.title.text_font_size=value("1.2em") plot.title.text_font_size=value("1.2em")
plot.xaxis.axis_label = "Time" plot.xaxis.axis_label = "Time"
+1 -1
View File
@@ -189,7 +189,7 @@ def make_new_workout_from_email(rr,f2,name,cntr=0):
res = queuehigh.enqueue(handle_sendemail_unrecognized, res = queuehigh.enqueue(handle_sendemail_unrecognized,
f2,"roosendaalsander@gmail.com") f2,"roosendaalsander@gmail.com")
return 0 return 1
summary = '' summary = ''
# handle non-Painsled # handle non-Painsled
+9 -1
View File
@@ -209,7 +209,6 @@ class Rower(models.Model):
runkeepertoken = models.CharField(default='',max_length=200, runkeepertoken = models.CharField(default='',max_length=200,
blank=True,null=True) blank=True,null=True)
# runkeepertokenexpirydate = models.DateTimeField(blank=True,null=True) # runkeepertokenexpirydate = models.DateTimeField(blank=True,null=True)
# runkeeperrefreshtoken = models.CharField(default='',max_length=200, # runkeeperrefreshtoken = models.CharField(default='',max_length=200,
# blank=True,null=True) # blank=True,null=True)
@@ -324,6 +323,15 @@ class BaseFavoriteFormSet(BaseFormSet):
if not yparam2: if not yparam2:
yparam2 = 'None' yparam2 = 'None'
# Check if workout is owned by this user
def checkworkoutuser(user,workout):
try:
r = Rower.objects.get(user=user)
return (workout.user == r)
except Rower.DoesNotExist:
return(False)
# Workout # Workout
class Workout(models.Model): class Workout(models.Model):
workouttypes = ( workouttypes = (
+89 -4
View File
@@ -27,7 +27,7 @@ from django.contrib.auth.decorators import login_required
# from .models import Profile # from .models import Profile
from rowingdata import rowingdata from rowingdata import rowingdata
import pandas as pd import pandas as pd
from rowers.models import Rower,Workout from rowers.models import Rower,Workout,checkworkoutuser
from rowsandall_app.settings import ( from rowsandall_app.settings import (
C2_CLIENT_ID, C2_REDIRECT_URI, C2_CLIENT_SECRET, C2_CLIENT_ID, C2_REDIRECT_URI, C2_CLIENT_SECRET,
@@ -86,6 +86,17 @@ def custom_exception_handler(exc,message):
return res return res
# Checks if user has SportTracks token, renews them if they are expired
def runkeeper_open(user):
r = Rower.objects.get(user=user)
if (r.runkeepertoken == '') or (r.runkeepertoken is None):
s = "Token doesn't exist. Need to authorize"
raise RunKeeperNoTokenError("User has no token")
else:
thetoken = r.runkeepertoken
return thetoken
# Exchange access code for long-lived access token # Exchange access code for long-lived access token
def get_token(code): def get_token(code):
client_auth = requests.auth.HTTPBasicAuth(RUNKEEPER_CLIENT_ID, RUNKEEPER_CLIENT_SECRET) client_auth = requests.auth.HTTPBasicAuth(RUNKEEPER_CLIENT_ID, RUNKEEPER_CLIENT_SECRET)
@@ -261,6 +272,26 @@ def getidfromresponse(response):
return int(id) return int(id)
def geturifromid(access_token,id):
authorizationstring = str('Bearer ' + access_token)
headers = {'Authorization': authorizationstring,
'user-agent': 'sanderroosendaal',
'Content-Type': 'application/json'}
import urllib
url = "https://api.runkeeper.com/fitnessActivities/"+str(id)
response = requests.get(url,headers=headers)
try:
me_json = response.json()
except:
return ''
try:
res = me_json['uri']
except KeyError:
res = ''
return res
# Get user id, having access token # Get user id, having access token
# Handy for checking if the API access is working # Handy for checking if the API access is working
@@ -277,11 +308,65 @@ def get_userid(access_token):
try: try:
me_json = response.json() me_json = response.json()
except: except:
return 0 return ''
try: try:
res = me_json['userID'] res = me_json['userID']
except KeyError: except KeyError:
res = 0 res = ''
return res print res,'userID'
return str(res)
def workout_runkeeper_upload(user,w):
message = ""
rkid = 0
r = w.user
thetoken = runkeeper_open(r.user)
# ready to upload. Hurray
if (checkworkoutuser(user,w)):
data = createrunkeeperworkoutdata(w)
if not data:
message = "Data error"
rkid = 0
return message, rkid
authorizationstring = str('Bearer ' + thetoken)
headers = {'Authorization': authorizationstring,
'user-agent': 'sanderroosendaal',
'Content-Type': 'application/vnd.com.runkeeper.NewFitnessActivity+json',
'Content-Length':'nnn'}
url = "https://api.runkeeper.com/fitnessActivities"
response = requests.post(url,headers=headers,data=json.dumps(data))
# check for duplicate error first
if (response.status_code == 409 ):
message = "Duplicate error"
w.uploadedtorunkeeper = -1
rkid = -1
w.save()
return message, rkid
elif (response.status_code == 201 or response.status_code==200):
rkid = getidfromresponse(response)
rkuri = geturifromid(thetoken,rkid)
w.uploadedtorunkeeper = rkid
w.save()
return '',rkid
else:
s = response
message = "Something went wrong in workout_runkeeper_upload_view: %s - %s" % (s.reason,s.text)
rkid = 0
return message, rkid
else:
message = "You are not authorized to upload this workout"
rkid = 0
return message, rkid
return message,rkid
+64 -1
View File
@@ -29,7 +29,7 @@ from django.contrib.auth.decorators import login_required
# from .models import Profile # from .models import Profile
from rowingdata import rowingdata from rowingdata import rowingdata
import pandas as pd import pandas as pd
from rowers.models import Rower,Workout from rowers.models import Rower,Workout,checkworkoutuser
from rowsandall_app.settings import C2_CLIENT_ID, C2_REDIRECT_URI, C2_CLIENT_SECRET, STRAVA_CLIENT_ID, STRAVA_REDIRECT_URI, STRAVA_CLIENT_SECRET, SPORTTRACKS_CLIENT_SECRET, SPORTTRACKS_CLIENT_ID, SPORTTRACKS_REDIRECT_URI from rowsandall_app.settings import C2_CLIENT_ID, C2_REDIRECT_URI, C2_CLIENT_SECRET, STRAVA_CLIENT_ID, STRAVA_REDIRECT_URI, STRAVA_CLIENT_SECRET, SPORTTRACKS_CLIENT_SECRET, SPORTTRACKS_CLIENT_ID, SPORTTRACKS_REDIRECT_URI
@@ -61,6 +61,21 @@ def custom_exception_handler(exc,message):
return res return res
# Checks if user has SportTracks token, renews them if they are expired
def sporttracks_open(user):
r = Rower.objects.get(user=user)
if (r.sporttrackstoken == '') or (r.sporttrackstoken is None):
s = "Token doesn't exist. Need to authorize"
raise SportTracksNoTokenError("User has no token")
else:
if (timezone.now()>r.sporttrackstokenexpirydate):
thetoken = sporttracksstuff.rower_sporttracks_token_refresh(user)
else:
thetoken = r.sporttrackstoken
return thetoken
# Refresh ST token using refresh token # Refresh ST token using refresh token
def do_refresh_token(refreshtoken): def do_refresh_token(refreshtoken):
client_auth = requests.auth.HTTPBasicAuth(SPORTTRACKS_CLIENT_ID, SPORTTRACKS_CLIENT_SECRET) client_auth = requests.auth.HTTPBasicAuth(SPORTTRACKS_CLIENT_ID, SPORTTRACKS_CLIENT_SECRET)
@@ -303,3 +318,51 @@ def getidfromresponse(response):
return int(id) return int(id)
def workout_sporttracks_upload(user,w):
message = ""
stid = 0
# ready to upload. Hurray
r = w.user
thetoken = sporttracks_open(user)
if (checkworkoutuser(user,w)):
data = createsporttracksworkoutdata(w)
if not data:
message = "Data error"
stid = 0
return message,stid
authorizationstring = str('Bearer ' + thetoken)
headers = {'Authorization': authorizationstring,
'user-agent': 'sanderroosendaal',
'Content-Type': 'application/json'}
url = "https://api.sporttracks.mobi/api/v2/fitnessActivities.json"
response = requests.post(url,headers=headers,data=json.dumps(data))
# check for duplicate error first
if (response.status_code == 409 ):
message = "Duplicate error"
w.uploadedtosporttracks = -1
stid = -1
w.save()
return message, stid
elif (response.status_code == 201 or response.status_code==200):
s= json.loads(response.text)
stid = getidfromresponse(response)
w.uploadedtosporttracks = stid
w.save()
return '',stid
else:
s = response
message = "Something went wrong in workout_sporttracks_upload_view: %s" % s.reason
stid = 0
return message,stid
else:
message = "You are not authorized to upload this workout"
stid = 0
return message,stid
return message,stid
+64
View File
@@ -29,8 +29,10 @@ from django.contrib.auth.decorators import login_required
from rowingdata import rowingdata from rowingdata import rowingdata
import pandas as pd import pandas as pd
from rowers.models import Rower,Workout from rowers.models import Rower,Workout
from rowers.models import checkworkoutuser
import stravalib import stravalib
from stravalib.exc import ActivityUploadFailed,TimeoutExceeded
from rowsandall_app.settings import C2_CLIENT_ID, C2_REDIRECT_URI, C2_CLIENT_SECRET, STRAVA_CLIENT_ID, STRAVA_REDIRECT_URI, STRAVA_CLIENT_SECRET from rowsandall_app.settings import C2_CLIENT_ID, C2_REDIRECT_URI, C2_CLIENT_SECRET, STRAVA_CLIENT_ID, STRAVA_REDIRECT_URI, STRAVA_CLIENT_SECRET
@@ -77,6 +79,15 @@ def custom_exception_handler(exc,message):
return res return res
# Custom error class - to raise a NoTokenError
class StravaNoTokenError(Exception):
def __init__(self,value):
self.value=value
def __str__(self):
return repr(self.value)
# Exchange access code for long-lived access token # Exchange access code for long-lived access token
def get_token(code): def get_token(code):
client_auth = requests.auth.HTTPBasicAuth(STRAVA_CLIENT_ID, STRAVA_CLIENT_SECRET) client_auth = requests.auth.HTTPBasicAuth(STRAVA_CLIENT_ID, STRAVA_CLIENT_SECRET)
@@ -277,3 +288,56 @@ def handle_stravaexport(f2,workoutname,stravatoken,description=''):
return (res.id,message) return (res.id,message)
def workout_strava_upload(user,w):
message = ""
stravaid=-1
r = Rower.objects.get(user=user)
res = -1
if (r.stravatoken == '') or (r.stravatoken is None):
s = "Token doesn't exist. Need to authorize"
raise StravaNoTokenError
else:
if (checkworkoutuser(user,w)):
try:
tcxfile = createstravaworkoutdata(w)
if tcxfile:
with open(tcxfile,'rb') as f:
res,mes = handle_stravaexport(f,w.name,
r.stravatoken,
description=w.notes+'\n from '+w.workoutsource+' via rowsandall.com')
if res==0:
message = mes
w.uploadedtostrava = -1
stravaid = -1
w.save()
try:
os.remove(tcxfile)
except WindowsError:
pass
return message,stravaid
w.uploadedtostrava = res
w.save()
try:
os.remove(tcxfile)
except WindowsError:
pass
message = ''
stravaid = res
return message,stravaid
else:
message = "Strava Upload error"
w.uploadedtostrava = -1
stravaid = -1
w.save()
return message, stravaid
except ActivityUploadFailed as e:
message = "Strava Upload error: %s" % e
w.uploadedtostrava = -1
stravaid = -1
w.save()
os.remove(tcxfile)
return message,stravaid
return message,stravaid
return message,stravaid
+32 -32
View File
@@ -1,40 +1,40 @@
{% extends "basebase.html" %} {% extends "basebase.html" %}
{% block filters %} {% block filters %}
{% load rowerfilters %} {% load rowerfilters %}
{% endblock %} {% endblock %}
{% block teams %} {% block teams %}
{% if user.is_authenticated and user|has_teams %} {% if user.is_authenticated and user|has_teams %}
<div class="grid_1 alpha dropdown"> <div class="grid_1 alpha dropdown">
<button class="grid_1 alpha button gray small dropbtn"> <button class="grid_1 alpha button gray small dropbtn">
Teams Teams
</button> </button>
<div class="dropdown-content"> <div class="dropdown-content">
<a class="button gray small" href="/rowers/me/teams/">Manage Teams</a> <a class="button gray small" href="/rowers/me/teams/">Manage Teams</a>
{% if user|is_manager %} {% if user|is_manager %}
<a class="button gray small" href="/rowers/workout/upload/team/">Upload Team Member Workout</a> <a class="button gray small" href="/rowers/workout/upload/team/">Upload Team Member Workout</a>
{% endif %} {% endif %}
{% for t in user|user_teams %} {% for t in user|user_teams %}
<a class="button gray small" href="/rowers/list-workouts/team/{{ t.id }}/">{{ t.name }}</a> <a class="button gray small" href="/rowers/list-workouts/team/{{ t.id }}/">{{ t.name }}</a>
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
<span class="tooltiptext">See recent workouts for your team</span> <span class="tooltiptext">See recent workouts for your team</span>
{% elif user.is_authenticated and user.rower.team.all %} {% elif user.is_authenticated and user.rower.team.all %}
<div class="grid_1 alpha dropdown"> <div class="grid_1 alpha dropdown">
<button class="grid_1 alpha button gray small dropbtn"> <button class="grid_1 alpha button gray small dropbtn">
Teams Teams
</button> </button>
<div class="dropdown-content"> <div class="dropdown-content">
{% for t in user.rower.team.all %} {% for t in user.rower.team.all %}
<a class="button gray small" href="/rowers/list-workouts/team/{{ t.id }}/">{{ t.name }}</a> <a class="button gray small" href="/rowers/list-workouts/team/{{ t.id }}/">{{ t.name }}</a>
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
<span class="tooltiptext">See recent workouts for your team</span> <span class="tooltiptext">See recent workouts for your team</span>
{% else %} {% else %}
<p>&nbsp;</p> <p>&nbsp;</p>
{% endif %} {% endif %}
{% endblock %} {% endblock %}
{% block content %} {% block content %}
+5 -1
View File
@@ -184,7 +184,11 @@
<p id="footer"> <p id="footer">
<a href="/rowers/about">About</a></p> <a href="/rowers/about">About</a></p>
</div> </div>
<div class="grid_2"> <div class="grid_1">
<p id="footer">
<a href="/rowers/brochure">Brochure</a></p>
</div>
<div class="grid_1">
<p id="footer"> <p id="footer">
<a href="/rowers/developers">Developers</a></p> <a href="/rowers/developers">Developers</a></p>
</div> </div>
+185 -181
View File
@@ -2,188 +2,192 @@
<html lang="en"> <html lang="en">
<head> <head>
<script src="/static/cookielaw/js/cookielaw.js"></script> <script src="/static/cookielaw/js/cookielaw.js"></script>
<link rel="stylesheet" href="/static/css/bokeh-0.12.3.min.css" type="text/css" /> <link rel="stylesheet" href="/static/css/bokeh-0.12.3.min.css" type="text/css" />
<link rel="stylesheet" href="/static/css/bokeh-widgets-0.12.3.min.css" type="text/css" /> <link rel="stylesheet" href="/static/css/bokeh-widgets-0.12.3.min.css" type="text/css" />
<link rel="shortcut icon" href="/static/img/myicon.png" /> <link rel="shortcut icon" href="/static/img/myicon.png" />
<link rel="shortcut icon" href="/static/img/favicon.ico" /> <link rel="shortcut icon" href="/static/img/favicon.ico" />
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="initial-scale=0.67"> <meta name="viewport" content="initial-scale=0.67">
<title>Rowsandall</title> <title>Rowsandall</title>
<link rel="stylesheet" href="/static/css/reset.css" /> <link rel="stylesheet" href="/static/css/reset.css" />
<link rel="stylesheet" href="/static/css/text.css" /> <link rel="stylesheet" href="/static/css/text.css" />
<link rel="stylesheet" href="/static/css/960_12_col.css" /> <link rel="stylesheet" href="/static/css/960_12_col.css" />
<link rel="stylesheet" href="/static/css/rowsandall.css" /> <link rel="stylesheet" href="/static/css/rowsandall.css" />
{% block meta %} {% endblock %} {% block meta %} {% endblock %}
</head> </head>
<body> <body>
<div class="container_12"> <div class="container_12">
<div id="logo" class="grid_2"> <div id="logo" class="grid_2">
{% if user.rower.rowerplan == 'pro' or user.rower.rowerplan == 'coach' %} {% if user.rower.rowerplan == 'pro' or user.rower.rowerplan == 'coach' %}
<p><a href="/"><img src="/static/img/logocroppedpro.gif" <p><a href="/"><img src="/static/img/logocroppedpro.gif"
alt="Rowsandall logo" width="110" heigt="110"></a></p> alt="Rowsandall logo" width="110" heigt="110"></a></p>
{% else %} {% else %}
<p><a href="/"><img src="/static/img/logocropped.gif" <p><a href="/"><img src="/static/img/logocropped.gif"
alt="Rowsandall logo" width="110" heigt="110"></a></p> alt="Rowsandall logo" width="110" heigt="110"></a></p>
{% endif %} {% endif %}
</div> </div>
<div class="grid_10 omega"> <div class="grid_10 omega">
<div class="grid_8 alpha"><p>&nbsp</p></div> <div class="grid_8 alpha"><p>&nbsp</p></div>
<div class="grid_2 omega"> <div class="grid_2 omega">
{% if user.is_authenticated %} {% if user.is_authenticated %}
<p><a class="button gray small" href="/password_change/">Password Change</a></p> <p><a class="button gray small" href="/password_change/">Password Change</a></p>
{% else %} {% else %}
<p><a class="button gray small" href="/password_reset/">Forgotten Password?</a></p> <p><a class="button gray small" href="/password_reset/">Forgotten Password?</a></p>
{% endif %} {% endif %}
</div>
</div>
<div class="grid_10 omega">
<div class="grid_4 suffix_2 alpha">
<p>Free Data and Analysis. For Rowers. By Rowers.</p>
</div>
<div class="grid_3">
{% if user.rower.rowerplan == 'pro' or user.rower.rowerplan == 'coach' %}
<h6>Pro Member</h6>
{% else %}
<p>&nbsp;</p>
{% endif %}
</div>
<div class="grid_1 omega">
{% if user.is_authenticated %}
<p><a class="button gray small" href="{% url 'logout' %}">logout</a></p>
{% else %}
<p>&nbsp</p>
{% endif %}
</div>
</div>
<div class="grid_10" omega>
<div class="grid_1 alpha tooltip">
{% if user.is_authenticated %}
<p><a class="button gray small" href="/rowers/workout/upload/">Upload</a></p>
<span class="tooltiptext">Upload CSV, TCX, FIT data files to rowsandall.com</span>
{% else %}
<p><a class="button green small" href="/rowers/register">Register (free)</a></p>
{% endif %}
</div>
<div class="grid_1 tooltip">
{% if user.is_authenticated %}
<p>
<a class="button gray small" href="/rowers/imports/">Import</a>
</p>
<span class="tooltiptext">Import workouts from Strava, SportTracks, and C2 logbook</span>
{% else %}
<p>&nbsp;</p>
{% endif %}
</div>
<div class="grid_2 tooltip">
{% if user.is_authenticated %}
<p>
<a class="button gray small" href="/rowers/list-workouts/">Workouts</a>
</p>
<span class="tooltiptext">See your list of workouts</span>
{% else %}
<p>&nbsp;</p>
{% endif %}
</div>
<div class="grid_2 tooltip">
{% if user.is_authenticated %}
<p>
<a class="button gray small" href="/rowers/list-graphs/">Graphs</a>
</p>
<span class="tooltiptext">See your most recent charts</span>
{% else %}
<p>&nbsp;</p>
{% endif %}
</div>
<div class="grid_2 suffix_1 tooltip">
{% if user.is_authenticated %}
<p>
<a class="button gray small" href="/rowers/analysis">Analysis</a>
</p>
<span class="tooltiptext">Analysis of workouts over a period of time</span>
{% else %}
<p>&nbsp;</p>
{% endif %}
</div>
<div class="grid_1 omega tooltip">
{% if user.is_authenticated %}
<p>
<a class="button gray small" href="/rowers/me/edit">{{ user.first_name }}</a>
</p>
<span class="tooltiptext">Edit user data, e.g. heart rate zones</span>
{% else %}
<p><a class="button gray small" href="{% url 'login' %}">login</a> </p>
{% endif %}
</div>
</div>
<div class="clear"></div>
<div class="grid_12">
{% block message %}
{% if message %}
<p class="message">
{{ message }}
</p>
{% endif %}
{% if successmessage %}
<p class="successmessage">
{{ successmessage }}
</p>
{% endif %}
{% endblock %}
</div>
<div class="grid_12">
{% load tz %}
{% block content %}{% endblock %}
</div>
<div class="clear"></div>
<div class="grid_12 omega" >
{% block footer %}
<p id="footer"
>{{ versionstring }}</p>
<div class="grid_2 alpha">
<p id="footer"><a href="/rowers/email/">&copy; Sander Roosendaal</a></p>
</div>
<div class="grid_1">
<p id="footer">
<a href="/rowers/about">About</a></p>
</div>
<div class="grid_2">
<p id="footer">
<a href="/rowers/developers">Developers</a></p>
</div>
<div class="grid_1">
<p id="footer">
<a href="/rowers/legal">Legal</a></p>
</div>
<div class="grid_1">
<p id="footer">
<a href="/rowers/physics">Physics</a></p>
</div>
<div class="grid_2">
<p id="footer">
<a href="/rowers/videos">Videos</a></p>
</div>
<div class="grid_2">
<p id="footer">
<a href="http://analytics.rowsandall.com/">Rowing Analytics BLOG</a></p>
</div>
<div class="grid_1 omega">
<p id="footer">
<a href="/rowers/email">Contact</a></p>
</div>
{% endblock %}
</div>
</div> </div>
<!-- end container --> </div>
</body>
<div class="grid_10 omega">
<div class="grid_4 suffix_2 alpha">
<p>Free Data and Analysis. For Rowers. By Rowers.</p>
</div>
<div class="grid_3">
{% if user.rower.rowerplan == 'pro' or user.rower.rowerplan == 'coach' %}
<h6>Pro Member</h6>
{% else %}
<p>&nbsp;</p>
{% endif %}
</div>
<div class="grid_1 omega">
{% if user.is_authenticated %}
<p><a class="button gray small" href="{% url 'logout' %}">logout</a></p>
{% else %}
<p>&nbsp</p>
{% endif %}
</div>
</div>
<div class="grid_10" omega>
<div class="grid_1 alpha tooltip">
{% if user.is_authenticated %}
<p><a class="button gray small" href="/rowers/workout/upload/">Upload</a></p>
<span class="tooltiptext">Upload CSV, TCX, FIT data files to rowsandall.com</span>
{% else %}
<p><a class="button green small" href="/rowers/register">Register (free)</a></p>
{% endif %}
</div>
<div class="grid_1 tooltip">
{% if user.is_authenticated %}
<p>
<a class="button gray small" href="/rowers/imports/">Import</a>
</p>
<span class="tooltiptext">Import workouts from Strava, SportTracks, and C2 logbook</span>
{% else %}
<p>&nbsp;</p>
{% endif %}
</div>
<div class="grid_2 tooltip">
{% if user.is_authenticated %}
<p>
<a class="button gray small" href="/rowers/list-workouts/">Workouts</a>
</p>
<span class="tooltiptext">See your list of workouts</span>
{% else %}
<p>&nbsp;</p>
{% endif %}
</div>
<div class="grid_2 tooltip">
{% if user.is_authenticated %}
<p>
<a class="button gray small" href="/rowers/list-graphs/">Graphs</a>
</p>
<span class="tooltiptext">See your most recent charts</span>
{% else %}
<p>&nbsp;</p>
{% endif %}
</div>
<div class="grid_2 suffix_1 tooltip">
{% if user.is_authenticated %}
<p>
<a class="button gray small" href="/rowers/analysis">Analysis</a>
</p>
<span class="tooltiptext">Analysis of workouts over a period of time</span>
{% else %}
<p>&nbsp;</p>
{% endif %}
</div>
<div class="grid_1 omega tooltip">
{% if user.is_authenticated %}
<p>
<a class="button gray small" href="/rowers/me/edit">{{ user.first_name }}</a>
</p>
<span class="tooltiptext">Edit user data, e.g. heart rate zones</span>
{% else %}
<p><a class="button gray small" href="{% url 'login' %}">login</a> </p>
{% endif %}
</div>
</div>
<div class="clear"></div>
<div class="grid_12">
{% block message %}
{% if message %}
<p class="message">
{{ message }}
</p>
{% endif %}
{% if successmessage %}
<p class="successmessage">
{{ successmessage }}
</p>
{% endif %}
{% endblock %}
</div>
<div class="grid_12">
{% load tz %}
{% block content %}{% endblock %}
</div>
<div class="clear"></div>
<div class="grid_12 omega" >
{% block footer %}
<p id="footer"
>{{ versionstring }}</p>
<div class="grid_2 alpha">
<p id="footer"><a href="/rowers/email/">&copy; Sander Roosendaal</a></p>
</div>
<div class="grid_1">
<p id="footer">
<a href="/rowers/about">About</a></p>
</div>
<div class="grid_1">
<p id="footer">
<a href="/rowers/brochure">Brochure</a></p>
</div>
<div class="grid_1">
<p id="footer">
<a href="/rowers/developers">Developers</a></p>
</div>
<div class="grid_1">
<p id="footer">
<a href="/rowers/legal">Legal</a></p>
</div>
<div class="grid_1">
<p id="footer">
<a href="/rowers/physics">Physics</a></p>
</div>
<div class="grid_2">
<p id="footer">
<a href="/rowers/videos">Videos</a></p>
</div>
<div class="grid_2">
<p id="footer">
<a href="http://analytics.rowsandall.com/">Rowing Analytics BLOG</a></p>
</div>
<div class="grid_1 omega">
<p id="footer">
<a href="/rowers/email">Contact</a></p>
</div>
{% endblock %}
</div>
</div>
<!-- end container -->
</body>
</html> </html>
+12
View File
@@ -0,0 +1,12 @@
{% extends "base.html" %}
{% block title %}Brochure{% endblock title %}
{% block content %}
<div class="grid_12 alpha">
<h2>Read our Brochure</h2>
<embed src="/static/brochure WEB.pdf" width="960" height="650">
</div>
{% endblock content %}
+4 -1
View File
@@ -35,7 +35,10 @@
</table> </table>
</p> </p>
<p> <p>
You can select one static plot to be generated immediately for this workout. You can select to upload to Concept2 automatically. If you check "make private", this workout will not be visible to your followers and will not show up in your teams' workouts list. You can select one static plot to be generated immediately for
this workout. You can select to upload to major fitness
platforms automatically.
If you check "make private", this workout will not be visible to your followers and will not show up in your teams' workouts list.
</p> </p>
<p> <p>
+2 -2
View File
@@ -106,7 +106,7 @@
{% endif %} {% endif %}
{% else %} {% else %}
<div class="grid_1 alpha"> <div class="grid_1 alpha">
<a href="https://runkeeper.com/fitnessActivity/{{ workout.uploadedtorunkeeper }}"> <a href="https://runkeeper.com/activity/{{ workout.uploadedtorunkeeper }}">
<img src="/static/img/rkchecked.png" alt="Runkeeper icon" width="60" height="60"></a> <img src="/static/img/rkchecked.png" alt="Runkeeper icon" width="60" height="60"></a>
</div> </div>
{% endif %} {% endif %}
@@ -142,7 +142,7 @@
{% endif %} {% endif %}
{% else %} {% else %}
<div class="grid_1"> <div class="grid_1">
<a href="https://app.sandbox.trainingpeaks.com"> <a href="https://app.trainingpeaks.com">
<img src="/static/img/tpchecked.png" alt="TrainingPeaks icon" width="60" height="60"></a> <img src="/static/img/tpchecked.png" alt="TrainingPeaks icon" width="60" height="60"></a>
</div> </div>
{% endif %} {% endif %}
+74 -5
View File
@@ -8,6 +8,7 @@ import requests.auth
import json import json
from django.utils import timezone from django.utils import timezone
from datetime import datetime from datetime import datetime
from datetime import timedelta
import numpy as np import numpy as np
from dateutil import parser from dateutil import parser
import time import time
@@ -31,7 +32,7 @@ from django.contrib.auth.decorators import login_required
# from .models import Profile # from .models import Profile
from rowingdata import rowingdata from rowingdata import rowingdata
import pandas as pd import pandas as pd
from rowers.models import Rower,Workout from rowers.models import Rower,Workout,checkworkoutuser
from rowsandall_app.settings import ( from rowsandall_app.settings import (
C2_CLIENT_ID, C2_REDIRECT_URI, C2_CLIENT_SECRET, C2_CLIENT_ID, C2_REDIRECT_URI, C2_CLIENT_SECRET,
@@ -40,7 +41,7 @@ from rowsandall_app.settings import (
TP_REDIRECT_URI,TP_CLIENT_KEY, TP_REDIRECT_URI,TP_CLIENT_KEY,
) )
tpapilocation = "https://api.sandbox.trainingpeaks.com" tpapilocation = "https://api.trainingpeaks.com"
# Custom error class - to raise a NoTokenError # Custom error class - to raise a NoTokenError
class TPNoTokenError(Exception): class TPNoTokenError(Exception):
@@ -93,6 +94,29 @@ def custom_exception_handler(exc,message):
return res return res
# 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 = do_refresh_token(r.tprefreshtoken)
if res[0] != 0:
r.tptoken = res[0]
r.tprefreshtoken = res[2]
expirydatetime = timezone.now()+timedelta(seconds=res[1])
r.tptokenexpirydate = expirydatetime
r.save()
thetoken = r.tptoken
else:
raise TPNoTokenError("Refresh token invalid")
else:
thetoken = r.tptoken
return thetoken
# Refresh ST token using refresh token # Refresh ST token using refresh token
def do_refresh_token(refreshtoken): def do_refresh_token(refreshtoken):
client_auth = requests.auth.HTTPBasicAuth(TP_CLIENT_KEY, TP_CLIENT_SECRET) client_auth = requests.auth.HTTPBasicAuth(TP_CLIENT_KEY, TP_CLIENT_SECRET)
@@ -107,7 +131,7 @@ def do_refresh_token(refreshtoken):
} }
url = "https://oauth.sandbox.trainingpeaks.com/oauth/token" url = "https://oauth.trainingpeaks.com/oauth/token"
response = requests.post(url, response = requests.post(url,
data=post_data, data=post_data,
@@ -141,7 +165,7 @@ def get_token(code):
'Content-Type': 'application/x-www-form-urlencoded', 'Content-Type': 'application/x-www-form-urlencoded',
} }
response = requests.post("https://oauth.sandbox.trainingpeaks.com/oauth/token", response = requests.post("https://oauth.trainingpeaks.com/oauth/token",
data=post_data) data=post_data)
@@ -169,7 +193,7 @@ def make_authorization_url(request):
"redirect_uri": TP_REDIRECT_URI, "redirect_uri": TP_REDIRECT_URI,
"scope": "file:write", "scope": "file:write",
} }
url = "https://oauth.sandbox.trainingpeaks.com/oauth/authorize?" +urllib.urlencode(params) url = "https://oauth.trainingpeaks.com/oauth/authorize?" +urllib.urlencode(params)
return HttpResponseRedirect(url) return HttpResponseRedirect(url)
@@ -270,3 +294,48 @@ def uploadactivity(access_token,filename,description='',
return 0 return 0
def workout_tp_upload(user,w):
message = ""
tpid = 0
r = w.user
thetoken = tp_open(r.user)
if (checkworkoutuser(user,w)):
tcxfile = createtpworkoutdata(w)
if tcxfile:
res,reason,status_code,headers = uploadactivity(
r.tptoken,tcxfile,
name=w.name
)
if res == 0:
message = "Upload to TrainingPeaks failed with status code "+str(status_code)+": "+reason
w.uploadedtotp = -1
w.tpid = -1
w.save()
try:
os.remove(tcxfile)
except WindowsError:
pass
return message,tpid
else: # res != 0
w.uploadedtotp = res
tpid = res
w.save()
os.remove(tcxfile)
return '',tpid
else: # no tcxfile
message = "Upload to TrainingPeaks failed"
w.uploadedtotp = -1
tpid = -1
w.save()
return message,tpid
else: # not allowed to upload
message = "You are not allowed to export this workout to TP"
tpid = 0
return message,tpid
return message,tpid
+67 -1
View File
@@ -28,7 +28,7 @@ from django.contrib.auth.decorators import login_required
# from .models import Profile # from .models import Profile
from rowingdata import rowingdata from rowingdata import rowingdata
import pandas as pd import pandas as pd
from rowers.models import Rower,Workout from rowers.models import Rower,Workout,checkworkoutuser
from rowsandall_app.settings import ( from rowsandall_app.settings import (
C2_CLIENT_ID, C2_REDIRECT_URI, C2_CLIENT_SECRET, C2_CLIENT_ID, C2_REDIRECT_URI, C2_CLIENT_SECRET,
@@ -88,6 +88,20 @@ def custom_exception_handler(exc,message):
return res return res
# Checks if user has UnderArmour token, renews them if they are expired
def underarmour_open(user):
r = Rower.objects.get(user=user)
if (r.underarmourtoken == '') or (r.underarmourtoken is None):
s = "Token doesn't exist. Need to authorize"
raise UnderArmourNoTokenError("User has no token")
else:
if (timezone.now()>r.underarmourtokenexpirydate):
thetoken = underarmourstuff.rower_underarmour_token_refresh(user)
else:
thetoken = r.underarmourtoken
return thetoken
# Refresh ST token using refresh token # Refresh ST token using refresh token
def do_refresh_token(refreshtoken,access_token): def do_refresh_token(refreshtoken,access_token):
client_auth = requests.auth.HTTPBasicAuth(UNDERARMOUR_CLIENT_KEY, UNDERARMOUR_CLIENT_SECRET) client_auth = requests.auth.HTTPBasicAuth(UNDERARMOUR_CLIENT_KEY, UNDERARMOUR_CLIENT_SECRET)
@@ -385,3 +399,55 @@ def get_userid(access_token):
res = 0 res = 0
return res return res
def workout_ua_upload(user,w):
message = ""
uaid = 0
r = w.user
thetoken = underarmour_open(r.user)
# ready to upload. Hurray
if (checkworkoutuser(user,w)):
data = createunderarmourworkoutdata(w)
# return HttpResponse(json.dumps(data))
if not data:
message = "Data error"
uaid = 0
return message, uaid
authorizationstring = str('Bearer ' + thetoken)
headers = {'Authorization': authorizationstring,
'Api-Key': UNDERARMOUR_CLIENT_KEY,
'user-agent': 'sanderroosendaal',
'Content-Type': 'application/json',
}
url = "https://api.ua.com/v7.1/workout/"
response = requests.post(url,headers=headers,data=json.dumps(data))
# check for duplicate error first
if (response.status_code == 409 ):
message = "Duplicate error"
w.uploadedtounderarmour = -1
uaid = -1
w.save()
elif (response.status_code == 201 or response.status_code==200):
uaid = getidfromresponse(response)
w.uploadedtounderarmour = uaid
w.save()
return '',uaid
else:
s = response
message = "Something went wrong in workout_underarmour_upload_view: %s - %s" % (s.reason,s.text)
uaid = 0
return message, uaid
else:
message = "You are not authorized to upload this workout"
uaid = 0
return message, uaid
return message, uaid
+2
View File
@@ -286,6 +286,8 @@ urlpatterns = [
url(r'^email/thankyou/$', TemplateView.as_view(template_name='thankyou.html'), name='thankyou'), url(r'^email/thankyou/$', TemplateView.as_view(template_name='thankyou.html'), name='thankyou'),
url(r'^email/$', TemplateView.as_view(template_name='email.html'), name='email'), url(r'^email/$', TemplateView.as_view(template_name='email.html'), name='email'),
url(r'^about', TemplateView.as_view(template_name='about_us.html'),name='about'), url(r'^about', TemplateView.as_view(template_name='about_us.html'),name='about'),
url(r'^brochure$',TemplateView.as_view(template_name='brochure.html'),
name='brochure'),
url(r'^developers', TemplateView.as_view(template_name='developers.html'),name='about'), url(r'^developers', TemplateView.as_view(template_name='developers.html'),name='about'),
url(r'^compatibility', TemplateView.as_view(template_name='compatibility.html'),name='about'), url(r'^compatibility', TemplateView.as_view(template_name='compatibility.html'),name='about'),
url(r'^videos', TemplateView.as_view(template_name='videos.html'),name='videos'), url(r'^videos', TemplateView.as_view(template_name='videos.html'),name='videos'),
+2
View File
@@ -3,6 +3,8 @@ import numpy as np
lbstoN = 4.44822 lbstoN = 4.44822
def serialize_list(value,token=','): def serialize_list(value,token=','):
assert(isinstance(value, list) or isinstance(value, tuple) or isinstance(value,np.ndarray)) assert(isinstance(value, list) or isinstance(value, tuple) or isinstance(value,np.ndarray))
return token.join([unicode(s) for s in value]) return token.join([unicode(s) for s in value])
+111 -213
View File
@@ -51,14 +51,16 @@ import os,sys
import datetime import datetime
import iso8601 import iso8601
import c2stuff import c2stuff
from c2stuff import C2NoTokenError from c2stuff import C2NoTokenError,c2_open
from runkeeperstuff import RunKeeperNoTokenError from runkeeperstuff import RunKeeperNoTokenError,runkeeper_open
from sporttracksstuff import SportTracksNoTokenError from sporttracksstuff import SportTracksNoTokenError,sporttracks_open
from tpstuff import TPNoTokenError from tpstuff import TPNoTokenError,tp_open
from iso8601 import ParseError from iso8601 import ParseError
import stravastuff import stravastuff
from stravastuff import StravaNoTokenError
import sporttracksstuff import sporttracksstuff
import underarmourstuff import underarmourstuff
from underarmourstuff import UnderArmourNoTokenError,underarmour_open
import tpstuff import tpstuff
import runkeeperstuff import runkeeperstuff
import ownapistuff import ownapistuff
@@ -261,6 +263,7 @@ def splitstdata(lijst):
return [np.array(t),np.array(latlong)] return [np.array(t),np.array(latlong)]
from utils import geo_distance,serialize_list,deserialize_list from utils import geo_distance,serialize_list,deserialize_list
from rowers.models import checkworkoutuser
# Check if a user is a Coach member # Check if a user is a Coach member
def iscoachmember(user): def iscoachmember(user):
@@ -369,14 +372,6 @@ def sendmail(request):
else: else:
return HttpResponseRedirect('/rowers/email/') return HttpResponseRedirect('/rowers/email/')
# Check if workout belongs to this user
def checkworkoutuser(user,workout):
try:
r = Rower.objects.get(user=user)
managers = [team.manager for team in workout.team.all()]
return (workout.user == r or user in managers)
except Rower.DoesNotExist:
return(False)
# Create workout data from Strava or Concept2 # Create workout data from Strava or Concept2
# data and create the associated Workout object and save it # data and create the associated Workout object and save it
@@ -1039,86 +1034,9 @@ def add_workout_from_underarmourdata(user,importid,data):
return (id,message) return (id,message)
# Checks if user has Concept2 tokens, resets tokens if they are
# expired.
def c2_open(user):
r = Rower.objects.get(user=user)
if (r.c2token == '') or (r.c2token is None):
s = "Token doesn't exist. Need to authorize"
raise C2NoTokenError("User has no token")
else:
if (timezone.now()>r.tokenexpirydate):
res = c2stuff.rower_c2_token_refresh(user)
if res[0] != None:
thetoken = res[0]
else:
raise C2NoTokenError("User has no token")
else:
thetoken = r.c2token
return thetoken
# Checks if user has SportTracks token, renews them if they are expired
def sporttracks_open(user):
r = Rower.objects.get(user=user)
if (r.sporttrackstoken == '') or (r.sporttrackstoken is None):
s = "Token doesn't exist. Need to authorize"
raise SportTracksNoTokenError("User has no token")
else:
if (timezone.now()>r.sporttrackstokenexpirydate):
thetoken = sporttracksstuff.rower_sporttracks_token_refresh(user)
else:
thetoken = r.sporttrackstoken
return thetoken
# Checks if user has UnderArmour token, renews them if they are expired
def underarmour_open(user):
r = Rower.objects.get(user=user)
if (r.underarmourtoken == '') or (r.underarmourtoken is None):
s = "Token doesn't exist. Need to authorize"
raise UnderarmourNoTokenError("User has no token")
else:
if (timezone.now()>r.underarmourtokenexpirydate):
thetoken = underarmourstuff.rower_underarmour_token_refresh(user)
else:
thetoken = r.underarmourtoken
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)
if res[0] != 0:
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:
raise TPNoTokenError("Refresh token invalid")
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)
if (r.runkeepertoken == '') or (r.runkeepertoken is None):
s = "Token doesn't exist. Need to authorize"
raise RunKeeperNoTokenError("User has no token")
else:
thetoken = r.runkeepertoken
return thetoken
# Export workout to TCX and send to user's email address # Export workout to TCX and send to user's email address
@login_required() @login_required()
@@ -1395,76 +1313,20 @@ def workout_c2_upload_view(request,id=0):
raise Http404("Workout doesn't exist") raise Http404("Workout doesn't exist")
try: try:
thetoken = c2_open(r.user) message,c2id = c2stuff.workout_c2_upload(request.user,w)
except C2NoTokenError: except C2NoTokenError:
return HttpResponseRedirect("/rowers/me/c2authorize/") return HttpResponseRedirect("/rowers/me/c2authorize/")
if (checkworkoutuser(request.user,w)):
c2userid = c2stuff.get_userid(thetoken)
if not c2userid:
return HttpResponseRedirect("/rowers/me/c2authorize")
data = c2stuff.createc2workoutdata(w)
if data == 0:
message = "Error: No data file. Contact info@rowsandall.com if this problem persists"
url = reverse(workout_export_view,
kwargs = {
'message':str(message),
'id':str(w.id),
})
return HttpResponseRedirect(url)
authorizationstring = str('Bearer ' + thetoken)
headers = {'Authorization': authorizationstring,
'user-agent': 'sanderroosendaal',
'Content-Type': 'application/json'}
try:
url = "https://log.concept2.com/api/users/%s/results" % (c2userid)
response = requests.post(url,headers=headers,data=json.dumps(data))
except:
message = "Unexpected Error: "+str(sys.exc_info()[0])
with open("media/c2errors.log","a") as errorlog:
errorstring = str(sys.exc_info()[0])
timestr = strftime("%Y%m%d-%H%M%S")
errorlog.write(timestr+errorstring+"\r\n")
# check for duplicate error first
if (response.status_code == 409 ):
message = "Duplicate error"
w.uploadedtoc2 = -1
w.save()
elif (response.status_code == 201 or response.status_code == 200):
try:
s= json.loads(response.text)
c2id = s['data']['id']
w.uploadedtoc2 = c2id
w.save()
url = "/rowers/workout/"+str(w.id)+"/export"
return HttpResponseRedirect(url)
except:
message = "Something went wrong in workout_c2_upload_view. Response code 200/201 but C2 sync failed: "+response.text
with open("media/c2errors.log","a") as errorlog:
errorstring = str(sys.exc_info()[0])
timestr = strftime("%Y%m%d-%H%M%S")
errorlog.write(timestr+errorstring+"\r\n")
else:
s = response
message = "Something went wrong in workout_c2_upload_view. C2 sync failed."
with open("media/c2errors.log","a") as errorlog:
errorstring = str(sys.exc_info()[0])
timestr = strftime("%Y%m%d-%H%M%S")
errorlog.write(timestr+errorstring+"\r\n")
if message:
url = reverse(workout_export_view,
kwargs = {
'message':str(message),
'id':str(w.id),
})
else: else:
message = "You are not authorized to upload this workout" url = reverse(workout_export_view,
kwargs = {
url = reverse(workout_export_view, 'id':str(w.id),
kwargs = {
'message':str(message),
'id':str(w.id),
}) })
return HttpResponseRedirect(url) return HttpResponseRedirect(url)
@@ -1544,7 +1406,7 @@ def workout_underarmour_upload_view(request,id=0):
try: try:
thetoken = underarmour_open(r.user) thetoken = underarmour_open(r.user)
except UnderarmourNoTokenError: except UnderArmourNoTokenError:
return HttpResponseRedirect("/rowers/me/underarmourauthorize/") return HttpResponseRedirect("/rowers/me/underarmourauthorize/")
# ready to upload. Hurray # ready to upload. Hurray
@@ -1757,7 +1619,7 @@ def rower_tp_authorize(request):
"redirect_uri": TP_REDIRECT_URI, "redirect_uri": TP_REDIRECT_URI,
"scope": "file:write", "scope": "file:write",
} }
url = "https://oauth.sandbox.trainingpeaks.com/oauth/authorize/?" +urllib.urlencode(params) url = "https://oauth.trainingpeaks.com/oauth/authorize/?" +urllib.urlencode(params)
return HttpResponseRedirect(url) return HttpResponseRedirect(url)
@@ -4604,7 +4466,7 @@ def workout_otwpowerplot_view(request,id=0,message="",successmessage=""):
'the_div':div, 'the_div':div,
'mayedit':mayedit}) 'mayedit':mayedit})
# the page where you can chose where to export this workout # the page where you can choose where to export this workout
@login_required() @login_required()
def workout_export_view(request,id=0, message="", successmessage=""): def workout_export_view(request,id=0, message="", successmessage=""):
request.session[translation.LANGUAGE_SESSION_KEY] = USER_LANGUAGE request.session[translation.LANGUAGE_SESSION_KEY] = USER_LANGUAGE
@@ -5610,6 +5472,7 @@ def workout_getrunkeeperworkout_view(request,runkeeperid):
id,message = add_workout_from_runkeeperdata(request.user,runkeeperid,data) id,message = add_workout_from_runkeeperdata(request.user,runkeeperid,data)
w = Workout.objects.get(id=id) w = Workout.objects.get(id=id)
w.uploadedtorunkeeper=runkeeperid w.uploadedtorunkeeper=runkeeperid
thetoken = runkeeper_open(request.user)
w.save() w.save()
if message: if message:
url = reverse(workout_edit_view, url = reverse(workout_edit_view,
@@ -5820,6 +5683,31 @@ def workout_upload_view(request,message="",
except KeyError: except KeyError:
upload_toc2 = False upload_toc2 = False
try:
upload_tostrava = uploadoptions['upload_to_Strava']
except KeyError:
upload_tostrava = False
try:
upload_tost = uploadoptions['upload_to_SportTracks']
except KeyError:
upload_tost = False
try:
upload_tork = uploadoptions['upload_to_RunKeeper']
except KeyError:
upload_tork = False
try:
upload_toua = uploadoptions['upload_to_MapMyFitness']
except KeyError:
upload_toua = False
try:
upload_totp = uploadoptions['upload_to_TrainingPeaks']
except KeyError:
upload_totp = False
r = Rower.objects.get(user=request.user) r = Rower.objects.get(user=request.user)
if request.method == 'POST': if request.method == 'POST':
form = DocumentsForm(request.POST,request.FILES) form = DocumentsForm(request.POST,request.FILES)
@@ -5836,6 +5724,11 @@ def workout_upload_view(request,message="",
make_plot = optionsform.cleaned_data['make_plot'] make_plot = optionsform.cleaned_data['make_plot']
plottype = optionsform.cleaned_data['plottype'] plottype = optionsform.cleaned_data['plottype']
upload_to_c2 = optionsform.cleaned_data['upload_to_C2'] upload_to_c2 = optionsform.cleaned_data['upload_to_C2']
upload_to_strava = optionsform.cleaned_data['upload_to_Strava']
upload_to_st = optionsform.cleaned_data['upload_to_SportTracks']
upload_to_rk = optionsform.cleaned_data['upload_to_RunKeeper']
upload_to_ua = optionsform.cleaned_data['upload_to_MapMyFitness']
upload_to_tp = optionsform.cleaned_data['upload_to_TrainingPeaks']
makeprivate = optionsform.cleaned_data['makeprivate'] makeprivate = optionsform.cleaned_data['makeprivate']
uploadoptions = { uploadoptions = {
@@ -5843,6 +5736,11 @@ def workout_upload_view(request,message="",
'make_plot':make_plot, 'make_plot':make_plot,
'plottype':plottype, 'plottype':plottype,
'upload_to_C2':upload_to_c2, 'upload_to_C2':upload_to_c2,
'upload_to_Strava':upload_to_strava,
'upload_to_SportTracks':upload_to_st,
'upload_to_RunKeeper':upload_to_rk,
'upload_to_MapMyFitness':upload_to_ua,
'upload_to_TrainingPeaks':upload_to_tp,
} }
@@ -5933,65 +5831,65 @@ def workout_upload_view(request,message="",
filename=fullpathimagename) filename=fullpathimagename)
i.save() i.save()
# upload to C2 # upload to C2
if (upload_to_c2): if (upload_to_c2):
try: try:
thetoken = c2_open(request.user) c2message,c2id = c2stuff.workout_c2_upload(request.user,w)
except C2NoTokenError: except C2NoTokenError:
return HttpResponseRedirect("/rowers/me/c2authorize/") pass
try: if (upload_to_strava):
c2userid = c2stuff.get_userid(thetoken) try:
if not c2userid: stravamessage,stravaid = stravastuff.workout_strava_upload(
return HttpResponseRedirect("/rowers/me/c2authorize") request.user,w
data = c2stuff.createc2workoutdata(w) )
authorizationstring = str('Bearer ' + thetoken) except StravaNoTokenError:
headers = {'Authorization': authorizationstring, pass
'user-agent': 'sanderroosendaal',
'Content-Type': 'application/json'}
url = "https://log.concept2.com/api/users/%s/results" % (c2userid) if (upload_to_st):
response = requests.post(url,headers=headers,data=json.dumps(data)) try:
stmessage,stid = sporttracksstuff.workout_sporttracks_upload(
request.user,w
)
except SportTracksNoTokenError:
pass
# response = c2stuff.workout_c2_upload(request.user,w) if (upload_to_rk):
if (response.status_code != 201 and response.status_code != 200): try:
if settings.DEBUG: rkmessage,rkid = runkeeperstuff.workout_runkeeper_upload(
return HttpResponse(response) request.user,w
else: )
message = "C2 upload failed" except RunKeeperNoTokenError:
url = reverse(workout_edit_view, pass
kwargs={
'message':message,
'id':str(w.id),
})
return HttpResponseRedirect(url)
else:
s= json.loads(response.text)
c2id = s['data']['id']
w.uploadedtoc2 = c2id
w.save()
except:
message = "C2 upload failed"
url = reverse(workout_edit_view,
kwargs={
'message':message,
'id':str(w.id),
})
return HttpResponseRedirect(url)
if (upload_to_ua):
try:
uamessage,uaid = underarmourstuff.workout_ua_upload(
request.user,w
)
except UnderArmourNoTokenError:
pass
if message: if (upload_to_tp):
url = reverse(workout_edit_view, try:
kwargs={ tpmessage,tpid = tpstuff.workout_tp_upload(
'message':message, request.user,w
'id':w.id, )
}) except TPNoTokenError:
pass
else: if message:
url = reverse(workout_edit_view, url = reverse(workout_edit_view,
kwargs = { kwargs={
'id':w.id, 'message':message,
}) 'id':w.id,
return HttpResponseRedirect(url) })
else:
url = reverse(workout_edit_view,
kwargs = {
'id':w.id,
})
return HttpResponseRedirect(url)
else: else:
response = render(request, response = render(request,
'document_form.html', 'document_form.html',
+2 -1
View File
@@ -241,7 +241,8 @@ RUNKEEPER_REDIRECT_URI = CFG['runkeeper_callback']
UNDERARMOUR_CLIENT_ID = CFG['underarmour_client_name'] UNDERARMOUR_CLIENT_ID = CFG['underarmour_client_name']
UNDERARMOUR_CLIENT_SECRET = CFG['underarmour_client_secret'] UNDERARMOUR_CLIENT_SECRET = CFG['underarmour_client_secret']
UNDERARMOUR_CLIENT_KEY = CFG['underarmour_client_key'] UNDERARMOUR_CLIENT_KEY = CFG['underarmour_client_key']
UNDERARMOUR_REDIRECT_URI = "http://rowsandall.com/underarmour_callback" UNDERARMOUR_REDIRECT_URI = CFG['underarmour_callback']
#UNDERARMOUR_REDIRECT_URI = "http://rowsandall.com/underarmour_callback"
#UNDERARMOUR_REDIRECT_URI = "http://localhost:8000/underarmour_callback" #UNDERARMOUR_REDIRECT_URI = "http://localhost:8000/underarmour_callback"
# TrainingPeaks # TrainingPeaks
+2
View File
File diff suppressed because one or more lines are too long
+84
View File
@@ -0,0 +1,84 @@
(function($) {
var POLL_INTERVAL = 2500;
var error = function(xhr, textStatus, errorThrown) {
var html = $('script[name=error-row]').html()
var tbody = $('#queues tbody');
tbody.empty();
tbody.append(html)
var tbody = $('#workers tbody');
tbody.empty();
tbody.append(html)
var tbody = $('#scheduled tbody');
tbody.empty();
tbody.append(html)
}
var success = function(data, textStatus, xhr) {
if (textStatus != 'success') {
return;
}
var tbody = $('#queues tbody');
tbody.empty();
if (data.queues.length > 0) {
var template = _.template($('script[name=queue-row]').html());
$.each(data.queues, function(i, queue) {
queue.klass = i % 2 == 0 ? 'row2' : 'row1';
var html = template(queue);
tbody.append($(html));
});
} else {
tbody.append($('script[name=no-queue-row]').html())
}
var tbody = $('#workers tbody');
tbody.empty();
if (data.workers.length > 0) {
var template = _.template($('script[name=worker-row]').html())
$.each(data.workers, function(i, worker) {
worker.klass = i % 2 == 0 ? 'row2' : 'row1';
worker.queues = worker.queues.join(', ');
var html = template(worker);
tbody.append($(html));
});
} else {
tbody.append($('script[name=no-worker-row]').html())
}
var tbody = $('#scheduled tbody');
tbody.empty();
if (data.scheduled_queues.length > 0) {
var template = _.template($('script[name=scheduled-row]').html())
$.each(data.scheduled_queues, function(i, queue) {
queue.klass = i % 2 == 0 ? 'row2' : 'row1';
var html = template(queue);
tbody.append($(html));
});
} else {
tbody.append($('script[name=no-scheduled-row]').html())
}
};
var refresh = function() {
$.ajax({
url: window.location.href,
dataType: 'json',
success: success,
error: error,
});
};
$(document).ready(function() {
setInterval(refresh, POLL_INTERVAL);
});
})($)
+32
View File
@@ -0,0 +1,32 @@
// Underscore.js 1.3.3
// (c) 2009-2012 Jeremy Ashkenas, DocumentCloud Inc.
// Underscore is freely distributable under the MIT license.
// Portions of Underscore are inspired or borrowed from Prototype,
// Oliver Steele's Functional, and John Resig's Micro-Templating.
// For all details and documentation:
// http://documentcloud.github.com/underscore
(function(){function r(a,c,d){if(a===c)return 0!==a||1/a==1/c;if(null==a||null==c)return a===c;a._chain&&(a=a._wrapped);c._chain&&(c=c._wrapped);if(a.isEqual&&b.isFunction(a.isEqual))return a.isEqual(c);if(c.isEqual&&b.isFunction(c.isEqual))return c.isEqual(a);var e=l.call(a);if(e!=l.call(c))return!1;switch(e){case "[object String]":return a==""+c;case "[object Number]":return a!=+a?c!=+c:0==a?1/a==1/c:a==+c;case "[object Date]":case "[object Boolean]":return+a==+c;case "[object RegExp]":return a.source==
c.source&&a.global==c.global&&a.multiline==c.multiline&&a.ignoreCase==c.ignoreCase}if("object"!=typeof a||"object"!=typeof c)return!1;for(var f=d.length;f--;)if(d[f]==a)return!0;d.push(a);var f=0,g=!0;if("[object Array]"==e){if(f=a.length,g=f==c.length)for(;f--&&(g=f in a==f in c&&r(a[f],c[f],d)););}else{if("constructor"in a!="constructor"in c||a.constructor!=c.constructor)return!1;for(var h in a)if(b.has(a,h)&&(f++,!(g=b.has(c,h)&&r(a[h],c[h],d))))break;if(g){for(h in c)if(b.has(c,h)&&!f--)break;
g=!f}}d.pop();return g}var s=this,I=s._,o={},k=Array.prototype,p=Object.prototype,i=k.slice,J=k.unshift,l=p.toString,K=p.hasOwnProperty,y=k.forEach,z=k.map,A=k.reduce,B=k.reduceRight,C=k.filter,D=k.every,E=k.some,q=k.indexOf,F=k.lastIndexOf,p=Array.isArray,L=Object.keys,t=Function.prototype.bind,b=function(a){return new m(a)};"undefined"!==typeof exports?("undefined"!==typeof module&&module.exports&&(exports=module.exports=b),exports._=b):s._=b;b.VERSION="1.3.3";var j=b.each=b.forEach=function(a,
c,d){if(a!=null)if(y&&a.forEach===y)a.forEach(c,d);else if(a.length===+a.length)for(var e=0,f=a.length;e<f;e++){if(e in a&&c.call(d,a[e],e,a)===o)break}else for(e in a)if(b.has(a,e)&&c.call(d,a[e],e,a)===o)break};b.map=b.collect=function(a,c,b){var e=[];if(a==null)return e;if(z&&a.map===z)return a.map(c,b);j(a,function(a,g,h){e[e.length]=c.call(b,a,g,h)});if(a.length===+a.length)e.length=a.length;return e};b.reduce=b.foldl=b.inject=function(a,c,d,e){var f=arguments.length>2;a==null&&(a=[]);if(A&&
a.reduce===A){e&&(c=b.bind(c,e));return f?a.reduce(c,d):a.reduce(c)}j(a,function(a,b,i){if(f)d=c.call(e,d,a,b,i);else{d=a;f=true}});if(!f)throw new TypeError("Reduce of empty array with no initial value");return d};b.reduceRight=b.foldr=function(a,c,d,e){var f=arguments.length>2;a==null&&(a=[]);if(B&&a.reduceRight===B){e&&(c=b.bind(c,e));return f?a.reduceRight(c,d):a.reduceRight(c)}var g=b.toArray(a).reverse();e&&!f&&(c=b.bind(c,e));return f?b.reduce(g,c,d,e):b.reduce(g,c)};b.find=b.detect=function(a,
c,b){var e;G(a,function(a,g,h){if(c.call(b,a,g,h)){e=a;return true}});return e};b.filter=b.select=function(a,c,b){var e=[];if(a==null)return e;if(C&&a.filter===C)return a.filter(c,b);j(a,function(a,g,h){c.call(b,a,g,h)&&(e[e.length]=a)});return e};b.reject=function(a,c,b){var e=[];if(a==null)return e;j(a,function(a,g,h){c.call(b,a,g,h)||(e[e.length]=a)});return e};b.every=b.all=function(a,c,b){var e=true;if(a==null)return e;if(D&&a.every===D)return a.every(c,b);j(a,function(a,g,h){if(!(e=e&&c.call(b,
a,g,h)))return o});return!!e};var G=b.some=b.any=function(a,c,d){c||(c=b.identity);var e=false;if(a==null)return e;if(E&&a.some===E)return a.some(c,d);j(a,function(a,b,h){if(e||(e=c.call(d,a,b,h)))return o});return!!e};b.include=b.contains=function(a,c){var b=false;if(a==null)return b;if(q&&a.indexOf===q)return a.indexOf(c)!=-1;return b=G(a,function(a){return a===c})};b.invoke=function(a,c){var d=i.call(arguments,2);return b.map(a,function(a){return(b.isFunction(c)?c||a:a[c]).apply(a,d)})};b.pluck=
function(a,c){return b.map(a,function(a){return a[c]})};b.max=function(a,c,d){if(!c&&b.isArray(a)&&a[0]===+a[0])return Math.max.apply(Math,a);if(!c&&b.isEmpty(a))return-Infinity;var e={computed:-Infinity};j(a,function(a,b,h){b=c?c.call(d,a,b,h):a;b>=e.computed&&(e={value:a,computed:b})});return e.value};b.min=function(a,c,d){if(!c&&b.isArray(a)&&a[0]===+a[0])return Math.min.apply(Math,a);if(!c&&b.isEmpty(a))return Infinity;var e={computed:Infinity};j(a,function(a,b,h){b=c?c.call(d,a,b,h):a;b<e.computed&&
(e={value:a,computed:b})});return e.value};b.shuffle=function(a){var b=[],d;j(a,function(a,f){d=Math.floor(Math.random()*(f+1));b[f]=b[d];b[d]=a});return b};b.sortBy=function(a,c,d){var e=b.isFunction(c)?c:function(a){return a[c]};return b.pluck(b.map(a,function(a,b,c){return{value:a,criteria:e.call(d,a,b,c)}}).sort(function(a,b){var c=a.criteria,d=b.criteria;return c===void 0?1:d===void 0?-1:c<d?-1:c>d?1:0}),"value")};b.groupBy=function(a,c){var d={},e=b.isFunction(c)?c:function(a){return a[c]};
j(a,function(a,b){var c=e(a,b);(d[c]||(d[c]=[])).push(a)});return d};b.sortedIndex=function(a,c,d){d||(d=b.identity);for(var e=0,f=a.length;e<f;){var g=e+f>>1;d(a[g])<d(c)?e=g+1:f=g}return e};b.toArray=function(a){return!a?[]:b.isArray(a)||b.isArguments(a)?i.call(a):a.toArray&&b.isFunction(a.toArray)?a.toArray():b.values(a)};b.size=function(a){return b.isArray(a)?a.length:b.keys(a).length};b.first=b.head=b.take=function(a,b,d){return b!=null&&!d?i.call(a,0,b):a[0]};b.initial=function(a,b,d){return i.call(a,
0,a.length-(b==null||d?1:b))};b.last=function(a,b,d){return b!=null&&!d?i.call(a,Math.max(a.length-b,0)):a[a.length-1]};b.rest=b.tail=function(a,b,d){return i.call(a,b==null||d?1:b)};b.compact=function(a){return b.filter(a,function(a){return!!a})};b.flatten=function(a,c){return b.reduce(a,function(a,e){if(b.isArray(e))return a.concat(c?e:b.flatten(e));a[a.length]=e;return a},[])};b.without=function(a){return b.difference(a,i.call(arguments,1))};b.uniq=b.unique=function(a,c,d){var d=d?b.map(a,d):a,
e=[];a.length<3&&(c=true);b.reduce(d,function(d,g,h){if(c?b.last(d)!==g||!d.length:!b.include(d,g)){d.push(g);e.push(a[h])}return d},[]);return e};b.union=function(){return b.uniq(b.flatten(arguments,true))};b.intersection=b.intersect=function(a){var c=i.call(arguments,1);return b.filter(b.uniq(a),function(a){return b.every(c,function(c){return b.indexOf(c,a)>=0})})};b.difference=function(a){var c=b.flatten(i.call(arguments,1),true);return b.filter(a,function(a){return!b.include(c,a)})};b.zip=function(){for(var a=
i.call(arguments),c=b.max(b.pluck(a,"length")),d=Array(c),e=0;e<c;e++)d[e]=b.pluck(a,""+e);return d};b.indexOf=function(a,c,d){if(a==null)return-1;var e;if(d){d=b.sortedIndex(a,c);return a[d]===c?d:-1}if(q&&a.indexOf===q)return a.indexOf(c);d=0;for(e=a.length;d<e;d++)if(d in a&&a[d]===c)return d;return-1};b.lastIndexOf=function(a,b){if(a==null)return-1;if(F&&a.lastIndexOf===F)return a.lastIndexOf(b);for(var d=a.length;d--;)if(d in a&&a[d]===b)return d;return-1};b.range=function(a,b,d){if(arguments.length<=
1){b=a||0;a=0}for(var d=arguments[2]||1,e=Math.max(Math.ceil((b-a)/d),0),f=0,g=Array(e);f<e;){g[f++]=a;a=a+d}return g};var H=function(){};b.bind=function(a,c){var d,e;if(a.bind===t&&t)return t.apply(a,i.call(arguments,1));if(!b.isFunction(a))throw new TypeError;e=i.call(arguments,2);return d=function(){if(!(this instanceof d))return a.apply(c,e.concat(i.call(arguments)));H.prototype=a.prototype;var b=new H,g=a.apply(b,e.concat(i.call(arguments)));return Object(g)===g?g:b}};b.bindAll=function(a){var c=
i.call(arguments,1);c.length==0&&(c=b.functions(a));j(c,function(c){a[c]=b.bind(a[c],a)});return a};b.memoize=function(a,c){var d={};c||(c=b.identity);return function(){var e=c.apply(this,arguments);return b.has(d,e)?d[e]:d[e]=a.apply(this,arguments)}};b.delay=function(a,b){var d=i.call(arguments,2);return setTimeout(function(){return a.apply(null,d)},b)};b.defer=function(a){return b.delay.apply(b,[a,1].concat(i.call(arguments,1)))};b.throttle=function(a,c){var d,e,f,g,h,i,j=b.debounce(function(){h=
g=false},c);return function(){d=this;e=arguments;f||(f=setTimeout(function(){f=null;h&&a.apply(d,e);j()},c));g?h=true:i=a.apply(d,e);j();g=true;return i}};b.debounce=function(a,b,d){var e;return function(){var f=this,g=arguments;d&&!e&&a.apply(f,g);clearTimeout(e);e=setTimeout(function(){e=null;d||a.apply(f,g)},b)}};b.once=function(a){var b=false,d;return function(){if(b)return d;b=true;return d=a.apply(this,arguments)}};b.wrap=function(a,b){return function(){var d=[a].concat(i.call(arguments,0));
return b.apply(this,d)}};b.compose=function(){var a=arguments;return function(){for(var b=arguments,d=a.length-1;d>=0;d--)b=[a[d].apply(this,b)];return b[0]}};b.after=function(a,b){return a<=0?b():function(){if(--a<1)return b.apply(this,arguments)}};b.keys=L||function(a){if(a!==Object(a))throw new TypeError("Invalid object");var c=[],d;for(d in a)b.has(a,d)&&(c[c.length]=d);return c};b.values=function(a){return b.map(a,b.identity)};b.functions=b.methods=function(a){var c=[],d;for(d in a)b.isFunction(a[d])&&
c.push(d);return c.sort()};b.extend=function(a){j(i.call(arguments,1),function(b){for(var d in b)a[d]=b[d]});return a};b.pick=function(a){var c={};j(b.flatten(i.call(arguments,1)),function(b){b in a&&(c[b]=a[b])});return c};b.defaults=function(a){j(i.call(arguments,1),function(b){for(var d in b)a[d]==null&&(a[d]=b[d])});return a};b.clone=function(a){return!b.isObject(a)?a:b.isArray(a)?a.slice():b.extend({},a)};b.tap=function(a,b){b(a);return a};b.isEqual=function(a,b){return r(a,b,[])};b.isEmpty=
function(a){if(a==null)return true;if(b.isArray(a)||b.isString(a))return a.length===0;for(var c in a)if(b.has(a,c))return false;return true};b.isElement=function(a){return!!(a&&a.nodeType==1)};b.isArray=p||function(a){return l.call(a)=="[object Array]"};b.isObject=function(a){return a===Object(a)};b.isArguments=function(a){return l.call(a)=="[object Arguments]"};b.isArguments(arguments)||(b.isArguments=function(a){return!(!a||!b.has(a,"callee"))});b.isFunction=function(a){return l.call(a)=="[object Function]"};
b.isString=function(a){return l.call(a)=="[object String]"};b.isNumber=function(a){return l.call(a)=="[object Number]"};b.isFinite=function(a){return b.isNumber(a)&&isFinite(a)};b.isNaN=function(a){return a!==a};b.isBoolean=function(a){return a===true||a===false||l.call(a)=="[object Boolean]"};b.isDate=function(a){return l.call(a)=="[object Date]"};b.isRegExp=function(a){return l.call(a)=="[object RegExp]"};b.isNull=function(a){return a===null};b.isUndefined=function(a){return a===void 0};b.has=function(a,
b){return K.call(a,b)};b.noConflict=function(){s._=I;return this};b.identity=function(a){return a};b.times=function(a,b,d){for(var e=0;e<a;e++)b.call(d,e)};b.escape=function(a){return(""+a).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#x27;").replace(/\//g,"&#x2F;")};b.result=function(a,c){if(a==null)return null;var d=a[c];return b.isFunction(d)?d.call(a):d};b.mixin=function(a){j(b.functions(a),function(c){M(c,b[c]=a[c])})};var N=0;b.uniqueId=
function(a){var b=N++;return a?a+b:b};b.templateSettings={evaluate:/<%([\s\S]+?)%>/g,interpolate:/<%=([\s\S]+?)%>/g,escape:/<%-([\s\S]+?)%>/g};var u=/.^/,n={"\\":"\\","'":"'",r:"\r",n:"\n",t:"\t",u2028:"\u2028",u2029:"\u2029"},v;for(v in n)n[n[v]]=v;var O=/\\|'|\r|\n|\t|\u2028|\u2029/g,P=/\\(\\|'|r|n|t|u2028|u2029)/g,w=function(a){return a.replace(P,function(a,b){return n[b]})};b.template=function(a,c,d){d=b.defaults(d||{},b.templateSettings);a="__p+='"+a.replace(O,function(a){return"\\"+n[a]}).replace(d.escape||
u,function(a,b){return"'+\n_.escape("+w(b)+")+\n'"}).replace(d.interpolate||u,function(a,b){return"'+\n("+w(b)+")+\n'"}).replace(d.evaluate||u,function(a,b){return"';\n"+w(b)+"\n;__p+='"})+"';\n";d.variable||(a="with(obj||{}){\n"+a+"}\n");var a="var __p='';var print=function(){__p+=Array.prototype.join.call(arguments, '')};\n"+a+"return __p;\n",e=new Function(d.variable||"obj","_",a);if(c)return e(c,b);c=function(a){return e.call(this,a,b)};c.source="function("+(d.variable||"obj")+"){\n"+a+"}";return c};
b.chain=function(a){return b(a).chain()};var m=function(a){this._wrapped=a};b.prototype=m.prototype;var x=function(a,c){return c?b(a).chain():a},M=function(a,c){m.prototype[a]=function(){var a=i.call(arguments);J.call(a,this._wrapped);return x(c.apply(b,a),this._chain)}};b.mixin(b);j("pop,push,reverse,shift,sort,splice,unshift".split(","),function(a){var b=k[a];m.prototype[a]=function(){var d=this._wrapped;b.apply(d,arguments);var e=d.length;(a=="shift"||a=="splice")&&e===0&&delete d[0];return x(d,
this._chain)}});j(["concat","join","slice"],function(a){var b=k[a];m.prototype[a]=function(){return x(b.apply(this._wrapped,arguments),this._chain)}});m.prototype.chain=function(){this._chain=true;return this};m.prototype.value=function(){return this._wrapped}}).call(this);
Binary file not shown.
+6 -2
View File
@@ -184,9 +184,13 @@
<p id="footer"> <p id="footer">
<a href="/rowers/about">About</a></p> <a href="/rowers/about">About</a></p>
</div> </div>
<div class="grid_2"> <div class="grid_1">
<p id="footer">
<a href="/rowers/brochure">Brochure</a></p>
</div>
<div class="grid_1">
<p id="footer"> <p id="footer">
<a href="/rowers/developers">Developers</a></p> <a href="/rowers/developers">Develop</a></p>
</div> </div>
<div class="grid_1"> <div class="grid_1">
<p id="footer"> <p id="footer">