diff --git a/rowers/forms.py b/rowers/forms.py
index 30c6836b..760b536c 100644
--- a/rowers/forms.py
+++ b/rowers/forms.py
@@ -721,6 +721,17 @@ class HistoForm(forms.Form):
histoparam = forms.ChoiceField(choices=parchoices,initial='power',
label='Metric')
+class AnalysisOptionsForm(forms.Form):
+ modality = forms.ChoiceField(choices=workouttypes,
+ label='Workout Type',
+ initial='all')
+ waterboattype = forms.MultipleChoiceField(choices=boattypes,
+ label='Water Boat Type',
+ initial = mytypes.waterboattype)
+ rankingonly = forms.BooleanField(initial=False,
+ label='Only Ranking Pieces',
+ required=False)
+
# form to select modality and boat type for trend flex
class TrendFlexModalForm(forms.Form):
@@ -809,7 +820,30 @@ class PlannedSessionMultipleCloneForm(forms.Form):
label='Planned Sessions'
)
+analysischoices = (
+ ('boxplot','Box Chart'),
+ ('trendflex','Trend Flex'),
+ )
+
+class AnalysisChoiceForm(forms.Form):
+ function = forms.ChoiceField(choices=analysischoices,initial='boxplot',
+ label='Analysis')
+ yparam = forms.ChoiceField(choices=parchoices,initial='spm',
+ label='Metric')
+ spmmin = forms.FloatField(initial=15,
+ required=False,label = 'Min SPM')
+ spmmax = forms.FloatField(initial=55,
+ required=False,label = 'Max SPM')
+ workmin = forms.FloatField(initial=0,
+ required=False,label = 'Min Work per Stroke')
+ workmax = forms.FloatField(initial=1500,
+ required=False,label = 'Max Work per Stroke')
+ includereststrokes = forms.BooleanField(initial=False,
+ required=False,
+ label='Include Rest Strokes')
+
+
class BoxPlotChoiceForm(forms.Form):
yparam = forms.ChoiceField(choices=parchoices,initial='spm',
label='Metric')
diff --git a/rowers/templates/user_analysis_select.html b/rowers/templates/user_analysis_select.html
new file mode 100644
index 00000000..8c35838d
--- /dev/null
+++ b/rowers/templates/user_analysis_select.html
@@ -0,0 +1,158 @@
+{% extends "newbase.html" %}
+{% load staticfiles %}
+{% load rowerfilters %}
+
+{% block title %}Workouts{% endblock %}
+
+{% block main %}
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+ {{ the_div|safe }}
+
+
+ -
+
You can use the date and search forms to search through all
+ workouts from this team.
+ TIP: Agree with your team members to put tags (e.g. '8x500m') in the notes section of
+ your workouts. That makes it easy to search.
+
+ -
+
+
+ -
+
Select two or more workouts, set your plot settings below,
+ and press submit
+
+ {% csrf_token %}
+
+ {{ chartform.as_table }}
+
+
+ -
+
+
+
+
+
+{% endblock %}
+
+{% block scripts %}
+{% if request.method == 'POST' %}
+
+
+
+
+{% endif %}
+
+{% endblock %}
+
+{% block sidebar %}
+{% include 'menu_analytics.html' %}
+{% endblock %}
diff --git a/rowers/urls.py b/rowers/urls.py
index f4d1ace5..22847ffa 100644
--- a/rowers/urls.py
+++ b/rowers/urls.py
@@ -223,6 +223,10 @@ urlpatterns = [
re_path(r'^workouts-join-select/user/(?P\d+)/$',views.workouts_join_select,name='workouts_join_select'),
re_path(r'^user-boxplot-select/user/(?P\d+)/$',views.user_boxplot_select,name='user_boxplot_select'),
re_path(r'^user-boxplot-select/$',views.user_boxplot_select,name='user_boxplot_select'),
+ re_path(r'^user-analysis-select/(?P\w.*)/user/(?P\d+)/$',views.analysis_new,name='analysis_new'),
+ re_path(r'^user-analysis-select/(?P\w.*)/$',views.analysis_new,name='analysis_new'),
+ re_path(r'^user-analysis-select/user/(?P\d+)/$',views.analysis_new,name='analysis_new'),
+ re_path(r'^user-analysis-select/$',views.analysis_new,name='analysis_new'),
# re_path(r'^user-multiflex-select/user/(?P\d+)/(?P\d+-\d+-\d+)/(?P\d+-\d+-\d+)/$',views.user_multiflex_select,name='user_multiflex_select'),
re_path(r'^user-multiflex-select/user/(?P\d+)/$',views.user_multiflex_select,name='user_multiflex_select'),
# re_path(r'^user-multiflex-select/(?P\d+-\d+-\d+)/(?P\d+-\d+-\d+)/$',views.user_multiflex_select,name='user_multiflex_select'),
@@ -259,6 +263,7 @@ urlpatterns = [
# re_path(r'^flexall/(?P\w+.*)/(?P\w+.*)/(?P\w+.*)/(?P\d+-\d+-\d+)/(?P\d+-\d+-\d+)/user/(?P\d+)/$',views.cum_flex,name='cum_flex'),
# re_path(r'^flexall/(?P\w+.*)/(?P\w+.*)/(?P\w+.*)/(?P\d+-\d+-\d+)/(?P\d+-\d+-\d+)/$',views.cum_flex,name='cum_flex'),
re_path(r'^flexall/(?P\w+.*)/(?P\w+.*)/(?P\w+.*)/$',views.cum_flex,name='cum_flex'),
+ re_path(r'^analysisdata/$',views.analysis_view_data,name='analysis_view_data'),
re_path(r'^flexall/user/(?P\d+)/$',views.cum_flex,name='cum_flex'),
re_path(r'^flexall/$',views.cum_flex,name='cum_flex'),
re_path(r'^flexalldata/$',views.cum_flex_data,name='cum_flex_data'),
diff --git a/rowers/views/analysisviews.py b/rowers/views/analysisviews.py
index 7a44188d..b1f4272e 100644
--- a/rowers/views/analysisviews.py
+++ b/rowers/views/analysisviews.py
@@ -5,6 +5,341 @@ from __future__ import unicode_literals
from __future__ import unicode_literals, absolute_import
from rowers.views.statements import *
+# generic Analysis view -
+
+defaultoptions = {
+ 'includereststrokes': False,
+ 'workouttypes':['rower','dynamic','slides'],
+ 'waterboattype': mytypes.waterboattype,
+ 'rankingonly': False,
+ 'function':'boxplot'
+}
+
+
+@user_passes_test(ispromember, login_url="/rowers/paidplans",
+ message="This functionality requires a Pro plan or higher",
+ redirect_field_name=None)
+def analysis_new(request,userid=0,function='boxplot'):
+ r = getrequestrower(request, userid=userid)
+ user = r.user
+ userid = user.id
+
+ if 'options' in request.session:
+ options = request.session['options']
+ else:
+ options=defaultoptions
+
+ try:
+ workouttypes = options['workouttypes']
+ except KeyError:
+ workouttypes = ['rower','dynamic','slides']
+
+ try:
+ rankingonly = options['rankingonly']
+ except KeyError:
+ rankingonly = False
+
+ try:
+ includereststrokes = options['includereststrokes']
+ except KeyError:
+ includereststrokes = False
+
+ if 'startdate' in request.session:
+ startdate = iso8601.parse_date(request.session['startdate'])
+
+
+ if 'enddate' in request.session:
+ enddate = iso8601.parse_date(request.session['enddate'])
+
+ workstrokesonly = not includereststrokes
+
+ waterboattype = mytypes.waterboattype
+
+ if request.method == 'POST':
+ dateform = DateRangeForm(request.POST)
+ if dateform.is_valid():
+ startdate = dateform.cleaned_data['startdate']
+ enddate = dateform.cleaned_data['enddate']
+ startdatestring = startdate.strftime('%Y-%m-%d')
+ enddatestring = enddate.strftime('%Y-%m-%d')
+ request.session['startdate'] = startdatestring
+ request.session['enddate'] = enddatestring
+ optionsform = AnalysisOptionsForm(request.POST)
+ if optionsform.is_valid():
+ for key, value in optionsform.cleaned_data.items():
+ options[key] = value
+
+ modality = optionsform.cleaned_data['modality']
+ waterboattype = optionsform.cleaned_data['waterboattype']
+ if modality == 'all':
+ modalities = [m[0] for m in mytypes.workouttypes]
+ else:
+ modalities = [modality]
+ if modality != 'water':
+ waterboattype = [b[0] for b in mytypes.boattypes]
+
+
+ if 'rankingonly' in optionsform.cleaned_data:
+ rankingonly = optionsform.cleaned_data['rankingonly']
+ else:
+ rankingonly = False
+
+ options['modalities'] = modalities
+ options['waterboattype'] = waterboattype
+
+ chartform = AnalysisChoiceForm(request.POST)
+ if chartform.is_valid():
+ for key, value in chartform.cleaned_data.items():
+ options[key] = value
+
+ form = WorkoutMultipleCompareForm(request.POST)
+ if form.is_valid():
+ cd = form.cleaned_data
+ selectedworkouts = cd['workouts']
+ ids = [int(w.id) for w in selectedworkouts]
+ options['ids'] = ids
+ else:
+ ids = []
+ options['ids'] = ids
+ else:
+ dateform = DateRangeForm(initial={
+ 'startdate':startdate,
+ 'enddate':enddate,
+ })
+
+ if 'modalities' in request.session:
+ modalities = request.session['modalities']
+ if len(modalities) > 1:
+ modality = 'all'
+ else:
+ modality = modalities[0]
+ else:
+ modalities = [m[0] for m in mytypes.workouttypes]
+ modality = 'all'
+
+
+
+
+ negtypes = []
+ for b in mytypes.boattypes:
+ if b[0] not in waterboattype:
+ negtypes.append(b[0])
+
+
+ startdate = datetime.datetime.combine(startdate,datetime.time())
+ enddate = datetime.datetime.combine(enddate,datetime.time(23,59,59))
+
+ if enddate < startdate:
+ s = enddate
+ enddate = startdate
+ startdate = s
+
+ negtypes = []
+ for b in mytypes.boattypes:
+ if b[0] not in waterboattype:
+ negtypes.append(b[0])
+
+
+ workouts = Workout.objects.filter(user=r,
+ startdatetime__gte=startdate,
+ startdatetime__lte=enddate,
+ workouttype__in=modalities,
+ ).order_by(
+ "-date", "-starttime"
+ ).exclude(boattype__in=negtypes)
+ if rankingonly:
+ workouts = workouts.exclude(rankingpiece=False)
+
+ query = request.GET.get('q')
+ if query:
+ query_list = query.split()
+ workouts = workouts.filter(
+ reduce(operator.and_,
+ (Q(name__icontains=q) for q in query_list)) |
+ reduce(operator.and_,
+ (Q(notes__icontains=q) for q in query_list))
+ )
+ searchform = SearchForm(initial={'q':query})
+ else:
+ searchform = SearchForm()
+
+ if request.method != 'POST':
+ form = WorkoutMultipleCompareForm()
+ chartform = AnalysisChoiceForm()
+ selectedworkouts = Workout.objects.none()
+ else:
+ selectedworkouts = Workout.objects.filter(id__in=ids)
+
+ form.fields["workouts"].queryset = workouts | selectedworkouts
+
+
+ optionsform = AnalysisOptionsForm(initial={
+ 'modality':modality,
+ 'waterboattype':waterboattype,
+ 'rankingonly':rankingonly,
+ })
+
+
+
+ startdatestring = startdate.strftime('%Y-%m-%d')
+ enddatestring = enddate.strftime('%Y-%m-%d')
+ request.session['startdate'] = startdatestring
+ request.session['enddate'] = enddatestring
+ request.session['options'] = options
+
+
+ breadcrumbs = [
+ {
+ 'url':'/rowers/analysis',
+ 'name':'Analysis'
+ },
+ {
+ 'url':reverse('analysis_new',kwargs={'userid':userid}),
+ 'name': 'Analysis Select'
+ },
+ ]
+ return render(request, 'user_analysis_select.html',
+ {'workouts': workouts,
+ 'dateform':dateform,
+ 'startdate':startdate,
+ 'enddate':enddate,
+ 'rower':r,
+ 'breadcrumbs':breadcrumbs,
+ 'theuser':user,
+ 'form':form,
+ 'active':'nav-analysis',
+ 'chartform':chartform,
+ 'searchform':searchform,
+ 'optionsform':optionsform,
+ 'teams':get_my_teams(request.user),
+ })
+
+
+def boxplotdata(workouts,options):
+ 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']
+ function = options['function']
+ except KeyError:
+ includereststrokes = False
+ spmmin = 15
+ spmmax = 55
+ workmin = 0
+ workmax = 55
+ ids = []
+ userid = 0
+ plotfield = 'spm'
+ function = 'boxplot'
+
+
+ workstrokesonly = not includereststrokes
+ labeldict = {
+ int(w.id): w.__str__() for w in workouts
+ }
+
+
+ datemapping = {
+ w.id:w.date for w in workouts
+ }
+
+
+
+ fieldlist,fielddict = dataprep.getstatsfields()
+ fieldlist = [plotfield,'workoutid','spm','driveenergy',
+ 'workoutstate']
+
+ ids = [w.id for w in workouts]
+
+ # prepare data frame
+ datadf,extracols = dataprep.read_cols_df_sql(ids,fieldlist)
+
+
+
+ datadf = dataprep.clean_df_stats(datadf,workstrokesonly=workstrokesonly)
+
+ datadf = dataprep.filter_df(datadf,'spm',spmmin,
+ largerthan=True)
+ datadf = dataprep.filter_df(datadf,'spm',spmmax,
+ largerthan=False)
+ datadf = dataprep.filter_df(datadf,'driveenergy',workmin,
+ largerthan=True)
+ datadf = dataprep.filter_df(datadf,'driveneergy',workmax,
+ largerthan=False)
+
+ datadf.dropna(axis=0,how='any',inplace=True)
+
+
+ datadf['workoutid'].replace(datemapping,inplace=True)
+ datadf.rename(columns={"workoutid":"date"},inplace=True)
+ datadf = datadf.sort_values(['date'])
+
+ if userid == 0:
+ extratitle = ''
+ else:
+ u = User.objects.get(id=userid)
+ extratitle = ' '+u.first_name+' '+u.last_name
+
+
+
+ script,div = interactive_boxchart(datadf,plotfield,
+ extratitle=extratitle,
+ spmmin=spmmin,spmmax=spmmax,workmin=workmin,workmax=workmax)
+
+ scripta = script.split('\n')[2:-1]
+ script = ''.join(scripta)
+
+ return(script,div)
+
+@user_passes_test(ispromember,login_url="/rowers/paidplans",
+ message="This functionality requires a Pro plan or higher",
+ redirect_field_name=None)
+def analysis_view_data(request,userid=0):
+
+ if 'options' in request.session:
+ options = request.session['options']
+ else:
+ options = defaultoptions
+
+
+ if userid==0:
+ userid = request.user.id
+
+ workouts = []
+
+ ids = options['ids']
+ function = options['function']
+
+ if not ids:
+ return JSONResponse({
+ "script":'',
+ "div":'No data found'
+ })
+
+ for id in ids:
+ try:
+ workouts.append(Workout.objects.get(id=id))
+ except Workout.DoesNotExist:
+ pass
+
+ if function == 'boxplot':
+ script, div = boxplotdata(workouts,options)
+ else:
+ script = ''
+ div = 'Unknown analysis functions'
+
+
+ return JSONResponse({
+ "script":script,
+ "div":div,
+ })
+
+
# Histogram for a date/time range
@user_passes_test(ispromember,login_url="/rowers/paidplans",
message="This functionality requires a Pro plan or higher",
@@ -2769,7 +3104,7 @@ def multiflex_view(request,userid=0,
options['spmmax'] = spmmax
options['workmin'] = workmin
options['workmax'] = workmax
- options['ids'] = ids
+ options['idso'] = ids
request.session['options'] = options
diff --git a/rowers/views/statements.py b/rowers/views/statements.py
index 40087500..420b560c 100644
--- a/rowers/views/statements.py
+++ b/rowers/views/statements.py
@@ -80,6 +80,7 @@ from rowers.forms import (
UpdateStreamForm,WorkoutMultipleCompareForm,ChartParamChoiceForm,
FusionMetricChoiceForm,BoxPlotChoiceForm,MultiFlexChoiceForm,
TrendFlexModalForm,WorkoutSplitForm,WorkoutJoinParamForm,
+ AnalysisOptionsForm, AnalysisChoiceForm,
PlannedSessionMultipleCloneForm,SessionDateShiftForm,
)
from rowers.models import (