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.

    +
  • +
  • +
    + {{ searchform }} + +
    +
    + + {% if workouts %} + + Toggle All
    + + {{ form.as_table }} +
    + {% else %} +

    No workouts found

    + {% endif %} +
  • +
  • +

    Select two or more workouts, set your plot settings below, + and press submit +

    + {% csrf_token %} + + {{ chartform.as_table }} +
    +
  • +
  • + + + {{ dateform.as_table }} +
    + + {{ optionsform.as_table }} +
    + {% csrf_token %} + + +
  • +
+ + +{% 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 (