Private
Public Access
1
0

Merge branch 'feature/seilerchart' into develop

This commit is contained in:
Sander Roosendaal
2021-05-31 11:09:09 +02:00
12 changed files with 547 additions and 5 deletions
+18
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'),
+318 -2
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
+4
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],
+69
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 %}
+38
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,
+7
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',
}
+2
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,
Binary file not shown.
+4
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'),
+83
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",
+2 -1
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
+1 -1
View File
@@ -6171,7 +6171,7 @@ def workout_summary_edit_view(request,id,message="",successmessage=""
try:
rowdata.updateinterval_string(s)
except:
except: # pragma: no cover
messages.error(request,"Nope, you were not lucky")
intervalstats = rowdata.allstats()