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/dataprep.py b/rowers/dataprep.py index cac4d28a..5d5bf406 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 @@ -897,7 +955,48 @@ def smalldataprep(therows,xparam,yparam1,yparam2): pass return df - + +# data fusion +def datafusion(id1,id2,columns,offset): + df1,w1 = getrowdata_db(id=id1) + df1 = df1.drop([#'cumdist', + 'hr_ut2', + 'hr_ut1', + 'hr_at', + 'hr_tr', + 'hr_an', + 'hr_max', + 'ftime', + 'fpace', + 'workoutid', + 'id'], + 1,errors='ignore') + + 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', + limit=10) + df.fillna(method='bfill',inplace=True) + + df['time'] = df['time']/1000. + df['pace'] = df['pace']/1000. + df['cum_dist'] = df['cumdist'] + + 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/forms.py b/rowers/forms.py index c4926376..8e35e8c4 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 @@ -259,8 +261,9 @@ class WorkoutMultipleCompareForm(forms.Form): from rowers.interactiveplots import axlabels -axlabels.pop('None') -axlabels = list(sorted(axlabels.items(), key = lambda x:x[1])) +formaxlabels = axlabels.copy() +formaxlabels.pop('None') +parchoices = list(sorted(formaxlabels.items(), key = lambda x:x[1])) class ChartParamChoiceForm(forms.Form): @@ -268,7 +271,46 @@ 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()) + +formaxlabels.pop('time') +metricchoices = list(sorted(formaxlabels.items(), key = lambda x:x[1])) + +class FusionMetricChoiceForm(ModelForm): + class Meta: + model = Workout + fields = [] + + 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()) + + def __init__(self, *args, **kwargs): + super(FusionMetricChoiceForm, self).__init__(*args, **kwargs) + # need to add code to remove "empty" fields + + 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 self.fields['columns'].choices} + + 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/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/stravastuff.py b/rowers/stravastuff.py index 98c58ce5..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,39 +151,43 @@ 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: 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") 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() @@ -193,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)) 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 new file mode 100644 index 00000000..52de737e --- /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.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. +

+

+ 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/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 24945f8e..6a524940 100644 --- a/rowers/urls.py +++ b/rowers/urls.py @@ -189,6 +189,10 @@ 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'^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 2d66c2d1..a5df9370 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 ( @@ -2319,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: @@ -2626,7 +2706,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 @@ -2741,7 +2821,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) @@ -2901,7 +2981,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 @@ -3218,7 +3298,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 @@ -3537,7 +3617,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 @@ -3578,7 +3658,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 @@ -3659,7 +3739,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() @@ -3776,6 +3856,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") @@ -3825,12 +3906,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" @@ -3855,7 +3935,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 @@ -3924,7 +4004,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, @@ -3981,7 +4061,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, @@ -4039,7 +4119,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, @@ -4095,7 +4175,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, @@ -4152,7 +4232,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, @@ -4207,7 +4287,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, @@ -4264,7 +4344,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, @@ -4462,7 +4542,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] @@ -4958,7 +5038,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, @@ -4998,6 +5078,63 @@ def workout_summary_restore_view(request,id,message="",successmessage=""): ) return HttpResponseRedirect(url) +# Fuse two workouts +@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) + 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,instance=w2) + if form.is_valid(): + cd = form.cleaned_data + columns = cd['columns'] + 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) + 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: + return render(request, 'fusion.html', + {'form':form, + 'workout1':w1, + 'workout2':w2, + }) + + + form = FusionMetricChoiceForm(instance=w2) + + return render(request, 'fusion.html', + {'form':form, + 'workout1':w1, + 'workout2':w2, + }) + # Edit the splits/summary @login_required() @@ -5013,7 +5150,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,