Private
Public Access
1
0

Merge branch 'release/v16.4.0'

This commit is contained in:
Sander Roosendaal
2021-05-31 11:15:37 +02:00
15 changed files with 680 additions and 62 deletions

View File

@@ -468,7 +468,6 @@ def getfastest(df,thevalue,mode='distance'):
starttime = griddata(restime,starttimes,[thevalue*60*1000],method='linear',rescale=True)
duration = griddata(restime,restime,[thevalue*60*1000],method='linear',rescale=True)
endtime = starttime+duration
print(distance,starttime,endtime )
return distance[0],starttime[0]/1000.,endtime[0]/1000.
return 0 # pragma: no cover

View File

@@ -58,6 +58,24 @@ class FlexibleDecimalField(forms.DecimalField):
value = value.replace('.', '').replace(',', '.')
return super(FlexibleDecimalField, self).to_python(value)
class TrainingZonesForm(forms.Form):
zoneschoices = (
('power','Power Zones'),
('hr','Heart Rate Zones')
)
zones = forms.ChoiceField(initial='hr',label='Training Zones',choices=zoneschoices)
startdate = forms.DateField(
initial=timezone.now()-datetime.timedelta(days=365),
widget=AdminDateWidget(), #format='%Y-%m-%d'),
label='Start Date')
enddate = forms.DateField(
initial=timezone.now(),
# widget=SelectDateWidget(years=range(1990,2050)),
widget=AdminDateWidget(), #format='%Y-%m-%d'),
label='End Date')
class InstantPlanSelectForm(forms.Form):
datechoices = (
('startdate','start date'),

View File

@@ -715,13 +715,14 @@ def interactive_activitychart2(workouts,startdate,enddate,stack='type',toolbar_l
dd = w.date.strftime('%m/%d')
dd2 = w.date.strftime('%Y/%m/%d')
dd3 = w.date.strftime('%Y/%m')
du = w.duration.hour*60+w.duration.minute
trimp = w.trimp
rscore = w.rscore
if rscore == 0: # pragma: no cover
rscore = w.hrtss
if totaldays<30: # pragma: no cover
if totaldays<=30: # pragma: no cover
dates.append(dd)
dates_sorting.append(dd2)
else:
@@ -759,7 +760,7 @@ def interactive_activitychart2(workouts,startdate,enddate,stack='type',toolbar_l
dd = d.strftime('%d')
if totaldays<30:
if totaldays<=30:
dates.append(d.strftime('%m/%d'))
dates_sorting.append(d.strftime('%Y/%m/%d'))
else:
@@ -6557,3 +6558,318 @@ def interactive_otw_advanced_pace_chart(id=0,promember=0):
div = ''
return [script,div]
def get_zones_report(rower,startdate,enddate,trainingzones='hr'):
duration = enddate-startdate
totaldays = duration.total_seconds()/(24*3600)
dates = []
dates_sorting = []
minutes = []
hours = []
zones = []
workouts = Workout.objects.filter(
user=rower,
startdatetime__gte=startdate,
startdatetime__lte=enddate,
duplicate=False,
).order_by("-startdatetime")
ids = [w.id for w in workouts]
columns = ['workoutid','hr','power','time']
df = dataprep.getsmallrowdata_db(columns,ids=ids)
try:
df['deltat'] = df['time'].diff().clip(lower=0)
except KeyError: # pragma: no cover
pass
df = dataprep.clean_df_stats(df,workstrokesonly=False,
ignoreadvanced=True,ignorehr=False)
#totalmeters,totalhours, totalminutes, totalseconds = get_totals(workouts)
hrzones = rower.hrzones
powerzones = rower.powerzones
for w in workouts:
dd = w.date.strftime('%m/%d')
dd2 = w.date.strftime('%Y/%m/%d')
dd3 = w.date.strftime('%Y/%m')
dd4 = arrow.get(w.date).isocalendar()[1]
#print(w.date,arrow.get(w.date),arrow.get(w.date).isocalendar())
qryw = 'workoutid == {workoutid}'.format(workoutid=w.id)
qry = 'hr < {ut2}'.format(ut2=rower.ut2)
if trainingzones == 'power':
qry = 'power < {ut2}'.format(ut2=rower.pw_ut2)
timeinzone = df.query(qry).query(qryw)['deltat'].sum()/(60*1e3)
if totaldays<=30:
dates.append(dd)
dates_sorting.append(dd2)
elif totaldays<=121: # pragma: no cover
dates.append(dd4)
dates_sorting.append(dd4)
else: # pragma: no cover
dates.append(dd3)
dates_sorting.append(dd3)
minutes.append(timeinzone)
hours.append(timeinzone/60.)
if trainingzones == 'hr':
zones.append('<{ut2}'.format(ut2=hrzones[1]))
else:
zones.append('<{ut2}'.format(ut2=powerzones[1]))
#print(w,dd,timeinzone,'<UT2')
qry = '{ut2} <= hr < {ut1}'.format(ut1=rower.ut1,ut2=rower.ut2)
if trainingzones == 'power':
qry = '{ut2} <= power < {ut2}'.format(ut1=rower.pw_ut1,ut2=rower.pw_ut2)
timeinzone = df.query(qry).query(qryw)['deltat'].sum()/(60*1e3)
if totaldays<=30:
dates.append(dd)
dates_sorting.append(dd2)
elif totaldays<=121: # pragma: no cover
dates.append(dd4)
dates_sorting.append(dd4)
else: # pragma: no cover
dates.append(dd3)
dates_sorting.append(dd3)
minutes.append(timeinzone)
hours.append(timeinzone/60.)
if trainingzones == 'hr':
zones.append(hrzones[1])
else:
zones.append(powerzones[1])
#print(w,dd,timeinzone,'UT2')
qry = '{ut1} <= hr < {at}'.format(ut1=rower.ut1,at=rower.at)
if trainingzones == 'power':
qry = '{ut1} <= power < {at}'.format(ut1=rower.pw_ut1,at=rower.pw_at)
timeinzone = df.query(qry).query(qryw)['deltat'].sum()/(60*1e3)
if totaldays<=30:
dates.append(dd)
dates_sorting.append(dd2)
elif totaldays<=121: # pragma: no cover
dates.append(dd4)
dates_sorting.append(dd4)
else: # pragma: no cover
dates.append(dd3)
dates_sorting.append(dd3)
minutes.append(timeinzone)
hours.append(timeinzone/60.)
if trainingzones == 'hr':
zones.append(hrzones[2])
else:
zones.append(powerzones[2])
#print(w,dd,timeinzone,'UT1')
qry = '{at} <= hr < {tr}'.format(at=rower.at,tr=rower.tr)
if trainingzones == 'power':
qry = '{at} <= power < {tr}'.format(at=rower.pw_at,tr=rower.pw_tr)
timeinzone = df.query(qry).query(qryw)['deltat'].sum()/(60*1e3)
if totaldays<=30:
dates.append(dd)
dates_sorting.append(dd2)
elif totaldays<=121: # pragma: no cover
dates.append(dd4)
dates_sorting.append(dd4)
else: # pragma: no cover
dates.append(dd3)
dates_sorting.append(dd3)
minutes.append(timeinzone)
hours.append(timeinzone/60.)
if trainingzones == 'hr':
zones.append(hrzones[3])
else:
zones.append(powerzones[3])
#print(w,dd,timeinzone,'AT')
qry = '{tr} <= hr < {an}'.format(tr=rower.tr,an=rower.an)
if trainingzones == 'power':
qry = '{tr} <= power < {an}'.format(tr=rower.pw_tr,an=rower.pw_an)
timeinzone = df.query(qry).query(qryw)['deltat'].sum()/(60*1e3)
if totaldays<=30:
dates.append(dd)
dates_sorting.append(dd2)
elif totaldays<=121: # pragma: no cover
dates.append(dd4)
dates_sorting.append(dd4)
else: # pragma: no cover
dates.append(dd3)
dates_sorting.append(dd3)
minutes.append(timeinzone)
hours.append(timeinzone/60.)
if trainingzones == 'hr':
zones.append(hrzones[4])
else:
zones.append(powerzones[4])
#print(w,dd,timeinzone,'TR')
qry = 'hr >= {an}'.format(an=rower.an)
if trainingzones == 'power':
qry = 'power >= {an}'.format(an=rower.pw_an)
timeinzone = df.query(qry).query(qryw)['deltat'].sum()/(60*1e3)
if totaldays<=30:
dates.append(dd)
dates_sorting.append(dd2)
elif totaldays<=121: # pragma: no cover
dates.append(dd4)
dates_sorting.append(dd4)
else: # pragma: no cover
dates.append(dd3)
dates_sorting.append(dd3)
minutes.append(timeinzone)
hours.append(timeinzone/60.)
if trainingzones == 'hr':
zones.append(hrzones[5])
else:
zones.append(powerzones[5])
#print(w,dd,timeinzone,'AN')
try:
d = utc.localize(startdate)
except (ValueError,AttributeError): # pragma: no cover
d = startdate
try:
enddate = utc.localize(enddate)
except (ValueError,AttributeError): # pragma: no cover
pass
while d<=enddate:
dd = d.strftime('%d')
if totaldays <= 30:
dates.append(d.strftime('%m/%d'))
dates_sorting.append(d.strftime('%Y/%m/%d'))
elif totaldays<=121: # pragma: no cover
dd4 = arrow.get(d).isocalendar()[1]
dates.append(dd4)
dates_sorting.append(dd4)
else:
dates.append(d.strftime('%Y/%m'))
dates_sorting.append(d.strftime('%Y/%m'))
minutes.append(0)
hours.append(0)
if trainingzones == 'hr':
zones.append(hrzones[1])
else:
zones.append(powerzones[1])
d += datetime.timedelta(days=1)
# this should be renamed with rower zones
data = {
'date':dates,
'date_sorting':dates_sorting,
'minutes': minutes,
'zones':zones,
'hours':hours,
}
#print(pd.DataFrame(data).head())
return data
def interactive_zoneschart(rower,data,startdate,enddate,trainingzones='hr'):
duration = enddate-startdate
totaldays = duration.total_seconds()/(24*3600)
colors = ['gray','yellow','lime','blue','purple','red']
hrzones = rower.hrzones
powerzones = rower.powerzones
color_map = {
'<{ut2}'.format(ut2=hrzones[1]):'gray',
hrzones[1]:'lime',
hrzones[2]:'yellow',
hrzones[3]:'blue',
hrzones[4]:'purple',
hrzones[5]:'red',
}
if trainingzones == 'power':
color_map = {
'<{ut2}'.format(ut2=powerzones[1]):'gray',
powerzones[1]:'lime',
powerzones[2]:'yellow',
powerzones[3]:'blue',
powerzones[4]:'purple',
powerzones[5]:'red',
}
zones_order = [
'<{ut2}'.format(ut2=hrzones[1]),
hrzones[1],
hrzones[2],
hrzones[3],
hrzones[4],
hrzones[5]
]
if trainingzones == 'power':
zones_order = [
'<{ut2}'.format(ut2=powerzones[1]),
powerzones[1],
powerzones[2],
powerzones[3],
powerzones[4],
powerzones[5]
]
df = pd.DataFrame(data)
if totaldays > 30:
df.drop('minutes',inplace=True,axis='columns')
else:
df.drop('hours',inplace=True,axis='columns')
source = ColumnDataSource(df)
df.sort_values('date_sorting',inplace=True)
df.drop('date_sorting',inplace=True,axis='columns')
hv.extension('bokeh')
bars = hv.Bars(df, kdims = ['date','zones']).aggregate(function=np.sum).redim.values(zones=zones_order)
#bars = table.to.bars(['date','zones'],['minutes'])
bars.opts(
opts.Bars(cmap=color_map,show_legend=True,stacked=True,
tools=['tap','hover'],width=550,padding=(0,(0,.1)),
legend_position='bottom',
show_frame=False)
)
p = hv.render(bars)
p.title.text = 'Activity {d1} to {d2} for {r}'.format(
d1 = startdate.strftime("%Y-%m-%d"),
d2 = enddate.strftime("%Y-%m-%d"),
r = str(rower),
)
if totaldays >= 30:
p.xaxis.axis_label = 'Week'
if totaldays >= 121:
p.xaxis.axis_label = 'Month'
p.plot_width=550
p.plot_height=350
p.toolbar_location = 'above'
p.y_range.start = 0
p.sizing_mode = 'stretch_both'
script,div = components(p)
return script,div

View File

@@ -359,6 +359,8 @@ def get_todays_micro(plan,thedate=date.today()):
enddate__gte = thedate
)
if thismacro:
thismeso = TrainingMesoCycle.objects.filter(
plan=thismacro[0],
@@ -366,6 +368,8 @@ def get_todays_micro(plan,thedate=date.today()):
enddate__gte = thedate
)
if thismeso:
thismicro = TrainingMicroCycle.objects.filter(
plan=thismeso[0],

View File

@@ -2806,7 +2806,7 @@ def handle_update_wps(rid,types,ids,mode,debug=False,**kwargs):
return 0
try:
wps_median = int(df.loc[mask,'driveenergy'].median())
except ValueError:
except ValueError: # pragma: no cover
return 0
if mode == 'water':
@@ -3029,7 +3029,7 @@ def df_from_summary(data):
weightclass = data['weight_class']
try:
title = data['name']
except KeyError:
except KeyError: # pragma: no cover
title = ""
try:
t = data['comments'].split('\n', 1)[0]
@@ -3043,13 +3043,22 @@ def df_from_summary(data):
startdatetime,starttime,workoutdate,duration,starttimeunix,timezone = utils.get_startdatetime_from_c2data(data)
splits = data['workout']['splits']
try:
splits = data['workout']['splits']
except KeyError:
splits = [0]
time = starttimeunix
elapsed_distance = 0
times = [0]
distances = [0]
spms = [splits[0]['stroke_rate']]
hrs = [splits[0]['heart_rate']['average']]
try:
spms = [splits[0]['stroke_rate']]
except KeyError:
spms = [0]
try:
hrs = [splits[0]['heart_rate']['average']]
except KeyError: # pragma: no cover
hrs = [0]
for split in splits:
time += split['time']/10.
@@ -3057,7 +3066,10 @@ def df_from_summary(data):
times.append(time)
distances.append(elapsed_distance)
spms.append(split['stroke_rate'])
hrs.append(split['heart_rate']['average'])
try:
hrs.append(split['heart_rate']['average'])
except KeyError: # pragma: no cover
hrs.append(0)
df = pd.DataFrame({
'TimeStamp (sec)': times,
@@ -3090,7 +3102,7 @@ def handle_c2_async_workout(alldata,userid,c2token,c2id,delaysec,defaulttimezone
try:
has_strokedata = data['stroke_data']
except KeyError:
except KeyError: # pragma: no cover
has_strokedata = True
@@ -3146,7 +3158,7 @@ def handle_c2_async_workout(alldata,userid,c2token,c2id,delaysec,defaulttimezone
dologging('debuglog.log',s.text)
has_strokedata = False
if not has_strokedata:
if not has_strokedata: # pragma: no cover
df = df_from_summary(data)
else:
dologging('debuglog.log',json.dumps(s.json()))

View File

@@ -41,24 +41,79 @@
title="Jump to following workout"><em>Next</em></a>
{% endif %}
</p>
<p>
<table width=100%>
<tr>
<th>Name</th><td>{{ workout.name }}</td>
</tr><tr>
<th>Distance:</th><td>{{ workout.distance }}m</td>
</tr><tr>
<th>Duration:</th><td>{{ workout.duration |durationprint:"%H:%M:%S.%f" }}</td>
</tr><tr>
<th>Public link to this workout</th>
<td>
<a href="/rowers/workout/{{ workout.id|encode }}/">https://rowsandall.com/rowers/workout/{{ workout.id|encode }}</a>
<td>
</tr>
</table>
</p>
<h1>Edit Workout Interval Data</h1>
<p>
Use "Interval Shorthand", "Feeling lucky?", or "Intervals by power/pace" methods below to edit work and rest
intervals. To save your work, press the Save button above the updated summary.
</p>
<ul class="main-content">
<li class="grid_2">
<table width=100%>
<tr>
<th>Name</th><td>{{ workout.name }}</td>
</tr><tr>
<th>Distance:</th><td>{{ workout.distance }}m</td>
</tr><tr>
<th>Duration:</th><td>{{ workout.duration |durationprint:"%H:%M:%S.%f" }}</td>
</tr><tr>
<th>Public link to this workout</th>
<td>
<a href="/rowers/workout/{{ workout.id|encode }}/">https://rowsandall.com/rowers/workout/{{ workout.id|encode }}</a>
<td>
</tr>
</table>
<li class="grid_2">
<h1>Updated Summary</h1>
<form enctype="multipart/form-data" action="/rowers/workout/{{ workout.id|encode }}/editintervals/" method="post">
{% csrf_token %}
<input type="hidden" name="{{ savebutton }}" value="{{ intervalstring }}">
<input type="hidden" name="nrintervals" value={{ nrintervals }}>
{% for key,value in formvalues.items %}
<input type="hidden" name="{{ key }}" value="{{ value|safe }}">
{% endfor %}
<p>
<input type="submit" value="Save">
</p>
<span>
<a href="">Reset to last saved</a>
&nbsp;
<a href="/rowers/workout/{{ workout.id|encode }}/restore/">Restore Original data</a>
</span>
</form>
<p>
<pre>
{{ intervalstats }}
</pre>
</p>
</li>
<li class="grid_2">
<script src="https://cdn.pydata.org/bokeh/release/bokeh-2.2.3.min.js"></script>
<script async="true" type="text/javascript">
Bokeh.set_log_level("info");
</script>
{{ interactiveplot |safe }}
{{ the_div |safe }}
</li>
<li class="grid_2">
<h1>Feeling lucky?</h1>
<p>This new, experimental feature tries to find intervals by looking for patterns in the data. It does not
autodetect rest and work intervals yet. The resulting "interval string" is copied to the Interval Shorthand
section below, where you can edit it.
<form enctype="multipart/form-data" method="post">
<input type="hidden" name="ruptures" value="ruptures">
<table width=100%>
{{ ruptureform.as_table }}
</table>
{% csrf_token %}
<input class="button" type="submit" value="I'm feeling lucky">
</form>
</p>
</li>
<li class="grid_2">
<h1>Interval Shorthand</h1>
<p>
See the how-to <a href="#howto">at the bottom of this page</a> for details on how to use this form.
@@ -70,6 +125,8 @@
{% csrf_token %}
<input class="button" type="submit" value="Update">
</form>
</li>
<li class="grid_2">
<h1>Intervals by Power/Pace</h1>
<p>With this form, you can specify a power or pace level. Everything faster/harder than the
@@ -92,40 +149,6 @@
<input class="button" type="submit" value="Submit">
</form>
</li>
<li class="grid_2">
<script src="https://cdn.pydata.org/bokeh/release/bokeh-2.2.3.min.js"></script>
<script async="true" type="text/javascript">
Bokeh.set_log_level("info");
</script>
{{ interactiveplot |safe }}
{{ the_div |safe }}
</li>
<li class="grid_2">
<h1>Updated Summary</h1>
<form enctype="multipart/form-data" action="/rowers/workout/{{ workout.id|encode }}/editintervals/" method="post">
{% csrf_token %}
<input type="hidden" name="{{ savebutton }}" value="{{ intervalstring }}">
<input type="hidden" name="nrintervals" value={{ nrintervals }}>
{% for key,value in formvalues.items %}
<input type="hidden" name="{{ key }}" value="{{ value|safe }}">
{% endfor %}
<p>
<input type="submit" value="Save">
</p>
<span>
<a href="">Reset to last saved</a>
&nbsp;
<a href="/rowers/workout/{{ workout.id|encode }}/restore/">Restore Original data</a>
</span>
</form>
<p>
<pre>
{{ intervalstats }}
</pre>
</p>
</li>
<li class="grid_2">
<h1 id="howto">Interval Shorthand How-To</h1>
<p>This is a quick way to enter the intervals using a special mini-language.</p>

View File

@@ -0,0 +1,69 @@
{% extends "newbase.html" %}
{% load static %}
{% load rowerfilters %}
{% block title %}Rowsandall Training Plans{% endblock %}
{% block main %}
<script src="https://cdn.pydata.org/bokeh/release/bokeh-2.2.3.min.js"></script>
<script src="https://cdn.pydata.org/bokeh/release/bokeh-widgets-2.2.3.min.js"></script>
<script async="true" type="text/javascript">
Bokeh.set_log_level("info");
</script>
<h1>Training Zones</h1>
<div id="id_script">
</div>
<ul class="main-content">
<li class="grid_4">
<div id="id_chart">
{{ the_div|safe }}
</div>
</li>
<li class="grid_4">
<p>
<form method="post">
{% csrf_token %}
<table>
{{ form.as_table }}
</table>
<input class="button" type="submit" value="Submit">
</form>
</p>
</li>
</ul>
{% endblock %}
{% block scripts %}
<script type='text/javascript'
src='https://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js'>
</script>
<script>
$(function($) {
console.log('loading script for chart');
$.getJSON(window.location.protocol + '//'+window.location.host + '/rowers/trainingzones/user/{{ rower.user.id }}/data/?startdate={{ startdate|date:"Y-m-d" }}&enddate={{ enddate|date:"Y-m-d" }}&zones={{ zones }}', function(json) {
var script = json.script;
var div = json.div;
$("#id_sitready").remove();
$("#id_chart").append(div);
$("#id_script").append(script);
})
});
</script>
{% endblock %}
{% block sidebar %}
{% include 'menu_analytics.html' %}
{% endblock %}

View File

@@ -1107,6 +1107,44 @@ class MarkerPerformanceTest(TestCase):
self.assertRedirects(response, expected_url=expected_url, status_code=302,target_status_code=200)
@patch('rowers.dataprep.getsmallrowdata_db', side_effect=mocked_getsmallrowdata_uh)
def test_trainingzones_view(self,mocked_getsmallrowdata_db):
login = self.c.login(username=self.u.username,password=self.password)
self.assertTrue(login)
startdate = (self.user_workouts[0].startdatetime-datetime.timedelta(days=3)).date()
enddate = (self.user_workouts[0].startdatetime+datetime.timedelta(days=3)).date()
zones = 'hr'
url = reverse('trainingzones_view')
response = self.c.get(url)
self.assertEqual(response.status_code,200)
url = reverse('trainingzones_view_data')
response = self.c.get(url)
self.assertEqual(response.status_code,200)
url += '?startdate={startdate}&enddate={enddate}&zones={zones}'.format(
startdate = startdate.strftime("%Y-%m-%d"),
enddate = enddate.strftime("%Y-%m-%d"),
zones=zones,
)
response = self.c.get(url)
self.assertEqual(response.status_code,200)
zones = 'power'
url = reverse('trainingzones_view_data')
url += '?startdate={startdate}&enddate={enddate}&zones={zones}'.format(
startdate = startdate.strftime("%Y-%m-%d"),
enddate = enddate.strftime("%Y-%m-%d"),
zones=zones,
)
response = self.c.get(url)
self.assertEqual(response.status_code,200)
@patch('rowers.dataprep.create_engine')
@patch('rowers.dataprep.getsmallrowdata_db')
def test_performancemanager_view(self, mocked_sqlalchemy,

View File

@@ -820,6 +820,13 @@ class WorkoutViewTest(TestCase):
response = self.c.get(url)
self.assertEqual(response.status_code,200)
form_data = {
'ruptures':'ruptures',
}
response = self.c.post(url,form_data)
self.assertEqual(response.status_code,200)
form_data = {
'intervalstring':'4x2min/1min',
}

View File

@@ -21,6 +21,7 @@ import json
import rowers.utils as utils
from django.db import transaction
import rowers.garmin_stuff as gs
@@ -342,6 +343,9 @@ class C2Objects(DjangoTestCase):
self.assertEqual(str(timezone),'America/Los_Angeles')
df = tasks.df_from_summary(data)
self.assertEqual(len(df),8)
got = arrow.get(startdatetime).isoformat()
want = arrow.get('2021-05-23 09:11:37.100000-07:00').isoformat()

View File

@@ -232,6 +232,7 @@ class PlannedSessionTests(TestCase):
elif not sundays:
sundays = [cycle.enddate]
for i in range(len(sundays)):
if i==0:
monday = cycle.startdate
@@ -240,6 +241,7 @@ class PlannedSessionTests(TestCase):
if monday < cycle.startdate:
monday = cycle.startdate
nextsunday = sundays[i]
micro = TrainingMicroCycle(startdate=monday,

View File

@@ -352,6 +352,10 @@ urlpatterns = [
re_path(r'^performancemanager/$',views.performancemanager_view,name='performancemanager_view'),
re_path(r'^performancemanager/user/(?P<userid>\d+)/$',views.performancemanager_view,name='performancemanager_view'),
re_path(r'^performancemanager/user/(?P<userid>\d+)/(?P<mode>\w+.*)/$',views.performancemanager_view,name='performancemanager_view'),
re_path(r'^trainingzones/$',views.trainingzones_view,name='trainingzones_view'),
re_path(r'^trainingzones/user/(?P<userid>\d+)/$',views.trainingzones_view,name='trainingzones_view'),
re_path(r'^trainingzones/user/(?P<userid>\d+)/data/$',views.trainingzones_view_data,name="trainingzones_view_data"),
re_path(r'^trainingzones/data/$',views.trainingzones_view_data,name="trainingzones_view_data"),
re_path(r'^ote-bests2/user/(?P<userid>\d+)/$',views.rankings_view2,name='rankings_view2'),
re_path(r'^ote-bests2/$',views.rankings_view2,name='rankings_view2'),
re_path(r'^analysisdata/$',views.analysis_view_data,name='analysis_view_data'),

View File

@@ -1049,6 +1049,89 @@ def goldmedalscores_view(request,userid=0,
'bestworkouts':bestworkouts,
})
@user_passes_test(ispromember, login_url="/rowers/paidplans",
message="This functionality requires a Pro plan or higher. If you are already a Pro user, please log in to access this functionality",
redirect_field_name=None)
@permission_required('rower.is_coach',fn=get_user_by_userid,raise_exception=True)
def trainingzones_view(request,userid=0,mode='rower',
startdate=timezone.now()-timezone.timedelta(days=365),
enddate=timezone.now()):
is_ajax = request_is_ajax(request)
r = getrequestrower(request,userid=userid)
enddate = timezone.now()
startdate = enddate-datetime.timedelta(days=365)
form = TrainingZonesForm()
zones = 'hr'
if request.method == 'POST': # pragma: no cover
form = TrainingZonesForm(request.POST)
if form.is_valid():
startdate = form.cleaned_data['startdate']
enddate = form.cleaned_data['enddate']
zones = form.cleaned_data['zones']
script = ''
div = get_call()
breadcrumbs = [
{
'url':'/rowers/analysis',
'name':'Analysis'
},
{
'url':reverse('trainingzones_view'),
'name': 'Training Zones'
}
]
return render(request,'trainingzones.html',
{
'active':'nav-analysis',
'breadcrumbs':breadcrumbs,
'rower':r,
'the_script':script,
'the_div':div,
'form':form,
'startdate':startdate,
'enddate':enddate,
'zones':zones,
}
)
@login_required()
def trainingzones_view_data(request,userid=0):
r = getrequestrower(request,userid=userid)
startdate = timezone.now()-datetime.timedelta(days=365)
enddate = timezone.now()
zones = 'hr'
if request.GET.get('zones'):
zones = request.GET.get('zones')
if request.GET.get('startdate'):
startdate = datetime.datetime.strptime(request.GET.get('startdate'),"%Y-%m-%d")
startdate = arrow.get(startdate).datetime
if request.GET.get('enddate'):
enddate = datetime.datetime.strptime(request.GET.get('enddate'),"%Y-%m-%d")
enddate = arrow.get(enddate).datetime
data = get_zones_report(r,startdate,enddate,trainingzones=zones)
script, div = interactive_zoneschart(r,data,startdate,enddate,trainingzones=zones)
return JSONResponse({
'script': script,
'div': div,
})
@user_passes_test(ispromember, login_url="/rowers/paidplans",
message="This functionality requires a Pro plan or higher. If you are already a Pro user, please log in to access this functionality",

View File

@@ -77,7 +77,8 @@ from rowers.forms import (
VideoAnalysisCreateForm,WorkoutSingleSelectForm,
VideoAnalysisMetricsForm,SurveyForm,HistorySelectForm,
StravaChartForm,FitnessFitForm,PerformanceManagerForm,
TrainingPlanBillingForm,InstantPlanSelectForm
TrainingPlanBillingForm,InstantPlanSelectForm,
TrainingZonesForm,
)
from django.urls import reverse, reverse_lazy

View File

@@ -18,6 +18,8 @@ from rowers.utils import intervals_to_string
from urllib.parse import urlparse, parse_qs
from json.decoder import JSONDecodeError
import ruptures as rpt
def default(o): # pragma: no cover
if isinstance(o, numpy.int64): return int(o)
if isinstance(o, numpy.int32): return int(o)
@@ -6144,6 +6146,42 @@ def workout_summary_edit_view(request,id,message="",successmessage=""
powerupdateform = PowerIntervalUpdateForm(initial=data)
# feeling lucky / ruptures
if request.method == 'POST' and "ruptures" in request.POST:
df = pd.DataFrame({
'spm':rowdata.df[' Cadence (stokes/min)'],
'power':rowdata.df[' Power (watts)'],
'v':rowdata.df[' AverageBoatSpeed (m/s)']
})
algo = rpt.Pelt(model="rbf").fit(df.values)
result = algo.predict(pen=10)
df['time'] = rowdata.df['TimeStamp (sec)'].values
timeprev = int(df['time'].values[0])
timenext = int(df['time'].values[result[0]])
s = '{delta}sec'.format(delta=timenext-timeprev)
for i in range(len(result)-1):
timeprev = int(df['time'].values[result[i]-1])
timenext = int(df['time'].values[result[i+1]-1])
interval = '+{delta}sec'.format(delta=timenext-timeprev)
s += interval
try:
rowdata.updateinterval_string(s)
except: # pragma: no cover
messages.error(request,"Nope, you were not lucky")
intervalstats = rowdata.allstats()
itime, idist, itype = rowdata.intervalstats_values()
nrintervals = len(idist)
savebutton = 'savestringform'
intervalString = s
form = SummaryStringForm(initial={'intervalstring':intervalString})
# We have submitted the mini language interpreter
if request.method == 'POST' and "intervalstring" in request.POST:
form = SummaryStringForm(request.POST)