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 615a70d9..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 @@ -1021,7 +1120,10 @@ def dataprep(rowdatadf,id=0,bands=True,barchart=True,otwpower=True, drivelength = arclength else: drivelength = driveenergy/(averageforce*4.44822) + slip = rowdatadf.ix[:,'slip'] + totalangle = finish-catch + effectiveangle = finish-wash-catch-slip if windowsize > 3 and windowsize
-
+

{% 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/comparisonchart2.html b/rowers/templates/comparisonchart2.html index c6f664a0..a07b41bd 100644 --- a/rowers/templates/comparisonchart2.html +++ b/rowers/templates/comparisonchart2.html @@ -6,144 +6,134 @@ {% block content %} - - + + - {{ interactiveplot |safe }} +{{ interactiveplot |safe }} - - + +

 

+
+ - -
- - -
- -
-
- Line Plot -
- -
- +
+ +
+
+ Line Plot +
+ +
+
- {{ the_div|safe }} + {{ the_div|safe }}
diff --git a/rowers/templates/developers.html b/rowers/templates/developers.html index 268fed20..b81bb596 100644 --- a/rowers/templates/developers.html +++ b/rowers/templates/developers.html @@ -47,13 +47,13 @@
  • Advantages
      -
    • It may take up to five minutes for the workout to show up - on the site.
    • +
    • It's a simple process, which can be automated.
  • Disadvantages
      -
    • It's a simple process, which can be automated.
    • +
    • It may take up to five minutes for the workout to show up + on the site.
@@ -86,7 +86,7 @@
  • The API is not stable and not fully tested yet.
  • You need to register your app with us. We can revoke your permissions if you misuse them.
  • -
  • The first time user must grant permissions to your app.
  • +
  • The user user must grant permissions to your app.
  • You need to manage authorization tokens.
  • diff --git a/rowers/templates/flexchart3otw.html b/rowers/templates/flexchart3otw.html index 4697f3a7..52a8eeaa 100644 --- a/rowers/templates/flexchart3otw.html +++ b/rowers/templates/flexchart3otw.html @@ -51,122 +51,70 @@ -