diff --git a/rowers/c2stuff.py b/rowers/c2stuff.py index 2a2021e5..91752ca1 100644 --- a/rowers/c2stuff.py +++ b/rowers/c2stuff.py @@ -575,7 +575,7 @@ def workout_c2_upload(user,w): c2id = s['data']['id'] w.uploadedtoc2 = c2id w.save() - message = "" + message = "Upload to Concept2 was successful" except: message = "Something went wrong in workout_c2_upload_view. Response code 200/201 but C2 sync failed: "+response.text c2id = 0 diff --git a/rowers/dataprep.py b/rowers/dataprep.py index a9ad37f1..cb226c5f 100644 --- a/rowers/dataprep.py +++ b/rowers/dataprep.py @@ -970,8 +970,13 @@ def read_cols_df_sql(ids,columns): connection = engine.raw_connection() df = pd.read_sql_query(query,engine) df = df.fillna(value=0) + try: df['peakforce'] = df['peakforce']*lbstoN + except KeyError: + pass + + try: df['averageforce'] = df['averageforce']*lbstoN except KeyError: pass @@ -990,6 +995,10 @@ def read_df_sql(id): df = df.fillna(value=0) try: df['peakforce'] = df['peakforce']*lbstoN + except KeyError: + pass + + try: df['averageforce'] = df['averageforce']*lbstoN except KeyError: pass @@ -1037,6 +1046,10 @@ def smalldataprep(therows,xparam,yparam1,yparam2): try: df['peakforce'] = df['peakforce']*lbstoN + except KeyError: + pass + + try: df['averageforce'] = df['averageforce']*lbstoN except KeyError: pass diff --git a/rowers/forms.py b/rowers/forms.py index d5a1715d..69b6870f 100644 --- a/rowers/forms.py +++ b/rowers/forms.py @@ -297,6 +297,9 @@ formaxlabels = axlabels.copy() formaxlabels.pop('None') parchoices = list(sorted(formaxlabels.items(), key = lambda x:x[1])) +class BoxPlotChoiceForm(forms.Form): + yparam = forms.ChoiceField(choices=parchoices,initial='spm', + label='Metric') class ChartParamChoiceForm(forms.Form): plotchoices = ( diff --git a/rowers/interactiveplots.py b/rowers/interactiveplots.py index b13841e0..aa9f45c1 100644 --- a/rowers/interactiveplots.py +++ b/rowers/interactiveplots.py @@ -108,6 +108,14 @@ def interactive_boxchart(datadf,fieldname): months=["%d %B %Y"], years=["%d %B %Y"], ) + + if fieldname == 'pace': + plot.yaxis[0].formatter = DatetimeTickFormatter( + seconds = ["%S"], + minutes = ["%M"] + ) + + plot.xaxis.major_label_orientation = pi/4 script, div = components(plot) diff --git a/rowers/models.py b/rowers/models.py index b618a24c..fa0c839d 100644 --- a/rowers/models.py +++ b/rowers/models.py @@ -433,12 +433,14 @@ class Workout(models.Model): ownerfirst = self.user.user.first_name ownerlast = self.user.user.last_name duration = self.duration + workouttype = self.workouttype - stri = u'{d} {n} {dist}m {duration:%H:%M:%S} {ownerfirst} {ownerlast}'.format( + stri = u'{d} {n} {dist}m {duration:%H:%M:%S} {workouttype} {ownerfirst} {ownerlast}'.format( d = date.strftime('%Y-%m-%d'), n = name, dist = distance, duration = duration, + workouttype = workouttype, ownerfirst = ownerfirst, ownerlast = ownerlast, ) diff --git a/rowers/sporttracksstuff.py b/rowers/sporttracksstuff.py index 2319cf77..b16df358 100644 --- a/rowers/sporttracksstuff.py +++ b/rowers/sporttracksstuff.py @@ -278,6 +278,11 @@ def createsporttracksworkoutdata(w): powerdata.append(power[i]) + try: + w.notes = w.notes+'\n from '+w.workoutsource+' via rowsandall.com', + except TypeError: + w.notes = w.notes+' via rowsandall.com' + if haslatlon: data = { "type": "Rowing", @@ -286,7 +291,7 @@ def createsporttracksworkoutdata(w): "start_time": w.startdatetime.isoformat(), "total_distance": int(w.distance), "duration": duration, - "notes": w.notes+'\n from '+w.workoutsource+' via rowsandall.com', + "notes": w.notes, "avg_heartrate": averagehr, "max_heartrate": maxhr, "location": locdata, @@ -302,7 +307,7 @@ def createsporttracksworkoutdata(w): "start_time": w.startdatetime.isoformat(), "total_distance": int(w.distance), "duration": duration, - "notes": w.notes+'\n from '+w.workoutsource+' via rowsandall.com', + "notes": w.notes, "avg_heartrate": averagehr, "max_heartrate": maxhr, "distance": distancedata, diff --git a/rowers/templates/analysis.html b/rowers/templates/analysis.html index 4cdbf511..2dc2cf43 100644 --- a/rowers/templates/analysis.html +++ b/rowers/templates/analysis.html @@ -64,11 +64,15 @@

-

- Pro Feature 3 +

+ {% if user.rower.rowerplan == 'pro' or user.rower.rowerplan == 'coach' %} + Box Chart + {% else %} + Box Chart + {% endif %}

- Reserved for future functionality. + BETA: Box Chart Statistics of stroke metrics over a date range

diff --git a/rowers/templates/boxplot.html b/rowers/templates/boxplot.html new file mode 100644 index 00000000..ce250dd2 --- /dev/null +++ b/rowers/templates/boxplot.html @@ -0,0 +1,63 @@ +{% extends "base.html" %} +{% load staticfiles %} +{% load rowerfilters %} + +{% block title %}View Comparison {% endblock %} + +{% block content %} + + + + +{{ interactiveplot |safe }} + + + + + +
+

Box Chart

+
+
+
+ {% csrf_token %} + + {{ chartform.as_table }} +
+
+

+ +

+
+
+
+
+ + + +
+ {{ the_div|safe }} +
+
+ + +{% endblock %} diff --git a/rowers/templates/user_boxplot_select.html b/rowers/templates/user_boxplot_select.html new file mode 100644 index 00000000..e5e5c611 --- /dev/null +++ b/rowers/templates/user_boxplot_select.html @@ -0,0 +1,93 @@ +{% extends "base.html" %} +{% load staticfiles %} +{% load rowerfilters %} + +{% block title %}Workouts{% endblock %} + +{% block content %} + + +
+
+ {% include "teambuttons.html" with teamid=team.id %} +
+
+
+ {% if theuser %} +

{{ theuser.first_name }}'s Workouts

+ {% else %} +

{{ user.first_name }}'s Workouts

+ {% endif %} +
+
+
+ + {% if theuser %} +
+ {% else %} + + {% endif %} + + + {{ dateform.as_table }} +
+ {% csrf_token %} +
+
+ +
+
+
+
+ +
+
+ +
+
+
+ +
+ + +
+
+ + + {% if workouts %} + + + + {{ form.as_table }} +
+{% else %} +

No workouts found

+{% endif %} +
+
+

Warning: You are on an experimental part of the site. Use at your own risk.

+

Select two or more workouts on the left, set your plot settings below, + and press submit"

+ {% csrf_token %} + + {{ chartform.as_table }} +
+
+

+ +

+
+
+

You can use the date and search forms above 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.

+
+
+
+ + +{% endblock %} diff --git a/rowers/urls.py b/rowers/urls.py index 952b6175..d7b8f857 100644 --- a/rowers/urls.py +++ b/rowers/urls.py @@ -124,6 +124,10 @@ urlpatterns = [ url(r'^team-compare-select/team/(?P\d+)/$',views.team_comparison_select), url(r'^team-compare-select/(?P\w+.*)/(?P\w+.*)$',views.team_comparison_select), url(r'^team-compare-select/$',views.team_comparison_select), + url(r'^user-boxplot-select/team/(?P\d+)/(?P\w+.*)/(?P\w+.*)$',views.user_boxplot_select), + url(r'^user-boxplot-select/user/(?P\d+)/$',views.user_boxplot_select), + url(r'^user-boxplot-select/(?P\w+.*)/(?P\w+.*)$',views.user_boxplot_select), + url(r'^user-boxplot-select/$',views.user_boxplot_select), url(r'^list-graphs/$',views.graphs_view), url(r'^(?P\d+)/ote-bests/(?P\w+.*)/(?P\w+.*)$',views.rankings_view), url(r'^(?P\d+)/ote-bests/(?P\d+)$',views.rankings_view), @@ -220,6 +224,7 @@ urlpatterns = [ url(r'^workout/(?P\d+)/underarmouruploadw/$',views.workout_underarmour_upload_view), url(r'^workout/(?P\d+)/tpuploadw/$',views.workout_tp_upload_view), url(r'^multi-compare$',views.multi_compare_view), + url(r'^user-boxplot$',views.boxplot_view), url(r'^me/teams/$',views.rower_teams_view), url(r'^team/(?P\d+)/$',views.team_view), url(r'^team/(?P\d+)/edit$',views.team_edit_view), diff --git a/rowers/views.py b/rowers/views.py index f98d3a29..506fb65a 100644 --- a/rowers/views.py +++ b/rowers/views.py @@ -34,7 +34,7 @@ from rowers.forms import ( EmailForm, RegistrationForm, RegistrationFormTermsOfService, RegistrationFormUniqueEmail,CNsummaryForm,UpdateWindForm, UpdateStreamForm,WorkoutMultipleCompareForm,ChartParamChoiceForm, - FusionMetricChoiceForm, + FusionMetricChoiceForm,BoxPlotChoiceForm, ) from rowers.models import Workout, User, Rower, WorkoutForm,FavoriteChart from rowers.models import ( @@ -2847,6 +2847,8 @@ def team_comparison_select(request, 'teams':get_my_teams(request.user), }) + +# Team comparison @login_required() def multi_compare_view(request): promember=0 @@ -2926,6 +2928,181 @@ def multi_compare_view(request): url = reverse(workouts_view) return HttpResponseRedirect(url) +# Box plots +@user_passes_test(ispromember,login_url="/",redirect_field_name=None) +def user_boxplot_select(request, + startdatestring="", + enddatestring="", + message='', + successmessage='', + startdate=timezone.now()-datetime.timedelta(days=30), + enddate=timezone.now()+datetime.timedelta(days=1), + userid=0): + + if userid == 0: + user = request.user + else: + user = User.objects.get(id=userid) + + try: + r = Rower.objects.get(user=user) + except Rower.DoesNotExist: + raise Http404("Rower doesn't exist") + + if request.method == 'POST': + dateform = DateRangeForm(request.POST) + if dateform.is_valid(): + startdate = dateform.cleaned_data['startdate'] + enddate = dateform.cleaned_data['enddate'] + else: + dateform = DateRangeForm(initial={ + 'startdate':startdate, + 'enddate':enddate, + }) + + startdate = datetime.datetime.combine(startdate,datetime.time()) + enddate = datetime.datetime.combine(enddate,datetime.time(23,59,59)) + enddate = enddate+datetime.timedelta(days=1) + + if startdatestring: + startdate = iso8601.parse_date(startdatestring) + if enddatestring: + enddate = iso8601.parse_date(enddatestring) + + if enddate < startdate: + s = enddate + enddate = startdate + startdate = s + + + workouts = Workout.objects.filter(user=r, + startdatetime__gte=startdate, + startdatetime__lte=enddate).order_by("-date", "-starttime") + + 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)) + ) + + form = WorkoutMultipleCompareForm() + form.fields["workouts"].queryset = workouts + + chartform = BoxPlotChoiceForm() + + messages.info(request,successmessage) + messages.error(request,message) + + return render(request, 'user_boxplot_select.html', + {'workouts': workouts, + 'dateform':dateform, + 'startdate':startdate, + 'enddate':enddate, + 'theuser':user, + 'form':form, + 'chartform':chartform, + 'teams':get_my_teams(request.user), + }) + +@user_passes_test(ispromember,login_url="/",redirect_field_name=None) +def boxplot_view(request,userid=0, + options={ + 'includereststrokes':False, + }): + + if 'options' in request.session: + options = request.session['options'] + + includereststrokes = options['includereststrokes'] + workstrokesonly = not includereststrokes + + if request.method == 'POST' and 'workouts' in request.POST: + form = WorkoutMultipleCompareForm(request.POST) + chartform = BoxPlotChoiceForm(request.POST) + if form.is_valid() and chartform.is_valid(): + cd = form.cleaned_data + workouts = cd['workouts'] + plotfield = chartform.cleaned_data['yparam'] + ids = [int(w.id) for w in workouts] + request.session['ids'] = ids + + labeldict = { + int(w.id): w.__unicode__() for w in workouts + } + + + datemapping = { + w.id:w.date for w in workouts + } + + + + fieldlist,fielddict = dataprep.getstatsfields() + fieldlist = [plotfield,'workoutid'] + + # prepare data frame + datadf = dataprep.read_cols_df_sql(ids,fieldlist) + + datadf = dataprep.clean_df_stats(datadf,workstrokesonly=workstrokesonly) + datadf['workoutid'].replace(datemapping,inplace=True) + datadf.rename(columns={"workoutid":"date"},inplace=True) + datadf = datadf.sort_values(['date']) + script,div = interactive_boxchart(datadf,plotfield) + + + return render(request,'boxplot.html', + {'interactiveplot':script, + 'the_div':div, + 'chartform':chartform, + 'teams':get_my_teams(request.user), + }) + else: + return HttpResponse("Form is not valid") + if request.method == 'POST' and 'ids' in request.session: + chartform = BoxPlotChoiceForm(request.POST) + if chartform.is_valid(): + plotfield = chartform.cleaned_data['yparam'] + ids = request.session['ids'] + request.session['ids'] = ids + workouts = [Workout.objects.get(id=id) for id in ids] + + labeldict = { + int(w.id): w.__unicode__() for w in workouts + } + + datemapping = { + w.id:w.date for w in workouts + } + + fieldlist,fielddict = dataprep.getstatsfields() + fieldlist = [plotfield,'workoutid'] + + # prepare data frame + datadf = dataprep.read_cols_df_sql(ids,fieldlist) + + datadf = dataprep.clean_df_stats(datadf,workstrokesonly=workstrokesonly) + datadf['workoutid'].replace(datemapping,inplace=True) + datadf.rename(columns={"workoutid":"date"},inplace=True) + datadf = datadf.sort_values(['date']) + script,div = interactive_boxchart(datadf,plotfield) + + + return render(request,'boxplot.html', + {'interactiveplot':script, + 'the_div':div, + 'chartform':chartform, + 'teams':get_my_teams(request.user), + }) + else: + return HttpResponse("invalid form") + else: + url = reverse(workouts_view) + return HttpResponseRedirect(url) + # List Workouts @login_required() def workouts_view(request,message='',successmessage='', @@ -4113,7 +4290,8 @@ def workout_stats_view(request,id=0,message="",successmessage=""): fieldlist,fielddict = dataprep.getstatsfields() fielddict.pop('workoutstate') - + fielddict.pop('workoutid') + for field,verbosename in fielddict.iteritems(): thedict = { 'mean':datadf[field].mean(),