From 97189ef02be7adba4f84dce1e78b73839658dccc Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Fri, 24 Feb 2017 15:57:20 +0100 Subject: [PATCH 01/10] dataprep routine fusing two dataframes --- rowers/dataprep.py | 22 +++++++++++++++++++++- rowers/views.py | 2 +- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/rowers/dataprep.py b/rowers/dataprep.py index 71e6d585..d7497a78 100644 --- a/rowers/dataprep.py +++ b/rowers/dataprep.py @@ -772,7 +772,27 @@ def smalldataprep(therows,xparam,yparam1,yparam2): pass return df - + +# data fusion +def datafusion(id1,id2,column,offset): + df1 = getrowdata_db(id=id1) + columns = ['time',column] + df2 = getsmallrowdata_db(columns,ids=[id2]) + + keep1 = set(df1.columns) + keep1.pop(column) + + for c in df1.columns: + if not c in keep1: + df1 = df1.drop(c,1,errors='ignore') + + df = pd.concat([df1,df2],ignore_index=True) + df = df.sort_value(['time']) + df.interpolate(method='linear',axis=0,limit_direction='both') + df.fillna(method='bfill',inplace=True) + + return df + # This is the main routine. # it reindexes, sorts, filters, and smooths the data, then # saves it to the stroke_data table in the database diff --git a/rowers/views.py b/rowers/views.py index 26ad82c9..dbf7f9d8 100644 --- a/rowers/views.py +++ b/rowers/views.py @@ -3601,7 +3601,7 @@ def workout_unsubscribe_view(request,id=0): user=request.user).order_by("created") for c in comments: - c.notify = False + c.notification = False c.save() form = WorkoutCommentForm() From 0db945ee396a3a575de7b05a98074b419990e6cd Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Fri, 24 Feb 2017 15:59:51 +0100 Subject: [PATCH 02/10] dataprep --- rowers/dataprep.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/rowers/dataprep.py b/rowers/dataprep.py index d7497a78..33827e10 100644 --- a/rowers/dataprep.py +++ b/rowers/dataprep.py @@ -774,13 +774,14 @@ def smalldataprep(therows,xparam,yparam1,yparam2): return df # data fusion -def datafusion(id1,id2,column,offset): +def datafusion(id1,id2,columns,offset): df1 = getrowdata_db(id=id1) - columns = ['time',column] + columns = ['time']+columns df2 = getsmallrowdata_db(columns,ids=[id2]) keep1 = set(df1.columns) - keep1.pop(column) + for c in columns: + keep1.pop(c) for c in df1.columns: if not c in keep1: From 0480489519aa0ce981a67ead6cfe0a5f3b12a44e Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Mon, 6 Mar 2017 15:23:13 +0100 Subject: [PATCH 03/10] initial nonfunct fusion form and view --- rowers/forms.py | 14 +++++++-- rowers/templates/fusion.html | 44 +++++++++++++++++++++++++++ rowers/urls.py | 1 + rowers/views.py | 59 ++++++++++++++++++++++++------------ 4 files changed, 96 insertions(+), 22 deletions(-) create mode 100644 rowers/templates/fusion.html diff --git a/rowers/forms.py b/rowers/forms.py index c4926376..79db0781 100644 --- a/rowers/forms.py +++ b/rowers/forms.py @@ -260,7 +260,7 @@ class WorkoutMultipleCompareForm(forms.Form): from rowers.interactiveplots import axlabels axlabels.pop('None') -axlabels = list(sorted(axlabels.items(), key = lambda x:x[1])) +parchoices = list(sorted(axlabels.items(), key = lambda x:x[1])) class ChartParamChoiceForm(forms.Form): @@ -268,7 +268,15 @@ class ChartParamChoiceForm(forms.Form): ('line','Line Plot'), ('scatter','Scatter Plot'), ) - xparam = forms.ChoiceField(choices=axlabels,initial='distance') - yparam = forms.ChoiceField(choices=axlabels,initial='hr') + xparam = forms.ChoiceField(choices=parchoices,initial='distance') + yparam = forms.ChoiceField(choices=parchoices,initial='hr') plottype = forms.ChoiceField(choices=plotchoices,initial='scatter') teamid = forms.IntegerField(widget=forms.HiddenInput()) + +axlabels.pop('time') +metricchoices = list(sorted(axlabels.items(), key = lambda x:x[1])) + +class FusionMetricChoiceForm(forms.Form): + columns = forms.MultipleChoiceField(choices=metricchoices, + initial=[], + widget=forms.CheckboxSelectMultiple()) diff --git a/rowers/templates/fusion.html b/rowers/templates/fusion.html new file mode 100644 index 00000000..bb0840d6 --- /dev/null +++ b/rowers/templates/fusion.html @@ -0,0 +1,44 @@ +{% extends "base.html" %} +{% load staticfiles %} +{% load rowerfilters %} + +{% block title %}Workouts{% endblock %} + +{% block content %} + + +
+

Fusion Editor

+
+
+
+

+ Adding sensor data from workout {{ workout2.id }} into workout {{ workout1.id2 }}. + This will create a new workout. After you submit the form, you will be + taken to the newly created workout. If you are happy with the result, you + can delete the two original workouts manually. +

+

+ Workout 1: {{ workout1.name }} +

+

+ Workout 2: {{ workout2.name }} +

+

On the right hand side, please select the columns from workout 2 that + you want to replace the equivalent columns in workout 1.

