Merge branch 'release/v2.5'
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 149 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 209 KiB |
|
After Width: | Height: | Size: 138 KiB |
|
After Width: | Height: | Size: 158 KiB |
|
After Width: | Height: | Size: 158 KiB |
|
After Width: | Height: | Size: 250 KiB |
|
After Width: | Height: | Size: 115 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 164 KiB |
|
After Width: | Height: | Size: 137 KiB |
|
After Width: | Height: | Size: 121 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 85 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 115 KiB |
|
After Width: | Height: | Size: 113 KiB |
|
After Width: | Height: | Size: 125 KiB |
|
After Width: | Height: | Size: 193 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 86 KiB |
|
After Width: | Height: | Size: 136 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 149 KiB |
|
After Width: | Height: | Size: 113 KiB |
|
After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 66 KiB |
@@ -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;
|
||||
}
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -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
|
||||
);
|
||||
@@ -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);
|
||||
@@ -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
|
||||
@@ -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):
|
||||
|
||||
@@ -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)'])
|
||||
|
||||
@@ -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')
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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> </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> </p>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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> </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> </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> </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> </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> </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> </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> </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/">© 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> </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> </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> </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> </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> </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> </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> </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/">© 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>
|
||||
|
||||
@@ -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 %}
|
||||
@@ -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>
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
})($)
|
||||
@@ -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,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'").replace(/\//g,"/")};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);
|
||||
@@ -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">
|
||||
|
||||