Private
Public Access
1
0

Merge branch 'release/v5.10'

This commit is contained in:
Sander Roosendaal
2017-11-20 09:36:00 +01:00
7 changed files with 510 additions and 10 deletions

View File

@@ -555,6 +555,71 @@ def fetchcp(rower,theworkouts,table='cpdata'):
return [],[],avgpower2
# create a new workout from manually entered data
def create_row_df(r,distance,duration,startdatetime,
title = 'Manually added workout',notes='',
workouttype='rower'):
nr_strokes = int(distance/10.)
unixstarttime = arrow.get(startdatetime).timestamp
totalseconds = duration.hour*3600.
totalseconds += duration.minute*60.
totalseconds += duration.second
totalseconds += duration.microsecond/1.e6
spm = 60.*nr_strokes/totalseconds
step = totalseconds/float(nr_strokes)
elapsed = np.arange(0,totalseconds+step,step)
dstep = distance/float(nr_strokes)
d = np.arange(0,distance+dstep,dstep)
unixtime = unixstarttime + elapsed
pace = 500.*totalseconds/distance
if workouttype in ['rower','slides','dynamic']:
velo = distance/totalseconds
power = 2.8*velo**3
else:
power = 0
df = pd.DataFrame({
'TimeStamp (sec)': unixtime,
' Horizontal (meters)': d,
' Cadence (stokes/min)': spm,
' Stroke500mPace (sec/500m)':pace,
' ElapsedTime (sec)':elapsed,
' Power (watts)':power,
})
timestr = strftime("%Y%m%d-%H%M%S")
csvfilename = 'media/df_' + timestr + '.csv'
df[' ElapsedTime (sec)'] = df['TimeStamp (sec)']
row = rrdata(df=df)
row.write_csv(csvfilename, gzip = True)
id, message = save_workout_database(csvfilename, r,
title=title,
notes=notes,
dosmooth=False,
workouttype=workouttype,
consistencychecks=False,
totaltime=totalseconds)
return (id, message)
# Processes painsled CSV file to database

View File

