Merge branch 'release/v3.08'
This commit is contained in:
@@ -398,7 +398,8 @@ def save_workout_database(f2,r,dosmooth=True,workouttype='rower',
|
||||
notes='',totaldist=0,totaltime=0,
|
||||
summary='',
|
||||
makeprivate=False,
|
||||
oarlength=2.89,inboard=0.88):
|
||||
oarlength=2.89,inboard=0.88,
|
||||
consistencychecks=True):
|
||||
message = None
|
||||
powerperc = 100*np.array([r.pw_ut2,
|
||||
r.pw_ut1,
|
||||
@@ -417,10 +418,13 @@ def save_workout_database(f2,r,dosmooth=True,workouttype='rower',
|
||||
for key,value in checks.iteritems():
|
||||
if not value:
|
||||
allchecks = 0
|
||||
a_messages.error(r.user,'Failed consistency check: '+key+', autocorrected')
|
||||
if consistencychecks:
|
||||
a_messages.error(r.user,'Failed consistency check: '+key+', autocorrected')
|
||||
else:
|
||||
a_messages.error(r.user,'Failed consistency check: '+key+', not corrected')
|
||||
|
||||
if not allchecks:
|
||||
#row.repair()
|
||||
if not allchecks and consistencychecks:
|
||||
# row.repair()
|
||||
pass
|
||||
|
||||
|
||||
@@ -529,6 +533,7 @@ def save_workout_database(f2,r,dosmooth=True,workouttype='rower',
|
||||
user=r)
|
||||
if (len(ws) != 0):
|
||||
message = "Warning: This workout probably already exists in the database"
|
||||
privacy = 'private'
|
||||
|
||||
# checking for inf values
|
||||
totaldist = np.nan_to_num(totaldist)
|
||||
@@ -781,7 +786,8 @@ def new_workout_from_df(r,df,
|
||||
oarlength=oarlength,
|
||||
inboard=inboard,
|
||||
makeprivate=makeprivate,
|
||||
dosmooth=False)
|
||||
dosmooth=False,
|
||||
consistencychecks=False)
|
||||
|
||||
|
||||
return (id,message)
|
||||
|
||||
@@ -243,6 +243,7 @@ def save_workout_database(f2,r,dosmooth=True,workouttype='rower',
|
||||
user=r)
|
||||
if (len(ws) != 0):
|
||||
message = "Warning: This workout probably already exists in the database"
|
||||
privacy = 'private'
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -596,6 +596,106 @@ def googlemap_chart(lat,lon,name=""):
|
||||
return [script,div]
|
||||
|
||||
|
||||
def interactive_otwcpchart(powerdf,promember=0):
|
||||
powerdf = powerdf[~(powerdf == 0).any(axis=1)]
|
||||
# plot tools
|
||||
if (promember==1):
|
||||
TOOLS = 'save,pan,box_zoom,wheel_zoom,reset,tap,hover,resize,crosshair'
|
||||
else:
|
||||
TOOLS = 'pan,box_zoom,wheel_zoom,reset,tap,hover,crosshair'
|
||||
|
||||
|
||||
x_axis_type = 'log'
|
||||
y_axis_type = 'linear'
|
||||
|
||||
deltas = powerdf['Delta'].apply(lambda x: timedeltaconv(x))
|
||||
powerdf['ftime'] = niceformat(deltas)
|
||||
|
||||
|
||||
source = ColumnDataSource(
|
||||
data = powerdf
|
||||
)
|
||||
|
||||
# there is no Paul's law for OTW
|
||||
|
||||
# Fit the data to thee parameter CP model
|
||||
fitfunc = lambda pars,x: pars[0]/(1+(x/pars[2])) + pars[1]/(1+(x/pars[3]))
|
||||
errfunc = lambda pars,x,y: fitfunc(pars,x)-y
|
||||
|
||||
p0 = [500,350,10,8000]
|
||||
|
||||
p1 = p0
|
||||
|
||||
thesecs = powerdf['Delta']
|
||||
theavpower = powerdf['CP']
|
||||
|
||||
if len(thesecs)>=4:
|
||||
p1, success = optimize.leastsq(errfunc, p0[:], args = (thesecs,theavpower))
|
||||
else:
|
||||
factor = fitfunc(p0,thesecs.mean())/theavpower.mean()
|
||||
p1 = [p0[0]/factor,p0[1]/factor,p0[2],p0[3]]
|
||||
|
||||
|
||||
fitt = pd.Series(10**(4*np.arange(100)/100.))
|
||||
|
||||
fitpower = fitfunc(p1,fitt)
|
||||
|
||||
message = ""
|
||||
#if len(fitpower[fitpower<0]) > 0:
|
||||
# message = "CP model fit didn't give correct results"
|
||||
|
||||
|
||||
sourcecomplex = ColumnDataSource(
|
||||
data = dict(
|
||||
power = fitpower,
|
||||
duration = fitt
|
||||
)
|
||||
)
|
||||
|
||||
# making the plot
|
||||
plot = Figure(tools=TOOLS,x_axis_type=x_axis_type,
|
||||
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('Delta','CP',source=source,fill_color='red',size=15,
|
||||
legend='Power Data')
|
||||
plot.xaxis.axis_label = "Duration (seconds)"
|
||||
plot.yaxis.axis_label = "Power (W)"
|
||||
|
||||
plot.y_range = Range1d(0,1.5*max(theavpower))
|
||||
plot.x_range = Range1d(1,2*max(thesecs))
|
||||
plot.legend.orientation = "vertical"
|
||||
|
||||
hover = plot.select(dict(type=HoverTool))
|
||||
|
||||
hover.tooltips = OrderedDict([
|
||||
('Duration ','@ftime'),
|
||||
('Power (W)','@CP{int}'),
|
||||
])
|
||||
|
||||
hover.mode = 'mouse'
|
||||
|
||||
plot.line('duration','power',source=sourcecomplex,legend="CP Model",
|
||||
color='green')
|
||||
|
||||
script, div = components(plot)
|
||||
|
||||
return [script,div,p1,message]
|
||||
|
||||
def interactive_cpchart(thedistances,thesecs,theavpower,
|
||||
theworkouts,promember=0):
|
||||
|
||||
|
||||
@@ -237,6 +237,7 @@ def make_new_workout_from_email(rr,f2,name,cntr=0):
|
||||
inboard=inboard,
|
||||
oarlength=oarlength,
|
||||
title=name,
|
||||
workoutsource=fileformat,
|
||||
notes='imported through email')
|
||||
|
||||
|
||||
|
||||
@@ -425,7 +425,8 @@ class Workout(models.Model):
|
||||
summary = models.TextField(blank=True)
|
||||
privacy = models.CharField(default='visible',max_length=30,
|
||||
choices=privacychoices)
|
||||
|
||||
rankingpiece = models.BooleanField(default=False,verbose_name='Ranking Piece')
|
||||
|
||||
def __unicode__(self):
|
||||
|
||||
date = self.date
|
||||
@@ -552,7 +553,7 @@ class WorkoutForm(ModelForm):
|
||||
duration = forms.TimeInput(format='%H:%M:%S.%f')
|
||||
class Meta:
|
||||
model = Workout
|
||||
fields = ['name','date','starttime','duration','distance','workouttype','notes','privacy','boattype']
|
||||
fields = ['name','date','starttime','duration','distance','workouttype','notes','privacy','rankingpiece','boattype']
|
||||
widgets = {
|
||||
'date': DateInput(),
|
||||
'notes': forms.Textarea,
|
||||
|
||||
@@ -245,7 +245,7 @@ def get_strava_workout(user,stravaid):
|
||||
return [workoutsummary,df]
|
||||
|
||||
# Generate Workout data for Strava (a TCX file)
|
||||
def createstravaworkoutdata(w):
|
||||
def createstravaworkoutdata(w,dozip=True):
|
||||
filename = w.csvfilename
|
||||
|
||||
row = rowingdata(filename)
|
||||
@@ -256,18 +256,22 @@ def createstravaworkoutdata(w):
|
||||
newnotes = 'from '+w.workoutsource+' via rowsandall.com'
|
||||
|
||||
row.exporttotcx(tcxfilename,notes=newnotes)
|
||||
gzfilename = tcxfilename+'.gz'
|
||||
with file(tcxfilename,'rb') as inF:
|
||||
s = inF.read()
|
||||
with gzip.GzipFile(gzfilename,'wb') as outF:
|
||||
outF.write(s)
|
||||
if dozip:
|
||||
gzfilename = tcxfilename+'.gz'
|
||||
with file(tcxfilename,'rb') as inF:
|
||||
s = inF.read()
|
||||
with gzip.GzipFile(gzfilename,'wb') as outF:
|
||||
outF.write(s)
|
||||
|
||||
try:
|
||||
os.remove(tcxfilename)
|
||||
except WindowError:
|
||||
pass
|
||||
|
||||
return gzfilename,""
|
||||
try:
|
||||
os.remove(tcxfilename)
|
||||
except WindowError:
|
||||
pass
|
||||
|
||||
return gzfilename,""
|
||||
|
||||
else:
|
||||
return tcxfilename,""
|
||||
|
||||
|
||||
# Upload the TCX file to Strava and set the workout activity type
|
||||
|
||||
@@ -77,8 +77,28 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid_12 alpha">
|
||||
<div class="grid_6 alpha">
|
||||
<p> </p>
|
||||
</div>
|
||||
|
||||
<div class="grid_6 omega">
|
||||
<div class="grid_2 suffix_4 alpha">
|
||||
<p>
|
||||
{% if user.rower.rowerplan == 'pro' or user.rower.rowerplan == 'coach' %}
|
||||
<a class="button blue small" href="/rowers/otw-bests">OTW Ranking Pieces</a>
|
||||
{% else %}
|
||||
<a class="button blue small" href="/rowers/promembership">OTW Ranking Pieces</a>
|
||||
{% endif %}
|
||||
</p>
|
||||
<p>
|
||||
Analyse power vs piece duration to make predictions.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@@ -147,6 +147,10 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="grid_1 omega">
|
||||
<a href="/rowers/workout/{{ workout.id }}/emailgpx">
|
||||
<img src="/static/img/gpx.jpg" alt="GPX Export" width="60" height="60"></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
207
rowers/templates/otwrankings.html
Normal file
207
rowers/templates/otwrankings.html
Normal file
@@ -0,0 +1,207 @@
|
||||
{% extends "base.html" %}
|
||||
{% load staticfiles %}
|
||||
{% load rowerfilters %}
|
||||
|
||||
{% 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 %}
|
||||
<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>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
|
||||
{% 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:
|
||||
<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>
|
||||
</p>
|
||||
|
||||
<p>The table gives the OTW 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>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="70%" class="listtable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th> Duration</th>
|
||||
<th> Power </th>
|
||||
<tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for pred in cpredictions %}
|
||||
<tr>
|
||||
{% for key, value in pred.items %}
|
||||
{% if key == "power" %}
|
||||
<td> {{ value }} W </td>
|
||||
{% endif %}
|
||||
{% if key == "duration" %}
|
||||
<td> {{ value |deltatimeprint }} </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"
|
||||
formaction="/rowers/{{ id }}/otw-bests/{{ startdate|date:"Y-m-d" }}/{{ enddate|date:"Y-m-d" }}"
|
||||
type="submit" value="Add">
|
||||
</form>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
@@ -135,6 +135,12 @@ urlpatterns = [
|
||||
url(r'^ote-bests/(?P<deltadays>\d+)$',views.rankings_view),
|
||||
url(r'^ote-bests/$',views.rankings_view),
|
||||
url(r'^(?P<theuser>\d+)/ote-bests/$',views.rankings_view),
|
||||
url(r'^(?P<theuser>\d+)/otw-bests/(?P<startdatestring>\w+.*)/(?P<enddatestring>\w+.*)$',views.otwrankings_view),
|
||||
url(r'^(?P<theuser>\d+)/otw-bests/(?P<deltadays>\d+)$',views.otwrankings_view),
|
||||
url(r'^otw-bests/(?P<startdatestring>\w+.*)/(?P<enddatestring>\w+.*)$',views.otwrankings_view),
|
||||
url(r'^otw-bests/(?P<deltadays>\d+)$',views.otwrankings_view),
|
||||
url(r'^otw-bests/$',views.otwrankings_view),
|
||||
url(r'^(?P<theuser>\d+)/otw-bests/$',views.otwrankings_view),
|
||||
url(r'^(?P<theuser>\d+)/flexall/(?P<xparam>\w+.*)/(?P<yparam1>\w+.*)/(?P<yparam2>\w+.*)/(?P<startdatestring>\w+.*)/(?P<enddatestring>\w+.*)$',views.cum_flex),
|
||||
url(r'^flexall/(?P<xparam>\w+.*)/(?P<yparam1>\w+.*)/(?P<yparam2>\w+.*)/(?P<startdatestring>\w+.*)/(?P<enddatestring>\w+.*)$',views.cum_flex),
|
||||
url(r'^flexall/(?P<xparam>\w+.*)/(?P<yparam1>\w+.*)/(?P<yparam2>\w+.*)$',views.cum_flex),
|
||||
@@ -169,6 +175,7 @@ urlpatterns = [
|
||||
url(r'^workout/(?P<id>\d+)/export$',views.workout_export_view),
|
||||
url(r'^workout/(?P<id>\d+)/comment$',views.workout_comment_view),
|
||||
url(r'^workout/(?P<id>\d+)/emailtcx$',views.workout_tcxemail_view),
|
||||
url(r'^workout/(?P<id>\d+)/emailgpx$',views.workout_gpxemail_view),
|
||||
url(r'^workout/(?P<id>\d+)/emailcsv$',views.workout_csvemail_view),
|
||||
url(r'^workout/(?P<id>\d+)/csvtoadmin$',views.workout_csvtoadmin_view),
|
||||
url(r'^workout/compare/(?P<id>\d+)/$',views.workout_comparison_list),
|
||||
|
||||
542
rowers/views.py
542
rowers/views.py
File diff suppressed because it is too large
Load Diff
BIN
static/img/gpx.jpg
Normal file
BIN
static/img/gpx.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 27 KiB |
Reference in New Issue
Block a user