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
+74 -38
View File
@@ -27,6 +27,7 @@ from rowingdata import rowingdata
import pandas as pd
import numpy as np
from rowers.models import Rower,Workout
from rowers.models import checkworkoutuser
import sys
import urllib
from requests import Request, Session
@@ -60,13 +61,26 @@ def custom_exception_handler(exc,message):
return res
# 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)
# 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 = 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
def makeseconds(t):
@@ -249,9 +263,13 @@ def createc2workoutdata(w):
row = rowingdata(filename)
except IOError:
return 0
averagehr = int(row.df[' HRCur (bpm)'].mean())
maxhr = int(row.df[' HRCur (bpm)'].max())
try:
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
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)
spm = row.df[' Cadence (stokes/min)'].astype(int)
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 = []
for i in range(len(t)):
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
def workout_c2_upload(user,w):
response = 'trying C2 upload'
r = Rower.objects.get(user=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"
thetoken = c2_open(user)
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
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
def timedeltaconv(x):
if not np.isnan(x) and x != 0:
if np.isfinite(x) and x != 0:
dt = datetime.timedelta(seconds=x)
else:
dt = datetime.timedelta(seconds=350.)
@@ -471,6 +471,12 @@ def save_workout_database(f2,r,dosmooth=True,workouttype='rower',
if (len(ws) != 0):
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,
workouttype=workouttype,
@@ -1069,6 +1075,9 @@ def datafusion(id1,id2,columns,offset):
# Takes a rowingdata object's DataFrame as input
def dataprep(rowdatadf,id=0,bands=True,barchart=True,otwpower=True,
empower=True,inboard=0.88):
if rowdatadf.empty:
return 0
rowdatadf.set_index([range(len(rowdatadf))],inplace=True)
t = rowdatadf.ix[:,'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,
initial='timeplot',
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,
label='Make Workout Private')
+179 -6
View File
@@ -25,6 +25,8 @@ from bokeh.models import (
SaveTool, ResizeTool, ResetTool, TapTool,CrosshairTool,BoxZoomTool,
Span, Label
)
from bokeh.models.glyphs import ImageURL
#from bokeh.models.widgets import Slider, Select, TextInput
from bokeh.core.properties import value
@@ -54,6 +56,19 @@ from rowers.metrics import axes,axlabels,yaxminima,yaxmaxima
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):
""" Calculates head-on head/tailwind in direction of rowing
@@ -161,6 +176,21 @@ def interactive_forcecurve(theworkouts,workstrokesonly=False):
plot = Figure(tools=TOOLS,
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',
line_dash=[6,6],line_width=2)
@@ -400,6 +430,21 @@ def interactive_histoall(theworkouts):
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)
histsum = np.cumsum(hist)
@@ -434,7 +479,7 @@ def interactive_histoall(theworkouts):
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",
y_range_name="fraction")
plot.add_layout(LinearAxis(y_range_name="fraction",
@@ -444,6 +489,8 @@ def interactive_histoall(theworkouts):
return [script,div]
def googlemap_chart(lat,lon,name=""):
if lat.empty or lon.empty:
return [0,"invalid coordinate data"]
# plot tools
TOOLS = 'save,pan,box_zoom,wheel_zoom,reset,tap,resize'
@@ -595,6 +642,20 @@ def interactive_cpchart(thedistances,thesecs,theavpower,
plot_width=900,
toolbar_location="above",
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,
legend='Power')
plot.xaxis.axis_label = "Duration (seconds)"
@@ -665,6 +726,7 @@ def interactive_windchart(id=0,promember=0):
# create interactive plot
plot = Figure(plot_width=400,plot_height=300)
# get user
# u = User.objects.get(id=row.user.id)
r = row.user
@@ -836,6 +898,21 @@ def interactive_chart(id=0,promember=0):
toolbar_sticky=False,
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.title.text = row.name
plot.title.text_font_size=value("1.0em")
@@ -875,7 +952,7 @@ def interactive_chart(id=0,promember=0):
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",
y_range_name="hrax", legend="Heart Rate")
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_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',
line_dash=[6,6], line_width=2)
@@ -1034,7 +1126,7 @@ def interactive_cum_flex_chart2(theworkouts,promember=0,
if yparam2 != 'None':
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",
legend=yparamname2,
@@ -1323,7 +1415,24 @@ def interactive_flex_chart2(id=0,promember=0,
tools=TOOLS,
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',
line_dash=[6,6], line_width=2)
@@ -1409,9 +1518,11 @@ def interactive_flex_chart2(id=0,promember=0,
minutes = ["%M"]
)
if yparam2 != 'None':
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':
plot.line('x1','y2',color="red",y_range_name="yax2",
@@ -1461,7 +1572,8 @@ def interactive_flex_chart2(id=0,promember=0,
y2label=y2label,
xlabel=xlabel,
annolabel=annolabel,
y2means=y2means), code="""
y2means=y2means,
), code="""
var data = source.data
var data2 = source2.data
var x1 = data['x1']
@@ -1622,6 +1734,22 @@ def interactive_bar_chart(id=0,promember=0):
toolbar_sticky=False,
plot_width=920,
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_font_size=value("1.0em")
plot.xaxis.axis_label = "Time"
@@ -1662,7 +1790,7 @@ def interactive_bar_chart(id=0,promember=0):
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',
right='x_right',source=source,color="gray",
y_range_name="hr", legend="<UT2")
@@ -1765,6 +1893,21 @@ def interactive_multiple_compare_chart(ids,xparam,yparam,plottype='line',
plot_width=920,
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)
cntr = 0
@@ -1980,6 +2123,21 @@ def interactive_comparison_chart(id1=0,id2=0,xparam='distance',yparam='spm',
plot_width=920,
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([
('time','@ftime1'),
('pace','@fpace1'),
@@ -2076,6 +2234,21 @@ def interactive_otw_advanced_pace_chart(id=0,promember=0):
plot_width=920,
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_font_size=value("1.2em")
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,
f2,"roosendaalsander@gmail.com")
return 0
return 1
summary = ''
# handle non-Painsled
+10 -2
View File
@@ -209,7 +209,6 @@ class Rower(models.Model):
runkeepertoken = models.CharField(default='',max_length=200,
blank=True,null=True)
# runkeepertokenexpirydate = models.DateTimeField(blank=True,null=True)
# runkeeperrefreshtoken = models.CharField(default='',max_length=200,
# blank=True,null=True)
@@ -324,6 +323,15 @@ class BaseFavoriteFormSet(BaseFormSet):
if not yparam2:
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
class Workout(models.Model):
workouttypes = (
@@ -398,7 +406,7 @@ class Workout(models.Model):
uploadedtounderarmour = models.IntegerField(default=0)
uploadedtotp = models.IntegerField(default=0)
uploadedtorunkeeper = models.IntegerField(default=0)
# empower stuff
inboard = models.FloatField(default=0.88)
oarlength = models.FloatField(default=2.89)
+89 -4
View File
@@ -27,7 +27,7 @@ from django.contrib.auth.decorators import login_required
# from .models import Profile
from rowingdata import rowingdata
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,
@@ -86,6 +86,17 @@ def custom_exception_handler(exc,message):
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
def get_token(code):
client_auth = requests.auth.HTTPBasicAuth(RUNKEEPER_CLIENT_ID, RUNKEEPER_CLIENT_SECRET)
@@ -261,6 +272,26 @@ def getidfromresponse(response):
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
# Handy for checking if the API access is working
@@ -277,11 +308,65 @@ def get_userid(access_token):
try:
me_json = response.json()
except:
return 0
return ''
try:
res = me_json['userID']
except KeyError:
res = 0
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
return res
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 rowingdata import rowingdata
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
@@ -61,6 +61,21 @@ def custom_exception_handler(exc,message):
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
def do_refresh_token(refreshtoken):
client_auth = requests.auth.HTTPBasicAuth(SPORTTRACKS_CLIENT_ID, SPORTTRACKS_CLIENT_SECRET)
@@ -303,3 +318,51 @@ def getidfromresponse(response):
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
import pandas as pd
from rowers.models import Rower,Workout
from rowers.models import checkworkoutuser
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
@@ -77,6 +79,15 @@ def custom_exception_handler(exc,message):
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
def get_token(code):
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)
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" %}
{% block filters %}
{% load rowerfilters %}
{% load rowerfilters %}
{% endblock %}
{% block teams %}
{% if user.is_authenticated and user|has_teams %}
<div class="grid_1 alpha dropdown">
<button class="grid_1 alpha button gray small dropbtn">
Teams
</button>
<div class="dropdown-content">
<a class="button gray small" href="/rowers/me/teams/">Manage Teams</a>
{% if user|is_manager %}
<a class="button gray small" href="/rowers/workout/upload/team/">Upload Team Member Workout</a>
{% endif %}
{% for t in user|user_teams %}
<a class="button gray small" href="/rowers/list-workouts/team/{{ t.id }}/">{{ t.name }}</a>
{% endfor %}
</div>
</div>
<span class="tooltiptext">See recent workouts for your team</span>
{% elif user.is_authenticated and user.rower.team.all %}
<div class="grid_1 alpha dropdown">
<button class="grid_1 alpha button gray small dropbtn">
Teams
</button>
<div class="dropdown-content">
{% for t in user.rower.team.all %}
<a class="button gray small" href="/rowers/list-workouts/team/{{ t.id }}/">{{ t.name }}</a>
{% endfor %}
</div>
</div>
<span class="tooltiptext">See recent workouts for your team</span>
{% else %}
<p>&nbsp;</p>
{% endif %}
{% if user.is_authenticated and user|has_teams %}
<div class="grid_1 alpha dropdown">
<button class="grid_1 alpha button gray small dropbtn">
Teams
</button>
<div class="dropdown-content">
<a class="button gray small" href="/rowers/me/teams/">Manage Teams</a>
{% if user|is_manager %}
<a class="button gray small" href="/rowers/workout/upload/team/">Upload Team Member Workout</a>
{% endif %}
{% for t in user|user_teams %}
<a class="button gray small" href="/rowers/list-workouts/team/{{ t.id }}/">{{ t.name }}</a>
{% endfor %}
</div>
</div>
<span class="tooltiptext">See recent workouts for your team</span>
{% elif user.is_authenticated and user.rower.team.all %}
<div class="grid_1 alpha dropdown">
<button class="grid_1 alpha button gray small dropbtn">
Teams
</button>
<div class="dropdown-content">
{% for t in user.rower.team.all %}
<a class="button gray small" href="/rowers/list-workouts/team/{{ t.id }}/">{{ t.name }}</a>
{% endfor %}
</div>
</div>
<span class="tooltiptext">See recent workouts for your team</span>
{% else %}
<p>&nbsp;</p>
{% endif %}
{% endblock %}
{% block content %}
+5 -1
View File
@@ -184,7 +184,11 @@
<p id="footer">
<a href="/rowers/about">About</a></p>
</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">
<a href="/rowers/developers">Developers</a></p>
</div>
+186 -182
View File
@@ -2,188 +2,192 @@
<html lang="en">
<head>
<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-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/favicon.ico" />
<meta charset="utf-8" />
<meta name="viewport" content="initial-scale=0.67">
<title>Rowsandall</title>
<link rel="stylesheet" href="/static/css/reset.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/rowsandall.css" />
{% block meta %} {% endblock %}
</head>
<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="shortcut icon" href="/static/img/myicon.png" />
<link rel="shortcut icon" href="/static/img/favicon.ico" />
<meta charset="utf-8" />
<meta name="viewport" content="initial-scale=0.67">
<title>Rowsandall</title>
<link rel="stylesheet" href="/static/css/reset.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/rowsandall.css" />
{% block meta %} {% endblock %}
</head>
<body>
<div class="container_12">
<div id="logo" class="grid_2">
{% if user.rower.rowerplan == 'pro' or user.rower.rowerplan == 'coach' %}
<p><a href="/"><img src="/static/img/logocroppedpro.gif"
alt="Rowsandall logo" width="110" heigt="110"></a></p>
{% else %}
<p><a href="/"><img src="/static/img/logocropped.gif"
alt="Rowsandall logo" width="110" heigt="110"></a></p>
{% endif %}
</div>
<div class="grid_10 omega">
<div class="grid_8 alpha"><p>&nbsp</p></div>
<div class="grid_2 omega">
{% if user.is_authenticated %}
<p><a class="button gray small" href="/password_change/">Password Change</a></p>
{% else %}
<p><a class="button gray small" href="/password_reset/">Forgotten Password?</a></p>
{% 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 class="container_12">
<div id="logo" class="grid_2">
{% if user.rower.rowerplan == 'pro' or user.rower.rowerplan == 'coach' %}
<p><a href="/"><img src="/static/img/logocroppedpro.gif"
alt="Rowsandall logo" width="110" heigt="110"></a></p>
{% else %}
<p><a href="/"><img src="/static/img/logocropped.gif"
alt="Rowsandall logo" width="110" heigt="110"></a></p>
{% endif %}
</div>
<div class="grid_10 omega">
<div class="grid_8 alpha"><p>&nbsp</p></div>
<div class="grid_2 omega">
{% if user.is_authenticated %}
<p><a class="button gray small" href="/password_change/">Password Change</a></p>
{% else %}
<p><a class="button gray small" href="/password_reset/">Forgotten Password?</a></p>
{% endif %}
</div>
<!-- end container -->
</body>
</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_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>
+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>
</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>
+2 -2
View File
@@ -106,7 +106,7 @@
{% endif %}
{% else %}
<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>
</div>
{% endif %}
@@ -142,7 +142,7 @@
{% endif %}
{% else %}
<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>
</div>
{% endif %}
+74 -5
View File
@@ -8,6 +8,7 @@ import requests.auth
import json
from django.utils import timezone
from datetime import datetime
from datetime import timedelta
import numpy as np
from dateutil import parser
import time
@@ -31,7 +32,7 @@ from django.contrib.auth.decorators import login_required
# from .models import Profile
from rowingdata import rowingdata
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,
@@ -40,7 +41,7 @@ from rowsandall_app.settings import (
TP_REDIRECT_URI,TP_CLIENT_KEY,
)
tpapilocation = "https://api.sandbox.trainingpeaks.com"
tpapilocation = "https://api.trainingpeaks.com"
# Custom error class - to raise a NoTokenError
class TPNoTokenError(Exception):
@@ -93,6 +94,29 @@ def custom_exception_handler(exc,message):
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
def do_refresh_token(refreshtoken):
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,
data=post_data,
@@ -141,7 +165,7 @@ def get_token(code):
'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)
@@ -169,7 +193,7 @@ def make_authorization_url(request):
"redirect_uri": TP_REDIRECT_URI,
"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)
@@ -270,3 +294,48 @@ def uploadactivity(access_token,filename,description='',
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 rowingdata import rowingdata
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,
@@ -88,6 +88,20 @@ def custom_exception_handler(exc,message):
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
def do_refresh_token(refreshtoken,access_token):
client_auth = requests.auth.HTTPBasicAuth(UNDERARMOUR_CLIENT_KEY, UNDERARMOUR_CLIENT_SECRET)
@@ -385,3 +399,55 @@ def get_userid(access_token):
res = 0
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/$', TemplateView.as_view(template_name='email.html'), name='email'),
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'^compatibility', TemplateView.as_view(template_name='compatibility.html'),name='about'),
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
def serialize_list(value,token=','):
assert(isinstance(value, list) or isinstance(value, tuple) or isinstance(value,np.ndarray))
return token.join([unicode(s) for s in value])
+112 -214
View File
@@ -51,14 +51,16 @@ import os,sys
import datetime
import iso8601
import c2stuff
from c2stuff import C2NoTokenError
from runkeeperstuff import RunKeeperNoTokenError
from sporttracksstuff import SportTracksNoTokenError
from tpstuff import TPNoTokenError
from c2stuff import C2NoTokenError,c2_open
from runkeeperstuff import RunKeeperNoTokenError,runkeeper_open
from sporttracksstuff import SportTracksNoTokenError,sporttracks_open
from tpstuff import TPNoTokenError,tp_open
from iso8601 import ParseError
import stravastuff
from stravastuff import StravaNoTokenError
import sporttracksstuff
import underarmourstuff
from underarmourstuff import UnderArmourNoTokenError,underarmour_open
import tpstuff
import runkeeperstuff
import ownapistuff
@@ -261,6 +263,7 @@ def splitstdata(lijst):
return [np.array(t),np.array(latlong)]
from utils import geo_distance,serialize_list,deserialize_list
from rowers.models import checkworkoutuser
# Check if a user is a Coach member
def iscoachmember(user):
@@ -369,14 +372,6 @@ def sendmail(request):
else:
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
# 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)
# 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
@login_required()
@@ -1395,76 +1313,20 @@ def workout_c2_upload_view(request,id=0):
raise Http404("Workout doesn't exist")
try:
thetoken = c2_open(r.user)
message,c2id = c2stuff.workout_c2_upload(request.user,w)
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:
message = "You are not authorized to upload this workout"
url = reverse(workout_export_view,
kwargs = {
'message':str(message),
'id':str(w.id),
url = reverse(workout_export_view,
kwargs = {
'id':str(w.id),
})
return HttpResponseRedirect(url)
@@ -1544,7 +1406,7 @@ def workout_underarmour_upload_view(request,id=0):
try:
thetoken = underarmour_open(r.user)
except UnderarmourNoTokenError:
except UnderArmourNoTokenError:
return HttpResponseRedirect("/rowers/me/underarmourauthorize/")
# ready to upload. Hurray
@@ -1757,7 +1619,7 @@ def rower_tp_authorize(request):
"redirect_uri": TP_REDIRECT_URI,
"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)
@@ -4604,7 +4466,7 @@ def workout_otwpowerplot_view(request,id=0,message="",successmessage=""):
'the_div':div,
'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()
def workout_export_view(request,id=0, message="", successmessage=""):
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)
w = Workout.objects.get(id=id)
w.uploadedtorunkeeper=runkeeperid
thetoken = runkeeper_open(request.user)
w.save()
if message:
url = reverse(workout_edit_view,
@@ -5820,6 +5683,31 @@ def workout_upload_view(request,message="",
except KeyError:
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)
if request.method == 'POST':
form = DocumentsForm(request.POST,request.FILES)
@@ -5836,6 +5724,11 @@ def workout_upload_view(request,message="",
make_plot = optionsform.cleaned_data['make_plot']
plottype = optionsform.cleaned_data['plottype']
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']
uploadoptions = {
@@ -5843,6 +5736,11 @@ def workout_upload_view(request,message="",
'make_plot':make_plot,
'plottype':plottype,
'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)
i.save()
# upload to C2
if (upload_to_c2):
try:
thetoken = c2_open(request.user)
except C2NoTokenError:
return HttpResponseRedirect("/rowers/me/c2authorize/")
try:
c2userid = c2stuff.get_userid(thetoken)
if not c2userid:
return HttpResponseRedirect("/rowers/me/c2authorize")
data = c2stuff.createc2workoutdata(w)
authorizationstring = str('Bearer ' + thetoken)
headers = {'Authorization': authorizationstring,
'user-agent': 'sanderroosendaal',
'Content-Type': 'application/json'}
# upload to C2
if (upload_to_c2):
try:
c2message,c2id = c2stuff.workout_c2_upload(request.user,w)
except C2NoTokenError:
pass
if (upload_to_strava):
try:
stravamessage,stravaid = stravastuff.workout_strava_upload(
request.user,w
)
except StravaNoTokenError:
pass
url = "https://log.concept2.com/api/users/%s/results" % (c2userid)
response = requests.post(url,headers=headers,data=json.dumps(data))
if (upload_to_st):
try:
stmessage,stid = sporttracksstuff.workout_sporttracks_upload(
request.user,w
)
except SportTracksNoTokenError:
pass
if (upload_to_rk):
try:
rkmessage,rkid = runkeeperstuff.workout_runkeeper_upload(
request.user,w
)
except RunKeeperNoTokenError:
pass
# response = c2stuff.workout_c2_upload(request.user,w)
if (response.status_code != 201 and response.status_code != 200):
if settings.DEBUG:
return HttpResponse(response)
else:
message = "C2 upload failed"
url = reverse(workout_edit_view,
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 (upload_to_tp):
try:
tpmessage,tpid = tpstuff.workout_tp_upload(
request.user,w
)
except TPNoTokenError:
pass
if message:
url = reverse(workout_edit_view,
kwargs={
'message':message,
'id':w.id,
})
if message:
url = reverse(workout_edit_view,
kwargs={
'message':message,
'id':w.id,
})
else:
url = reverse(workout_edit_view,
kwargs = {
'id':w.id,
})
return HttpResponseRedirect(url)
else:
url = reverse(workout_edit_view,
kwargs = {
'id':w.id,
})
return HttpResponseRedirect(url)
else:
response = render(request,
'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_SECRET = CFG['underarmour_client_secret']
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"
# 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">
<a href="/rowers/about">About</a></p>
</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">
<a href="/rowers/developers">Developers</a></p>
<a href="/rowers/developers">Develop</a></p>
</div>
<div class="grid_1">
<p id="footer">