@@ -0,0 +1,38 @@
{% extends "base.html" %}
{% load staticfiles %}
{% load rowerfilters %}
{% load tz %}
{% get_current_timezone as TIME_ZONE %}
{% block content %}
<div class="grid_12 alpha">
<h1>Add Workout Manually</h1>
<div class="grid_6 alpha">
{% if form.errors %}
<p style="color: red;">
Please correct the error{{ form.errors|pluralize }} below.
</p>
{% endif %}
<form id="importantform"
enctype="multipart/form-data" action="" method="post">
<table width=100%>
{{ form.as_table }}
</table>
{% csrf_token %}
<div id="formbutton" class="grid_1 suffix_1 omega">
<input class="button green" type="submit" value="Save">
</div>
</form>
</div>
<div id="images" class="grid_6 omega">
<p>&nbsp;</p>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,232 @@
{% extends "base.html" %}
{% load staticfiles %}
{% load rowerfilters %}
{% block scripts %}
{% include "monitorjobs.html" %}
{% endblock %}
{% block title %}Workouts{% endblock %}
{% block content %}
<script type="text/javascript" src="/static/js/bokeh-0.12.3.min.js"></script>
<script async="true" type="text/javascript">
Bokeh.set_log_level("info");
</script>
{{ interactiveplot |safe }}
<script>
// Set things up to resize the plot on a window resize. You can play with
// the arguments of resize_width_height() to change the plot's behavior.
var plot_resize_setup = function () {
var plotid = Object.keys(Bokeh.index)[0]; // assume we have just one plot
var plot = Bokeh.index[plotid];
var plotresizer = function() {
// arguments: use width, use height, maintain aspect ratio
plot.resize_width_height(true, true, false);
};
window.addEventListener('resize', plotresizer);
plotresizer();
};
window.addEventListener('load', plot_resize_setup);
</script>
<style>
/* Need this to get the page in "desktop mode"; not having an infinite height.*/
html, body {height: 100%; margin:5px;}
</style>
<div id="title" class="grid_12 alpha">
<div class="grid_10 alpha">
{% if theuser %}
<h3>{{ theuser.first_name }}'s Ranking Pieces</h3>
{% else %}
<h3>{{ user.first_name }}'s Ranking Pieces</h3>
{% endif %}
</div>
<div class="grid_2 omega">
{% if user.is_authenticated and user|is_manager %}
<div class="grid_2 alpha dropdown">
<button class="grid_2 alpha button green small dropbtn">
Change Rower
</button>
<div class="dropdown-content">
{% for member in user|team_members %}
{% if workouttype == 'water' %}
<a class="button green small" href="/rowers/{{ member.id }}/otw-bests/{{ startdate|date:"Y-m-d" }}/{{ enddate|date:"Y-m-d" }}">{{ member.first_name }} {{ member.last_name }}</a>
{% else %}
<a class="button green small" href="/rowers/{{ member.id }}/ote-ranking/{{ startdate|date:"Y-m-d" }}/{{ enddate|date:"Y-m-d" }}">{{ member.first_name }} {{ member.last_name }}</a>
{% endif %}
{% endfor %}
</div>
{% else %}
&nbsp;
{% endif %}
</div>
</div>
<div id="summary" class="grid_6 alpha">
<p>Summary for {{ theuser.first_name }} {{ theuser.last_name }}
between {{ startdate|date }} and {{ enddate|date }}</p>
<p>Direct link for other users:
{% if workouttype == 'water' %}
<a href="/rowers/{{ id }}/otw-bests/{{ startdate|date:"Y-m-d" }}/{{ enddate|date:"Y-m-d" }}">https://rowsandall.com/rowers/{{ id }}/otw-bests/{{ startdate|date:"Y-m-d" }}/{{ enddate|date:"Y-m-d" }}</a>
{% else %}
<a href="/rowers/{{ id }}/ote-ranking/{{ startdate|date:"Y-m-d" }}/{{ enddate|date:"Y-m-d" }}">https://rowsandall.com/rowers/{{ id }}/ote-ranking/{{ startdate|date:"Y-m-d" }}/{{ enddate|date:"Y-m-d" }}</a>
{% endif %}
</p>
<p>The table gives the efforts you marked as Ranking Piece.
The graph shows the best segments from those pieces, plotted as
average power (over the segment) vs the duration of the segment/
In other words: How long you can hold that power.
</p>
<p>When you change the date range, the algorithm calculates new
parameters in a background process. You may have to reload the
page to get an updated prediction.</p>
<p>At the bottom of the page, you will find predictions derived from the model.</p>
</div>
<div id="form" class="grid_6 omega">
<p>Use this form to select a different date range:</p>
<p>
Select start and end date for a date range:
<div class="grid_4 alpha">
<form enctype="multipart/form-data" action="" method="post">
<table>
{{ dateform.as_table }}
</table>
{% csrf_token %}
</div>
<div class="grid_2 omega">
<input name='daterange' class="button green" type="submit" value="Submit"> </form>
</div>
<div class="grid_4 alpha">
<form enctype="multipart/form-data" action="" method="post">
Or use the last {{ deltaform }} days.
</div>
<div class="grid_2 omega">
{% csrf_token %}
<input name='datedelta' class="button green" type="submit" value="Submit">
</form>
</div>
</div>
<div id="theplot" class="grid_12 alpha">
<h2>Critical Power Plot</h2>
{{ the_div|safe }}
</div>
<div class="grid_12 alpha">
<h2>Ranking Piece Results</h2>
{% if rankingworkouts %}
<table width="70%" class="listtable">
<thead>
<tr>
<th> Distance</th>
<th> Duration</th>
<th> Avg Power</th>
<th> Date</th>
<th> Avg HR </th>
<th> Max HR </th>
<th> Edit</th>
<tr>
</thead>
<tbody>
{% for workout in rankingworkouts %}
<tr>
<td> {{ workout.distance }} m</td>
<td> {{ workout.duration |durationprint:"%H:%M:%S.%f" }} </td>
<td> {{ avgpower|lookup:workout.id }} W</td>
<td> {{ workout.date }} </td>
<td> {{ workout.averagehr }} </td>
<td> {{ workout.maxhr }} </td>
<td>
<a href="/rowers/workout/{{ workout.id }}/edit">{{ workout.name }}</a> </td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p> No ranking workouts found </p>
{% endif %}
</div>
<div id="predictions" class="grid_12 alpha">
<h2>Pace predictions for Ranking Pieces</h2>
<p>Add non-ranking piece using the form. The piece will be added in the prediction tables below. </p>
<div id="cpmodel" class="grid_6 alpha">
<table width="90%" class="listtable">
<thead>
<tr>
<th> Duration</th>
<th> Distance</th>
<th> Pace (upper)</th>
<th> Power &nbsp;&nbsp;&nbsp;</th>
<th> Power (upper)</th>
<tr>
</thead>
<tbody>
{% for pred in cpredictions %}
<tr>
{% for key, value in pred.items %}
{% if key == "power" or key == "upper" %}
<td> {{ value }} W </td>
{% endif %}
{% if key == "duration" %}
<td> {{ value |deltatimeprint }} </td>
{% endif %}
{% if key == "distance" %}
<td> {{ value }} m </td>
{% endif %}
{% if key == 'pace' %}
<td> {{ value|paceprint }}</td>
{% endif %}
{% endfor %}
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="grid_3">
<form enctype="multipart/form-data" action="{{ formloc }}" method="post">
{{ form.value }} {{ form.pieceunit }}
{% csrf_token %}
</div>
<div class="grid_1">
minutes
</div>
<div class="grid_2 omega">
<input name="piece" class="button green"
action=""
type="submit" value="Add">
</form>
</div>
</div>
{% endblock %}

