From 4b0707b7f4baa301de503906f21d8511879fcd86 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Wed, 14 Jun 2017 01:01:37 +0200 Subject: [PATCH 1/8] first test on breakthrough workouts (only on upload) --- rowers/dataprep.py | 39 +++++++++++++++++++++++++++++++++++++++ rowers/utils.py | 9 +++++++++ 2 files changed, 48 insertions(+) diff --git a/rowers/dataprep.py b/rowers/dataprep.py index d116dbc0..5e9c6022 100644 --- a/rowers/dataprep.py +++ b/rowers/dataprep.py @@ -43,6 +43,7 @@ from sqlalchemy import create_engine import sqlalchemy as sa import sys +import utils from utils import lbstoN from scipy.interpolate import griddata @@ -139,6 +140,34 @@ def filter_df(datadf,fieldname,value,largerthan=True): return datadf +def getsinglecp(df): + thesecs = df['TimeStamp (sec)'].max()-df['TimeStamp (sec)'].min() + if thesecs != 0: + maxt = 2*thesecs + else: + maxt = 1000. + + maxlog10 = np.log10(maxt) + logarr = np.arange(50)*maxlog10/50. + logarr = [int(10.**(la)) for la in logarr] + logarr = pd.Series(logarr) + logarr.drop_duplicates(keep='first',inplace=True) + + logarr = logarr.values + + + dfnew = pd.DataFrame({ + 'time':df['TimeStamp (sec)']-df.ix[0,'TimeStamp (sec)'], + 'power':df[' Power (watts)'] + }) + + dfnew['workoutid'] = 0 + + dfgrouped = dfnew.groupby(['workoutid']) + delta,cpvalue,avgpower = getcp(dfgrouped,logarr) + + return delta,cpvalue,avgpower + def getcp(dfgrouped,logarr): delta = [] cpvalue = [] @@ -488,6 +517,12 @@ def save_workout_database(f2,r,dosmooth=True,workouttype='rower', powerperc=powerperc,powerzones=r.powerzones) row = rdata(f2,rower=rr) + isbreakthrough = False + if workouttype == 'water': + delta,cpvalues,avgpower = getsinglecp(row.df) + if utils.isbreakthrough(delta,cpvalues,r.p0,r.p1,r.p2,r.p3): + isbreakthrough = True + dtavg = row.df['TimeStamp (sec)'].diff().mean() if dtavg < 1: @@ -643,6 +678,10 @@ def save_workout_database(f2,r,dosmooth=True,workouttype='rower', w.save() + # submit email task to send email about breakthrough workout + if isbreakthrough: + pass + if privacy == 'visible': ts = Team.objects.filter(rower=r) for t in ts: diff --git a/rowers/utils.py b/rowers/utils.py index 17dfffe6..7cc6c401 100644 --- a/rowers/utils.py +++ b/rowers/utils.py @@ -74,3 +74,12 @@ def geo_distance(lat1,lon1,lat2,lon2): bearing = math.degrees(tc1) return [distance,bearing] + + +def isbreakthrough(delta,cpvalues,p0,p1,p2,p3): + pwr = p0/(1+delta/p2) + pwr += p1/(1+delta/p3) + + res = np.sum(cpvalues>pwr) + + return res>1 From b7487f1f77c4e5f8497b4cd5d324cfcb63885f6a Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Fri, 16 Jun 2017 08:04:06 +0200 Subject: [PATCH 2/8] some optimizations --- rowers/dataprep.py | 74 +++++++++++++++++++--------------- rowers/views.py | 6 +-- rowsandall_app/settings.py | 2 - rowsandall_app/settings_dev.py | 4 ++ 4 files changed, 48 insertions(+), 38 deletions(-) diff --git a/rowers/dataprep.py b/rowers/dataprep.py index 5e9c6022..ae86fca3 100644 --- a/rowers/dataprep.py +++ b/rowers/dataprep.py @@ -179,16 +179,16 @@ def getcp(dfgrouped,logarr): ww = group['power'].copy() tmax = tt.max() - newlen = int(tmax/2000.) - print newlen,len(ww) - newt = np.arange(newlen)*tmax/float(newlen) - neww = griddata(tt.values, - ww.values, - newt,method='linear', - rescale=True) + newlen = int(tmax/5000.) + if newlen < len(tt): + newt = np.arange(newlen)*tmax/float(newlen) + ww = griddata(tt.values, + ww.values, + newt,method='linear', + rescale=True) - #tt = pd.Series(newt) - #ww = pd.Series(neww) + tt = pd.Series(newt) + ww = pd.Series(ww) try: avgpower[id] = int(ww.mean()) @@ -198,41 +198,49 @@ def getcp(dfgrouped,logarr): length = len(ww) dt = [] cpw = [] - for i in range(length-2): - w_roll = ww.rolling(i+2).mean().dropna() - if len(w_roll): - # now goes with # data points - should be fixed seconds - indexmax = w_roll.idxmax(axis=1) - try: - t_0 = tt.ix[indexmax] - t_1 = tt.ix[indexmax-i] - deltat = 1.0e-3*(t_0-t_1) - wmax = w_roll.ix[indexmax] - if not np.isnan(deltat) and not np.isnan(wmax): - dt.append(deltat) - cpw.append(wmax) - except KeyError: - pass + for i in xrange(length-2): + deltat,wmax = getmaxwattinterval(tt,ww,i) + if not np.isnan(deltat) and not np.isnan(wmax): + dt.append(deltat) + cpw.append(wmax) + dt = pd.Series(dt) cpw = pd.Series(cpw) - + if len(dt): - cpvalues = griddata(dt.values, - cpw.values, - logarr,method='linear', - rescale=True) + cpvalues = griddata(dt.values, + cpw.values, + logarr,method='linear', + rescale=True) - for cpv in cpvalues: - cpvalue.append(cpv) - for d in logarr: - delta.append(d) + for cpv in cpvalues: + cpvalue.append(cpv) + for d in logarr: + delta.append(d) delta = pd.Series(delta,name='Delta') cpvalue = pd.Series(cpvalue,name='CP') return delta,cpvalue,avgpower +def getmaxwattinterval(tt,ww,i): + w_roll = ww.rolling(i+2).mean().dropna() + if len(w_roll): + # now goes with # data points - should be fixed seconds + indexmax = w_roll.idxmax(axis=1) + try: + t_0 = tt.ix[indexmax] + t_1 = tt.ix[indexmax-i] + deltat = 1.0e-3*(t_0-t_1) + wmax = w_roll.ix[indexmax] + except KeyError: + pass + else: + wmax = 0 + deltat = 0 + + return deltat,wmax def df_resample(datadf): # time stamps must be in seconds diff --git a/rowers/views.py b/rowers/views.py index c1a8a455..325a43b1 100644 --- a/rowers/views.py +++ b/rowers/views.py @@ -7409,7 +7409,7 @@ def workout_summary_edit_view(request,id,message="",successmessage="" iunits = [] itypes = [] iresults = [] - for i in range(nrintervals): + for i in xrange(nrintervals): try: t = datetime.datetime.strptime(request.POST['intervalt_%s' % i],"%H:%M:%S.%f") except ValueError: @@ -7462,7 +7462,7 @@ def workout_summary_edit_view(request,id,message="",successmessage="" iunits = [] itypes = [] iresults = [] - for i in range(nrintervals): + for i in xrange(nrintervals): t = cd['intervalt_%s' % i] timesecs = t.total_seconds() itime += [timesecs] @@ -7496,7 +7496,7 @@ def workout_summary_edit_view(request,id,message="",successmessage="" form = SummaryStringForm() initial = {} - for i in range(nrintervals): + for i in xrange(nrintervals): initial['intervald_%s' % i] = idist[i] initial['intervalt_%s' % i] = get_time(itime[i]) initial['type_%s' % i] = itype[i] diff --git a/rowsandall_app/settings.py b/rowsandall_app/settings.py index 168952ac..2831f3ee 100644 --- a/rowsandall_app/settings.py +++ b/rowsandall_app/settings.py @@ -54,7 +54,6 @@ INSTALLED_APPS = [ 'django_rq', 'django_rq_dashboard', 'translation_manager', -# 'debug_toolbar', 'django_mailbox', 'rest_framework', 'rest_framework_swagger', @@ -90,7 +89,6 @@ MIDDLEWARE_CLASSES = [ 'django.contrib.messages.middleware.MessageMiddleware', 'async_messages.middleware.AsyncMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', -# 'debug_toolbar.middleware.DebugToolbarMiddleware', ] ROOT_URLCONF = 'rowsandall_app.urls' diff --git a/rowsandall_app/settings_dev.py b/rowsandall_app/settings_dev.py index 82d0dffe..69cbd074 100644 --- a/rowsandall_app/settings_dev.py +++ b/rowsandall_app/settings_dev.py @@ -50,6 +50,10 @@ TEMPLATES[0]['OPTIONS']['debug'] = DEBUG ALLOWED_HOSTS = ['localhost'] +INSTALLED_APPS += ['debug_toolbar',] + +MIDDLEWARE_CLASSES += ['debug_toolbar.middleware.DebugToolbarMiddleware',] + CACHES = { 'default': { 'BACKEND': 'django.core.cache.backends.filebased.FileBasedCache', From 4b8d9d02f1c8b9de38daac88775c226d632a9943 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Fri, 16 Jun 2017 08:17:30 +0200 Subject: [PATCH 3/8] constrain fit params to positive vals in CP --- rowers/interactiveplots.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rowers/interactiveplots.py b/rowers/interactiveplots.py index d1e64a9b..5474686c 100644 --- a/rowers/interactiveplots.py +++ b/rowers/interactiveplots.py @@ -619,7 +619,7 @@ def interactive_otwcpchart(powerdf,promember=0): # there is no Paul's law for OTW # Fit the data to thee parameter CP model - fitfunc = lambda pars,x: pars[0]/(1+(x/pars[2])) + pars[1]/(1+(x/pars[3])) + fitfunc = lambda pars,x: pars[0]/(1+(x/pars[2])) + pars[1]/(1+(x/abs(pars[3]))) errfunc = lambda pars,x,y: fitfunc(pars,x)-y p0 = [500,350,10,8000] @@ -636,6 +636,7 @@ def interactive_otwcpchart(powerdf,promember=0): p1 = [p0[0]/factor,p0[1]/factor,p0[2],p0[3]] + p1 = [abs(p) for p in p1] fitt = pd.Series(10**(4*np.arange(100)/100.)) fitpower = fitfunc(p1,fitt) From 252be03e473a4846a97918e84de933349020acbd Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Fri, 16 Jun 2017 08:20:07 +0200 Subject: [PATCH 4/8] added _all_ CP params to positive domain --- rowers/interactiveplots.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rowers/interactiveplots.py b/rowers/interactiveplots.py index 5474686c..7bc73bf4 100644 --- a/rowers/interactiveplots.py +++ b/rowers/interactiveplots.py @@ -619,7 +619,7 @@ def interactive_otwcpchart(powerdf,promember=0): # there is no Paul's law for OTW # Fit the data to thee parameter CP model - fitfunc = lambda pars,x: pars[0]/(1+(x/pars[2])) + pars[1]/(1+(x/abs(pars[3]))) + fitfunc = lambda pars,x: abs(pars[0])/(1+(x/abs(pars[2]))) + abs(pars[1])/(1+(x/abs(pars[3]))) errfunc = lambda pars,x,y: fitfunc(pars,x)-y p0 = [500,350,10,8000] From b0d6430e5beaae0442951dbf8410e79beb435211 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Fri, 16 Jun 2017 08:46:13 +0200 Subject: [PATCH 5/8] new user sample data --- rowers/dataprep.py | 11 ++++++++++- rowers/tasks.py | 36 ++++++++++++++++++++++++++++++++++++ rowers/views.py | 4 ++++ 3 files changed, 50 insertions(+), 1 deletion(-) diff --git a/rowers/dataprep.py b/rowers/dataprep.py index ae86fca3..634cb0b7 100644 --- a/rowers/dataprep.py +++ b/rowers/dataprep.py @@ -688,7 +688,16 @@ def save_workout_database(f2,r,dosmooth=True,workouttype='rower', # submit email task to send email about breakthrough workout if isbreakthrough: - pass + if settings.DEBUG: + res = handle_sendemail_breakthrough(w.id,r.user.email, + r.user.first_name, + r.user.last_name) + else: + res = queuehigh.enqueue( + handle_sendemail_breakthrough(w.id, + r.user.email, + r.user.first_name, + r.user.last_name)) if privacy == 'visible': ts = Team.objects.filter(rower=r) diff --git a/rowers/tasks.py b/rowers/tasks.py index 34e33726..56e20c35 100644 --- a/rowers/tasks.py +++ b/rowers/tasks.py @@ -47,6 +47,42 @@ def handle_new_workout_from_file(r,f2, return new_workout_from_file(r,f2,workouttype, title,makeprivate,notes) +# send email when a breakthrough workout is uploaded +@app.task +def handle_sendemail_breakthrough(workoutid,useremail,userfirstname,userlastname): + + # send email with attachment + subject = "A breakthrough workout on rowsandall.com" + message = "Dear "+userfirstname+",\n" + message += "Congratulations! Your recent workout has been analyzed" + message += " by Rowsandall.com and it appears your fitness," + message += " as measured by Critical Power, has improved!" + message += " Critical Power (CP) is the power that you can " + message += "sustain for a given duration. For more, see this " + message += " article in the analytics blog:\n\n" + message += " [link to article to be written]\n\n" + message += "Link to the workout http://rowsandall.com/rowers/workout/" + message += str(workoutid) + message +="/edit\n\n" + message +="To add the workout to your Ranking workouts and see the updated CP plot, click the following link:\n" + message += "http://rowsandall.com/rowers/workout/" + message += str(workoutid) + message += "/updatecp\n\n" + + message += "Best Regards, the Rowsandall Team" + + email = EmailMessage(subject, message, + 'Rowsandall ', + [useremail]) + + + res = email.send() + + # remove tcx file + os.remove(unrecognizedfile) + return 1 + + # send email to me when an unrecognized file is uploaded @app.task def handle_sendemail_unrecognized(unrecognizedfile,useremail): diff --git a/rowers/views.py b/rowers/views.py index 325a43b1..26e67f55 100644 --- a/rowers/views.py +++ b/rowers/views.py @@ -343,6 +343,10 @@ def rower_register_view(request): response = dataprep.new_workout_from_file(therower,f2, title='New User Sample Data', notes='This is an example workout to get you started') + newworkoutid = response[0] + w = Workout.objects.get(id=newworkoutid) + w.startdatetime = timezone.now() + w.save() # Create and send email fullemail = first_name + " " + last_name + " " + "<" + email + ">" From 81b6d2d9194c2dea2d5600f3e311ae060185dbca Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Fri, 16 Jun 2017 11:11:52 +0200 Subject: [PATCH 6/8] cha cha changes --- rowers/dataprep.py | 1 + 1 file changed, 1 insertion(+) diff --git a/rowers/dataprep.py b/rowers/dataprep.py index 634cb0b7..8cf854f4 100644 --- a/rowers/dataprep.py +++ b/rowers/dataprep.py @@ -688,6 +688,7 @@ def save_workout_database(f2,r,dosmooth=True,workouttype='rower', # submit email task to send email about breakthrough workout if isbreakthrough: + a_messages.info(r.user,'It looks like you have a new breakthrough workout') if settings.DEBUG: res = handle_sendemail_breakthrough(w.id,r.user.email, r.user.first_name, From a70a1e98acb82a0f5b597d96bdc2b18724d68fbd Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Fri, 16 Jun 2017 15:41:07 +0200 Subject: [PATCH 7/8] added notification to rowing physics --- rowers/dataprep.py | 125 ++++++-------------------------------------- rowers/datautils.py | 109 ++++++++++++++++++++++++++++++++++++++ rowers/models.py | 5 +- rowers/tasks.py | 11 +++- rowers/tests.py | 12 +++++ rowers/urls.py | 1 + rowers/utils.py | 1 + rowers/views.py | 27 +++++++++- 8 files changed, 177 insertions(+), 114 deletions(-) create mode 100644 rowers/datautils.py diff --git a/rowers/dataprep.py b/rowers/dataprep.py index 8cf854f4..16babd4a 100644 --- a/rowers/dataprep.py +++ b/rowers/dataprep.py @@ -36,7 +36,7 @@ import pandas as pd import numpy as np import itertools import math -from tasks import handle_sendemail_unrecognized +from tasks import handle_sendemail_unrecognized,handle_sendemail_breakthrough from django.conf import settings from sqlalchemy import create_engine @@ -44,6 +44,7 @@ import sqlalchemy as sa import sys import utils +import datautils from utils import lbstoN from scipy.interpolate import griddata @@ -140,107 +141,6 @@ def filter_df(datadf,fieldname,value,largerthan=True): return datadf -def getsinglecp(df): - thesecs = df['TimeStamp (sec)'].max()-df['TimeStamp (sec)'].min() - if thesecs != 0: - maxt = 2*thesecs - else: - maxt = 1000. - - maxlog10 = np.log10(maxt) - logarr = np.arange(50)*maxlog10/50. - logarr = [int(10.**(la)) for la in logarr] - logarr = pd.Series(logarr) - logarr.drop_duplicates(keep='first',inplace=True) - - logarr = logarr.values - - - dfnew = pd.DataFrame({ - 'time':df['TimeStamp (sec)']-df.ix[0,'TimeStamp (sec)'], - 'power':df[' Power (watts)'] - }) - - dfnew['workoutid'] = 0 - - dfgrouped = dfnew.groupby(['workoutid']) - delta,cpvalue,avgpower = getcp(dfgrouped,logarr) - - return delta,cpvalue,avgpower - -def getcp(dfgrouped,logarr): - delta = [] - cpvalue = [] - avgpower = {} - #avgpower[0] = 0 - - for id,group in dfgrouped: - tt = group['time'].copy() - ww = group['power'].copy() - - tmax = tt.max() - newlen = int(tmax/5000.) - if newlen < len(tt): - newt = np.arange(newlen)*tmax/float(newlen) - ww = griddata(tt.values, - ww.values, - newt,method='linear', - rescale=True) - - tt = pd.Series(newt) - ww = pd.Series(ww) - - try: - avgpower[id] = int(ww.mean()) - except ValueError: - avgpower[id] = '---' - if not np.isnan(ww.mean()): - length = len(ww) - dt = [] - cpw = [] - for i in xrange(length-2): - deltat,wmax = getmaxwattinterval(tt,ww,i) - if not np.isnan(deltat) and not np.isnan(wmax): - dt.append(deltat) - cpw.append(wmax) - - - - dt = pd.Series(dt) - cpw = pd.Series(cpw) - if len(dt): - - cpvalues = griddata(dt.values, - cpw.values, - logarr,method='linear', - rescale=True) - - for cpv in cpvalues: - cpvalue.append(cpv) - for d in logarr: - delta.append(d) - - delta = pd.Series(delta,name='Delta') - cpvalue = pd.Series(cpvalue,name='CP') - return delta,cpvalue,avgpower - -def getmaxwattinterval(tt,ww,i): - w_roll = ww.rolling(i+2).mean().dropna() - if len(w_roll): - # now goes with # data points - should be fixed seconds - indexmax = w_roll.idxmax(axis=1) - try: - t_0 = tt.ix[indexmax] - t_1 = tt.ix[indexmax-i] - deltat = 1.0e-3*(t_0-t_1) - wmax = w_roll.ix[indexmax] - except KeyError: - pass - else: - wmax = 0 - deltat = 0 - - return deltat,wmax def df_resample(datadf): # time stamps must be in seconds @@ -527,7 +427,7 @@ def save_workout_database(f2,r,dosmooth=True,workouttype='rower', isbreakthrough = False if workouttype == 'water': - delta,cpvalues,avgpower = getsinglecp(row.df) + delta,cpvalues,avgpower = datautils.getsinglecp(row.df) if utils.isbreakthrough(delta,cpvalues,r.p0,r.p1,r.p2,r.p3): isbreakthrough = True @@ -689,16 +589,21 @@ def save_workout_database(f2,r,dosmooth=True,workouttype='rower', # submit email task to send email about breakthrough workout if isbreakthrough: a_messages.info(r.user,'It looks like you have a new breakthrough workout') - if settings.DEBUG: - res = handle_sendemail_breakthrough(w.id,r.user.email, + if settings.DEBUG and r.getemailnotifications: + res = handle_sendemail_breakthrough.delay(w.id,r.user.email, r.user.first_name, r.user.last_name) + elif r.getemailnotifications: + try: + res = queuehigh.enqueue( + handle_sendemail_breakthrough(w.id, + r.user.email, + r.user.first_name, + r.user.last_name)) + except AttributeError: + pass else: - res = queuehigh.enqueue( - handle_sendemail_breakthrough(w.id, - r.user.email, - r.user.first_name, - r.user.last_name)) + pass if privacy == 'visible': ts = Team.objects.filter(rower=r) diff --git a/rowers/datautils.py b/rowers/datautils.py new file mode 100644 index 00000000..9118dc2c --- /dev/null +++ b/rowers/datautils.py @@ -0,0 +1,109 @@ +import pandas as pd +import numpy as np +from scipy.interpolate import griddata + +def getsinglecp(df): + thesecs = df['TimeStamp (sec)'].max()-df['TimeStamp (sec)'].min() + if thesecs != 0: + maxt = 2*thesecs + else: + maxt = 1000. + + maxlog10 = np.log10(maxt) + logarr = np.arange(50)*maxlog10/50. + logarr = [int(10.**(la)) for la in logarr] + logarr = pd.Series(logarr) + logarr.drop_duplicates(keep='first',inplace=True) + + logarr = logarr.values + + + dfnew = pd.DataFrame({ + 'time':1000*(df['TimeStamp (sec)']-df.ix[0,'TimeStamp (sec)']), + 'power':df[' Power (watts)'] + }) + + dfnew['workoutid'] = 0 + + dfgrouped = dfnew.groupby(['workoutid']) + delta,cpvalue,avgpower = getcp(dfgrouped,logarr) + + return delta,cpvalue,avgpower + +def getcp(dfgrouped,logarr): + delta = [] + cpvalue = [] + avgpower = {} + #avgpower[0] = 0 + + for id,group in dfgrouped: + tt = group['time'].copy() + ww = group['power'].copy() + + tmax = tt.max() + if tmax > 500000: + newlen = int(tmax/5000.) + else: + newlen = len(tt) + if newlen < len(tt): + newt = np.arange(newlen)*tmax/float(newlen) + ww = griddata(tt.values, + ww.values, + newt,method='linear', + rescale=True) + + tt = pd.Series(newt) + ww = pd.Series(ww) + + + try: + avgpower[id] = int(ww.mean()) + except ValueError: + avgpower[id] = '---' + if not np.isnan(ww.mean()): + length = len(ww) + dt = [] + cpw = [] + for i in xrange(length-2): + deltat,wmax = getmaxwattinterval(tt,ww,i) + if not np.isnan(deltat) and not np.isnan(wmax): + dt.append(deltat) + cpw.append(wmax) + + + + dt = pd.Series(dt) + cpw = pd.Series(cpw) + if len(dt): + + cpvalues = griddata(dt.values, + cpw.values, + logarr,method='linear', + rescale=True) + + for cpv in cpvalues: + cpvalue.append(cpv) + for d in logarr: + delta.append(d) + + delta = pd.Series(delta,name='Delta') + cpvalue = pd.Series(cpvalue,name='CP') + return delta,cpvalue,avgpower + +def getmaxwattinterval(tt,ww,i): + w_roll = ww.rolling(i+2).mean().dropna() + if len(w_roll): + # now goes with # data points - should be fixed seconds + indexmax = w_roll.idxmax(axis=1) + try: + t_0 = tt.ix[indexmax] + t_1 = tt.ix[indexmax-i] + deltat = 1.0e-3*(t_0-t_1) + wmax = w_roll.ix[indexmax] + except KeyError: + pass + else: + wmax = 0 + deltat = 0 + + return deltat,wmax diff --git a/rowers/models.py b/rowers/models.py index bdde5013..2eb2ec3c 100644 --- a/rowers/models.py +++ b/rowers/models.py @@ -232,6 +232,9 @@ class Rower(models.Model): ('hidden','Hidden'), ) + getemailnotifications = models.BooleanField(default=True, + verbose_name='Receive email notifications') + rowerplan = models.CharField(default='basic',max_length=30, choices=plans) @@ -746,7 +749,7 @@ class RowerPowerZonesForm(ModelForm): class AccountRowerForm(ModelForm): class Meta: model = Rower - fields = ['weightcategory'] + fields = ['weightcategory','getemailnotifications'] class UserForm(ModelForm): class Meta: diff --git a/rowers/tasks.py b/rowers/tasks.py index 56e20c35..8663b845 100644 --- a/rowers/tasks.py +++ b/rowers/tasks.py @@ -69,6 +69,8 @@ def handle_sendemail_breakthrough(workoutid,useremail,userfirstname,userlastname message += str(workoutid) message += "/updatecp\n\n" + message += "To opt out of these email notifications, deselect the checkbox on your Profile page under Account Information.\n\n" + message += "Best Regards, the Rowsandall Team" email = EmailMessage(subject, message, @@ -79,7 +81,6 @@ def handle_sendemail_breakthrough(workoutid,useremail,userfirstname,userlastname res = email.send() # remove tcx file - os.remove(unrecognizedfile) return 1 @@ -177,7 +178,7 @@ def handle_sendemailcsv(first_name,last_name,email,csvfile): # Calculate wind and stream corrections for OTW rowing @app.task def handle_otwsetpower(f1,boattype,weightvalue, - first_name,last_name,email,workoutid, + first_name,last_name,email,workoutid,ps=[1,1,1,1], debug=False): try: rowdata = rdata(f1) @@ -219,6 +220,12 @@ def handle_otwsetpower(f1,boattype,weightvalue, rowdata.write_csv(f1,gzip=True) update_strokedata(workoutid,rowdata.df,debug=debug) + delta,cpvalues,avgpower = datautils.getsinglecp(rowdata.df) + if utils.isbreakthrough(delta,cpvalues,ps[0],ps[1],ps[2],ps[3]): + handle_sendemail_breakthrough(workoutid,email, + first_name, + last_name) + # send email fullemail = first_name + " " + last_name + " " + "<" + email + ">" subject = "Your Rowsandall OTW calculations are ready" diff --git a/rowers/tests.py b/rowers/tests.py index 8ddbfaed..415154ef 100644 --- a/rowers/tests.py +++ b/rowers/tests.py @@ -68,6 +68,9 @@ class C2Objects(DjangoTestCase): u = User.objects.create_user('john', 'sander@ds.ds', 'koeinsloot') + u.first_name = 'John' + u.last_name = 'Sander' + u.save() r = Rower.objects.create(user=u) res = add_workout_from_strokedata(u,1,data,strokedata,source='c2') @@ -88,6 +91,9 @@ class C2Objects(DjangoTestCase): u = User.objects.create_user('john', 'sander@ds.ds', 'koeinsloot') + u.first_name = 'John' + u.last_name = 'Sander' + u.save() r = Rower.objects.create(user=u) res = add_workout_from_strokedata(u,1,data,strokedata,source='c2') @@ -162,6 +168,9 @@ class StravaObjects(DjangoTestCase): u = User.objects.create_user('john', 'sander@ds.ds', 'koeinsloot') + u.first_name = 'John' + u.last_name = 'Sander' + u.save() r = Rower.objects.create(user=u) res = add_workout_from_strokedata(u,1,workoutsummary,strokedata, @@ -235,6 +244,9 @@ class StravaObjects(DjangoTestCase): u = User.objects.create_user('john', 'sander@ds.ds', 'koeinsloot') + u.first_name = 'John' + u.last_name = 'Sander' + u.save() r = Rower.objects.create(user=u) res = add_workout_from_strokedata(u,1,workoutsummary,strokedata, diff --git a/rowers/urls.py b/rowers/urls.py index e850a4c6..844f9ae0 100644 --- a/rowers/urls.py +++ b/rowers/urls.py @@ -183,6 +183,7 @@ urlpatterns = [ url(r'^workout/compare/(?P\d+)/(?P\d+-\d+-\d+)/(?P\w+.*)$',views.workout_comparison_list), url(r'^workout/(?P\d+)/edit$',views.workout_edit_view), url(r'^workout/(?P\d+)/setprivate$',views.workout_setprivate_view), + url(r'^workout/(?P\d+)/updatecp$',views.workout_update_cp_view), url(r'^workout/(?P\d+)/makepublic$',views.workout_makepublic_view), url(r'^workout/(?P\d+)/geeky$',views.workout_geeky_view), url(r'^workout/(?P\d+)/advanced$',views.workout_advanced_view), diff --git a/rowers/utils.py b/rowers/utils.py index 7cc6c401..22c4680f 100644 --- a/rowers/utils.py +++ b/rowers/utils.py @@ -77,6 +77,7 @@ def geo_distance(lat1,lon1,lat2,lon2): def isbreakthrough(delta,cpvalues,p0,p1,p2,p3): + pwr = p0/(1+delta/p2) pwr += p1/(1+delta/p3) diff --git a/rowers/views.py b/rowers/views.py index 26e67f55..3ca3cffb 100644 --- a/rowers/views.py +++ b/rowers/views.py @@ -274,6 +274,8 @@ from utils import ( str2bool ) +import datautils + from rowers.models import checkworkoutuser # Check if a user is a Coach member @@ -2778,6 +2780,27 @@ def rankings_view(request,theuser=0, 'teams':get_my_teams(request.user), }) +@user_passes_test(ispromember,login_url="/",redirect_field_name=None) +def workout_update_cp_view(request,id=0): + try: + row = Workout.objects.get(id=id) + except Workout.DoesNotExist: + raise Http404("Workout doesn't exist") + + if (checkworkoutuser(request.user,row)==False): + message = "You are not allowed to edit this workout" + messages.error(request,message) + url = reverse(workouts_view) + + return HttpResponseRedirect(url) + + row.rankingpiece = True + row.save() + + url = reverse(otwrankings_view) + + return HttpResponseRedirect(url) + # Show ranking distances including predicted paces @user_passes_test(ispromember,login_url="/",redirect_field_name=None) def otwrankings_view(request,theuser=0, @@ -2919,7 +2942,7 @@ def otwrankings_view(request,theuser=0, dfgrouped = df.groupby(['workoutid']) - delta,cpvalue,avgpower = dataprep.getcp(dfgrouped,logarr) + delta,cpvalue,avgpower = datautils.getcp(dfgrouped,logarr) powerdf = pd.DataFrame({ @@ -7779,6 +7802,7 @@ def rower_edit_view(request,message=""): last_name = ucd['last_name'] email = ucd['email'] weightcategory = cd['weightcategory'] + getemailnotifications = cd['getemailnotifications'] u = request.user if len(first_name): u.first_name = first_name @@ -7788,6 +7812,7 @@ def rower_edit_view(request,message=""): u.save() r = getrower(u) r.weightcategory = weightcategory + r.getemailnotifications = getemailnotifications r.save() form = RowerForm(instance=r) powerform = RowerPowerForm(instance=r) From 25ea8e9d8894c8080bc0278e40d3f16a10629761 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Fri, 16 Jun 2017 16:37:34 +0200 Subject: [PATCH 8/8] debugged tasks --- rowers/tasks.py | 4 ++-- rowsanda_107501 | Bin 461824 -> 512000 bytes 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/rowers/tasks.py b/rowers/tasks.py index 8663b845..4d488f7c 100644 --- a/rowers/tasks.py +++ b/rowers/tasks.py @@ -29,8 +29,8 @@ from rowers.dataprepnodjango import new_workout_from_file from django.core.mail import send_mail, BadHeaderError,EmailMessage - - +import datautils +import utils # testing task @app.task diff --git a/rowsanda_107501 b/rowsanda_107501 index 93b5d65390548c22a4c18b81fea0c87ed29bf6de..c733aeb4ae830f8a27775f42f9acbbd5d90a2827 100644 GIT binary patch delta 45284 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 delta 316 zcmZozAm1=UW`Z;;Cj$fH;fV_NjGP-2Cb4sLl`=3ebhHaM|6*_d#m>0>7dsP|2uB_x z4^WtK-t+|%nB}%>$uP+?GbT(A=x2^(e%2n>zD%BJ`!adv=p1gQHikHeT6>UMyXiVT z%=@-;^fEUxG9H*dVG?sZvv5z+^fmp=A#6YsU-gJjck5?P2AMM5N1iE!hZQJV#i#ZsK&*3s6P#;;$VN`c7ut``x)VePH*dD-qkjR zxorwd+Z5JqQ`lH?rn78ei`|;YzL^>E69D9#UV#7r