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/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 }} +
    +
  • +
  • +

    +

    + {% csrf_token %} + + {{ form.as_table }} +
    + +
    +

    +
  • +
+ + + +{% 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_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/tests/testdata/testdata.tcx.gz b/rowers/tests/testdata/testdata.tcx.gz deleted file mode 100644 index 887f897c..00000000 Binary files a/rowers/tests/testdata/testdata.tcx.gz and /dev/null differ 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 4fb21bef..34b50c72 100644 --- a/rowers/views/workoutviews.py +++ b/rowers/views/workoutviews.py @@ -4915,7 +4915,7 @@ def workout_upload_api(request): w.timezone = timezone w.save() - + if make_plot: # pragma: no cover @@ -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()