View File

@@ -72,7 +72,7 @@
<p>The table gives the best efforts achieved on the official Concept2 ranking pieces in the selected date range.</p>
<p>This page will evolve and try to give you guidance on where to improve.</p>
</div>
<div id="form" class="grid_6 omega">
<p>Use this form to select a different date range:</p>
@@ -138,6 +138,19 @@
<p> No ranking workouts found </p>
{% endif %}
<p>Missing your best pieces? Upload stroke data of any Concept2
ranking piece and they will be automatically added to this page.</p>
<p> Don't have stroke data for official Concept2 ranking pieces?
The <a href="/rowers/promembership">PRO membership</a> ranking piece functionality
allows you to include your best non ranking pieces and even use
parts of workouts for improved calculation accuracy.
</p>
<p>Want to add race results but you don't have stroke data?
<a href="/rowers/addmanual">Click here.</a></p>
<p>Scroll down for the chart and pace predictions for ranking pieces.</p>
</div>

View File

@@ -126,6 +126,7 @@ urlpatterns = [
url(r'^list-workouts/team/(?P<teamid>\d+)/$',views.workouts_view),
url(r'^list-workouts/(?P<startdatestring>\w+.*)/(?P<enddatestring>\w+.*)$',views.workouts_view),
url(r'^list-workouts/$',views.workouts_view),
url(r'^addmanual/$',views.addmanual_view),
url(r'^team-compare-select/team/(?P<teamid>\d+)/(?P<startdatestring>\w+.*)/(?P<enddatestring>\w+.*)$',views.team_comparison_select),
url(r'^team-compare-select/team/(?P<teamid>\d+)/$',views.team_comparison_select),
url(r'^team-compare-select/(?P<startdatestring>\w+.*)/(?P<enddatestring>\w+.*)$',views.team_comparison_select),

View File

@@ -3096,6 +3096,82 @@ def histo(request,theuser=0,
'teams':get_my_teams(request.user),
})
# add a workout manually
@login_required()
def addmanual_view(request):
r = Rower.objects.get(user=request.user)
if request.method == 'POST':
# Form was submitted
form = WorkoutForm(request.POST)
if form.is_valid():
# Get values from form
name = form.cleaned_data['name']
date = form.cleaned_data['date']
starttime = form.cleaned_data['starttime']
workouttype = form.cleaned_data['workouttype']
duration = form.cleaned_data['duration']
distance = form.cleaned_data['distance']
notes = form.cleaned_data['notes']
thetimezone = form.cleaned_data['timezone']
try:
boattype = request.POST['boattype']
except KeyError:
boattype = '1x'
try:
privacy = request.POST['privacy']
except KeyError:
privacy = 'visible'
try:
rankingpiece = form.cleaned_data['rankingpiece']
except KeyError:
rankingpiece =- Workout.objects.get(id=id).rankingpiece
startdatetime = (str(date) + ' ' + str(starttime))
startdatetime = datetime.datetime.strptime(startdatetime,
"%Y-%m-%d %H:%M:%S")
startdatetime = timezone.make_aware(startdatetime)
startdatetime = startdatetime.astimezone(
pytz.timezone(thetimezone)
)
print name
id,message = dataprep.create_row_df(r,
distance,
duration,startdatetime,
title = name,
notes=notes,
workouttype=workouttype)
if message:
messages.error(request,message)
if id:
w = Workout.objects.get(id=id)
w.rankingpiece = rankingpiece
w.notes = notes
w.save()
messages.info(request,'New workout created')
initial = {
'workouttype':'rower',
'date':datetime.date.today(),
'starttime':timezone.now(),
'timezone':r.defaulttimezone,
'duration':datetime.timedelta(minutes=2),
'distance':500,
}
form = WorkoutForm(initial=initial)
return render(request,'manualadd.html',
{'form':form,
})
# Show ranking distances including predicted paces
@login_required()
def rankings_view(request,theuser=0,
@@ -3779,6 +3855,8 @@ def oterankings_view(request,theuser=0,
rankingdurations.append(datetime.time(hour=1))
rankingdurations.append(datetime.time(hour=1,minute=15))
rankingdistances = [100,500,1000,2000,5000,6000,10000,21097,42195,100000]
thedistances = []
theworkouts = []
thesecs = []
@@ -3874,21 +3952,29 @@ def oterankings_view(request,theuser=0,
form = PredictedPieceForm(request.POST)
clean = form.is_valid()
value = form.cleaned_data['value']
hourvalue,value = divmod(value,60)
hourvalue,tvalue = divmod(value,60)
hourvalue = int(hourvalue)
minutevalue = int(value)
value = int(60*(value-minutevalue))
minutevalue = int(tvalue)
tvalue = int(60*(tvalue-minutevalue))
if hourvalue >= 24:
hourvalue = 23
rankingdurations.append(datetime.time(minute=minutevalue,
hour=hourvalue,
second=value))
pieceunit = form.cleaned_data['pieceunit']
if pieceunit == 'd':
rankingdistances.append(value)
else:
rankingdurations.append(datetime.time(
minute=minutevalue,
hour=hourvalue,
second=tvalue
))
else:
form = PredictedPieceForm()
cpredictions = []
for rankingduration in rankingdurations:
t = 3600.*rankingduration.hour
t += 60.*rankingduration.minute
@@ -3900,6 +3986,9 @@ def oterankings_view(request,theuser=0,
pwr = p1[0]/(1+t/p1[2])
pwr += p1[1]/(1+t/p1[3])
velo = (pwr/2.8)**(1./3.)
p = 500./velo
d = t*velo
if pwr <= 0:
pwr = 50.
@@ -3912,16 +4001,78 @@ def oterankings_view(request,theuser=0,
pwr2 = pwr
a = {
'distance':int(d),
'duration':timedeltaconv(t),
'power':int(pwr),
'upper':int(pwr2)}
'upper':int(pwr2),
'pace':timedeltaconv(p)}
cpredictions.append(a)
del form.fields["pieceunit"]
# initiation - get 10 min power, then use Paul's law
t_10 = 600.
power_10 = p1[0]/(1+t_10/p1[2])
power_10 += p1[1]/(1+t_10/p1[3])
velo_10 = (power_10/2.8)**(1./3.)
pace_10 = 500./velo_10
distance_10 = t_10*velo_10
paulslope = 5.
for rankingdistance in rankingdistances:
delta = paulslope * np.log(rankingdistance/distance_10)/np.log(2)
p = pace_10+delta
velo = 500./p
t = rankingdistance/velo
pwr2 = p1[0]/(1+t/p1[2])
pwr2 += p1[1]/(1+t/p1[3])
pwr2 *= ratio
if pwr2 <= 0:
pwr2 = 50.
velo2 = (pwr2/2.8)**(1./3.)
if np.isnan(velo2) or velo2 <= 0:
velo2 = 1.0
t2 = rankingdistance/velo2
pwr3 = p1[0]/(1+t2/p1[2])
pwr3 += p1[1]/(1+t2/p1[3])
pwr3 *= ratio
if pwr3 <= 0:
pwr3 = 50.
velo3 = (pwr3/2.8)**(1./3.)
if np.isnan(velo3) or velo3 <= 0:
velo3 = 1.0
t3 = rankingdistance/velo3
p3 = 500./velo3
a = {
'distance':rankingdistance,
'duration':timedeltaconv(t3),
'power':'--',
'upper':int(pwr3),
'pace':timedeltaconv(p3)}
cpredictions.append(a)
# del form.fields["pieceunit"]
messages.error(request,message)
return render(request, 'otwrankings.html',
return render(request, 'oterankings.html',
{'rankingworkouts':theworkouts,
'interactiveplot':script,
'the_div':div,

BIN
static/img/image.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB