Private
Public Access
1
0

Merge branch 'develop' into feature/stravadevice

This commit is contained in:
Sander Roosendaal
2017-04-18 08:05:34 +02:00
47 changed files with 3490 additions and 138 deletions

View File

@@ -4,6 +4,7 @@ from rowers.models import Workout, User, Rower,StrokeData
from rowingdata import rowingdata as rrdata
from rowers.tasks import handle_sendemail_unrecognized
from rowers.tasks import handle_zip_file
from rowingdata import rower as rrower
from rowingdata import main as rmain
@@ -44,7 +45,7 @@ import sys
import django_rq
queue = django_rq.get_queue('default')
queuelow = django_rq.get_queue('low')
queuehigh = django_rq.get_queue('low')
queuehigh = django_rq.get_queue('default')
user = settings.DATABASES['default']['USER']
@@ -362,7 +363,10 @@ def save_workout_database(f2,r,dosmooth=True,workouttype='rower',
velo = 500./pace
f = row.df['TimeStamp (sec)'].diff().mean()
windowsize = 2*(int(10./(f)))+1
if f !=0:
windowsize = 2*(int(10./(f)))+1
else:
windowsize = 1
if not 'originalvelo' in row.df:
row.df['originalvelo'] = velo
@@ -569,12 +573,21 @@ def new_workout_from_file(r,f2,
inboard = 0.88
if len(fileformat)==3 and fileformat[0]=='zip':
f_to_be_deleted = f2
with zipfile.ZipFile(f2) as z:
# for now, we're getting only the first file
# from the NK zip file (issue #69 on bitbucket)
f2 = z.extract(z.namelist()[0],path='media/')
fileformat = fileformat[2]
os.remove(f_to_be_deleted)
title = os.path.basename(f2)
if settings.DEBUG:
res = handle_zip_file.delay(
r.user.email,title,f2
)
else:
res = queuelow.enqueue(
handle_zip_file,
r.user.email,
title,
f2
)
return -1,message,f2
# Some people try to upload Concept2 logbook summaries
if fileformat == 'c2log':
@@ -770,6 +783,8 @@ def rdata(file,rower=rrower()):
res = rrdata(csvfile=file+'.gz',rower=rower)
except IOError,IndexError:
res = 0
except:
res = 0
return res
@@ -1047,7 +1062,10 @@ def dataprep(rowdatadf,id=0,bands=True,barchart=True,otwpower=True,
rhythm = 0.0*forceratio
f = rowdatadf['TimeStamp (sec)'].diff().mean()
windowsize = 2*(int(10./(f)))+1
if f != 0:
windowsize = 2*(int(10./(f)))+1
else:
windowsize = 1
if windowsize <= 3:
windowsize = 5

View File

@@ -91,6 +91,311 @@ def rdata(file,rower=rrower()):
return res
# Processes painsled CSV file to database
def save_workout_database(f2,r,dosmooth=True,workouttype='rower',
dosummary=True,title='Workout',
notes='',totaldist=0,totaltime=0,
summary='',
makeprivate=False,
oarlength=2.89,inboard=0.88):
message = None
powerperc = 100*np.array([r.pw_ut2,
r.pw_ut1,
r.pw_at,
r.pw_tr,r.pw_an])/r.ftp
# make workout and put in database
rr = rrower(hrmax=r.max,hrut2=r.ut2,
hrut1=r.ut1,hrat=r.at,
hrtr=r.tr,hran=r.an,ftp=r.ftp,
powerperc=powerperc,powerzones=r.powerzones)
row = rdata(f2,rower=rr)
if row == 0:
return (0,'Error: CSV data file not found')
if dosmooth:
# auto smoothing
pace = row.df[' Stroke500mPace (sec/500m)'].values
velo = 500./pace
f = row.df['TimeStamp (sec)'].diff().mean()
if f !=0:
windowsize = 2*(int(10./(f)))+1
else:
windowsize = 1
if not 'originalvelo' in row.df:
row.df['originalvelo'] = velo
if windowsize > 3 and windowsize<len(velo):
velo2 = savgol_filter(velo,windowsize,3)
else:
velo2 = velo
velo3 = pd.Series(velo2)
velo3 = velo3.replace([-np.inf,np.inf],np.nan)
velo3 = velo3.fillna(method='ffill')
pace2 = 500./abs(velo3)
row.df[' Stroke500mPace (sec/500m)'] = pace2
row.df = row.df.fillna(0)
row.write_csv(f2,gzip=True)
try:
os.remove(f2)
except:
pass
# recalculate power data
if workouttype == 'rower' or workouttype == 'dynamic' or workouttype == 'slides':
try:
row.erg_recalculatepower()
row.write_csv(f2,gzip=True)
except:
pass
averagehr = row.df[' HRCur (bpm)'].mean()
maxhr = row.df[' HRCur (bpm)'].max()
if totaldist == 0:
totaldist = row.df['cum_dist'].max()
if totaltime == 0:
totaltime = row.df['TimeStamp (sec)'].max()-row.df['TimeStamp (sec)'].min()
totaltime = totaltime+row.df.ix[0,' ElapsedTime (sec)']
hours = int(totaltime/3600.)
if hours>23:
message = 'Warning: The workout duration was longer than 23 hours. '
hours = 23
minutes = int((totaltime - 3600.*hours)/60.)
if minutes>59:
minutes = 59
if not message:
message = 'Warning: there is something wrong with the workout duration'
seconds = int(totaltime - 3600.*hours - 60.*minutes)
if seconds > 59:
seconds = 59
if not message:
message = 'Warning: there is something wrong with the workout duration'
tenths = int(10*(totaltime - 3600.*hours - 60.*minutes - seconds))
if tenths > 9:
tenths = 9
if not message:
message = 'Warning: there is something wrong with the workout duration'
duration = "%s:%s:%s.%s" % (hours,minutes,seconds,tenths)
if dosummary:
summary = row.summary()
summary += '\n'
summary += row.intervalstats()
workoutdate = row.rowdatetime.strftime('%Y-%m-%d')
workoutstarttime = row.rowdatetime.strftime('%H:%M:%S')
workoutstartdatetime = thetimezone.localize(row.rowdatetime).astimezone(utc)
if makeprivate:
privacy = 'private'
else:
privacy = 'visible'
# check for duplicate start times
ws = Workout.objects.filter(startdatetime=workoutstartdatetime,
user=r)
if (len(ws) != 0):
message = "Warning: This workout probably already exists in the database"
w = Workout(user=r,name=title,date=workoutdate,
workouttype=workouttype,
duration=duration,distance=totaldist,
weightcategory=r.weightcategory,
starttime=workoutstarttime,
csvfilename=f2,notes=notes,summary=summary,
maxhr=maxhr,averagehr=averagehr,
startdatetime=workoutstartdatetime,
inboard=inboard,oarlength=oarlength,
privacy=privacy)
w.save()
if privacy == 'visible':
ts = Team.objects.filter(rower=r)
for t in ts:
w.team.add(t)
# put stroke data in database
res = dataprep(row.df,id=w.id,bands=True,
barchart=True,otwpower=True,empower=True,inboard=inboard)
return (w.id,message)
def handle_nonpainsled(f2,fileformat,summary=''):
oarlength = 2.89
inboard = 0.88
# handle RowPro:
if (fileformat == 'rp'):
row = RowProParser(f2)
# handle TCX
if (fileformat == 'tcx'):
row = TCXParser(f2)
# handle Mystery
if (fileformat == 'mystery'):
row = MysteryParser(f2)
# handle TCX no HR
if (fileformat == 'tcxnohr'):
row = TCXParserNoHR(f2)
# handle RowPerfect
if (fileformat == 'rowperfect3'):
row = RowPerfectParser(f2)
# handle ErgData
if (fileformat == 'ergdata'):
row = ErgDataParser(f2)
# handle Mike
if (fileformat == 'bcmike'):
row = BoatCoachAdvancedParser(f2)
# handle BoatCoach
if (fileformat == 'boatcoach'):
row = BoatCoachParser(f2)
# handle painsled desktop
if (fileformat == 'painsleddesktop'):
row = painsledDesktopParser(f2)
# handle speed coach GPS
if (fileformat == 'speedcoach'):
row = speedcoachParser(f2)
# handle speed coach GPS 2
if (fileformat == 'speedcoach2'):
row = SpeedCoach2Parser(f2)
try:
oarlength,inboard = get_empower_rigging(f2)
summary = row.allstats()
except:
pass
# handle ErgStick
if (fileformat == 'ergstick'):
row = ErgStickParser(f2)
# handle FIT
if (fileformat == 'fit'):
row = FITParser(f2)
s = fitsummarydata(f2)
s.setsummary()
summary = s.summarytext
f_to_be_deleted = f2
# should delete file
f2 = f2[:-4]+'o.csv'
row.write_csv(f2,gzip=True)
#os.remove(f2)
try:
os.remove(f_to_be_deleted)
except:
os.remove(f_to_be_deleted+'.gz')
return (f2,summary,oarlength,inboard)
# Create new workout from file and store it in the database
# This routine should be used everywhere in views.py and mailprocessing.py
# Currently there is code duplication
def new_workout_from_file(r,f2,
workouttype='rower',
title='Workout',
makeprivate=False,
notes=''):
message = None
fileformat = get_file_type(f2)
summary = ''
oarlength = 2.89
inboard = 0.88
if len(fileformat)==3 and fileformat[0]=='zip':
f_to_be_deleted = f2
with zipfile.ZipFile(f2) as z:
for fname in z.namelist():
f3 = z.extract(fname,path='media/')
id,message,f2 = new_workout_from_file(r,f3,
workouttype=workouttype,
makeprivate=makeprivate,
title = title,
notes='')
os.remove(f_to_be_deleted)
return id,message,f2
# Some people try to upload Concept2 logbook summaries
if fileformat == 'c2log':
os.remove(f2)
message = "This C2 logbook summary does not contain stroke data. Please download the Export Stroke Data file from the workout details on the C2 logbook."
return (0,message,f2)
if fileformat == 'nostrokes':
os.remove(f2)
message = "It looks like this file doesn't contain stroke data."
return (0,message,f2)
# Some people try to upload RowPro summary logs
if fileformat == 'rowprolog':
os.remove(f2)
message = "This RowPro logbook summary does not contain stroke data. Please use the Stroke Data CSV file for the individual workout in your log."
return (0,message,f2)
# Sometimes people try an unsupported file type.
# Send an email to info@rowsandall.com with the file attached
# for me to check if it is a bug, or a new file type
# worth supporting
if fileformat == 'unknown':
message = "We couldn't recognize the file type"
if settings.DEBUG:
res = handle_sendemail_unrecognized.delay(f2,
r.user.email)
else:
res = queuehigh.enqueue(handle_sendemail_unrecognized,
f2,r.user.email)
return (0,message,f2)
# handle non-Painsled by converting it to painsled compatible CSV
if (fileformat != 'csv'):
try:
f2,summary,oarlength,inboard = handle_nonpainsled(f2,
fileformat,
summary=summary)
except:
errorstring = str(sys.exc_info()[0])
message = 'Something went wrong: '+errorstring
return (0,message,'')
dosummary = (fileformat != 'fit')
id,message = save_workout_database(f2,r,
workouttype=workouttype,
makeprivate=makeprivate,
dosummary=dosummary,
summary=summary,
inboard=inboard,oarlength=oarlength,
title=title)
return (id,message,f2)
def delete_strokedata(id,debug=True):
if debug:
engine = create_engine(database_url_debug, echo=False)

View File

@@ -90,6 +90,24 @@ class UploadOptionsForm(forms.Form):
class Meta:
fields = ['make_plot','plottype','upload_toc2','makeprivate']
# The form to indicate additional actions to be performed immediately
# after a successful upload. This version allows the Team manager to select
# a team member
class TeamUploadOptionsForm(forms.Form):
plotchoices = (
('timeplot','Time Plot'),
('distanceplot','Distance Plot'),
('pieplot','Pie Chart'),
)
make_plot = forms.BooleanField(initial=False,required=False)
plottype = forms.ChoiceField(required=False,
choices=plotchoices,
initial='timeplot',
label='Plot Type')
class Meta:
fields = ['make_plot','plottype']
# This form is used on the Analysis page to add a custom distance/time
# trial and predict the pace
class PredictedPieceForm(forms.Form):

View File

@@ -63,12 +63,21 @@ class Command(BaseCommand):
z = zipfile.ZipFile(a.document)
for f in z.namelist():
f2 = z.extract(f,path='media/')
title = os.path.basename(f2)
wid = [
make_new_workout_from_email(rr,f2[6:],name)
make_new_workout_from_email(rr,f2[6:],title)
]
res += wid
link = 'http://rowsandall.com/rowers/workout/'+str(wid[0])+'/edit'
dd = send_confirm(rr.user,name,link)
try:
dd = send_confirm(rr.user,title,link)
time.sleep(10)
except:
try:
time.sleep(10)
dd = send_confirm(rr.user,title,link)
except:
pass
else:
# move attachment and make workout
@@ -87,6 +96,7 @@ class Command(BaseCommand):
donotdelete = 1
try:
dd = send_confirm(rr.user,name,link)
time.sleep(10)
except:
pass

View File

@@ -195,6 +195,10 @@ class Rower(models.Model):
sporttrackstokenexpirydate = models.DateTimeField(blank=True,null=True)
sporttracksrefreshtoken = models.CharField(default='',max_length=200,
blank=True,null=True)
underarmourtoken = models.CharField(default='',max_length=200,blank=True,null=True)
underarmourtokenexpirydate = models.DateTimeField(blank=True,null=True)
underarmourrefreshtoken = models.CharField(default='',max_length=200,
blank=True,null=True)
stravatoken = models.CharField(default='',max_length=200,blank=True,null=True)
runkeepertoken = models.CharField(default='',max_length=200,
@@ -360,6 +364,7 @@ class Workout(models.Model):
maxhr = models.IntegerField(blank=True,null=True)
uploadedtostrava = models.IntegerField(default=0)
uploadedtosporttracks = models.IntegerField(default=0)
uploadedtounderarmour = models.IntegerField(default=0)
uploadedtorunkeeper = models.IntegerField(default=0)
# empower stuff

View File

@@ -21,7 +21,7 @@ import stravalib
from utils import serialize_list,deserialize_list
from rowers.dataprepnodjango import update_strokedata
from rowers.dataprepnodjango import new_workout_from_file
from django.core.mail import send_mail, BadHeaderError,EmailMessage
@@ -30,6 +30,16 @@ from django.core.mail import send_mail, BadHeaderError,EmailMessage
def add(x, y):
return x + y
# create workout
@app.task
def handle_new_workout_from_file(r,f2,
workouttype='rower',
title='Workout',
makeprivate=False,
notes=''):
return new_workout_from_file(r,f2,workouttype,
title,makeprivate,notes)
# send email to me when an unrecognized file is uploaded
@app.task
def handle_sendemail_unrecognized(unrecognizedfile,useremail):
@@ -80,6 +90,17 @@ def handle_sendemailtcx(first_name,last_name,email,tcxfile):
os.remove(tcxfile)
return 1
@app.task
def handle_zip_file(emailfrom,subject,file):
message = "... zip processing ... "
email = EmailMessage(subject,message,
emailfrom,
['workouts@rowsandall.com'])
email.attach_file(file)
res = email.send()
time.sleep(60)
return 1
# Send email with CSV attachment
@app.task
def handle_sendemailcsv(first_name,last_name,email,csvfile):

View File

@@ -159,7 +159,7 @@ def create_request(team,user):
if r2 in Rower.objects.filter(team=team):
return (0,'Already a member of that team')
if count_club_members(team.manager)+count_invites(team.manager) < r.clubsize:
if count_club_members(team.manager)+count_invites(team.manager) <= r.clubsize:
codes = [i.code for i in TeamRequest.objects.all()]
code = uuid.uuid4().hex[:10].upper()
# prevent duplicates
@@ -200,7 +200,7 @@ def create_invite(team,manager,user=None,email=''):
except Rower.MultipleObjectsReturned:
return (0,'There is more than one user with that email address')
if count_club_members(team.manager)+count_invites(team.manager) < r.clubsize:
if count_club_members(team.manager)+count_invites(team.manager) <= r.clubsize:
codes = [i.code for i in TeamInvite.objects.all()]
code = uuid.uuid4().hex[:10].upper()
# prevent duplicates

View File

@@ -1,4 +1,4 @@
{% extends "bases.html" %}
{% extends "base.html" %}
{% load staticfiles %}
{% load rowerfilters %}

View File

@@ -1,4 +1,4 @@
{% extends "bases.html" %}
{% extends "base.html" %}
{% load staticfiles %}
{% load rowerfilters %}

View File

@@ -1,4 +1,4 @@
{% extends "bases.html" %}
{% extends "base.html" %}
{% load staticfiles %}
{% load rowerfilters %}

View File

@@ -1,4 +1,4 @@
{% extends "bases.html" %}
{% extends "base.html" %}
{% load staticfiles %}
{% load rowerfilters %}

View File

@@ -1,5 +1,6 @@
{% load cookielaw_tags %}
{% load analytical %}
{% load rowerfilters %}
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN">
<html lang="en">
<head>
@@ -143,13 +144,17 @@
{% endif %}
</div>
<div class="grid_1 tooltip">
{% if user.is_authenticated and 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
Teams
</button>
<div class="dropdown-content">
{% for t in teams %}
<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>
@@ -214,7 +219,7 @@
<p id="footer">
<a href="/rowers/legal">Legal</a></p>
</div>
<div class="grid_2">
<div class="grid_1">
<p id="footer">
<a href="/rowers/partners">Partners</a></p>
</div>
@@ -226,9 +231,9 @@
<p id="footer">
<a href="http://analytics.rowsandall.com/">Rowing Analytics BLOG</a></p>
</div>
<div class="grid_1 omega">
<div class="grid_2 omega">
<p id="footer">
<a href="/rowers/email">Contact</a></p>
<a href="https://www.facebook.com/groups/rowsandall/">Facebook group</a></p>
</div>
{% endblock %}
</div>

View File

@@ -1,5 +1,6 @@
{% load cookielaw_tags %}
{% load analytical %}
{% load rowerfilters %}
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN">
<html lang="en">
<head>
@@ -31,7 +32,6 @@
<style>
.splash {
background-color: transparent;
background-image: url("/static/img/landing1.jpg");
-webkit-background-size: cover;
-moz-background-size: cover;
-o-background-size: cover;
@@ -41,11 +41,13 @@
width: 100%;
height: auto;
}
.container_12 {background-color: rgba(255,255,255,0.7);}
.container_12 {background-color: rgba(255,255,255,0.0);}
.container_top {background-color: rgba(255,255,255,0.7);}
</style>
<div class="splash">
<div id="bgpic" class="splash">
{% analytical_body_top %}
<div class="container_top">
<div class="container_12">
<div class="grid_12">
&nbsp;
@@ -161,13 +163,17 @@
{% endif %}
</div>
<div class="grid_1 tooltip">
{% if user.is_authenticated and teams %}
{% if user.is_authenticated and user|user_teams %}
<div class="grid_1 alpha dropdown">
<button class="grid_1 alpha button gray small dropbtn">
Teams
</button>
<div class="dropdown-content">
{% for t in teams %}
<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>
@@ -232,7 +238,7 @@
<p id="footer">
<a href="/rowers/legal">Legal</a></p>
</div>
<div class="grid_2">
<div class="grid_1">
<p id="footer">
<a href="/rowers/partners">Partners</a></p>
</div>
@@ -244,15 +250,27 @@
<p id="footer">
<a href="http://analytics.rowsandall.com/">Rowing Analytics BLOG</a></p>
</div>
<div class="grid_1 omega">
<div class="grid_2 omega">
<p id="footer">
<a href="/rowers/email">Contact</a></p>
<a href="https://www.facebook.com/groups/rowsandall/">Facebook group</a></p>
</div>
{% endblock %}
</div>
{% cookielaw_banner %}
</div>
</div>
<!-- end container -->
{% analytical_body_bottom %}
<script type="text/javascript">
var num = (Math.floor(Math.random()*4));
var array = ['one', 'two', 'three', 'four'];
var elem = document.getElementById('bgpic');
console.log(elem);
elem.classList.add(array[num]);
</script>
</body>
</html>

View File

@@ -1,12 +1,17 @@
{% extends "base.html" %}
{% load staticfiles %}
{% load rowerfilters %}
{% block title %}File loading{% endblock %}
{% block content %}
<form enctype="multipart/form-data" action="{{ formloc }}" method="post">
<div id="left" class="grid_6 alpha">
<h1>Upload Workout File</h1>
<h1>Upload Workout File</h1>
{% if user.is_authenticated and user|is_manager %}
<p>Looking for <a href="/rowers/workout/upload/team/">Team Manager
Upload?</a></p>
{% endif %}
{% if form.errors %}
<p style="color: red;">
Please correct the error{{ form.errors|pluralize }} below.

View File

@@ -68,6 +68,14 @@ Bug reports and feature requests can be done through our BitBucket page. Please
</ul>
</p>
<h1>Facebook Group</h1>
<p>We run a facebook group where you can post questions and report problems,
especially if you think the wider user community benefits from the answers.</p>
<ul>
<li><a href="https://www.facebook.com/groups/rowsandall/">https://www.facebook.com/groups/rowsandall/</a></li>
</ul>
<h1>Twitter</h1>
<p>You can also check me on Twitter:
@@ -79,4 +87,4 @@ When the site is down, this is the appropriate channel to look for apologies, up
</div>
{% endblock content %}

View File

@@ -111,6 +111,24 @@
</div>
{% endif %}
{% if workout.uploadedtounderarmour == 0 %}
{% if user.rower.underarmourtoken == None or user.rower.underarmourtoken == '' %}
<div class="grid_1">
<a href="/rowers/me/underarmourauthorize">
<img src="/static/img/uagray.png" alt="Underarmour icon" width="60" height="60"></a>
</div>
{% else %}
<div class="grid_1">
<a href="/rowers/workout/{{ workout.id }}/underarmouruploadw"><img src="/static/img/uasquare.png" alt="Underarmour icon" width="60" height="60"></a>
</div>
{% endif %}
{% else %}
<div class="grid_1">
<a href="https://www.mapmyfitness.com/workout/{{ workout.uploadedtounderarmour }}">
<img src="/static/img/uachecked.png" alt="Underarmour icon" width="60" height="60"></a>
</div>
{% endif %}
</div>
</div>
@@ -138,8 +156,11 @@
</div>
<div class="grid_6">
<div class="grid_2 alpha suffix_4">
<p><a href="/rowers/me/runkeeperauthorize/"><img src="/static/img/rk-logo.png" alt="connect with RunKeeper" width="120"></a></p>
<div class="grid_2 alpha">
<p><a href="/rowers/me/runkeeperauthorize/"><img src="/static/img/rk-logo.png" alt="connect with Runkeeper" width="120"></a></p>
</div>
<div class="grid_2">
<p><a href="/rowers/me/underarmourauthorize/"><img src="/static/img/UAbtn.png" alt="connect with Under Armour" width="120"></a></p>
</div>
</div>
</div>

View File

@@ -43,12 +43,22 @@
<p>Import workouts from RunKeeper</p>
</div>
</div>
<div class="grid_6">
<div class="grid_3 alpha">
<p>
<a href="/rowers/workout/underarmourimport"><img src="/static/img/UAbtn.png" alt="Under Armour logo" width="140"></a>
</p>
</div>
<div class="grid_3 omega">
<p>Import workouts from MapMyFitness/UnderArmour</p>
</div>
</div>
</div>
<div class="grid_6 omega">
<h3>Connect</h3>
<div class="grid_6">
<div class="grid_6 alpha">
<p>Click one of the below logos to connect to the service of your choice.
You only need to do this once. After that, the site will have access until you
revoke the authorization for the "rowingdata" app.</p>
@@ -67,10 +77,13 @@
</div>
</div>
<div class="grid_6">
<div class="grid_2 alpha suffix_4">
<div class="grid_6 alpha">
<div class="grid_2 alpha">
<p><a href="/rowers/me/runkeeperauthorize/"><img src="/static/img/rk-logo.png" alt="connect with RunKeeper" width="120"></a></p>
</div>
<div class="grid_2 suffix_2 omega">
<p><a href="/rowers/me/underarmourauthorize/"><img src="/static/img/UAbtn.png" alt="connect with Under Armour" width="120"></a></p>
</div>
</div>
</div>

View File

@@ -35,7 +35,9 @@
<div id="workouts_table" class="grid_8 alpha">
{% if team %}
{% include "teambuttons.html" with teamid=team.id %}
<div class="grid_8 alpha">
{% include "teambuttons.html" with teamid=team.id %}
</div>
<h3>{{ team.name }} Team Workouts</h3>
{% else %}
<h3>My Workouts</h3>

View File

@@ -0,0 +1,55 @@
{% extends "base.html" %}
{% load staticfiles %}
{% block title %}File loading{% endblock %}
{% block content %}
<form enctype="multipart/form-data" action="{{ formloc }}" method="post">
<div id="left" class="grid_6 alpha">
<h1>Upload Workout File</h1>
{% if form.errors %}
<p style="color: red;">
Please correct the error{{ form.errors|pluralize }} below.
</p>
{% endif %}
<table>
{{ rowerform.as_table }}
{{ form.as_table }}
</table>
{% csrf_token %}
<div id="formbutton" class="grid_1 prefix_4 suffix_1">
<input class="button green" type="submit" value="Submit">
</div>
</div>
<div id="right" class="grid_6 omega">
<h1>Optional extra actions</h1>
<p>
<table>
{{ optionsform.as_table }}
</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.
</p>
<p>
Valid file types are:
<ul>
<li>Painsled iOS Stroke Export (CSV)</li>
<li>Painsled desktop version Stroke Export (CSV)</li>
<li>A TCX file with location data (lat,long) - with or without Heart Rate value, for example from RiM or CrewNerd</li>
<li>RowPro CSV export</li>
<li>SpeedCoach GPS and SpeedCoach GPS 2 CSV export</li>
<li>ErgData CSV export</li>
<li>ErgStick CSV export</li>
<li>BoatCoach CSV export</li>
<li>A FIT file with location data (experimental)</li>
</ul>
</p>
</div>
</form>
{% endblock %}

View File

@@ -1,3 +1,4 @@
{% load rowerfilters %}
<div class="grid_2 alpha">
<p>
<a class="button gray small" href="/rowers/list-workouts/team/{{ teamid }}/">Team Workouts</a>
@@ -8,8 +9,17 @@
<a class="button gray small" href="/rowers/team-compare-select/team/{{ teamid }}/">Multi Compare</a>
</p>
</div>
<div class="grid_2 suffix_2 omega">
<div class="grid_2">
<p>
<a class="button gray small" href="/rowers/team/{{ teamid }}/">Team Page</a>
</p>
</div>
</p>
</div>
<div class="grid_2 omega">
{% if user|is_manager and user|has_teams %}
<p>
<a class="button gray small" href="/rowers/workout/upload/team/">Upload Workout</a>
</p>
{% else %}
<p>&nbsp;</p>
{% endif %}
</div>

View File

@@ -36,10 +36,76 @@
</p>
</div>
<div class="grid_6 omega">
{% if otherteams %}
<h2>Other Teams</h2>
<table width="70%" class="listtable">
<thead>
<tr>
<th>Name</th>
<th>Manager</th>
</tr>
</thead>
<tbody>
{% for team in otherteams %}
<tr>
<td>
<a href="/rowers/team/{{ team.id }}/">{{ team.name }}</a>
</td>
<td>
{{ team.manager.first_name }} {{ team.manager.last_name }}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p>&nbsp;</p>
{% endif %}
</div>
</div>
<div class="grid_12 alpha">
<div class="grid_6 alpha">
{% if user.rower.rowerplan == 'coach' %}
<h2>Teams I manage</h2>
<p>Number of members: {{ clubsize }}</p>
<p>Maximum club size: {{ max_clubsize }}</p>
{% if myteams %}
<table width="70%" class="listtable">
<thead>
<tr>
<th>Name</th>
<th>Manager</th>
</tr>
</thead>
<tbody>
{% for team in myteams %}
<tr>
<td>
<a href="/rowers/team/{{ team.id }}/">{{ team.name }}</a>
</td>
<td>
<div class="grid_1">
<a class="button small red" href="/rowers/team/{{ team.id }}/deleteconfirm">Delete</a>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
<div class="grid_2 suffix_4 alpha">
<a class="button green" href="/rowers/team/create">New Team</a>
</div>
{% else %}
<p>&nbsp;</p>
{% endif %}
</div>
<div class="grid_6 omega">
{% if invites or requests or myrequests or myinvites %}
<p>
<h2>Invitations and Requests</h2>
<table width="90%" class="listtable">
<thead>
@@ -126,70 +192,5 @@
</div>
</div>
<div class="grid_12 alpha">
<div class="grid_6 alpha">
{% if user.rower.rowerplan == 'coach' %}
<h2>Teams I manage</h2>
{% if myteams %}
<table width="70%" class="listtable">
<thead>
<tr>
<th>Name</th>
<th>Manager</th>
</tr>
</thead>
<tbody>
{% for team in myteams %}
<tr>
<td>
<a href="/rowers/team/{{ team.id }}/">{{ team.name }}</a>
</td>
<td>
<div class="grid_1">
<a class="button small red" href="/rowers/team/{{ team.id }}/deleteconfirm">Delete</a>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
<div class="grid_2 suffix_4 alpha">
<a class="button green" href="/rowers/team/create">New Team</a>
</div>
{% else %}
<p>&nbsp;</p>
{% endif %}
</div>
<div class="grid_6 omega">
{% if otherteams %}
<h2>Other Teams</h2>
<table width="70%" class="listtable">
<thead>
<tr>
<th>Name</th>
<th>Manager</th>
</tr>
</thead>
<tbody>
{% for team in otherteams %}
<tr>
<td>
<a href="/rowers/team/{{ team.id }}/">{{ team.name }}</a>
</td>
<td>
{{ team.manager.first_name }} {{ team.manager.last_name }}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p>&nbsp;</p>
{% endif %}
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,37 @@
{% extends "base.html" %}
{% load staticfiles %}
{% load rowerfilters %}
{% block title %}Workouts{% endblock %}
{% block content %}
<h1>Available on MapMyFitness (UnderArmour)</h1>
{% if workouts %}
<table width="70%" class="listtable">
<thead>
<tr>
<th> Import </th>
<th> Date/Time </th>
<th> Duration </th>
<th> Total Distance</th>
<th> Type</th>
</tr>
</thead>
<tbody>
{% for workout in workouts %}
<tr>
<td>
<a href="/rowers/workout/underarmourimport/{{ workout|ualookup:'id' }}/">Import</a></td>
<td>{{ workout|ualookup:'starttime' }}</td>
<td>{{ workout|ualookup:'duration' }} </td>
<td>{{ workout|ualookup:'distance' }} m</td>
<td>{{ workout|ualookup:'type' }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p> No workouts found. We only list workouts with time data series. </p>
{% endif %}
{% endblock %}

View File

@@ -1,5 +1,6 @@
from django import template
from time import strftime
import dateutil.parser
register = template.Library()
@@ -27,6 +28,17 @@ def strfdeltah(tdelta):
return res
def secondstotimestring(tdelta):
hours, rest = divmod(tdelta,3600)
minutes,seconds = divmod(rest,60)
res = "{hours:0>2}:{minutes:0>2}:{seconds:0>2}".format(
hours=hours,
minutes=minutes,
seconds=seconds,
)
return res
@register.filter
def durationprint(d,dstring):
if (d == None):
@@ -57,6 +69,22 @@ def lookup(dict, key):
s = s[:22]
return s
@register.filter
def ualookup(dict, key):
s = dict.get(key)
if key=='distance':
s = int(float(s))
if key=='duration':
s = secondstotimestring(int(s))
if key=='starttime':
s = dateutil.parser.parse(s)
return s
@register.filter(name='times')
def times(number):
return range(number)
@@ -65,3 +93,35 @@ def times(number):
def get_field_id(id,s,form):
field_name = s+str(id)
return form.__getitem__(field_name)
from rowers.models import Rower,Team
@register.filter
def is_manager(user):
r = Rower.objects.get(user=user)
return r.rowerplan == 'coach'
@register.filter
def user_teams(user):
try:
therower = Rower.objects.get(user=user)
teams1 = therower.team.all()
teams2 = Team.objects.filter(manager=user)
teams = list(set(teams1).union(set(teams2)))
except TypeError:
teams = []
return teams
@register.filter
def has_teams(user):
try:
therower = Rower.objects.get(user=user)
teams1 = therower.team.all()
teams2 = Team.objects.filter(manager=user)
teams = list(set(teams1).union(set(teams2)))
return True
except TypeError:
return False
return False

View File

@@ -702,7 +702,7 @@ class ViewTest(TestCase):
f.close()
self.assertRedirects(response, expected_url='/rowers/workout/upload/This%20C2%20logbook%20summary%20does%20not%20contain%20stroke%20data.%20Please%20download%20the%20Export%20Stroke%20Data%20file%20from%20the%20workout%20details%20on%20the%20C2%20logbook.',
self.assertRedirects(response, expected_url='/rowers/workout/upload/c/This%20C2%20logbook%20summary%20does%20not%20contain%20stroke%20data.%20Please%20download%20the%20Export%20Stroke%20Data%20file%20from%20the%20workout%20details%20on%20the%20C2%20logbook.',
status_code=302,target_status_code=200)
self.assertEqual(response.status_code, 200)

385
rowers/underarmourstuff.py Normal file
View File

@@ -0,0 +1,385 @@
# 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
import urllib
# 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,
UNDERARMOUR_CLIENT_ID, UNDERARMOUR_CLIENT_SECRET,
UNDERARMOUR_REDIRECT_URI,UNDERARMOUR_CLIENT_KEY,
)
# Custom error class - to raise a NoTokenError
class UnderArmourNoTokenError(Exception):
def __init__(self,value):
self.value=value
def __str__(self):
return repr(self.value)
# Exponentially weighted moving average
# Used for data smoothing of the jagged data obtained by Strava
# See bitbucket issue 72
def ewmovingaverage(interval,window_size):
# Experimental code using Exponential Weighted moving average
try:
intervaldf = pd.DataFrame({'v':interval})
idf_ewma1 = intervaldf.ewm(span=window_size)
idf_ewma2 = intervaldf[::-1].ewm(span=window_size)
i_ewma1 = idf_ewma1.mean().ix[:,'v']
i_ewma2 = idf_ewma2.mean().ix[:,'v']
interval2 = np.vstack((i_ewma1,i_ewma2[::-1]))
interval2 = np.mean( interval2, axis=0) # average
except ValueError:
interval2 = interval
return interval2
from utils import geo_distance
# Custom exception handler, returns a 401 HTTP message
# with exception details in the json data
def custom_exception_handler(exc,message):
response = {
"errors": [
{
"code": str(exc),
"detail": message,
}
]
}
res = HttpResponse(message)
res.status_code = 401
res.json = json.dumps(response)
return res
# Refresh ST token using refresh token
def do_refresh_token(refreshtoken,access_token):
client_auth = requests.auth.HTTPBasicAuth(UNDERARMOUR_CLIENT_KEY, UNDERARMOUR_CLIENT_SECRET)
post_data = {"grant_type": "refresh_token",
"client_secret": UNDERARMOUR_CLIENT_SECRET,
"client_id":UNDERARMOUR_CLIENT_KEY,
"refresh_token": refreshtoken,
}
headers = {'user-agent': 'sanderroosendaal',
"Api-Key":UNDERARMOUR_CLIENT_KEY,
'Accept': 'application/json',
'Content-Type': 'application/x-www-form-urlencoded',
'authorization': 'Bearer %s' % access_token}
url = "https://api.ua.com/v7.1/oauth2/access_token/"
response = requests.post(url,
data=post_data,
headers=headers)
token_json = response.json()
thetoken = token_json['access_token']
expires_in = token_json['expires_in']
try:
refresh_token = token_json['refresh_token']
except KeyError:
refresh_token = refreshtoken
return [thetoken,expires_in,refresh_token]
# Exchange access code for long-lived access token
def get_token(code):
client_auth = requests.auth.HTTPBasicAuth(UNDERARMOUR_CLIENT_KEY, UNDERARMOUR_CLIENT_SECRET)
post_data = {
"grant_type": "authorization_code",
"code": code,
"client_secret": UNDERARMOUR_CLIENT_SECRET,
"client_id":UNDERARMOUR_CLIENT_KEY,
}
headers = {
'user-agent': 'sanderroosendaal',
"Api-Key":UNDERARMOUR_CLIENT_KEY,
}
response = requests.post("https://api.ua.com/v7.1/oauth2/access_token/",
data=post_data,
headers=headers)
try:
token_json = response.json()
thetoken = token_json['access_token']
expires_in = token_json['expires_in']
refresh_token = token_json['refresh_token']
except KeyError:
thetoken = 0
return thetoken,expires_in,refresh_token
# Make authorization URL including random string
def make_authorization_url(request):
# Generate a random string for the state parameter
# Save it for use later to prevent xsrf attacks
from uuid import uuid4
state = str(uuid4())
params = {"client_id": UNDERARMOUR_CLIENT_KEY,
"response_type": "code",
"redirect_uri": UNDERARMOUR_REDIRECT_URI,
}
url = "https://www.mapmyfitness.com/v7.1/oauth2/uacf/authorize/" +urllib.urlencode(params)
return HttpResponseRedirect(url)
# Get list of workouts available on Underarmour
def get_underarmour_workout_list(user):
r = Rower.objects.get(user=user)
if (r.underarmourtoken == '') or (r.underarmourtoken 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.underarmourtoken)
headers = {'Authorization': authorizationstring,
'Api-Key': UNDERARMOUR_CLIENT_KEY,
'user-agent': 'sanderroosendaal',
'Content-Type': 'application/json'}
url = "https://api.ua.com/v7.1/workout/?user="+str(get_userid(r.underarmourtoken))
s = requests.get(url,headers=headers)
return s
# Get workout summary data by Underarmour ID
def get_underarmour_workout(user,underarmourid):
r = Rower.objects.get(user=user)
if (r.underarmourtoken == '') or (r.underarmourtoken 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.underarmourtoken)
headers = {'Authorization': authorizationstring,
'Api-Key': UNDERARMOUR_CLIENT_KEY,
'user-agent': 'sanderroosendaal',
'Content-Type': 'application/json'}
url = "https://api.ua.com/v7.1/workout/"+str(underarmourid)+"/?field_set=time_series"
s = requests.get(url,headers=headers)
return s
# Create Workout Data for upload to Underarmour
def createunderarmourworkoutdata(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
name = w.name
notes = w.notes
# 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 = {
'lat':e[1],
'lng':e[2],
'elevation':0,
}
locdata.append([e[0],point])
hrdata = []
for e in zip(t,hr):
point = [e[0],
e[1]
]
hrdata.append(point)
distancedata = []
for e in zip(t,d):
point = [e[0],
e[1]
]
distancedata.append(point)
spmdata = []
for e in zip(t,spm):
spmdata.append([e[0],e[1]])
start_time = w.startdatetime.isoformat()
timeseries = {
"distance": distancedata,
"heartrate": hrdata,
"cadence": spmdata,
}
aggregrates = {
"elapsed_time_total": int(duration),
"distance_total": int(max(d)),
"heartrate_avg": averagehr,
"heart_rate_min": int(min(hr)),
"heart_rate_max": int(max(hr)),
}
# if haslatlon:
# timeseries["position"] = locdata
data = {
"name": name,
"start_datetime": start_time,
"time_series": timeseries,
"start_locale_timezone": "Etc/UTC",
"activity_type": "/v7.1/activity_type/128/",
"notes": notes,
}
return data
# Obtain Underarmour Workout ID and activity type
def get_idfromuri(user,links):
id = links['self'][0]['id']
typeid = links['activity_type'][0]['id']
typename = get_typefromid(typeid,user)
return id,typename
def getidfromresponse(response):
t = json.loads(response.text)
links = t["_links"]
id = links["self"][0]["id"]
return int(id)
def refresh_ua_actlist(user):
r = Rower.objects.get(user=user)
authorizationstring = str('Bearer ' + r.underarmourtoken)
headers = {'Authorization': authorizationstring,
'Api-Key': UNDERARMOUR_CLIENT_KEY,
'user-agent': 'sanderroosendaal',
'Content-Type': 'application/json'}
url = "https://api.ua.com/v7.1/activity_type/"
response = requests.get(url,headers=headers)
me_json = response.json()
types = me_json["_embedded"]["activity_types"]
w = {int(t["_links"]["self"][0]["id"]):t["name"] for t in types}
wdf = pd.Series(w,name='Name')
wdf.to_csv('static/rigging/ua2.csv',index_label='id',header=True)
return w
try:
activities = pd.read_csv('static/rigging/ua2.csv',index_col='id')
actdict = activities.to_dict()['Name']
except:
actdict = {}
def get_typefromid(typeid,user):
r = Rower.objects.get(user=user)
try:
res = actdict[int(typeid)]
except KeyError:
authorizationstring = str('Bearer ' + r.underarmourtoken)
headers = {'Authorization': authorizationstring,
'Api-Key': UNDERARMOUR_CLIENT_KEY,
'user-agent': 'sanderroosendaal',
'Content-Type': 'application/json'}
url = "https://api.ua.com/v7.1/activity_type/"+str(typeid)
response = requests.get(url,headers=headers)
me_json = response.json()
try:
res = me_json['name']
except KeyError:
res = 0
return res
# 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,
'Api-Key': UNDERARMOUR_CLIENT_KEY,
'user-agent': 'sanderroosendaal',
'Content-Type': 'application/json'}
url = "https://api.ua.com/v7.1/user/self"
response = requests.get(url,headers=headers)
me_json = response.json()
try:
res = me_json['id']
except KeyError:
res = 0
return res

View File

@@ -157,8 +157,12 @@ urlpatterns = [
url(r'^graph/(\d+)/$',views.graph_show_view),
url(r'^graph/(\d+)/deleteconfirm$',views.graph_delete_confirm_view),
url(r'^graph/(\d+)/delete$',views.graph_delete_view),
url(r'^workout/upload/team/s/(?P<successmessage>\w+.*)/c/(?P<message>\w+.*)/$',views.team_workout_upload_view),
url(r'^workout/upload/team/c/(?P<message>\w+.*)/$',views.team_workout_upload_view),
url(r'^workout/upload/team/s/(?P<successmessage>\w+.*)/$',views.team_workout_upload_view),
url(r'^workout/upload/team/$',views.team_workout_upload_view),
url(r'^workout/upload/$',views.workout_upload_view),
url(r'^workout/upload/(.+.*)$',views.workout_upload_view),
url(r'^workout/upload/c/(?P<message>\w+.*)$',views.workout_upload_view),
url(r'^workout/(?P<id>\d+)/histo$',views.workout_histo_view),
url(r'^workout/(?P<id>\d+)/forcecurve$',views.workout_forcecurve_view),
url(r'^workout/(?P<id>\d+)/unsubscribe$',views.workout_unsubscribe_view),
@@ -225,12 +229,15 @@ urlpatterns = [
url(r'^workout/sporttracksimport/(\d+)/$',views.workout_getsporttracksworkout_view),
url(r'^workout/runkeeperimport/$',views.workout_runkeeperimport_view),
url(r'^workout/runkeeperimport/(\d+)/$',views.workout_getrunkeeperworkout_view),
url(r'^workout/underarmourimport/$',views.workout_underarmourimport_view),
url(r'^workout/underarmourimport/(\d+)/$',views.workout_getunderarmourworkout_view),
url(r'^workout/(\d+)/deleteconfirm$',views.workout_delete_confirm_view),
url(r'^workout/(\d+)/c2uploadw/$',views.workout_c2_upload_view),
url(r'^workout/(\d+)/stravauploadw/$',views.workout_strava_upload_view),
url(r'^workout/(\d+)/recalcsummary/$',views.workout_recalcsummary_view),
url(r'^workout/(\d+)/sporttracksuploadw/$',views.workout_sporttracks_upload_view),
url(r'^workout/(\d+)/runkeeperuploadw/$',views.workout_runkeeper_upload_view),
url(r'^workout/(\d+)/underarmouruploadw/$',views.workout_underarmour_upload_view),
url(r'^multi-compare$',views.multi_compare_view),
url(r'^me/teams/c/(?P<message>\w+.*)/s/(?P<successmessage>\w+.*)$',views.rower_teams_view),
url(r'^me/teams/s/(?P<successmessage>\w+.*)$',views.rower_teams_view),
@@ -265,8 +272,10 @@ urlpatterns = [
url(r'^me/revokeapp/(\d+)$',views.rower_revokeapp_view),
url(r'^me/stravaauthorize/$',views.rower_strava_authorize),
url(r'^me/sporttracksauthorize/$',views.rower_sporttracks_authorize),
url(r'^me/underarmourauthorize/$',views.rower_underarmour_authorize),
url(r'^me/runkeeperauthorize/$',views.rower_runkeeper_authorize),
url(r'^me/sporttracksrefresh/$',views.rower_sporttracks_token_refresh),
url(r'^me/underarmourrefresh/$',views.rower_underarmour_token_refresh),
url(r'^me/c2refresh/$',views.rower_c2_token_refresh),
url(r'^me/favoritecharts/$',views.rower_favoritecharts_view),
url(r'^email/send/$', views.sendmail),

View File

@@ -14,7 +14,10 @@ from django.http import (
HttpResponseNotFound,Http404
)
from django.contrib.auth import authenticate, login, logout
from rowers.forms import LoginForm,DocumentsForm,UploadOptionsForm
from rowers.forms import (
LoginForm,DocumentsForm,UploadOptionsForm,
TeamUploadOptionsForm,
)
from django.core.urlresolvers import reverse
from django.core.exceptions import PermissionDenied
from django.template import RequestContext
@@ -53,6 +56,7 @@ from sporttracksstuff import SportTracksNoTokenError
from iso8601 import ParseError
import stravastuff
import sporttracksstuff
import underarmourstuff
import runkeeperstuff
import ownapistuff
from ownapistuff import TEST_CLIENT_ID, TEST_CLIENT_SECRET, TEST_REDIRECT_URI
@@ -61,6 +65,8 @@ from rowsandall_app.settings import (
STRAVA_CLIENT_ID, STRAVA_REDIRECT_URI, STRAVA_CLIENT_SECRET,
SPORTTRACKS_CLIENT_ID, SPORTTRACKS_REDIRECT_URI,
SPORTTRACKS_CLIENT_SECRET,
UNDERARMOUR_CLIENT_ID, UNDERARMOUR_REDIRECT_URI,
UNDERARMOUR_CLIENT_SECRET,UNDERARMOUR_CLIENT_KEY,
RUNKEEPER_CLIENT_ID,RUNKEEPER_REDIRECT_URI,RUNKEEPER_CLIENT_SECRET,
)
@@ -208,6 +214,15 @@ def get_time(second):
def getidfromsturi(uri,length=8):
return uri[len(uri)-length:]
def splituadata(lijst):
t = []
y = []
for d in lijst:
t.append(d[0])
y.append(d[1])
return np.array(t),np.array(y)
def splitrunkeeperlatlongdata(lijst,tname,latname,lonname):
t = []
lat = []
@@ -810,6 +825,173 @@ def add_workout_from_stdata(user,importid,data):
unixtime = cum_time+starttimeunix
unixtime[0] = starttimeunix
df['TimeStamp (sec)'] = unixtime
dt = np.diff(cum_time).mean()
wsize = round(5./dt)
velo2 = stravastuff.ewmovingaverage(velo,wsize)
df[' Stroke500mPace (sec/500m)'] = 500./velo2
df = df.fillna(0)
df.sort_values(by='TimeStamp (sec)',ascending=True)
timestr = strftime("%Y%m%d-%H%M%S")
csvfilename ='media/Import_'+str(importid)+'.csv'
res = df.to_csv(csvfilename+'.gz',index_label='index',
compression='gzip')
id,message = dataprep.save_workout_database(csvfilename,r,
workouttype=workouttype,
title=title,
notes=comments)
return (id,message)
# Create workout from SportTracks Data, which are slightly different
# than Strava or Concept2 data
def add_workout_from_underarmourdata(user,importid,data):
workouttype = 'water'
try:
comments = data['notes']
except:
comments = ''
try:
thetimezone = tz(data['start_locale_timezone'])
except:
thetimezone = 'UTC'
r = Rower.objects.get(user=user)
try:
rowdatetime = iso8601.parse_date(data['start_datetime'])
except iso8601.ParseError:
try:
rowdatetime = datetime.datetime.strptime(data['start_datetime'],"%Y-%m-%d %H:%M:%S")
rowdatetime = thetimezone.localize(rowdatetime).astimezone(utc)
except:
try:
rowdatetime = dateutil.parser.parse(data['start_datetime'])
rowdatetime = thetimezone.localize(rowdatetime).astimezone(utc)
except:
rowdatetime = datetime.datetime.strptime(data['date'],"%Y-%m-%d %H:%M:%S")
rowdatetime = thetimezone.localize(rowdatetime).astimezone(utc)
starttimeunix = mktime(rowdatetime.utctimetuple())
try:
title = data['name']
except:
title = "Imported data"
timeseries = data['time_series']
# position, distance, speed, cadence, power,
res = splituadata(timeseries['distance'])
distance = res[1]
times_distance = res[0]
print distance[0:5]
print times_distance[0:5]
try:
l = timeseries['position']
res = splituadata(l)
times_location = res[0]
latlong = res[1]
latcoord = []
loncoord = []
for coord in latlong:
lat = coord['lat']
lon = coord['lng']
latcoord.append(lat)
loncoord.append(lon)
except:
times_location = times_distance
latcoord = np.zeros(len(times_distance))
loncoord = np.zeros(len(times_distance))
if workouttype == 'water':
workouttype = 'rower'
try:
res = splituadata(timeseries['cadence'])
times_spm = res[0]
spm = res[1]
except KeyError:
times_spm = times_distance
spm = 0*times_distance
try:
res = splituadata(timeseries['heartrate'])
hr = res[1]
times_hr = res[0]
except KeyError:
times_hr = times_distance
hr = 0*times_distance
# create data series and remove duplicates
distseries = pd.Series(distance,index=times_distance)
distseries = distseries.groupby(distseries.index).first()
latseries = pd.Series(latcoord,index=times_location)
latseries = latseries.groupby(latseries.index).first()
lonseries = pd.Series(loncoord,index=times_location)
lonseries = lonseries.groupby(lonseries.index).first()
spmseries = pd.Series(spm,index=times_spm)
spmseries = spmseries.groupby(spmseries.index).first()
hrseries = pd.Series(hr,index=times_hr)
hrseries = hrseries.groupby(hrseries.index).first()
# Create dicts and big dataframe
d = {
' Horizontal (meters)': distseries,
' latitude': latseries,
' longitude': lonseries,
' Cadence (stokes/min)': spmseries,
' HRCur (bpm)' : hrseries,
}
df = pd.DataFrame(d)
df = df.groupby(level=0).last()
cum_time = df.index.values
df[' ElapsedTime (sec)'] = cum_time
velo = df[' Horizontal (meters)'].diff()/df[' ElapsedTime (sec)'].diff()
df[' Power (watts)'] = 0.0*velo
nr_rows = len(velo.values)
df[' DriveLength (meters)'] = np.zeros(nr_rows)
df[' StrokeDistance (meters)'] = np.zeros(nr_rows)
df[' DriveTime (ms)'] = np.zeros(nr_rows)
df[' StrokeRecoveryTime (ms)'] = np.zeros(nr_rows)
df[' AverageDriveForce (lbs)'] = np.zeros(nr_rows)
df[' PeakDriveForce (lbs)'] = np.zeros(nr_rows)
df[' lapIdx'] = np.zeros(nr_rows)
unixtime = cum_time+starttimeunix
unixtime[0] = starttimeunix
@@ -875,6 +1057,20 @@ def sporttracks_open(user):
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 SportTracks token, renews them if they are expired
def runkeeper_open(user):
r = Rower.objects.get(user=user)
@@ -1004,6 +1200,7 @@ def workout_strava_upload_view(request,id=0):
# ready to upload. Hurray
try:
w = Workout.objects.get(id=id)
r = w.user
except Workout.DoesNotExist:
raise Http404("Workout doesn't exist")
if (checkworkoutuser(request.user,w)):
@@ -1086,16 +1283,18 @@ def workout_strava_upload_view(request,id=0):
@login_required()
def workout_c2_upload_view(request,id=0):
message = ""
try:
thetoken = c2_open(request.user)
except C2NoTokenError:
return HttpResponseRedirect("/rowers/me/c2authorize/")
# ready to upload. Hurray
try:
w = Workout.objects.get(id=id)
r = w.user
except Workout.DoesNotExist:
raise Http404("Workout doesn't exist")
try:
thetoken = c2_open(r.user)
except C2NoTokenError:
return HttpResponseRedirect("/rowers/me/c2authorize/")
if (checkworkoutuser(request.user,w)):
c2userid = c2stuff.get_userid(thetoken)
if not c2userid:
@@ -1172,15 +1371,17 @@ def workout_c2_upload_view(request,id=0):
def workout_runkeeper_upload_view(request,id=0):
message = ""
try:
thetoken = runkeeper_open(request.user)
w = Workout.objects.get(id=id)
r = w.user
except Workout.DoesNotExist:
raise Http404("Workout doesn't exist")
try:
thetoken = runkeeper_open(r.user)
except RunKeeperNoTokenError:
return HttpResponseRedirect("/rowers/me/runkeeperauthorize/")
# ready to upload. Hurray
try:
w = Workout.objects.get(id=id)
except Workout.DoesNotExist:
raise Http404("Workout doesn't exist")
if (checkworkoutuser(request.user,w)):
data = runkeeperstuff.createrunkeeperworkoutdata(w)
@@ -1229,21 +1430,89 @@ def workout_runkeeper_upload_view(request,id=0):
return HttpResponseRedirect(url)
# Upload workout to Underarmour
@login_required()
def workout_underarmour_upload_view(request,id=0):
message = ""
try:
w = Workout.objects.get(id=id)
r = w.user
except Workout.DoesNotExist:
raise Http404("Workout doesn't exist")
try:
thetoken = underarmour_open(r.user)
except UnderarmourNoTokenError:
return HttpResponseRedirect("/rowers/me/underarmourauthorize/")
# ready to upload. Hurray
if (checkworkoutuser(request.user,w)):
data = underarmourstuff.createunderarmourworkoutdata(w)
# return HttpResponse(json.dumps(data))
if not data:
message = "Data error"
url = reverse(workout_export_view,
kwargs = {
'message':str(message),
'id':str(w.id),
})
return HttpResponseRedirect(url)
authorizationstring = str('Bearer ' + thetoken)
headers = {'Authorization': authorizationstring,
'Api-Key': UNDERARMOUR_CLIENT_KEY,
'user-agent': 'sanderroosendaal',
'Content-Type': 'application/json',
}
import urllib
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
w.save()
elif (response.status_code == 201 or response.status_code==200):
underarmourid = underarmourstuff.getidfromresponse(response)
w.uploadedtounderarmour = underarmourid
w.save()
url = "/rowers/workout/"+str(w.id)+"/export"
return HttpResponseRedirect(url)
else:
s = response
message = "Something went wrong in workout_underarmour_upload_view: %s - %s" % (s.reason,s.text)
else:
message = "You are not authorized to upload this workout"
url = reverse(workout_export_view,
kwargs = {
'message':str(message),
'id':str(w.id),
})
return HttpResponseRedirect(url)
# Upload workout to SportTracks
@login_required()
def workout_sporttracks_upload_view(request,id=0):
message = ""
try:
thetoken = sporttracks_open(request.user)
except SportTracksNoTokenError:
return HttpResponseRedirect("/rowers/me/sporttracksauthorize/")
# ready to upload. Hurray
try:
w = Workout.objects.get(id=id)
r = w.user
except Workout.DoesNotExist:
raise Http404("Workout doesn't exist")
try:
thetoken = sporttracks_open(r.user)
except SportTracksNoTokenError:
return HttpResponseRedirect("/rowers/me/sporttracksauthorize/")
if (checkworkoutuser(request.user,w)):
data = sporttracksstuff.createsporttracksworkoutdata(w)
if not data:
@@ -1363,6 +1632,23 @@ def rower_sporttracks_authorize(request):
return HttpResponseRedirect(url)
# Underarmour Authorization
@login_required()
def rower_underarmour_authorize(request):
# Generate a random string for the state parameter
# Save it for use later to prevent xsrf attacks
from uuid import uuid4
state = str(uuid4())
redirect_uri = UNDERARMOUR_REDIRECT_URI
url = 'https://www.mapmyfitness.com/v7.1/oauth2/authorize/?' \
'client_id={0}&response_type=code&redirect_uri={1}'.format(
UNDERARMOUR_CLIENT_KEY, redirect_uri
)
return HttpResponseRedirect(url)
# Concept2 token refresh. URL for manual refresh. Not visible to users
@login_required()
def rower_c2_token_refresh(request):
@@ -1388,11 +1674,37 @@ def rower_c2_token_refresh(request):
return imports_view(request,successmessage=successmessage,message=message)
# Underarmour token refresh. URL for manual refresh. Not visible to users
@login_required()
def rower_underarmour_token_refresh(request):
r = Rower.objects.get(user=request.user)
res = underarmourstuff.do_refresh_token(
r.underarmourrefreshtoken,
r.underarmourtoken
)
access_token = res[0]
expires_in = res[1]
refresh_token = res[2]
expirydatetime = timezone.now()+datetime.timedelta(seconds=expires_in)
r = Rower.objects.get(user=request.user)
r.underarmourtoken = access_token
r.underarmourtokenexpirydate = expirydatetime
r.underarmourrefreshtoken = refresh_token
r.save()
successmessage = "Tokens refreshed. Good to go"
return imports_view(request,successmessage=successmessage)
# SportTracks token refresh. URL for manual refresh. Not visible to users
@login_required()
def rower_sporttracks_token_refresh(request):
r = Rower.objects.get(user=request.user)
res = sporttracksstuff.do_refresh_token(r.sporttracksrefreshtoken)
res = sporttracksstuff.do_refresh_token(
r.sporttracksrefreshtoken,
)
access_token = res[0]
expires_in = res[1]
refresh_token = res[2]
@@ -1525,6 +1837,28 @@ def rower_process_sporttrackscallback(request):
successmessage = "Tokens stored. Good to go"
return imports_view(request,successmessage=successmessage)
# Process Underarmour callback
@login_required()
def rower_process_underarmourcallback(request):
code = request.GET['code']
res = underarmourstuff.get_token(code)
access_token = res[0]
expires_in = res[1]
refresh_token = res[2]
expirydatetime = timezone.now()+datetime.timedelta(seconds=expires_in)
r = Rower.objects.get(user=request.user)
r.underarmourtoken = access_token
r.underarmourtokenexpirydate = expirydatetime
r.underarmourrefreshtoken = refresh_token
r.save()
successmessage = "Tokens stored. Good to go"
return imports_view(request,successmessage=successmessage)
# Process Own API callback - for API testing purposes
@login_required()
def rower_process_testcallback(request):
@@ -3866,6 +4200,7 @@ def workout_flexchart3_view(request,*args,**kwargs):
if request.method == 'POST' and 'savefavorite' in request.POST:
workstrokesonly = request.POST['workstrokesonlysave']
reststrokes = not workstrokesonly
r = Rower.objects.get(user=request.user)
f = FavoriteChart(user=r,xparam=xparam,
yparam1=yparam1,yparam2=yparam2,
plottype=plottype,workouttype=workouttype,
@@ -4829,6 +5164,57 @@ def workout_runkeeperimport_view(request,message=""):
return HttpResponse(res)
# The page where you select which RunKeeper workout to import
@login_required()
def workout_underarmourimport_view(request,message=""):
res = underarmourstuff.get_underarmour_workout_list(request.user)
if (res.status_code != 200):
if (res.status_code == 401):
r = Rower.objects.get(user=request.user)
if (r.underarmourtoken == '') or (r.underarmourtoken is None):
s = "Token doesn't exist. Need to authorize"
return HttpResponseRedirect("/rowers/me/underarmourauthorize/")
message = "Something went wrong in workout_underarmourimport_view"
if settings.DEBUG:
return HttpResponse(res)
else:
url = reverse(workouts_view,
kwargs = {
'message': str(message)
})
return HttpResponseRedirect(url)
else:
workouts = []
items = res.json()['_embedded']['workouts']
for item in items:
if 'has_time_series' in item:
if item['has_time_series']:
s = item['start_datetime']
i,r = underarmourstuff.get_idfromuri(request.user,item['_links'])
n = item['name']
try:
d = item['aggregates']['distance_total']
except KeyError:
d = 0
try:
ttot = item['aggregates']['active_time_total']
except KeyError:
ttot = 0
keys = ['id','distance','duration','starttime','type']
values = [i,d,ttot,s,r]
thedict = dict(zip(keys,values))
workouts.append(thedict)
return render(request,'underarmour_list_import.html',
{'workouts':workouts,
'teams':get_my_teams(request.user),
'message':message,
})
return HttpResponse(res)
# The page where you select which SportTracks workout to import
@login_required()
def workout_sporttracksimport_view(request,message=""):
@@ -5011,6 +5397,29 @@ def workout_getrunkeeperworkout_view(request,runkeeperid):
})
return HttpResponseRedirect(url)
# Imports a workout from Underarmour
@login_required()
def workout_getunderarmourworkout_view(request,underarmourid):
res = underarmourstuff.get_underarmour_workout(request.user,underarmourid)
data = res.json()
id,message = add_workout_from_underarmourdata(request.user,underarmourid,data)
w = Workout.objects.get(id=id)
w.uploadedtounderarmour=underarmourid
w.save()
if message:
url = reverse(workout_edit_view,
kwargs = {
'id':id,
'message':message,
})
else:
url = reverse(workout_edit_view,
kwargs = {
'id':id,
})
return HttpResponseRedirect(url)
# Imports a workout from SportTracks
@@ -5159,11 +5568,24 @@ def workout_upload_view(request,message="",
request.session['uploadoptions'] = uploadoptions
makeprivate = uploadoptions['makeprivate']
make_plot = uploadoptions['make_plot']
plottype = uploadoptions['plottype']
upload_toc2 = uploadoptions['upload_to_C2']
try:
makeprivate = uploadoptions['makeprivate']
except KeyError:
makeprivate = False
try:
make_plot = uploadoptions['make_plot']
except KeyError:
make_plot = False
try:
plottype = uploadoptions['plottype']
except KeyError:
plottype = 'timeplot'
try:
upload_toc2 = uploadoptions['upload_to_C2']
except KeyError:
upload_toc2 = False
r = Rower.objects.get(user=request.user)
if request.method == 'POST':
@@ -5207,7 +5629,12 @@ def workout_upload_view(request,message="",
args=[str(message)])
response = HttpResponseRedirect(url)
return response
elif id == -1:
message = 'The zip archive will be processed in the background. The files in the archive will only be uploaded without the extra actions. You will receive email when the workouts are ready.'
url = reverse(workout_upload_view,
args=[str(message)])
response = HttpResponseRedirect(url)
return response
else:
if message:
url = reverse(workout_edit_view,
@@ -5350,6 +5777,183 @@ def workout_upload_view(request,message="",
'optionsform': optionsform,
'message':message})
# This is the main view for processing uploaded files
@user_passes_test(iscoachmember,login_url="/",redirect_field_name=None)
def team_workout_upload_view(request,message="",
successmessage="",
uploadoptions={
'make_plot':False,
'plottype':'timeplot',
}):
if 'uploadoptions' in request.session:
uploadoptions = request.session['uploadoptions']
else:
request.session['uploadoptions'] = uploadoptions
myteams = Team.objects.filter(manager=request.user)
make_plot = uploadoptions['make_plot']
plottype = uploadoptions['plottype']
r = Rower.objects.get(user=request.user)
if request.method == 'POST':
form = DocumentsForm(request.POST,request.FILES)
optionsform = TeamUploadOptionsForm(request.POST)
rowerform = TeamInviteForm(request.POST)
rowerform.fields.pop('email')
rowerform.fields['user'].queryset = User.objects.filter(rower__isnull=False,rower__team__in=myteams).distinct()
if form.is_valid():
f = request.FILES['file']
res = handle_uploaded_file(f)
t = form.cleaned_data['title']
if rowerform.is_valid():
u = rowerform.cleaned_data['user']
if u:
r = Rower.objects.get(user=u)
else:
message = 'Please select a rower'
response = render(request,
'team_document_form.html',
{'form':form,
'teams':get_my_teams(request.user),
'optionsform': optionsform,
'rowerform': rowerform,
'message':message,
'successmessage':successmessage,
})
return response
workouttype = form.cleaned_data['workouttype']
notes = form.cleaned_data['notes']
if optionsform.is_valid():
make_plot = optionsform.cleaned_data['make_plot']
plottype = optionsform.cleaned_data['plottype']
uploadoptions = {
'makeprivate':False,
'make_plot':make_plot,
'plottype':plottype,
'upload_to_C2':False,
}
request.session['uploadoptions'] = uploadoptions
f1 = res[0] # file name
f2 = res[1] # file name incl media directory
id,message,f2 = dataprep.new_workout_from_file(r,f2,
workouttype=workouttype,
makeprivate=False,
title = t,
notes='')
if not id:
url = reverse(team_workout_upload_view,
args=[str(message)])
response = HttpResponseRedirect(url)
return response
else:
if message:
successmessage = "The workout was added to the user's account"
url = reverse(team_workout_upload_view,
kwargs = {
'message':message,
'successmessage':successmessage,
})
else:
successmessage = "The workout was added to the user's account"
url = reverse(team_workout_upload_view,
kwargs = {
'successmessage':successmessage,
})
response = HttpResponseRedirect(url)
w = Workout.objects.get(id=id)
if (make_plot):
imagename = f1[:-4]+'.png'
fullpathimagename = 'static/plots/'+imagename
powerperc = 100*np.array([r.pw_ut2,
r.pw_ut1,
r.pw_at,
r.pw_tr,r.pw_an])/r.ftp
hrpwrdata = {
'hrmax':r.max,
'hrut2':r.ut2,
'hrut1':r.ut1,
'hrat':r.at,
'hrtr':r.tr,
'hran':r.an,
'ftp':r.ftp,
'powerperc':serialize_list(powerperc),
'powerzones':serialize_list(r.powerzones),
}
# make plot - asynchronous task
plotnrs = {
'timeplot':1,
'distanceplot':2,
'pieplot':3,
}
plotnr = plotnrs[plottype]
if (workouttype=='water'):
plotnr = plotnr+3
if settings.DEBUG:
res = handle_makeplot.delay(f1,f2,t,
hrpwrdata,plotnr,
imagename)
else:
res = queue.enqueue(handle_makeplot,f1,f2,
t,hrpwrdata,
plotnr,imagename)
i = GraphImage(workout=w,
creationdatetime=timezone.now(),
filename=fullpathimagename)
i.save()
else:
response = render(request,
'team_document_form.html',
{'form':form,
'teams':get_my_teams(request.user),
'optionsform': optionsform,
'rowerform': rowerform,
'message':message,
'successmessage':successmessage,
})
return response
else:
form = DocumentsForm()
optionsform = TeamUploadOptionsForm(initial=uploadoptions)
rowerform = TeamInviteForm()
rowerform.fields.pop('email')
rowerform.fields['user'].queryset = User.objects.filter(rower__isnull=False,rower__team__in=myteams).distinct()
return render(request, 'team_document_form.html',
{'form':form,
'teams':get_my_teams(request.user),
'optionsform': optionsform,
'rowerform':rowerform,
'message':message,
'successmessage':successmessage,
})
# Ask the user if he really wants to delete the workout
@@ -6416,11 +7020,14 @@ def rower_teams_view(request,message='',successmessage=''):
requests = TeamRequest.objects.filter(user=request.user)
myrequests = TeamRequest.objects.filter(team__in=myteams)
myinvites = TeamInvite.objects.filter(team__in=myteams)
clubsize = teams.count_invites(request.user)+teams.count_club_members(request.user)
max_clubsize = r.clubsize
return render(request, 'teams.html',
{
'teams':ts,
'teams':get_my_teams(request.user),
'clubsize':clubsize,
'max_clubsize':max_clubsize,
'myteams':myteams,
'invites':invites,
'otherteams':otherteams,
@@ -6431,6 +7038,7 @@ def rower_teams_view(request,message='',successmessage=''):
'successmessage':successmessage,
'myinvites':myinvites,
})
@user_passes_test(iscoachmember,login_url="/",redirect_field_name=None)
def invitation_revoke_view(request,id):
res,text = teams.revoke_invite(request.user,id)