+
+
+ +
+ + + {{ form.as_table }} +
+ {% csrf_token %} +
+
+ +
+
+ +{% endblock %} diff --git a/rowers/urls.py b/rowers/urls.py index 24945f8e..b5a8629c 100644 --- a/rowers/urls.py +++ b/rowers/urls.py @@ -189,6 +189,7 @@ urlpatterns = [ url(r'^workout/(\d+)/interactiveplot$',views.workout_biginteractive_view), url(r'^workout/(\d+)/view$',views.workout_view), url(r'^workout/(\d+)$',views.workout_view), + url(r'^workout/fusion/(?P\d+)/(?P\d+)$',views.workout_fusion_view), url(r'^physics$',TemplateView.as_view(template_name='physics.html'),name='physics'), url(r'^workout/(\d+)/$',views.workout_view), url(r'^workout/(\d+)/addtimeplot$',views.workout_add_timeplot_view), diff --git a/rowers/views.py b/rowers/views.py index c5aaa86f..f32974de 100644 --- a/rowers/views.py +++ b/rowers/views.py @@ -26,7 +26,8 @@ from rowers.forms import ( StatsOptionsForm,PredictedPieceForm,DateRangeForm,DeltaDaysForm, EmailForm, RegistrationForm, RegistrationFormTermsOfService, RegistrationFormUniqueEmail,CNsummaryForm,UpdateWindForm, - UpdateStreamForm,WorkoutMultipleCompareForm,ChartParamChoiceForm + UpdateStreamForm,WorkoutMultipleCompareForm,ChartParamChoiceForm, + FusionMetricChoiceForm, ) from rowers.models import Workout, User, Rower, WorkoutForm,FavoriteChart from rowers.models import ( @@ -2617,7 +2618,7 @@ def workout_wind_view(request,id=0,message="",successmessage=""): # get data f1 = row.csvfilename - u = request.user + u = row.user.user r = Rower.objects.get(user=u) # create bearing @@ -2732,7 +2733,7 @@ def workout_stream_view(request,id=0,message="",successmessage=""): # create interactive plot f1 = row.csvfilename - u = request.user + u = row.user.user r = Rower.objects.get(user=u) rowdata = rdata(f1) @@ -2892,7 +2893,7 @@ def workout_geeky_view(request,id=0,message="",successmessage=""): # create interactive plot f1 = row.csvfilename - u = request.user + u = row.user.user r = Rower.objects.get(user=u) # create interactive plot @@ -3209,7 +3210,7 @@ def workout_advanced_view(request,id=0,message="",successmessage=""): # create interactive plot f1 = row.csvfilename - u = request.user + u = row.user.user r = Rower.objects.get(user=u) # create interactive plot @@ -3527,7 +3528,7 @@ def workout_biginteractive_view(request,id=0,message="",successmessage=""): # create interactive plot f1 = row.csvfilename - u = request.user + u = row.user.user # r = Rower.objects.get(user=u) promember=0 @@ -3568,7 +3569,7 @@ def workout_otwpowerplot_view(request,id=0,message="",successmessage=""): # create interactive plot f1 = row.csvfilename - u = request.user + u = row.user.user # r = Rower.objects.get(user=u) promember=0 @@ -3766,6 +3767,7 @@ def workout_edit_view(request,id=0,message="",successmessage=""): try: # check if valid ID exists (workout exists) row = Workout.objects.get(id=id) + form = WorkoutForm(instance=row) except Workout.DoesNotExist: raise Http404("Workout doesn't exist") @@ -3815,12 +3817,11 @@ def workout_edit_view(request,id=0,message="",successmessage=""): r.write_csv(row.csvfilename,gzip=True) dataprep.update_strokedata(id,r.df) successmessage = "Changes saved" - url = "/rowers/workout/"+str(row.id)+"/edit" url = reverse(workout_edit_view, kwargs = { 'id':str(row.id), 'successmessage':str(successmessage), - }) + }) response = HttpResponseRedirect(url) else: message = "You are not allowed to change this workout" @@ -3845,7 +3846,7 @@ def workout_edit_view(request,id=0,message="",successmessage=""): # create interactive plot f1 = row.csvfilename - u = request.user + u = row.user.user r = Rower.objects.get(user=u) rowdata = rdata(f1) hascoordinates = 1 @@ -3914,7 +3915,7 @@ def workout_add_otw_powerplot_view(request,id): timestr = strftime("%Y%m%d-%H%M%S") imagename = f1+timestr+'.png' fullpathimagename = 'static/plots/'+imagename - u = request.user + u = w.user.user r = Rower.objects.get(user=u) powerperc = 100*np.array([r.pw_ut2, r.pw_ut1, @@ -3971,7 +3972,7 @@ def workout_add_piechart_view(request,id): timestr = strftime("%Y%m%d-%H%M%S") imagename = f1+timestr+'.png' fullpathimagename = 'static/plots/'+imagename - u = request.user + u = w.user.user r = Rower.objects.get(user=u) powerperc = 100*np.array([r.pw_ut2, @@ -4029,7 +4030,7 @@ def workout_add_power_piechart_view(request,id): timestr = strftime("%Y%m%d-%H%M%S") imagename = f1+timestr+'.png' fullpathimagename = 'static/plots/'+imagename - u = request.user + u = w.user.user r = Rower.objects.get(user=u) powerperc = 100*np.array([r.pw_ut2, @@ -4085,7 +4086,7 @@ def workout_add_timeplot_view(request,id): timestr = strftime("%Y%m%d-%H%M%S") imagename = f1+timestr+'.png' fullpathimagename = 'static/plots/'+imagename - u = request.user + u = w.user.user r = Rower.objects.get(user=u) powerperc = 100*np.array([r.pw_ut2, r.pw_ut1, @@ -4142,7 +4143,7 @@ def workout_add_distanceplot_view(request,id): timestr = strftime("%Y%m%d-%H%M%S") imagename = f1+timestr+'.png' fullpathimagename = 'static/plots/'+imagename - u = request.user + u = w.user.user r = Rower.objects.get(user=u) powerperc = 100*np.array([r.pw_ut2, r.pw_ut1, @@ -4197,7 +4198,7 @@ def workout_add_distanceplot2_view(request,id): timestr = strftime("%Y%m%d-%H%M%S") imagename = f1+timestr+'.png' fullpathimagename = 'static/plots/'+imagename - u = request.user + u = w.user.user r = Rower.objects.get(user=u) powerperc = 100*np.array([r.pw_ut2, r.pw_ut1, @@ -4254,7 +4255,7 @@ def workout_add_timeplot2_view(request,id): timestr = strftime("%Y%m%d-%H%M%S") imagename = f1+timestr+'.png' fullpathimagename = 'static/plots/'+imagename - u = request.user + u = w.user.user r = Rower.objects.get(user=u) powerperc = 100*np.array([r.pw_ut2, r.pw_ut1, @@ -4944,7 +4945,7 @@ def workout_summary_restore_view(request,id,message="",successmessage=""): s = "" # still here - this is a workout we may edit f1 = row.csvfilename - u = request.user + u = row.user.user r = Rower.objects.get(user=u) powerperc = 100*np.array([r.pw_ut2, r.pw_ut1, @@ -4984,6 +4985,26 @@ def workout_summary_restore_view(request,id,message="",successmessage=""): ) return HttpResponseRedirect(url) +# Fuse two workouts +@login_required() +def workout_fusion_view(request,id1=0,id2=1): + try: + w1 = Workout.objects.get(id=id1) + w2 = Workout.objects.get(id=id2) + if (checkworkoutuser(request.user,w1)==False) or \ + (checkworkoutuser(request.user,w2)==False): + raise PermissionDenied("You are not allowed to use these workouts") + except Workout.DoesNotExist: + raise Http404("One of the workouts doesn't exist") + + form = FusionMetricChoiceForm() + + return render(request, 'fusion.html', + {'form':form, + 'workout1':w1, + 'workout2':w2, + }) + # Edit the splits/summary @login_required() @@ -4999,7 +5020,7 @@ def workout_summary_edit_view(request,id,message="",successmessage="" s = "" # still here - this is a workout we may edit f1 = row.csvfilename - u = request.user + u = row.user.user r = Rower.objects.get(user=u) powerperc = 100*np.array([r.pw_ut2, r.pw_ut1, From 1dcfbb067c37a0521db9c6bd5633504ba9c1482d Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Thu, 9 Mar 2017 12:38:37 +0100 Subject: [PATCH 04/10] non working version --- rowers/#tasks.py# | 495 +++++++++++++++++++++++++++++++++++ rowers/.#interactiveplots.py | 1 - rowers/.#tasks.py | 1 + rowers/dataprep.py | 93 ++++++- rowers/views.py | 25 ++ 5 files changed, 604 insertions(+), 11 deletions(-) create mode 100644 rowers/#tasks.py# delete mode 100644 rowers/.#interactiveplots.py create mode 100644 rowers/.#tasks.py diff --git a/rowers/#tasks.py# b/rowers/#tasks.py# new file mode 100644 index 00000000..f37fe6ab --- /dev/null +++ b/rowers/#tasks.py# @@ -0,0 +1,495 @@ +from celery import Celery,app +import os +import time +import gc +import gzip +import shutil +import numpy as np + +import rowingdata +from rowingdata import main as rmain +from rowingdata import rowingdata as rdata +import rowingdata + +from matplotlib.backends.backend_agg import FigureCanvas +#from matplotlib.backends.backend_cairo import FigureCanvasCairo as FigureCanvas +import matplotlib.pyplot as plt +from matplotlib import figure + +import stravalib + +from utils import serialize_list,deserialize_list + +from rowers.dataprepnodjango import update_strokedata + + +from django.core.mail import send_mail, BadHeaderError,EmailMessage + +# testing task +@app.task +def add(x, y): + return x + y + +# send email to me when an unrecognized file is uploaded +@app.task +def handle_sendemail_unrecognized(unrecognizedfile,useremail): + + # send email with attachment + fullemail = 'roosendaalsander@gmail.com' + subject = "Unrecognized file from Rowsandall.com" + message = "Dear Sander,\n\n" + message += "Please find attached a file that someone tried to upload to rowsandall.com. The file was not recognized as a valid file type.\n\n" + message += "User Email "+useremail+"\n\n" + message += "Best Regards, the Rowsandall Team" + + email = EmailMessage(subject, message, + 'Rowsandall ', + [fullemail]) + + + email.attach_file(unrecognizedfile) + + res = email.send() + + # remove tcx file + os.remove(unrecognizedfile) + return 1 + + +# Send email with TCX attachment +@app.task +def handle_sendemailtcx(first_name,last_name,email,tcxfile): + + # send email with attachment + fullemail = first_name + " " + last_name + " " + "<" + email + ">" + subject = "File from Rowsandall.com" + message = "Dear "+first_name+",\n\n" + message += "Please find attached the requested file for your workout.\n\n" + message += "Best Regards, the Rowsandall Team" + + email = EmailMessage(subject, message, + 'Rowsandall ', + [fullemail]) + + + email.attach_file(tcxfile) + + res = email.send() + + # remove tcx file + os.remove(tcxfile) + return 1 + +# Send email with CSV attachment +@app.task +def handle_sendemailcsv(first_name,last_name,email,csvfile): + + # send email with attachment + fullemail = first_name + " " + last_name + " " + "<" + email + ">" + subject = "File from Rowsandall.com" + message = "Dear "+first_name+",\n\n" + message += "Please find attached the requested file for your workout.\n\n" + message += "Best Regards, the Rowsandall Team" + + email = EmailMessage(subject, message, + 'Rowsandall ', + [fullemail]) + + + if os.path.isfile(csvfile): + email.attach_file(csvfile) + else: + csvfile2 = csvfile + with gzip.open(csvfile+'.gz','rb') as f_in, open(csvfile2,'wb') as f_out: + shutil.copyfileobj(f_in, f_out) + + email.attach_file(csvfile2) + os.remove(csvfile2) + + res = email.send() + + return 1 + +# Calculate wind and stream corrections for OTW rowing +@app.task +def handle_otwsetpower(f1,boattype,weightvalue, + first_name,last_name,email,workoutid, + debug=False): + try: + rowdata = rdata(f1) + except IOError: + try: + rowdata = rdata(f1+'.csv') + except IOError: + rowdata = rdata(f1+'.gz') + + weightvalue = float(weightvalue) + + # do something with boat type + boatfile = { + '1x':'static/rigging/1x.txt', + '2x':'static/rigging/2x.txt', + '2-':'static/rigging/2-.txt', + '4x':'static/rigging/4x.txt', + '4-':'static/rigging/4-.txt', + '8+':'static/rigging/8+.txt', + } + try: + rg = rowingdata.getrigging(boatfile[boattype]) + except KeyError: + rg = rowingdata.getrigging('static/rigging/1x.txt') + + # do calculation, but do not overwrite NK Empower Power data + powermeasured = False + try: + w = rowdata.df['wash'] + powermeasured = True + except KeyError: + pass + + rowdata.otw_setpower_silent(skiprows=5,mc=weightvalue,rg=rg, + powermeasured=powermeasured) + + # save data + rowdata.write_csv(f1) + update_strokedata(workoutid,rowdata.df,debug=debug) + + # send email + fullemail = first_name + " " + last_name + " " + "<" + email + ">" + subject = "Your Rowsandall OTW calculations are ready" + message = "Dear "+first_name+",\n\n" + message += "Your Rowsandall OTW calculations are ready.\n" + message += "Thank you for using rowsandall.com.\n\n" + message += "Rowsandall OTW calculations have not been fully implemented yet.\n" + message += "We are now running an experimental version for debugging purposes. \n" + message += "Your wind/stream corrected plot is available here: http://rowsandall.com/rowers/workout/" + message += str(workoutid) + message +="/interactiveotwplot\n\n" + message += "Please report any bugs/inconsistencies/unexpected results at rowsandall.slack.com or by reply to this email.\n\n" + message += "Best Regards, The Rowsandall Physics Department." + + send_mail(subject, message, + 'Rowsandall Physics Department ', + [fullemail]) + + return 1 + +# This function generates all the static (PNG image) plots +@app.task +def handle_makeplot(f1,f2,t,hrdata,plotnr,imagename): + + hrmax = hrdata['hrmax'] + hrut2 = hrdata['hrut2'] + hrut1 = hrdata['hrut1'] + hrat = hrdata['hrat'] + hrtr = hrdata['hrtr'] + hran = hrdata['hran'] + ftp = hrdata['ftp'] + powerzones = deserialize_list(hrdata['powerzones']) + powerperc = np.array(deserialize_list(hrdata['powerperc'])).astype(int) + + rr = rowingdata.rower(hrmax=hrmax,hrut2=hrut2, + hrut1=hrut1,hrat=hrat, + hrtr=hrtr,hran=hran, + ftp=ftp,powerperc=powerperc, + powerzones=powerzones) + try: + row = rdata(f2,rower=rr) + except IOError: + row = rdata(f2+'.gz',rower=rr) + + + haspower = row.df[' Power (watts)'].mean() > 50 + + nr_rows = len(row.df) + if (plotnr in [1,2,4,5,8,11,9,12]) and (nr_rows > 1200): + bin = int(nr_rows/1200.) + df = row.df.groupby(lambda x:x/bin).mean() + row.df = df + nr_rows = len(row.df) + if (plotnr==1): + fig1 = row.get_timeplot_erg(t) + elif (plotnr==2): + fig1 = row.get_metersplot_erg(t) + elif (plotnr==3): + fig1 = row.get_piechart(t) + elif (plotnr==4): + if haspower: + fig1 = row.get_timeplot_otwempower(t) + else: + fig1 = row.get_timeplot_otw(t) + elif (plotnr==5): + if haspower: + fig1 = row.get_metersplot_otwempower(t) + else: + fig1 = row.get_metersplot_otw(t) + elif (plotnr==6): + fig1 = row.get_piechart(t) + elif (plotnr==7) or (plotnr==10): + fig1 = row.get_metersplot_erg2(t) + elif (plotnr==8) or (plotnr==11): + fig1 = row.get_timeplot_erg2(t) + elif (plotnr==9) or (plotnr==12): + fig1 = row.get_time_otwpower(t) + elif (plotnr==13) or (plotnr==16): + fig1 = row.get_power_piechart(t) + + canvas = FigureCanvas(fig1) + + # plt.savefig('static/plots/'+imagename,format='png') + canvas.print_figure('static/plots/'+imagename) + # plt.imsave(fname='static/plots/'+imagename) + plt.close(fig1) + fig1.clf() + gc.collect() + return imagename + +# Team related remote tasks + +@app.task +def handle_sendemail_invite(email,name,code,teamname,manager): + fullemail = name+' <'+email+'>' + subject = 'Invitation to join team '+teamname + message = 'Dear '+name+',\n\n' + message += manager+' is inviting you to join his team '+teamname + message += ' on rowsandall.com\n\n' + message += 'By accepting the invite, you will have access to your' + message += " team's workouts on rowsandall.com and your workouts will " + message += " be visible to " + message += "the members of the team.\n\n" + message += 'If you already have an account on rowsandall.com, you can login to the site and you will find the invitation here on the Teams page:\n' + message += ' https://rowsandall.com/rowers/me/teams \n\n' + message += 'You can also click the direct link: \n' + message += 'https://rowsandall.com/rowers/me/invitation/'+code+' \n\n' + message += 'If you are not yet registered to rowsandall.com, ' + message += 'you can register for free at https://rowsandall.com/rowers/register\n' + message += 'After you set up your account, you can use the direct link: ' + message += 'https://rowsandall.com/rowers/me/invitation/'+code+' \n\n' + + message += 'You can also manually accept your team membership with the code.\n' + message += 'You will need to do this if you registered under a different email address than this one.\n' + message += 'Code: '+code+'\n' + message += 'Link to manually accept your team membership: ' + message += 'https://rowsandall.com/rowers/me/invitation\n\n' + message += "Best Regards, the Rowsandall Team" + + email = EmailMessage(subject, message, + 'Rowsandall ', + [fullemail]) + + + res = email.send() + + return 1 + +@app.task +def handle_sendemailnewresponse(first_name,last_name, + email, + commenter_first_name, + commenter_last_name, + comment, + workoutname,workoutid,commentid): + fullemail = first_name+' '+last_name+' <'+email+'>' + subject = 'New comment on workout '+workoutname + message = 'Dear '+first_name+',\n\n' + message += commenter_first_name+' '+commenter_last_name + message += ' has written a new comment on the workout ' + message += workoutname+'\n\n' + message += comment + message += '\n\n' + message += 'You can read the comment here:\n' + message += 'https://rowsandall.com/rowers/workout/'+str(workoutid)+'/comment' + message += '\n\n' + message += 'You are receiving this email because you are subscribed ' + message += 'to comments on this workout. To unsubscribe, follow this link:\n' + message += 'https://rowsandall.com/rowers/workout/'+str(workoutid)+'/unsubscribe' + + email = EmailMessage(subject, message, + 'Rowsandall ', + [fullemail]) + + + res = email.send() + + return 1 + +@app.task +def handle_sendemailnewcomment(first_name, + last_name, + email, + commenter_first_name, + commenter_last_name, + comment,workoutname, + workoutid): + fullemail = first_name+' '+last_name+' <'+email+'>' + subject = 'New comment on workout '+workoutname + message = 'Dear '+first_name+',\n\n' + message += commenter_first_name+' '+commenter_last_name + message += ' has written a new comment on your workout ' + message += workoutname+'\n\n' + message += comment + message += '\n\n' + message += 'You can read the comment here:\n' + message += 'https://rowsandall.com/rowers/workout/'+str(workoutid)+'/comment' + + email = EmailMessage(subject, message, + 'Rowsandall ', + [fullemail]) + + + res = email.send() + + return 1 + + +@app.task +def handle_sendemail_request(email,name,code,teamname,requestor,id): + fullemail = name+' <'+email+'>' + subject = 'Request to join team '+teamname + message = 'Dear '+name+',\n\n' + message += requestor+' is requesting admission to your team '+teamname + message += ' on rowsandall.com\n\n' + message += 'Click the direct link to accept: \n' + message += 'https://rowsandall.com/rowers/me/request/'+code+' \n\n' + message += 'Click the following link to reject the request: \n' + message += 'https://rowsandall.com/rowers/me/request/'+str(id)+' \n\n' + message += 'You can find all pending requests on your team management page:\n' + message += 'https://rowsandall.com/rowers/me/teams\n\n' + message += "Best Regards, the Rowsandall Team" + + email = EmailMessage(subject, message, + 'Rowsandall ', + [fullemail]) + + + res = email.send() + + return 1 + +@app.task +def handle_sendemail_request_accept(email,name,teamname,managername): + fullemail = name+' <'+email+'>' + subject = 'Welcome to '+teamname + message = 'Dear '+name+',\n\n' + message += managername + message += ' has accepted your request to be part of the team ' + message += teamname + message += '\n\n' + message += "Best Regards, the Rowsandall Team" + + email = EmailMessage(subject, message, + 'Rowsandall ', + [fullemail]) + + + res = email.send() + + return 1 + +@app.task +def handle_sendemail_request_reject(email,name,teamname,managername): + fullemail = name+' <'+email+'>' + subject = 'Your application to '+teamname+' was rejected' + message = 'Dear '+name+',\n\n' + message += 'Unfortunately, ' + message += managername + message += ' has rejected your request to be part of the team ' + message += teamname + message += '\n\n' + message += "Best Regards, the Rowsandall Team" + + email = EmailMessage(subject, message, + 'Rowsandall ', + [fullemail]) + + + res = email.send() + + return 1 + +@app.task +def handle_sendemail_member_dropped(email,name,teamname,managername): + fullemail = name+' <'+email+'>' + subject = 'You were removed from '+teamname + message = 'Dear '+name+',\n\n' + message += 'Unfortunately, ' + message += managername + message += ' has removed you from the team ' + message += teamname + message += '\n\n' + message += "Best Regards, the Rowsandall Team" + + email = EmailMessage(subject, message, + 'Rowsandall ', + [fullemail]) + + + res = email.send() + + return 1 + +@app.task +def handle_sendemail_team_removed(email,name,teamname,managername): + fullemail = name+' <'+email+'>' + subject = 'Team '+teamname+' was deleted' + message = 'Dear '+name+',\n\n' + message += managername + message += ' has decided to delete the team ' + message += teamname + message += '\n\n' + message += 'The '+teamname+' tag has been removed from all your ' + message += 'workouts on rowsandall.com.\n\n' + message += "Best Regards, the Rowsandall Team" + + email = EmailMessage(subject, message, + 'Rowsandall ', + [fullemail]) + + + res = email.send() + + return 1 + +@app.task +def handle_sendemail_invite_reject(email,name,teamname,managername): + fullemail = managername+' <'+email+'>' + subject = 'Your invitation to '+name+' was rejected' + message = 'Dear '+managername+',\n\n' + message += 'Unfortunately, ' + message += name + message += ' has rejected your invitation to be part of the team ' + message += teamname + message += '\n\n' + message += "Best Regards, the Rowsandall Team" + + email = EmailMessage(subject, message, + 'Rowsandall ', + [fullemail]) + + + res = email.send() + + return 1 + +@app.task +def handle_sendemail_invite_accept(email,name,teamname,managername): + fullemail = managername+' <'+email+'>' + subject = 'Your invitation to '+name+' was accepted' + message = 'Dear '+managername+',\n\n' + message += name+' has accepted your invitation to be part of the team '+teamname+'\n\n' + message += "Best Regards, the Rowsandall Team" + + email = EmailMessage(subject, message, + 'Rowsandall ', + [fullemail]) + + + res = email.send() + + return 1 + + + +# Another simple task for debugging purposes +def add2(x,y): + return x+y diff --git a/rowers/.#interactiveplots.py b/rowers/.#interactiveplots.py deleted file mode 100644 index ef817ef8..00000000 --- a/rowers/.#interactiveplots.py +++ /dev/null @@ -1 +0,0 @@ -E408191@CZ27LT9RCGN72.15972:1488820163 \ No newline at end of file diff --git a/rowers/.#tasks.py b/rowers/.#tasks.py new file mode 100644 index 00000000..d6002777 --- /dev/null +++ b/rowers/.#tasks.py @@ -0,0 +1 @@ +E408191@CZ27LT9RCGN72.13016:1488979920 \ No newline at end of file diff --git a/rowers/dataprep.py b/rowers/dataprep.py index d15df5f0..6e3d177b 100644 --- a/rowers/dataprep.py +++ b/rowers/dataprep.py @@ -12,7 +12,8 @@ from rowingdata import get_file_type,get_empower_rigging from pandas import DataFrame,Series from pytz import timezone as tz,utc - +from django.utils import timezone +from time import strftime,strptime,mktime,time,daylight from django.utils.timezone import get_current_timezone thetimezone = get_current_timezone() from rowingdata import ( @@ -80,7 +81,8 @@ columndict = { 'finish':'finish', 'peakforceangle':'peakforceangle', 'wash':'wash', - 'slip':'wash', + 'slip':'wash', + 'workoutstate':' WorkoutState', } from scipy.signal import savgol_filter @@ -621,6 +623,62 @@ def new_workout_from_file(r,f2, return (id,message,f2) +# Create new workout from data frame and store it in the database +# This routine should be used everywhere in views.py and mailprocessing.py +# Currently there is code duplication +def new_workout_from_df(r,df, + title='New Workout', + parent=None): + + message = None + + summary = '' + if parent: + oarlength = parent.oarlength + inboard = parent.inboard + workouttype = parent.workouttype + notes=parent.notes + summary=parent.summary + makeprivate=parent.privacy + startdatetime=parent.startdatetime + else: + oarlength = 2.89 + inboard = 0.88 + workouttype = 'rower' + notes='' + summary='' + makeprivate=False + startdatetime = timezone.now() + + timestr = strftime("%Y%m%d-%H%M%S") + + csvfilename ='media/Fusion_'+timestr+'.csv' + + df.rename(columns = columndict,inplace=True) + starttimeunix = mktime(startdatetime.utctimetuple()) + df[' ElapsedTime (sec)'] = df['TimeStamp (sec)'] + df['TimeStamp (sec)'] = df['TimeStamp (sec)']+starttimeunix + + row = rrdata(df=df) + row.write_csv(csvfilename,gzip=True) + + #res = df.to_csv(csvfilename+'.gz',index_label='index', + # compression='gzip') + + id,message = save_workout_database(csvfilename,r, + workouttype=workouttype, + title=title, + notes=notes, + oarlength=oarlength, + inboard=inboard, + makeprivate=makeprivate, + dosmooth=False) + + + return (id,message) + + + # Compare the data from the CSV file and the database # Currently only calculates number of strokes. To be expanded with # more elaborate testing if needed @@ -696,10 +754,10 @@ def repair_data(verbose=False): # A wrapper around the rowingdata class, with some error catching def rdata(file,rower=rrower()): try: - res = rrdata(file,rower=rower) + res = rrdata(csvfile=file,rower=rower) except IOError,IndexError: try: - res = rrdata(file+'.gz',rower=rower) + res = rrdata(csvfile=file+'.gz',rower=rower) except IOError,IndexError: res = 0 @@ -900,11 +958,21 @@ def smalldataprep(therows,xparam,yparam1,yparam2): # data fusion def datafusion(id1,id2,columns,offset): - df1 = getrowdata_db(id=id1) + df1,w1 = getrowdata_db(id=id1) + df1 = df1.drop(['cumdist', + 'hr_ut2', + 'hr_ut1', + 'hr_at', + 'hr_tr', + 'hr_an', + 'hr_max',], + 1,errors='ignore') columns = ['time']+columns - df2 = getsmallrowdata_db(columns,ids=[id2]) + df2 = getsmallrowdata_db(columns,ids=[id2],doclean=False) - keep1 = set(df1.columns) + print df1['pace'].mean()/1000.,'mies' + + keep1 = {c:c for c in set(df1.columns)} for c in columns: keep1.pop(c) @@ -913,10 +981,14 @@ def datafusion(id1,id2,columns,offset): df1 = df1.drop(c,1,errors='ignore') df = pd.concat([df1,df2],ignore_index=True) - df = df.sort_value(['time']) - df.interpolate(method='linear',axis=0,limit_direction='both') + df = df.sort_values(['time']) + df = df.interpolate(method='linear',axis=0,limit_direction='both') df.fillna(method='bfill',inplace=True) - + + df['time'] = df['time']/1000. + df['pace'] = df['pace']/1000. + print df['pace'].mean(),'noot' + return df # This is the main routine. @@ -933,6 +1005,7 @@ def dataprep(rowdatadf,id=0,bands=True,barchart=True,otwpower=True, rowdatadf.loc[row_index,' Stroke500mPace (sec/500m)'] = 3000. p = rowdatadf.ix[:,' Stroke500mPace (sec/500m)'] + print p.mean(),'aap' hr = rowdatadf.ix[:,' HRCur (bpm)'] spm = rowdatadf.ix[:,' Cadence (stokes/min)'] cumdist = rowdatadf.ix[:,'cum_dist'] diff --git a/rowers/views.py b/rowers/views.py index 66defa93..ab49fc7a 100644 --- a/rowers/views.py +++ b/rowers/views.py @@ -4992,12 +4992,37 @@ def workout_fusion_view(request,id1=0,id2=1): try: w1 = Workout.objects.get(id=id1) w2 = Workout.objects.get(id=id2) + r = w1.user if (checkworkoutuser(request.user,w1)==False) or \ (checkworkoutuser(request.user,w2)==False): raise PermissionDenied("You are not allowed to use these workouts") except Workout.DoesNotExist: raise Http404("One of the workouts doesn't exist") + if request.method == 'POST': + form = FusionMetricChoiceForm(request.POST) + if form.is_valid(): + cd = form.cleaned_data + columns = cd['columns'] + df = dataprep.datafusion(id1,id2,columns,0) + idnew,message = dataprep.new_workout_from_df(r,df, + title='Fused data', + parent=w1) + url = reverse(workout_edit_view, + kwargs={ + 'message':message, + 'id':idnew, + }) + + return HttpResponseRedirect(url) + else: + return render(request, 'fusion.html', + {'form':form, + 'workout1':w1, + 'workout2':w2, + }) + + form = FusionMetricChoiceForm() return render(request, 'fusion.html', From 003078745392bfb0507a1ee01a7716562a711d82 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Thu, 9 Mar 2017 12:39:07 +0100 Subject: [PATCH 05/10] nothing --- rowers/.#tasks.py | 1 - 1 file changed, 1 deletion(-) delete mode 100644 rowers/.#tasks.py diff --git a/rowers/.#tasks.py b/rowers/.#tasks.py deleted file mode 100644 index d6002777..00000000 --- a/rowers/.#tasks.py +++ /dev/null @@ -1 +0,0 @@ -E408191@CZ27LT9RCGN72.13016:1488979920 \ No newline at end of file From 39b80b671659d1939a2529a1dbc3572fa720ccfa Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Thu, 9 Mar 2017 17:56:48 +0100 Subject: [PATCH 06/10] data fusion now working ... --- rowers/dataprep.py | 25 +++++++++++++++---------- rowers/forms.py | 11 +++++++++-- rowers/interactiveplots.py | 9 +++++---- rowers/views.py | 24 ++++++++++++++++++------ 4 files changed, 47 insertions(+), 22 deletions(-) diff --git a/rowers/dataprep.py b/rowers/dataprep.py index 6e3d177b..5d5bf406 100644 --- a/rowers/dataprep.py +++ b/rowers/dataprep.py @@ -959,35 +959,41 @@ def smalldataprep(therows,xparam,yparam1,yparam2): # data fusion def datafusion(id1,id2,columns,offset): df1,w1 = getrowdata_db(id=id1) - df1 = df1.drop(['cumdist', + df1 = df1.drop([#'cumdist', 'hr_ut2', 'hr_ut1', 'hr_at', 'hr_tr', 'hr_an', - 'hr_max',], + 'hr_max', + 'ftime', + 'fpace', + 'workoutid', + 'id'], 1,errors='ignore') - columns = ['time']+columns - df2 = getsmallrowdata_db(columns,ids=[id2],doclean=False) - print df1['pace'].mean()/1000.,'mies' - + df2 = getsmallrowdata_db(['time']+columns,ids=[id2],doclean=False) + offsetmillisecs = offset.seconds*1000+offset.microseconds/1000. + offsetmillisecs += offset.days*(3600*24*1000) + df2['time'] = df2['time']+offsetmillisecs + keep1 = {c:c for c in set(df1.columns)} for c in columns: keep1.pop(c) - + for c in df1.columns: if not c in keep1: df1 = df1.drop(c,1,errors='ignore') df = pd.concat([df1,df2],ignore_index=True) df = df.sort_values(['time']) - df = df.interpolate(method='linear',axis=0,limit_direction='both') + df = df.interpolate(method='linear',axis=0,limit_direction='both', + limit=10) df.fillna(method='bfill',inplace=True) df['time'] = df['time']/1000. df['pace'] = df['pace']/1000. - print df['pace'].mean(),'noot' + df['cum_dist'] = df['cumdist'] return df @@ -1005,7 +1011,6 @@ def dataprep(rowdatadf,id=0,bands=True,barchart=True,otwpower=True, rowdatadf.loc[row_index,' Stroke500mPace (sec/500m)'] = 3000. p = rowdatadf.ix[:,' Stroke500mPace (sec/500m)'] - print p.mean(),'aap' hr = rowdatadf.ix[:,' HRCur (bpm)'] spm = rowdatadf.ix[:,' Cadence (stokes/min)'] cumdist = rowdatadf.ix[:,'cum_dist'] diff --git a/rowers/forms.py b/rowers/forms.py index 79db0781..310ef7d5 100644 --- a/rowers/forms.py +++ b/rowers/forms.py @@ -259,7 +259,8 @@ class WorkoutMultipleCompareForm(forms.Form): from rowers.interactiveplots import axlabels -axlabels.pop('None') +formaxlabels = axlabels.copy() +formaxlabels.pop('None') parchoices = list(sorted(axlabels.items(), key = lambda x:x[1])) @@ -273,10 +274,16 @@ class ChartParamChoiceForm(forms.Form): plottype = forms.ChoiceField(choices=plotchoices,initial='scatter') teamid = forms.IntegerField(widget=forms.HiddenInput()) -axlabels.pop('time') +formaxlabels.pop('time') metricchoices = list(sorted(axlabels.items(), key = lambda x:x[1])) class FusionMetricChoiceForm(forms.Form): + posneg = ( + ('pos','Workout 2 starts after Workout 1'), + ('neg','Workout 2 starts before Workout 1'), + ) columns = forms.MultipleChoiceField(choices=metricchoices, initial=[], widget=forms.CheckboxSelectMultiple()) + posneg = forms.ChoiceField(choices=posneg,initial='pos') + offset = forms.DurationField(label='Time Offset',initial=datetime.timedelta()) diff --git a/rowers/interactiveplots.py b/rowers/interactiveplots.py index 899f0ef1..66ef7cf6 100644 --- a/rowers/interactiveplots.py +++ b/rowers/interactiveplots.py @@ -1291,10 +1291,10 @@ def interactive_flex_chart2(id=0,promember=0, y2means = y1means xlabel = Label(x=100,y=130,x_units='screen',y_units='screen', - text=axlabels[xparam]+": {x1mean:6.2f}".format(x1mean=x1mean), - background_fill_alpha=.7, - text_color='green', - ) + text=axlabels[xparam]+": {x1mean:6.2f}".format(x1mean=x1mean), + background_fill_alpha=.7, + text_color='green', + ) if (xparam != 'time') and (xparam != 'distance') and (xparam != 'cumdist'): plot.add_layout(x1means) @@ -1325,6 +1325,7 @@ def interactive_flex_chart2(id=0,promember=0, plot.title.text = row.name plot.title.text_font_size=value("1.0em") + plot.xaxis.axis_label = axlabels[xparam] plot.yaxis.axis_label = axlabels[yparam1] diff --git a/rowers/views.py b/rowers/views.py index a74953dc..96f656d3 100644 --- a/rowers/views.py +++ b/rowers/views.py @@ -5017,15 +5017,27 @@ def workout_fusion_view(request,id1=0,id2=1): if form.is_valid(): cd = form.cleaned_data columns = cd['columns'] - df = dataprep.datafusion(id1,id2,columns,0) + timeoffset = cd['offset'] + posneg = cd['posneg'] + if posneg == 'neg': + timeoffset = -timeoffset + df = dataprep.datafusion(id1,id2,columns,timeoffset) idnew,message = dataprep.new_workout_from_df(r,df, title='Fused data', parent=w1) - url = reverse(workout_edit_view, - kwargs={ - 'message':message, - 'id':idnew, - }) + if message != None: + url = reverse(workout_edit_view, + kwargs={ + 'message':message, + 'id':idnew, + }) + else: + successmessage = 'Data fused' + url = reverse(workout_edit_view, + kwargs={ + 'successmessage':successmessage, + 'id':idnew, + }) return HttpResponseRedirect(url) else: From 71bded30c0b3bbe43b9e01fa47a65fe8dd825379 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Thu, 9 Mar 2017 23:17:39 +0100 Subject: [PATCH 07/10] strava import - some improvements --- rowers/stravastuff.py | 2 ++ rowers/views.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/rowers/stravastuff.py b/rowers/stravastuff.py index 98c58ce5..6c0cf8f0 100644 --- a/rowers/stravastuff.py +++ b/rowers/stravastuff.py @@ -167,6 +167,8 @@ def get_strava_workout(user,stravaid): t = np.array(timejson.json()[0]['data']) d = np.array(distancejson.json()[0]['data']) nr_rows = len(t) + if nr_rows == 0: + return (0,"Error: Time data had zero length") except KeyError: return (0,"something went wrong with the Strava import") diff --git a/rowers/views.py b/rowers/views.py index 96f656d3..357eac60 100644 --- a/rowers/views.py +++ b/rowers/views.py @@ -4463,7 +4463,7 @@ def workout_c2import_view(request,message=""): def workout_getstravaworkout_view(request,stravaid): res = stravastuff.get_strava_workout(request.user,stravaid) if not res[0]: - message = "Something went wrong in Strava import" + message = res[1] return imports_view(request,message=message) strokedata = res[1] From 1e5d0a0b837dfc5e2f2aa074ff0cc36ab8abdfbf Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Thu, 9 Mar 2017 23:47:24 +0100 Subject: [PATCH 08/10] strava import now time based --- rowers/stravastuff.py | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/rowers/stravastuff.py b/rowers/stravastuff.py index 6c0cf8f0..13c20463 100644 --- a/rowers/stravastuff.py +++ b/rowers/stravastuff.py @@ -139,6 +139,7 @@ def get_strava_workout(user,stravaid): else: # ready to fetch. Hurray fetchresolution = 'high' + series_type = 'time' authorizationstring = str('Bearer ' + r.stravatoken) headers = {'Authorization': authorizationstring, 'user-agent': 'sanderroosendaal', @@ -150,17 +151,18 @@ def get_strava_workout(user,stravaid): workoutsummary['timezone'] = "Etc/UTC" startdatetime = workoutsummary['start_date'] - url = "https://www.strava.com/api/v3/activities/"+str(stravaid)+"/streams/cadence?resolution="+fetchresolution + url = "https://www.strava.com/api/v3/activities/"+str(stravaid)+"/streams/cadence?resolution="+fetchresolution+"&series_type="+series_type spmjson = requests.get(url,headers=headers) - url = "https://www.strava.com/api/v3/activities/"+str(stravaid)+"/streams/heartrate?resolution="+fetchresolution + url = "https://www.strava.com/api/v3/activities/"+str(stravaid)+"/streams/heartrate?resolution="+fetchresolution+"&series_type="+series_type hrjson = requests.get(url,headers=headers) - url = "https://www.strava.com/api/v3/activities/"+str(stravaid)+"/streams/time?resolution="+fetchresolution + url = "https://www.strava.com/api/v3/activities/"+str(stravaid)+"/streams/time?resolution="+fetchresolution+"&series_type="+series_type + print url timejson = requests.get(url,headers=headers) - url = "https://www.strava.com/api/v3/activities/"+str(stravaid)+"/streams/velocity_smooth?resolution="+fetchresolution + url = "https://www.strava.com/api/v3/activities/"+str(stravaid)+"/streams/velocity_smooth?resolution="+fetchresolution+"&series_type="+series_type velojson = requests.get(url,headers=headers) - url = "https://www.strava.com/api/v3/activities/"+str(stravaid)+"/streams/distance?resolution="+fetchresolution + url = "https://www.strava.com/api/v3/activities/"+str(stravaid)+"/streams/distance?resolution="+fetchresolution+"&series_type="+series_type distancejson = requests.get(url,headers=headers) - url = "https://www.strava.com/api/v3/activities/"+str(stravaid)+"/streams/latlng?resolution="+fetchresolution + url = "https://www.strava.com/api/v3/activities/"+str(stravaid)+"/streams/latlng?resolution="+fetchresolution+"&series_type="+series_type latlongjson = requests.get(url,headers=headers) try: @@ -173,18 +175,19 @@ def get_strava_workout(user,stravaid): return (0,"something went wrong with the Strava import") try: - spm = np.array( spmjson.json()[1]['data']) - except IndexError: + print spmjson.json() + spm = np.array(spmjson.json()[1]['data']) + except: spm = np.zeros(nr_rows) try: hr = np.array(hrjson.json()[1]['data']) - except IndexError: + except IndexError,KeyError: hr = np.zeros(nr_rows) try: velo = np.array(velojson.json()[1]['data']) - except IndexError: + except IndexError,KeyError: velo = np.zeros(nr_rows) dt = np.diff(t).mean() @@ -195,7 +198,7 @@ def get_strava_workout(user,stravaid): try: lat = coords[:,0] lon = coords[:,1] - except IndexError: + except IndexError,KeyError: lat = np.zeros(len(t)) lon = np.zeros(len(t)) From d9f59011b3ab0ef25fe736a564ebd4b48ef10a8f Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Sun, 12 Mar 2017 09:48:13 +0100 Subject: [PATCH 09/10] added buttons to fusion --- rowers/forms.py | 26 ++++++- rowers/templates/advancededit.html | 15 +++- rowers/templates/fusion.html | 2 +- rowers/templates/fusion_list.html | 121 +++++++++++++++++++++++++++++ rowers/urls.py | 3 + rowers/views.py | 81 ++++++++++++++++++- 6 files changed, 241 insertions(+), 7 deletions(-) create mode 100644 rowers/templates/fusion_list.html diff --git a/rowers/forms.py b/rowers/forms.py index 310ef7d5..d7210ac4 100644 --- a/rowers/forms.py +++ b/rowers/forms.py @@ -6,6 +6,8 @@ from django.contrib.auth.models import User from django.contrib.admin.widgets import AdminDateWidget from django.forms.extras.widgets import SelectDateWidget from django.utils import timezone,translation +from django.forms import ModelForm +import dataprep import datetime @@ -261,7 +263,7 @@ from rowers.interactiveplots import axlabels formaxlabels = axlabels.copy() formaxlabels.pop('None') -parchoices = list(sorted(axlabels.items(), key = lambda x:x[1])) +parchoices = list(sorted(formaxlabels.items(), key = lambda x:x[1])) class ChartParamChoiceForm(forms.Form): @@ -275,9 +277,13 @@ class ChartParamChoiceForm(forms.Form): teamid = forms.IntegerField(widget=forms.HiddenInput()) formaxlabels.pop('time') -metricchoices = list(sorted(axlabels.items(), key = lambda x:x[1])) +metricchoices = list(sorted(formaxlabels.items(), key = lambda x:x[1])) -class FusionMetricChoiceForm(forms.Form): +class FusionMetricChoiceForm(ModelForm): + class Meta: + model = Workout + fields = [] + posneg = ( ('pos','Workout 2 starts after Workout 1'), ('neg','Workout 2 starts before Workout 1'), @@ -287,3 +293,17 @@ class FusionMetricChoiceForm(forms.Form): widget=forms.CheckboxSelectMultiple()) posneg = forms.ChoiceField(choices=posneg,initial='pos') offset = forms.DurationField(label='Time Offset',initial=datetime.timedelta()) + + def __init__(self, *args, **kwargs): + super(FusionMetricChoiceForm, self).__init__(*args, **kwargs) + # need to add code to remove "empty" fields + + id = self.instance.id + df = dataprep.getrowdata_db(id=id) + + labeldict = {key:value for key,value in metricchoices} + + for label in labeldict: + if df[label].std() == 0: + self.fields['columns'].choices.remove((label, labeldict[label])) + diff --git a/rowers/templates/advancededit.html b/rowers/templates/advancededit.html index fe002787..6db5b03c 100644 --- a/rowers/templates/advancededit.html +++ b/rowers/templates/advancededit.html @@ -121,7 +121,7 @@
-
+

{% if user.rower.rowerplan == 'pro' or user.rower.rowerplan == 'coach' %} Power Histogram @@ -133,9 +133,20 @@ Plot the Power Histogram of this workout

+
+

+ {% if user.rower.rowerplan == 'pro' or user.rower.rowerplan == 'coach' %} + Sensor Fusion + {% else %} + Dist Metrics Plot + {% endif %} +

+

+ Merge data from another source into this workout +

+
-
diff --git a/rowers/templates/fusion.html b/rowers/templates/fusion.html index bb0840d6..52de737e 100644 --- a/rowers/templates/fusion.html +++ b/rowers/templates/fusion.html @@ -13,7 +13,7 @@

- Adding sensor data from workout {{ workout2.id }} into workout {{ workout1.id2 }}. + Adding sensor data from workout {{ workout2.id }} into workout {{ workout1.id }}. This will create a new workout. After you submit the form, you will be taken to the newly created workout. If you are happy with the result, you can delete the two original workouts manually. diff --git a/rowers/templates/fusion_list.html b/rowers/templates/fusion_list.html new file mode 100644 index 00000000..b7f285d7 --- /dev/null +++ b/rowers/templates/fusion_list.html @@ -0,0 +1,121 @@ +{% extends "base.html" %} +{% load staticfiles %} +{% load rowerfilters %} + +{% block title %}Workouts{% endblock %} + +{% block content %} +

+
+

Workout {{ id }}

+ + + + + + + + + + + + + + + + + + + + +
Rower:{{ first_name }} {{ last_name }}
Name:{{ workout.name }}
Date:{{ workout.date }}
Time:{{ workout.starttime }}
Distance:{{ workout.distance }}m
Duration:{{ workout.duration |durationprint:"%H:%M:%S.%f" }}
Type:{{ workout.workouttype }}
Weight Category:{{ workout.weightcategory }}
+
+
+

+

+ + +
+

+
+ + Select start and end date for a date range: +
+

+

+ + + {{ dateform.as_table }} +
+ {% csrf_token %} +
+
+ +

+
+ + +
+ +
+

Fuse this workout with data from:

+ {% if workouts %} + + + + + + + + + + + + + + + + {% for cworkout in workouts %} + + + + + + + + + + {% if id == cworkout.id %} + + {% else %} + + {% endif %} + + + {% endfor %} + +
Date Time Name Type Distance Duration Avg HR Max HR Fusion
{{ cworkout.date }} {{ cworkout.starttime }} {{ cworkout.name }} {{ cworkout.workouttype }} {{ cworkout.distance }}m {{ cworkout.duration |durationprint:"%H:%M:%S.%f" }} {{ cworkout.averagehr }} {{ cworkout.maxhr }}   Fusion
+ {% else %} +

No workouts found

+ {% endif %} + +
+ + {% if workouts.has_previous %} + < + {% endif %} + + + Page {{ workouts.number }} of {{ workouts.paginator.num_pages }}. + + + {% if workouts.has_next %} + > + {% endif %} + +
+
+{% endblock %} diff --git a/rowers/urls.py b/rowers/urls.py index b5a8629c..6a524940 100644 --- a/rowers/urls.py +++ b/rowers/urls.py @@ -190,6 +190,9 @@ urlpatterns = [ url(r'^workout/(\d+)/view$',views.workout_view), url(r'^workout/(\d+)$',views.workout_view), url(r'^workout/fusion/(?P\d+)/(?P\d+)$',views.workout_fusion_view), + url(r'^workout/fusion/(\d+)/$',views.workout_fusion_list), + url(r'^workout/fusion/(?P\d+)/(?P\d+-\d+-\d+)/(?P\w+.*)$',views.workout_fusion_list), + url(r'^physics$',TemplateView.as_view(template_name='physics.html'),name='physics'), url(r'^workout/(\d+)/$',views.workout_view), url(r'^workout/(\d+)/addtimeplot$',views.workout_add_timeplot_view), diff --git a/rowers/views.py b/rowers/views.py index 357eac60..6a5ccf3b 100644 --- a/rowers/views.py +++ b/rowers/views.py @@ -2320,6 +2320,85 @@ def workout_comparison_list(request,id=0,message='',successmessage='', except Rower.DoesNotExist: raise Http404("User has no rower instance") +# List of workouts to compare a selected workout to +@user_passes_test(ispromember,login_url="/",redirect_field_name=None) +def workout_fusion_list(request,id=0,message='',successmessage='', + startdatestring="",enddatestring="", + startdate=timezone.now()-datetime.timedelta(days=365), + enddate=timezone.now()): + + try: + r = Rower.objects.get(user=request.user) + u = User.objects.get(id=r.user.id) + 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, + }) + + if startdatestring: + startdate = iso8601.parse_date(startdatestring) + if enddatestring: + enddate = iso8601.parse_date(enddatestring) + + startdate = datetime.datetime.combine(startdate,datetime.time()) + enddate = datetime.datetime.combine(enddate,datetime.time(23,59,59)) + enddate = enddate+datetime.timedelta(days=1) + + 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").exclude(id=id) + + 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)) + ) + + paginator = Paginator(workouts,15) # show 25 workouts per page + page = request.GET.get('page') + + try: + workouts = paginator.page(page) + except PageNotAnInteger: + workouts = paginator.page(1) + except EmptyPage: + workouts = paginator.page(paginator.num_pages) + try: + row = Workout.objects.get(id=id) + except Workout.DoesNotExist: + raise Http404("Workout doesn't exist") + + + return render(request, 'fusion_list.html', + {'id':id, + 'workout':row, + 'workouts': workouts, + 'last_name':u.last_name, + 'first_name':u.first_name, + 'message': message, + 'successmessage':successmessage, + 'dateform':dateform, + 'startdate':startdate, + 'enddate':enddate, + }) + except Rower.DoesNotExist: + raise Http404("User has no rower instance") + # Basic 'EDIT' view of workout def workout_view(request,id=0): try: @@ -5048,7 +5127,7 @@ def workout_fusion_view(request,id1=0,id2=1): }) - form = FusionMetricChoiceForm() + form = FusionMetricChoiceForm(instance=w2) return render(request, 'fusion.html', {'form':form, From b2ce05f348e77bd8b878cfa71e59b4d859ea3938 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Sun, 12 Mar 2017 10:54:19 +0100 Subject: [PATCH 10/10] Fusion Metrics form only shows metrics that are available --- rowers/forms.py | 19 +++++++++++++------ rowers/views.py | 4 ++-- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/rowers/forms.py b/rowers/forms.py index d7210ac4..8e35e8c4 100644 --- a/rowers/forms.py +++ b/rowers/forms.py @@ -298,12 +298,19 @@ class FusionMetricChoiceForm(ModelForm): super(FusionMetricChoiceForm, self).__init__(*args, **kwargs) # need to add code to remove "empty" fields - id = self.instance.id - df = dataprep.getrowdata_db(id=id) + if self.instance.id is not None: + id = self.instance.id + df = dataprep.getrowdata_db(id=id)[0] - labeldict = {key:value for key,value in metricchoices} + labeldict = {key:value for key,value in self.fields['columns'].choices} - for label in labeldict: - if df[label].std() == 0: - self.fields['columns'].choices.remove((label, labeldict[label])) + for label in labeldict: + if df.ix[:,label].std() == 0: + try: + formaxlabels.pop(label) + except KeyError: + pass + + metricchoices = list(sorted(formaxlabels.items(), key = lambda x:x[1])) + self.fields['columns'].choices = metricchoices diff --git a/rowers/views.py b/rowers/views.py index 6a5ccf3b..a5df9370 100644 --- a/rowers/views.py +++ b/rowers/views.py @@ -5079,7 +5079,7 @@ def workout_summary_restore_view(request,id,message="",successmessage=""): return HttpResponseRedirect(url) # Fuse two workouts -@login_required() +@user_passes_test(ispromember,login_url="/",redirect_field_name=None) def workout_fusion_view(request,id1=0,id2=1): try: w1 = Workout.objects.get(id=id1) @@ -5092,7 +5092,7 @@ def workout_fusion_view(request,id1=0,id2=1): raise Http404("One of the workouts doesn't exist") if request.method == 'POST': - form = FusionMetricChoiceForm(request.POST) + form = FusionMetricChoiceForm(request.POST,instance=w2) if form.is_valid(): cd = form.cleaned_data columns = cd['columns']