diff --git a/rowers/datautils.py b/rowers/datautils.py
index 8c7727c7..8489af1f 100644
--- a/rowers/datautils.py
+++ b/rowers/datautils.py
@@ -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
diff --git a/rowers/forms.py b/rowers/forms.py
index 76d00219..2661e1a3 100644
--- a/rowers/forms.py
+++ b/rowers/forms.py
@@ -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'),
diff --git a/rowers/interactiveplots.py b/rowers/interactiveplots.py
index c4a92124..d8d0c035 100644
--- a/rowers/interactiveplots.py
+++ b/rowers/interactiveplots.py
@@ -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,' 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
diff --git a/rowers/plannedsessions.py b/rowers/plannedsessions.py
index 60e82e56..e57d9d2d 100644
--- a/rowers/plannedsessions.py
+++ b/rowers/plannedsessions.py
@@ -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],
diff --git a/rowers/tasks.py b/rowers/tasks.py
index 38df2741..427ce4b9 100644
--- a/rowers/tasks.py
+++ b/rowers/tasks.py
@@ -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()))
diff --git a/rowers/templates/summary_edit.html b/rowers/templates/summary_edit.html
index fb021a59..ee84aaa6 100644
--- a/rowers/templates/summary_edit.html
+++ b/rowers/templates/summary_edit.html
@@ -41,24 +41,79 @@
title="Jump to following workout">Next
{% endif %}
+
+
+
Edit Workout Interval Data
+
+ 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.
+
-
-
+
+ Updated Summary
+
+
+
+ {{ intervalstats }}
+
+
+
+
+
+
+
+ {{ interactiveplot |safe }}
+
+ {{ the_div |safe }}
+
+
+ Feeling lucky?
+ 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.
+
+
+
+
Interval Shorthand
See the how-to at the bottom of this page for details on how to use this form.
@@ -70,6 +125,8 @@
{% csrf_token %}
+
+
Intervals by Power/Pace
With this form, you can specify a power or pace level. Everything faster/harder than the
@@ -92,40 +149,6 @@
-
-
-
-
- {{ interactiveplot |safe }}
-
- {{ the_div |safe }}
-
-
- Updated Summary
-
-
-
- {{ intervalstats }}
-
-
-
Interval Shorthand How-To
This is a quick way to enter the intervals using a special mini-language.
diff --git a/rowers/templates/trainingzones.html b/rowers/templates/trainingzones.html
new file mode 100644
index 00000000..1bbe2149
--- /dev/null
+++ b/rowers/templates/trainingzones.html
@@ -0,0 +1,69 @@
+{% extends "newbase.html" %}
+{% load static %}
+{% load rowerfilters %}
+
+{% block title %}Rowsandall Training Plans{% endblock %}
+
+
+{% block main %}
+
+
+
+
+
+Training Zones
+
+
+
+
+
+
+
+
+
+
+
+ {{ the_div|safe }}
+
+
+
+
+
+
+
+
+
+
+
+{% endblock %}
+
+{% block scripts %}
+
+
+
+{% endblock %}
+
+{% block sidebar %}
+{% include 'menu_analytics.html' %}
+{% endblock %}
diff --git a/rowers/tests/test_analysis.py b/rowers/tests/test_analysis.py
index 2d880509..f12a469b 100644
--- a/rowers/tests/test_analysis.py
+++ b/rowers/tests/test_analysis.py
@@ -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,
diff --git a/rowers/tests/test_aworkouts.py b/rowers/tests/test_aworkouts.py
index 95c46902..f145156f 100644
--- a/rowers/tests/test_aworkouts.py
+++ b/rowers/tests/test_aworkouts.py
@@ -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',
}
diff --git a/rowers/tests/test_imports.py b/rowers/tests/test_imports.py
index 6c5b7f90..d81d07e7 100644
--- a/rowers/tests/test_imports.py
+++ b/rowers/tests/test_imports.py
@@ -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()
diff --git a/rowers/tests/test_unit_tests.py b/rowers/tests/test_unit_tests.py
index 9fd5c5a1..0bf42abb 100644
--- a/rowers/tests/test_unit_tests.py
+++ b/rowers/tests/test_unit_tests.py
@@ -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
@@ -239,6 +240,7 @@ class PlannedSessionTests(TestCase):
monday = sundays[i]-datetime.timedelta(days=6)
if monday < cycle.startdate:
monday = cycle.startdate
+
nextsunday = sundays[i]
diff --git a/rowers/urls.py b/rowers/urls.py
index 82a7cc1b..2b5d1cf2 100644
--- a/rowers/urls.py
+++ b/rowers/urls.py
@@ -352,6 +352,10 @@ urlpatterns = [
re_path(r'^performancemanager/$',views.performancemanager_view,name='performancemanager_view'),
re_path(r'^performancemanager/user/(?P\d+)/$',views.performancemanager_view,name='performancemanager_view'),
re_path(r'^performancemanager/user/(?P\d+)/(?P\w+.*)/$',views.performancemanager_view,name='performancemanager_view'),
+ re_path(r'^trainingzones/$',views.trainingzones_view,name='trainingzones_view'),
+ re_path(r'^trainingzones/user/(?P\d+)/$',views.trainingzones_view,name='trainingzones_view'),
+ re_path(r'^trainingzones/user/(?P\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\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'),
diff --git a/rowers/views/analysisviews.py b/rowers/views/analysisviews.py
index 365bf5dd..6fea37d8 100644
--- a/rowers/views/analysisviews.py
+++ b/rowers/views/analysisviews.py
@@ -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",
diff --git a/rowers/views/statements.py b/rowers/views/statements.py
index 5d9daa8d..b3d39580 100644
--- a/rowers/views/statements.py
+++ b/rowers/views/statements.py
@@ -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
diff --git a/rowers/views/workoutviews.py b/rowers/views/workoutviews.py
index fb740432..34b50c72 100644
--- a/rowers/views/workoutviews.py
+++ b/rowers/views/workoutviews.py
@@ -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)
@@ -4913,7 +4915,7 @@ def workout_upload_api(request):
w.timezone = timezone
w.save()
-
+
if make_plot: # pragma: no cover
@@ -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)