Private
Public Access
1
0

Merge branch 'develop' into feature/ranking

This commit is contained in:
Sander Roosendaal
2018-01-09 09:00:03 +01:00
16 changed files with 4531 additions and 4070 deletions

View File

@@ -56,7 +56,7 @@ import sys
import utils
import datautils
from utils import lbstoN,myqueue
from utils import lbstoN,myqueue,is_ranking_piece
from timezonefinder import TimezoneFinder
@@ -295,6 +295,12 @@ def clean_df_stats(datadf, workstrokesonly=True, ignorehr=True,
except KeyError:
pass
try:
mask = datadf['efficiency'] > 200.
datadf.loc[mask, 'efficiency'] = np.nan
except KeyError:
pass
try:
mask = datadf['spm'] < 10
datadf.loc[mask, 'spm'] = np.nan
@@ -921,6 +927,9 @@ def save_workout_database(f2, r, dosmooth=True, workouttype='rower',
w.startdatetime = timezone.now()
w.save()
if is_ranking_piece(w):
w.rankingpiece = True
w.save()
if privacy == 'visible':
ts = Team.objects.filter(rower=r)
@@ -1557,7 +1566,11 @@ def getsmallrowdata_db(columns, ids=[], doclean=True, workstrokesonly=True):
if extracols and len(ids)==1:
w = Workout.objects.get(id=ids[0])
row = rdata(w.csvfilename)
f = row.df['TimeStamp (sec)'].diff().mean()
try:
f = row.df['TimeStamp (sec)'].diff().mean()
except AttributeError:
f = 0
if f != 0 and not np.isnan(f):
windowsize = 2 * (int(10. / (f))) + 1
else:

View File

@@ -582,7 +582,53 @@ def deletecpdata_sql(rower_id,table='cpdata',debug=False):
conn.close()
engine.dispose()
def delete_agegroup_db(age,sex,weightcategory,debug=False):
if debug:
engine = create_engine(database_url_debug, echo=False)
else:
engine = create_engine(database_url, echo=False)
query = sa.text('DELETE from {table} WHERE age={age} and weightcategory = {weightcategory} and sex={sex};'.format(
sex=sex,
age=age,
weightcategory=weightcategory,
table='calcagegrouprecords'
))
with engine.connect() as conn, conn.begin():
try:
result = conn.execute(query)
except:
print "Database locked"
conn.close()
engine.dispose()
def update_agegroup_db(age,sex,weightcategory,wcdurations,wcpower,
debug=False):
delete_agegroup_db(age,sex,weightcategory,debug=debug)
df = pd.DataFrame(
{
'duration':wcdurations,
'power':wcpower,
}
)
df['sex'] = sex
df['age'] = age
df['weightcategory'] = weightcategory
if debug:
engine = create_engine(database_url_debug, echo=False)
else:
engine = create_engine(database_url, echo=False)
table = 'calcagegrouprecords'
with engine.connect() as conn, conn.begin():
df.to_sql(table, engine, if_exists='append', index=False)
conn.close()
engine.dispose()
def updatecpdata_sql(rower_id,delta,cp,table='cpdata',distance=pd.Series([]),debug=False):
deletecpdata_sql(rower_id,table=table,debug=debug)

File diff suppressed because it is too large Load Diff

View File

@@ -3,6 +3,7 @@ import numpy as np
from models import C2WorldClassAgePerformance
import pandas as pd
from scipy import optimize
from django.utils import timezone
rowingmetrics = (
('time',{
@@ -64,7 +65,7 @@ rowingmetrics = (
'null':True,
'verbose_name': 'Average Drive Force (N)',
'ax_min': 0,
'ax_max': 900,
'ax_max': 1200,
'mode':'both',
'type': 'pro'}),
@@ -73,7 +74,7 @@ rowingmetrics = (
'null':True,
'verbose_name': 'Peak Drive Force (N)',
'ax_min': 0,
'ax_max': 900,
'ax_max': 1500,
'mode':'both',
'type': 'pro'}),
@@ -321,28 +322,36 @@ def calc_trimp(df,sex,hrmax,hrmin):
return trimp
def getagegrouprecord(age,sex='male',weightcategory='hwt',
distance=2000,duration=None):
if not duration:
df = pd.DataFrame(
list(
C2WorldClassAgePerformance.objects.filter(
distance=distance,
sex=sex,
weightcategory=weightcategory
).values()
)
)
distance=2000,duration=None,indf=pd.DataFrame()):
if not indf.empty:
if not duration:
df = indf[indf['distance'] == distance]
else:
duration = 60*int(duration)
df = indf[indf['duration'] == duration]
else:
duration=60*int(duration)
df = pd.DataFrame(
list(
C2WorldClassAgePerformance.objects.filter(
duration=duration,
sex=sex,
weightcategory=weightcategory
).values()
if not duration:
df = pd.DataFrame(
list(
C2WorldClassAgePerformance.objects.filter(
distance=distance,
sex=sex,
weightcategory=weightcategory
).values()
)
)
else:
duration=60*int(duration)
df = pd.DataFrame(
list(
C2WorldClassAgePerformance.objects.filter(
duration=duration,
sex=sex,
weightcategory=weightcategory
).values()
)
)
)
if not df.empty:
ages = df['age']
@@ -354,14 +363,21 @@ def getagegrouprecord(age,sex='male',weightcategory='hwt',
p0 = [700,120,700,10,100,100]
p1, success = optimize.leastsq(errfunc,p0[:],
try:
p1, success = optimize.leastsq(errfunc,p0[:],
args = (ages,powers))
except:
p1 = p0
success = 0
power = fitfunc(p1, float(age))
if success:
power = fitfunc(p1, float(age))
#power = np.polyval(poly_coefficients,age)
#power = np.polyval(poly_coefficients,age)
power = 0.5*(np.abs(power)+power)
power = 0.5*(np.abs(power)+power)
else:
power = 0
else:
power = 0

View File

@@ -214,7 +214,34 @@ def update_records(url=c2url):
except:
print record
class CalcAgePerformance(models.Model):
weightcategories = (
('hwt','heavy-weight'),
('lwt','light-weight'),
)
sexcategories = (
('male','male'),
('female','female'),
)
weightcategory = models.CharField(default="hwt",
max_length=30,
choices=weightcategories)
sex = models.CharField(default="female",
max_length=30,
choices=sexcategories)
age = models.IntegerField(default=19,verbose_name="Age")
duration = models.FloatField(default=1,blank=True)
power = models.IntegerField(default=200)
class Meta:
db_table = 'calcagegrouprecords'
class C2WorldClassAgePerformance(models.Model):
weightcategories = (
('hwt','heavy-weight'),
@@ -875,9 +902,11 @@ class WorkoutForm(ModelForm):
# Used for the rowing physics calculations
class AdvancedWorkoutForm(ModelForm):
quick_calc = forms.BooleanField(initial=True,required=False)
class Meta:
model = Workout
fields = ['boattype','weightvalue']
fields = ['boattype','weightvalue','quick_calc']
class RowerExportForm(ModelForm):
class Meta:

View File

@@ -6,6 +6,8 @@ import gzip
import shutil
import numpy as np
from scipy import optimize
import rowingdata
from rowingdata import rowingdata as rdata
@@ -28,7 +30,8 @@ from utils import deserialize_list
from rowers.dataprepnodjango import (
update_strokedata, new_workout_from_file,
getsmallrowdata_db, updatecpdata_sql
getsmallrowdata_db, updatecpdata_sql,
update_agegroup_db,
)
from django.core.mail import send_mail, EmailMessage
@@ -46,6 +49,96 @@ import longtask
def add(x, y):
return x + y
def getagegrouprecord(age,sex='male',weightcategory='hwt',
distance=2000,duration=None,indf=pd.DataFrame()):
if not duration:
df = indf[indf['distance'] == distance]
else:
duration = 60*int(duration)
df = indf[indf['duration'] == duration]
if not df.empty:
ages = df['age']
powers = df['power']
#poly_coefficients = np.polyfit(ages,powers,6)
fitfunc = lambda pars, x: np.abs(pars[0])*(1-x/max(120,pars[1]))-np.abs(pars[2])*np.exp(-x/np.abs(pars[3]))+np.abs(pars[4])*(np.sin(np.pi*x/max(50,pars[5])))
errfunc = lambda pars, x,y: fitfunc(pars,x)-y
p0 = [700,120,700,10,100,100]
p1, success = optimize.leastsq(errfunc,p0[:],
args = (ages,powers))
if success:
power = fitfunc(p1, float(age))
#power = np.polyval(poly_coefficients,age)
power = 0.5*(np.abs(power)+power)
else:
power = 0
else:
power = 0
return power
@app.task(bind=True)
def handle_getagegrouprecords(self,
df,
distances,durations,
age,sex,weightcategory,
**kwargs):
wcdurations = []
wcpower = []
if 'debug' in kwargs:
debug = kwargs['debug']
else:
debug = False
df = pd.read_json(df)
for distance in distances:
worldclasspower = getagegrouprecord(
age,
sex=sex,
distance=distance,
weightcategory=weightcategory,indf=df,
)
velo = (worldclasspower/2.8)**(1./3.)
try:
duration = distance/velo
wcdurations.append(duration)
wcpower.append(worldclasspower)
except ZeroDivisionError:
pass
for duration in durations:
worldclasspower = getagegrouprecord(
age,
sex=sex,
duration=duration,
weightcategory=weightcategory,indf=df
)
try:
velo = (worldclasspower/2.8)**(1./3.)
distance = int(60*duration*velo)
wcdurations.append(60.*duration)
wcpower.append(worldclasspower)
except ValueError:
pass
update_agegroup_db(age,sex,weightcategory,wcdurations,wcpower,
debug=debug)
return 1
@app.task(bind=True)
def long_test_task(self,aantal,debug=False,job=None,session_key=None):
@@ -66,7 +159,10 @@ def long_test_task2(self,aantal,**kwargs):
kwargs['jobid'] = job_id
return longtask.longtask2(aantal,**kwargs)
# create workout
@app.task
def handle_new_workout_from_file(r, f2,
@@ -356,10 +452,7 @@ def handle_sendemailcsv(first_name, last_name, email, csvfile,**kwargs):
def handle_otwsetpower(self,f1, boattype, weightvalue,
first_name, last_name, email, workoutid,
**kwargs):
# ps=[
# 1, 1, 1, 1],
# ratio=1.0,
# debug=False):
job = self.request
job_id = job.id
@@ -378,7 +471,12 @@ def handle_otwsetpower(self,f1, boattype, weightvalue,
debug = kwargs['debug']
else:
debug = False
if 'quick_calc' in kwargs:
usetable = kwargs['quick_calc']
else:
usetable = False
kwargs['jobid'] = job_id
@@ -416,17 +514,25 @@ def handle_otwsetpower(self,f1, boattype, weightvalue,
pass
progressurl = SITE_URL
siteurl = SITE_URL
if debug:
progressurl = SITE_URL_DEV
siteurl = SITE_URL_DEV
secret = PROGRESS_CACHE_SECRET
progressurl += "/rowers/record-progress/"
progressurl += job_id
# determine cache file name
physics_cache = 'media/'+str(boattype)+'_'+str(int(weightvalue))
rowdata.otw_setpower_silent(skiprows=5, mc=weightvalue, rg=rg,
powermeasured=powermeasured,
progressurl=progressurl,
secret=secret
rowdata.otw_setpower(skiprows=5, mc=weightvalue, rg=rg,
powermeasured=powermeasured,
progressurl=progressurl,
secret=secret,
silent=True,
usetable=usetable,storetable=physics_cache,
)
# save data
@@ -465,7 +571,8 @@ def handle_otwsetpower(self,f1, boattype, weightvalue,
message += "Thank you for using rowsandall.com.\n\n"
message += "Rowsandall OTW calculations have not been fully implemented yet.\n"
message += "We are now running an experimental version for debugging purposes. \n"
message += "Your wind/stream corrected plot is available here: http://rowsandall.com/rowers/workout/"
message += "Your wind/stream corrected plot is available here: "
message += siteurl+"/rowers/workout/"
message += str(workoutid)
message += "/interactiveotwplot\n\n"
message += "Please report any bugs/inconsistencies/unexpected results at rowsandall.slack.com or by reply to this email.\n\n"
@@ -482,7 +589,17 @@ def handle_otwsetpower(self,f1, boattype, weightvalue,
def handle_updateergcp(rower_id,workoutfilenames,debug=False,**kwargs):
therows = []
for f1 in workoutfilenames:
rowdata = rdata(f1)
try:
rowdata = rdata(f1)
except IOError:
try:
rowdata = rdata(f1 + '.csv')
except IOError:
try:
rowdata = rdata(f1 + '.gz')
except IOError:
rowdata = 0
if rowdata != 0:
therows.append(rowdata)

View File

@@ -39,6 +39,11 @@
<h1>Interactive Plot</h1>
<p>This chart shows the <a href="http://www.concept2.com/indoor-rowers/racing/records/world">Indoor Rower World Records</a> for your gender and
weight class. The red dots are the official records, and hovering
over them with your mouse shows you the name of the record holder.
The blue line is a fit to the data, which is used by rowsandall.com
to calculate your performance assessment.</a>
{{ the_div|safe }}

View File

@@ -91,9 +91,9 @@
<div class="grid_2 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>
<a class="button blue small" href="/rowers/otw-bests">OTW Critical Power</a>
{% else %}
<a class="button blue small" href="/rowers/promembership">OTW Ranking Pieces</a>
<a class="button blue small" href="/rowers/promembership">OTW Critical Power</a>
{% endif %}
</p>
<p>
@@ -129,9 +129,9 @@
<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/ote-ranking">OTE Ranking Pieces</a>
<a class="button blue small" href="/rowers/ote-ranking">OTE Critical Power</a>
{% else %}
<a class="button blue small" href="/rowers/promembership">OTE Ranking Pieces</a>
<a class="button blue small" href="/rowers/promembership">OTE Critical Power</a>
{% endif %}
</p>
<p>

View File

@@ -4,12 +4,49 @@
{% block title %}Rowsandall Workouts List{% endblock %}
{% block content %}
{% block scripts %}
<script>
setTimeout("location.reload(true);",60000);
</script>
<script
type='text/javascript'
src='https://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js'>
</script>
<script>
$(function() {
$("td.rankingtoggle").click( function() {
var workout_id = $(this).attr('workoutid');
console.log(workout_id);
$.getJSON(window.location.protocol + '//'+window.location.host + '/rowers/workout/'+workout_id+'/toggle-ranking', function(json) {
console.log(JSON.stringify(json));
rankingpiece = json.result;
tdid = "#star"+workout_id;
console.log(rankingpiece);
console.log($(tdid).length);
if (rankingpiece) {
$(tdid).addClass('yellow');
$(tdid).removeClass('notyellow');
$(tdid).html('&starf;');
console.log('adding yellow '+tdid);
} else {
$(tdid).removeClass('yellow');
$(tdid).addClass('notyellow');
$(tdid).html('&star;');
console.log('remove yellow '+tdid);
};
});
});
});
</script>
{% endblock %}
{% block content %}
<style>
#mypointer {
cursor: pointer;
}
</style>
<div class="grid_12">
@@ -51,6 +88,7 @@
<table width="100%" class="listtable shortpadded">
<thead>
<tr>
<th> R</th>
<th style="width:80"> Date</th>
<th> Time</th>
<th> Name</th>
@@ -76,155 +114,166 @@
{% else %}
<tr>
{% endif %}
<td id="mypointer"
class="rankingtoggle" workoutid="{{ workout.id }}">
{% if workout.rankingpiece %}
<span id="star{{ workout.id }}" class="yellow">&starf;</span>
{% else %}
<span id="star{{ workout.id }}" class="notyellow">&star;</span>
{% endif %}
</td>
<td> {{ workout.date|date:"Y-m-d" }} </td>
<td> {{ workout.starttime|date:"H:i" }} </td>
<td>
{% if workout.user.user == user or user == team.manager %}
{% if workout.rankingpiece %}
[RANKING PIECE]
{% endif %}
{% if workout.name != '' %}
<a href={% url rower.defaultlandingpage id=workout.id %}>{{ workout.name }}</a> </td>
{% else %}
<a href={% url rower.defaultlandingpage id=workout.id %}>No Name</a> </td>
{% endif %}
{% else %}
{% if workout.name != '' %}
<a href="/rowers/workout/{{ workout.id }}/">{{ workout.name }}</a> </td>
{% else %}
<a href="/rowers/workout/{{ workout.id }}/">No Name</a> </td>
{% endif %}
{% endif %}
<td> {{ workout.workouttype }} </td>
<td> {{ workout.distance }}m</td>
<td> {{ workout.duration |durationprint:"%H:%M:%S.%f" }} </td>
<td> {{ workout.averagehr }} </td>
<td> {{ workout.maxhr }} </td>
{% if not team %}
<td>
<a class="small" href="/rowers/workout/{{ workout.id }}/export">Export</a>
</td>
<td>
<a href={% url rower.defaultlandingpage id=workout.id %}>
{{ workout.name }}
</a>
</td>
{% else %}
<td>
<a href={% url rower.defaultlandingpage
id=workout.id %}>No Name
</a></td>
{% endif %}
{% else %}
{% if workout.name != '' %}
<td><a href="/rowers/workout/{{ workout.id }}/">{{ workout.name }}</a></td>
{% else %}
<td><a href="/rowers/workout/{{ workout.id }}/">No Name</a> </td>
{% endif %}
{% endif %}
<td> {{ workout.workouttype }} </td>
<td> {{ workout.distance }}m</td>
<td> {{ workout.duration |durationprint:"%H:%M:%S.%f" }} </td>
<td> {{ workout.averagehr }} </td>
<td> {{ workout.maxhr }} </td>
{% if not team %}
<td>
<a class="small" href="/rowers/workout/{{ workout.id }}/export">Export</a>
</td>
{% else %}
<td colspan="2">
{{ workout.user.user.first_name }} {{ workout.user.user.last_name }}
</td>
{% endif %}
<td> <a class="small" href="/rowers/workout/{{ workout.id }}/flexchart">Flex</a> </td>
<td>
<a class="small" href="/rowers/workout/{{ workout.id }}/deleteconfirm">Delete
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<td colspan="2">
{{ workout.user.user.first_name }} {{ workout.user.user.last_name }}
</td>
<p> No workouts found </p>
{% endif %}
<td> <a class="small" href="/rowers/workout/{{ workout.id }}/flexchart">Flex</a> </td>
<td>
<a class="small" href="/rowers/workout/{{ workout.id }}/deleteconfirm">Delete
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p> No workouts found </p>
{% endif %}
</div>
<div class="grid_4 omega">
{% if team %}
<div class="grid_4" id="teambuttons">
<div class="grid_3 alpha">
<p>
&nbsp;
</p>
</div>
</div>
{% endif %}
<div class="grid_4" id="interactiveplot">
<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, true);
};
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>
{{ the_div |safe }}
</div>
<div class="grid_4" id="announcements">
{% if announcements %}
<h3>What's New?</h3>
{% for a in announcements %}
<div class="site-announcement-box">
<div class="site-announcement">
<i>{{ a.created }}:</i>
{{ a.announcement|urlize }}
<div class="grid_4 omega">
{% if team %}
<div class="grid_4" id="teambuttons">
<div class="grid_3 alpha">
<p>
&nbsp;
</p>
</div>
</div>
{% endfor %}
<p>&nbsp;</p>
{% endif %}
</div>
<div class="grid_4" id="about">
<h3>About</h3>
<p>This site is a beta site, pioneering rowing data visualization and analysis. No warranties. The site's author is
Sander Roosendaal. A Masters rower.
<div class="grid_4" id="interactiveplot">
<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>
Read his <a href="http://blog.rowsandall.com/">blog</a>
</p>
<div style="text-align: right; padding: 2em">
<a href="http://blog.rowsandall.com/">
<img src="/static/img/sander.jpg" width="80"></a>
{{ 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, true);
};
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>
{{ the_div |safe }}
</div>
<div class="grid_4" id="announcements">
{% if announcements %}
<h3>What's New?</h3>
{% for a in announcements %}
<div class="site-announcement-box">
<div class="site-announcement">
<i>{{ a.created }}:</i>
{{ a.announcement|urlize }}
</div>
</div>
{% endfor %}
<p>&nbsp;</p>
{% endif %}
</div>
<div class="grid_4" id="about">
<h3>About</h3>
<p>This site is a beta site, pioneering rowing data visualization and analysis. No warranties. The site's author is
Sander Roosendaal. A Masters rower.
Read his <a href="http://blog.rowsandall.com/">blog</a>
</p>
<div style="text-align: right; padding: 2em">
<a href="http://blog.rowsandall.com/">
<img src="/static/img/sander.jpg" width="80"></a>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="grid_6 alpha">
{% if rankingonly and not team %}
<div class="grid_2 prefix_1 alpha">
<a class="button small green" href="/rowers/list-workouts">All Workouts</a>
</div>
{% elif not team %}
<div class="grid_2 prefix_1 alpha">
<a class="button small green" href="/rowers/list-workouts/ranking">Ranking Pieces Only</a>
</div>
{% endif %}
<div class="grid_2 suffix_1 omega">
<a class="button small gray" href="/rowers/workouts-join-select">Glue Workouts</a>
</div>
<p>&nbsp;</p>
<div class="grid_2 prefix_1 alpha">
<a class="button small green" href="/rowers/list-workouts">All Workouts</a>
</div>
{% elif not team %}
<div class="grid_2 prefix_1 alpha">
<a class="button small green" href="/rowers/list-workouts/ranking">Ranking Pieces Only</a>
</div>
{% endif %}
<div class="grid_2 suffix_1 omega">
<a class="button small gray" href="/rowers/workouts-join-select">Glue Workouts</a>
</div>
<p>&nbsp;</p>
{% if team %}
<form id="searchform" action="/rowers/list-workouts/team/{{ team.id }}/{{ startdate|date:"Y-m-d" }}/{{ enddate|date:"Y-m-d" }}"
method="get" accept-charset="utf-8">
{% else %}
<form id="searchform" action="/rowers/list-workouts/{{ startdate|date:"Y-m-d" }}/{{ enddate|date:"Y-m-d" }}"
method="get" accept-charset="utf-8">
{% endif %}
<div class="grid_3 prefix_1 alpha">
<input class="searchfield" id="searchbox" name="q" type="text" placeholder="Search">
</div>
<div class="grid_1 omega">
<button class="button blue small" type="submit">
Search
</button>
</div>
</form>
<form id="searchform" action="/rowers/list-workouts/{{ startdate|date:"Y-m-d" }}/{{ enddate|date:"Y-m-d" }}"
method="get" accept-charset="utf-8">
{% endif %}
<div class="grid_3 prefix_1 alpha">
<input class="searchfield" id="searchbox" name="q" type="text" placeholder="Search">
</div>
<div class="grid_1 omega">
<button class="button blue small" type="submit">
Search
</button>
</div>
</form>
</div>
<div class="grid_2 omega">
<span class="button gray small">
@@ -248,5 +297,5 @@
{% endif %}
{% endif %}
</span>
{% endblock %}
{% endblock %}

View File

@@ -85,9 +85,9 @@
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>Whenever you load or reload the page, a new calculation is started
as a background process. The page will reload automatically when
calculation is ready.</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">

View File

@@ -35,6 +35,10 @@
We use FISA minimum boat weight and standard rigging for our calculations.
</p>
<p>The Quick calculation option potentially speeds up the calculation,
at the cost of a slight reduction in accuracy. It is recommended
to keep this option selected.</p>
<form enctype="multipart/form-data" action="{{ formloc }}" method="post">
{% if form.errors %}
<p style="color: red;">

View File

@@ -5,6 +5,10 @@
{% block title %}Workouts{% endblock %}
{% block scripts %}
{% include "monitorjobs.html" %}
<script type='text/javascript'
src='https://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js'>
</script>
{% endblock %}
{% block content %}
@@ -70,7 +74,9 @@
<a href="/rowers/{{ id }}/ote-bests/{{ startdate|date:"Y-m-d" }}/{{ enddate|date:"Y-m-d" }}">https://rowsandall.com/rowers/{{ id }}/ote-bests/{{ startdate|date:"Y-m-d" }}/{{ enddate|date:"Y-m-d" }}</a>
</p>
<p>The table gives the best efforts achieved on the official Concept2 ranking pieces in the selected date range.</p>
<p>The table gives the best efforts achieved on the
<a href="https://log.concept2.com/rankings">official Concept2 ranking pieces</a> in the selected date range. Also the percentile scores on the
chart are based on the Concept2 rankings.</p>
</div>

View File

@@ -123,6 +123,8 @@ urlpatterns = [
url(r'^imports/$', TemplateView.as_view(template_name='imports.html'), name='imports'),
url(r'^agegroupcp/(?P<age>\d+)$',views.agegroupcpview),
url(r'^agegroupcp/(?P<age>\d+)/(?P<normalize>\d+)$',views.agegroupcpview),
url(r'^ajax_agegroup/(?P<age>\d+)/(?P<weightcategory>\w+.*)/(?P<sex>\w+.*)/(?P<userid>\d+)$',
views.ajax_agegrouprecords),
url(r'^agegrouprecords/(?P<sex>\w+.*)/(?P<weightcategory>\w+.*)/(?P<distance>\d+)m$',
views.agegrouprecordview),
url(r'^agegrouprecords/(?P<sex>\w+.*)/(?P<weightcategory>\w+.*)/(?P<duration>\d+)min$',
@@ -212,6 +214,7 @@ urlpatterns = [
url(r'^graph/(?P<id>\d+)/deleteconfirm$',views.graph_delete_confirm_view),
url(r'^graph/(?P<id>\d+)/delete$',views.graph_delete_view),
url(r'^workout/(?P<id>\d+)/get-thumbnails$',views.get_thumbnails),
url(r'^workout/(?P<id>\d+)/toggle-ranking$',views.workout_toggle_ranking),
url(r'^workout/(?P<id>\d+)/get-testscript$',views.get_testscript),
url(r'^workout/upload/team/$',views.team_workout_upload_view),
url(r'^workout/upload/$',views.workout_upload_view,name='workout_upload_view'),

View File

@@ -5,6 +5,7 @@ import colorsys
from django.conf import settings
import uuid
import datetime
lbstoN = 4.44822
@@ -127,6 +128,22 @@ palettes = {
'yellow_red':trcolors(255,255,178,189,0,39)
}
rankingdistances = [100,500,1000,2000,5000,6000,10000,21097,42195,100000]
rankingdurations = []
rankingdurations.append(datetime.time(minute=1))
rankingdurations.append(datetime.time(minute=4))
rankingdurations.append(datetime.time(minute=30))
rankingdurations.append(datetime.time(hour=1,minute=15))
rankingdurations.append(datetime.time(hour=1))
def is_ranking_piece(workout):
if workout.distance in rankingdistances:
return True
elif workout.duration in rankingdurations:
return True
return False
def range_to_color_hex(groupcols,palette='monochrome_blue'):

View File

@@ -56,6 +56,7 @@ from rowers.models import (
RowerPowerZonesForm,AccountRowerForm,UserForm,StrokeData,
Team,TeamForm,TeamInviteForm,TeamInvite,TeamRequest,
WorkoutComment,WorkoutCommentForm,RowerExportForm,
CalcAgePerformance
)
from rowers.models import FavoriteForm,BaseFavoriteFormSet,SiteAnnouncement
from rowers.metrics import rowingmetrics,defaultfavoritecharts
@@ -108,7 +109,7 @@ from rowers.tasks import (
handle_sendemail_unrecognized,handle_sendemailnewcomment,
handle_sendemailnewresponse, handle_updatedps,
handle_updatecp,long_test_task,long_test_task2,
handle_zip_file
handle_zip_file,handle_getagegrouprecords
)
from scipy.signal import savgol_filter
@@ -284,6 +285,7 @@ verbose_job_status = {
'updatecp': 'Critical Power Calculation for Ergometer Workouts',
'updatecpwater': 'Critical Power Calculation for OTW Workouts',
'otwsetpower': 'Rowing Physics OTW Power Calculation',
'agegrouprecords': 'Calculate age group records',
'make_plot': 'Create static chart',
'long_test_task': 'Long Test Task',
'long_test_task2': 'Long Test Task 2',
@@ -711,7 +713,8 @@ def splitstdata(lijst):
from utils import (
geo_distance,serialize_list,deserialize_list,uniqify,
str2bool,range_to_color_hex,absolute,myqueue,get_call,
calculate_age
calculate_age,rankingdistances,rankingdurations,
is_ranking_piece
)
import datautils
@@ -2956,9 +2959,15 @@ def histo(request,theuser=0,
if 'options' in request.session:
options = request.session['options']
workouttypes = options['workouttypes']
includereststrokes = options['includereststrokes']
waterboattype = options['waterboattype']
try:
workouttypes = options['workouttypes']
includereststrokes = options['includereststrokes']
waterboattype = options['waterboattype']
except KeyError:
workouttypes = ['water','rower','dynamic','slides']
waterboattype = ['1x','2x','2-','4x','4-','8+']
includereststrokes = False
workstrokesonly = not includereststrokes
@@ -3161,6 +3170,7 @@ def addmanual_view(request):
)
print duration,'aap'
id,message = dataprep.create_row_df(r,
distance,
duration,startdatetime,
@@ -3175,7 +3185,7 @@ def addmanual_view(request):
if id:
w = Workout.objects.get(id=id)
w.rankingpiece = rankingpiece
w.rankingpiece = rankingpiece or is_ranking_piece(w)
w.notes = notes
w.save()
messages.info(request,'New workout created')
@@ -3297,29 +3307,22 @@ def rankings_view(request,theuser=0,
enddate = datetime.datetime.combine(enddate,datetime.time(23,59,59))
enddate = enddate+datetime.timedelta(days=1)
rankingdistances = [100,500,1000,2000,5000,6000,10000,21097,42195,100000]
rankingdurations = []
rankingdurations.append(datetime.time(minute=1))
rankingdurations.append(datetime.time(minute=4))
rankingdurations.append(datetime.time(minute=30))
rankingdurations.append(datetime.time(hour=1,minute=15))
rankingdurations.append(datetime.time(hour=1))
thedistances = []
theworkouts = []
thesecs = []
rankingdistances.sort()
rankingdurations.sort()
for rankingdistance in rankingdistances:
workouts = Workout.objects.filter(user=r,distance=rankingdistance,
workouttype__in=['rower','dynamic','slides'],
startdatetime__gte=startdate,
startdatetime__lte=enddate).order_by('duration')
workouts = Workout.objects.filter(
user=r,distance=rankingdistance,
workouttype__in=['rower','dynamic','slides'],
rankingpiece=True,
startdatetime__gte=startdate,
startdatetime__lte=enddate
).order_by('duration')
if workouts:
thedistances.append(rankingdistance)
theworkouts.append(workouts[0])
@@ -3333,10 +3336,13 @@ def rankings_view(request,theuser=0,
for rankingduration in rankingdurations:
workouts = Workout.objects.filter(user=r,duration=rankingduration,
workouttype='rower',
startdatetime__gte=startdate,
startdatetime__lte=enddate).order_by('-distance')
workouts = Workout.objects.filter(
user=r,duration=rankingduration,
workouttype='rower',
rankingpiece=True,
startdatetime__gte=startdate,
startdatetime__lte=enddate
).order_by('-distance')
if workouts:
thedistances.append(workouts[0].distance)
theworkouts.append(workouts[0])
@@ -3511,6 +3517,43 @@ def rankings_view(request,theuser=0,
'teams':get_my_teams(request.user),
})
@login_required()
def ajax_agegrouprecords(request,
age=25,
sex='female',
weightcategory='hwt',
userid=0):
wcdurations = []
wcpower = []
durations = [1,4,30,60]
distances = [100,500,1000,2000,5000,6000,10000,21097,42195]
df = pd.DataFrame(
list(
C2WorldClassAgePerformance.objects.filter(
sex=sex,
weightcategory=weightcategory
).values()
)
)
jsondf = df.to_json()
job = myqueue(queue,
handle_getagegrouprecords,
jsondf,distances,durations,age,sex,weightcategory,
)
return JSONResponse(
{
'job':job.id
}
)
# Show ranking distances including predicted paces
@login_required()
def rankings_view2(request,theuser=0,
@@ -3536,48 +3579,78 @@ def rankings_view2(request,theuser=0,
if theuser == 0:
theuser = request.user.id
else:
lastupdated = "01-01-1900"
promember=0
if not request.user.is_anonymous():
r = getrower(request.user)
r = getrower(theuser)
wcdurations = []
wcpower = []
lastupdated = "01-01-1900"
userid = 0
if 'options' in request.session:
options = request.session['options']
try:
wcdurations = options['wcdurations']
wcpower = options['wcpower']
lastupdated = options['lastupdated']
except KeyError:
pass
try:
userid = options['userid']
except KeyError:
userid = 0
else:
options = {}
lastupdatedtime = arrow.get(lastupdated).timestamp
current_time = arrow.utcnow().timestamp
deltatime_seconds = current_time - lastupdatedtime
recalc = False
if str(userid) != str(theuser) or deltatime_seconds > 3600:
recalc = True
options['lastupdated'] = arrow.utcnow().isoformat()
else:
recalc = False
options['userid'] = theuser
if r.birthdate:
age = calculate_age(r.birthdate)
durations = [1,4,30,60]
distances = [100,500,1000,2000,5000,6000,10000,21097,42195]
for distance in distances:
worldclasspower = metrics.getagegrouprecord(
age,
sex=r.sex,
distance=distance,
weightcategory=r.weightcategory
)
velo = (worldclasspower/2.8)**(1./3.)
try:
duration = distance/velo
wcdurations.append(duration)
wcpower.append(worldclasspower)
except ZeroDivisionError:
pass
for duration in durations:
worldclasspower = metrics.getagegrouprecord(
age,
sex=r.sex,
duration=duration,
weightcategory=r.weightcategory
)
try:
velo = (worldclasspower/2.8)**(1./3.)
distance = int(60*duration*velo)
wcdurations.append(60.*duration)
wcpower.append(worldclasspower)
except ValueError:
pass
age = calculate_age(r.birthdate)
else:
worldclasspower = None
age = 0
agerecords = CalcAgePerformance.objects.filter(
age = age,
sex = r.sex,
weightcategory = r.weightcategory)
print len(agerecords),'aap'
if len(agerecords) == 0:
recalc = True
wcpower = []
wcduration = []
else:
wcdurations = []
wcpower = []
for record in agerecords:
wcdurations.append(record.duration)
wcpower.append(record.power)
options['wcpower'] = wcpower
options['wcdurations'] = wcdurations
if theuser:
options['userid'] = theuser
request.session['options'] = options
result = request.user.is_authenticated() and ispromember(request.user)
if result:
@@ -3641,13 +3714,6 @@ def rankings_view2(request,theuser=0,
enddate = datetime.datetime.combine(enddate,datetime.time(23,59,59))
enddate = enddate+datetime.timedelta(days=1)
rankingdistances = [100,500,1000,2000,5000,6000,10000,21097,42195,100000]
rankingdurations = []
rankingdurations.append(datetime.time(minute=1))
rankingdurations.append(datetime.time(minute=4))
rankingdurations.append(datetime.time(minute=30))
rankingdurations.append(datetime.time(hour=1,minute=15))
rankingdurations.append(datetime.time(hour=1))
thedistances = []
theworkouts = []
@@ -3660,10 +3726,12 @@ def rankings_view2(request,theuser=0,
for rankingdistance in rankingdistances:
workouts = Workout.objects.filter(user=r,distance=rankingdistance,
workouttype__in=['rower','dynamic','slides'],
startdatetime__gte=startdate,
startdatetime__lte=enddate).order_by('duration')
workouts = Workout.objects.filter(
user=r,distance=rankingdistance,
workouttype__in=['rower','dynamic','slides'],
rankingpiece=True,
startdatetime__gte=startdate,
startdatetime__lte=enddate).order_by('duration')
if workouts:
thedistances.append(rankingdistance)
theworkouts.append(workouts[0])
@@ -3677,10 +3745,12 @@ def rankings_view2(request,theuser=0,
for rankingduration in rankingdurations:
workouts = Workout.objects.filter(user=r,duration=rankingduration,
workouttype='rower',
startdatetime__gte=startdate,
startdatetime__lte=enddate).order_by('-distance')
workouts = Workout.objects.filter(
user=r,duration=rankingduration,
workouttype='rower',
rankingpiece=True,
startdatetime__gte=startdate,
startdatetime__lte=enddate).order_by('-distance')
if workouts:
thedistances.append(workouts[0].distance)
theworkouts.append(workouts[0])
@@ -3841,6 +3911,32 @@ def rankings_view2(request,theuser=0,
'power':int(pwr)}
cpredictions.append(a)
if recalc:
wcdurations = []
wcpower = []
durations = [1,4,30,60]
distances = [100,500,1000,2000,5000,6000,10000,21097,42195]
df = pd.DataFrame(
list(
C2WorldClassAgePerformance.objects.filter(
sex=r.sex,
weightcategory=r.weightcategory
).values()
)
)
jsondf = df.to_json()
job = myqueue(queue,
handle_getagegrouprecords,
jsondf,distances,durations,age,r.sex,r.weightcategory)
try:
request.session['async_tasks'] += [(job.id,'agegrouprecords')]
except KeyError:
request.session['async_tasks'] = [(job.id,'agegrouprecords')]
messages.error(request,message)
return render(request, 'rankings.html',
@@ -3857,6 +3953,7 @@ def rankings_view2(request,theuser=0,
'theuser':uu,
'age':age,
'sex':r.sex,
'recalc':recalc,
'weightcategory':r.weightcategory,
'startdate':startdate,
'enddate':enddate,
@@ -3983,12 +4080,6 @@ def otwrankings_view(request,theuser=0,
enddate = enddate+datetime.timedelta(days=1)
rankingdurations = []
rankingdurations.append(datetime.time(minute=1))
rankingdurations.append(datetime.time(minute=4))
rankingdurations.append(datetime.time(minute=30))
rankingdurations.append(datetime.time(hour=1))
rankingdurations.append(datetime.time(hour=1,minute=15))
thedistances = []
theworkouts = []
@@ -4238,14 +4329,6 @@ def oterankings_view(request,theuser=0,
enddate = enddate+datetime.timedelta(days=1)
rankingdurations = []
rankingdurations.append(datetime.time(minute=1))
rankingdurations.append(datetime.time(minute=4))
rankingdurations.append(datetime.time(minute=30))
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 = []
@@ -5712,15 +5795,25 @@ def boxplot_view_data(request,userid=0,
if 'options' in request.session:
options = request.session['options']
includereststrokes = options['includereststrokes']
spmmin = options['spmmin']
spmmax = options['spmmax']
workmin = options['workmin']
workmax = options['workmax']
ids = options['ids']
userid = options['userid']
plotfield = options['plotfield']
try:
includereststrokes = options['includereststrokes']
spmmin = options['spmmin']
spmmax = options['spmmax']
workmin = options['workmin']
workmax = options['workmax']
ids = options['ids']
userid = options['userid']
plotfield = options['plotfield']
except KeyError:
includereststrokes = False
spmmin = 15
spmmax = 55
workmin = 0
workmax = 55
ids = []
userid = 0
plotfield = 'spm'
workstrokesonly = not includereststrokes
@@ -5792,8 +5885,13 @@ def boxplot_view(request,userid=0,
if 'options' in request.session:
options = request.session['options']
try:
includereststrokes = options['includereststrokes']
except KeyError:
includereststrokes = False
options['includereststrokes'] = False
includereststrokes = options['includereststrokes']
workstrokesonly = not includereststrokes
if userid==0:
@@ -5912,7 +6010,6 @@ def workouts_view(request,message='',successmessage='',
except ValueError:
activity_enddate = enddate
if teamid:
try:
theteam = Team.objects.get(id=teamid)
@@ -5979,7 +6076,7 @@ def workouts_view(request,message='',successmessage='',
stack='rower'
else:
stack='type'
script,div = interactive_activitychart(g_workouts,
activity_startdate,
activity_enddate,
@@ -6734,12 +6831,14 @@ def workout_otwsetpower_view(request,id=0,message="",successmessage=""):
form = AdvancedWorkoutForm(request.POST)
if form.is_valid():
quick_calc = form.cleaned_data['quick_calc']
boattype = form.cleaned_data['boattype']
weightvalue = form.cleaned_data['weightvalue']
row.boattype = boattype
row.weightvalue = weightvalue
row.save()
# load row data & create power/wind/bearing columns if not set
f1 = row.csvfilename
rowdata = rdata(f1)
@@ -6776,7 +6875,9 @@ def workout_otwsetpower_view(request,id=0,message="",successmessage=""):
weightvalue,
first_name,last_name,emailaddress,id,
ps=[r.p0,r.p1,r.p2,r.p3],
ratio=r.cpratio)
ratio=r.cpratio,
quick_calc = quick_calc,
)
try:
request.session['async_tasks'] += [(job.id,'otwsetpower')]
@@ -7312,7 +7413,7 @@ def workout_stats_view(request,id=0,message="",successmessage=""):
# Normalized power & TSS
duration = datadf['time'].max()-datadf['time'].min()
duration /= 1.0e3
pwr4 = datadf['power']**(4)
pwr4 = datadf['power']**(4.0)
normp = (pwr4.mean())**(0.25)
if not np.isnan(normp):
ftp = float(r.ftp)
@@ -9304,6 +9405,35 @@ def workout_getc2workout_view(request,c2id):
url = reverse(workout_c2import_view)
return HttpResponseRedirect(url)
@login_required
def workout_toggle_ranking(request,id=0):
is_ajax = False
if request.is_ajax():
is_ajax = True
try:
# check if valid ID exists (workout exists)
row = Workout.objects.get(id=id)
except Workout.DoesNotExist:
raise Http404("Workout doesn't exist")
if not checkworkoutuser(request.user,row):
message = "You are not allowed to change this workout"
messages.error(request,message)
# we are still here - we own the workout
row.rankingpiece = not row.rankingpiece
row.save()
if is_ajax:
return JSONResponse({'result':row.rankingpiece})
else:
url = reverse(workouts_view)
response = HttpResponseRedirect(url)
return response
# This is the main view for processing uploaded files
@login_required()
def workout_upload_view(request,