Private
Public Access
1
0

Merge branch 'feature/cpnotifications' into develop

This commit is contained in:
Sander Roosendaal
2017-06-16 16:38:30 +02:00
12 changed files with 249 additions and 77 deletions

View File

@@ -36,13 +36,15 @@ import pandas as pd
import numpy as np import numpy as np
import itertools import itertools
import math import math
from tasks import handle_sendemail_unrecognized from tasks import handle_sendemail_unrecognized,handle_sendemail_breakthrough
from django.conf import settings from django.conf import settings
from sqlalchemy import create_engine from sqlalchemy import create_engine
import sqlalchemy as sa import sqlalchemy as sa
import sys import sys
import utils
import datautils
from utils import lbstoN from utils import lbstoN
from scipy.interpolate import griddata from scipy.interpolate import griddata
@@ -139,71 +141,6 @@ def filter_df(datadf,fieldname,value,largerthan=True):
return datadf return datadf
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/2000.)
print newlen,len(ww)
newt = np.arange(newlen)*tmax/float(newlen)
neww = griddata(tt.values,
ww.values,
newt,method='linear',
rescale=True)
#tt = pd.Series(newt)
#ww = pd.Series(neww)
try:
avgpower[id] = int(ww.mean())
except ValueError:
avgpower[id] = '---'
if not np.isnan(ww.mean()):
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
dt = pd.Series(dt)
cpw = pd.Series(cpw)
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 df_resample(datadf): def df_resample(datadf):
# time stamps must be in seconds # time stamps must be in seconds
@@ -488,6 +425,12 @@ def save_workout_database(f2,r,dosmooth=True,workouttype='rower',
powerperc=powerperc,powerzones=r.powerzones) powerperc=powerperc,powerzones=r.powerzones)
row = rdata(f2,rower=rr) row = rdata(f2,rower=rr)
isbreakthrough = False
if workouttype == 'water':
delta,cpvalues,avgpower = datautils.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() dtavg = row.df['TimeStamp (sec)'].diff().mean()
if dtavg < 1: if dtavg < 1:
@@ -643,6 +586,25 @@ def save_workout_database(f2,r,dosmooth=True,workouttype='rower',
w.save() w.save()
# 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 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:
pass
if privacy == 'visible': if privacy == 'visible':
ts = Team.objects.filter(rower=r) ts = Team.objects.filter(rower=r)
for t in ts: for t in ts:

109
rowers/datautils.py Normal file
View File

@@ -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

View File

@@ -619,7 +619,7 @@ def interactive_otwcpchart(powerdf,promember=0):
# there is no Paul's law for OTW # there is no Paul's law for OTW
# Fit the data to thee parameter CP model # 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: 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 errfunc = lambda pars,x,y: fitfunc(pars,x)-y
p0 = [500,350,10,8000] 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 = [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.)) fitt = pd.Series(10**(4*np.arange(100)/100.))
fitpower = fitfunc(p1,fitt) fitpower = fitfunc(p1,fitt)

View File

@@ -232,6 +232,9 @@ class Rower(models.Model):
('hidden','Hidden'), ('hidden','Hidden'),
) )
getemailnotifications = models.BooleanField(default=True,
verbose_name='Receive email notifications')
rowerplan = models.CharField(default='basic',max_length=30, rowerplan = models.CharField(default='basic',max_length=30,
choices=plans) choices=plans)
@@ -746,7 +749,7 @@ class RowerPowerZonesForm(ModelForm):
class AccountRowerForm(ModelForm): class AccountRowerForm(ModelForm):
class Meta: class Meta:
model = Rower model = Rower
fields = ['weightcategory'] fields = ['weightcategory','getemailnotifications']
class UserForm(ModelForm): class UserForm(ModelForm):
class Meta: class Meta:

View File

@@ -29,8 +29,8 @@ from rowers.dataprepnodjango import new_workout_from_file
from django.core.mail import send_mail, BadHeaderError,EmailMessage from django.core.mail import send_mail, BadHeaderError,EmailMessage
import datautils
import utils
# testing task # testing task
@app.task @app.task
@@ -47,6 +47,43 @@ def handle_new_workout_from_file(r,f2,
return new_workout_from_file(r,f2,workouttype, return new_workout_from_file(r,f2,workouttype,
title,makeprivate,notes) 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 += "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,
'Rowsandall <info@rowsandall.com>',
[useremail])
res = email.send()
# remove tcx file
return 1
# send email to me when an unrecognized file is uploaded # send email to me when an unrecognized file is uploaded
@app.task @app.task
def handle_sendemail_unrecognized(unrecognizedfile,useremail): def handle_sendemail_unrecognized(unrecognizedfile,useremail):
@@ -141,7 +178,7 @@ def handle_sendemailcsv(first_name,last_name,email,csvfile):
# Calculate wind and stream corrections for OTW rowing # Calculate wind and stream corrections for OTW rowing
@app.task @app.task
def handle_otwsetpower(f1,boattype,weightvalue, 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): debug=False):
try: try:
rowdata = rdata(f1) rowdata = rdata(f1)
@@ -183,6 +220,12 @@ def handle_otwsetpower(f1,boattype,weightvalue,
rowdata.write_csv(f1,gzip=True) rowdata.write_csv(f1,gzip=True)
update_strokedata(workoutid,rowdata.df,debug=debug) 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 # send email
fullemail = first_name + " " + last_name + " " + "<" + email + ">" fullemail = first_name + " " + last_name + " " + "<" + email + ">"
subject = "Your Rowsandall OTW calculations are ready" subject = "Your Rowsandall OTW calculations are ready"

View File

@@ -68,6 +68,9 @@ class C2Objects(DjangoTestCase):
u = User.objects.create_user('john', u = User.objects.create_user('john',
'sander@ds.ds', 'sander@ds.ds',
'koeinsloot') 'koeinsloot')
u.first_name = 'John'
u.last_name = 'Sander'
u.save()
r = Rower.objects.create(user=u) r = Rower.objects.create(user=u)
res = add_workout_from_strokedata(u,1,data,strokedata,source='c2') res = add_workout_from_strokedata(u,1,data,strokedata,source='c2')
@@ -88,6 +91,9 @@ class C2Objects(DjangoTestCase):
u = User.objects.create_user('john', u = User.objects.create_user('john',
'sander@ds.ds', 'sander@ds.ds',
'koeinsloot') 'koeinsloot')
u.first_name = 'John'
u.last_name = 'Sander'
u.save()
r = Rower.objects.create(user=u) r = Rower.objects.create(user=u)
res = add_workout_from_strokedata(u,1,data,strokedata,source='c2') res = add_workout_from_strokedata(u,1,data,strokedata,source='c2')
@@ -162,6 +168,9 @@ class StravaObjects(DjangoTestCase):
u = User.objects.create_user('john', u = User.objects.create_user('john',
'sander@ds.ds', 'sander@ds.ds',
'koeinsloot') 'koeinsloot')
u.first_name = 'John'
u.last_name = 'Sander'
u.save()
r = Rower.objects.create(user=u) r = Rower.objects.create(user=u)
res = add_workout_from_strokedata(u,1,workoutsummary,strokedata, res = add_workout_from_strokedata(u,1,workoutsummary,strokedata,
@@ -235,6 +244,9 @@ class StravaObjects(DjangoTestCase):
u = User.objects.create_user('john', u = User.objects.create_user('john',
'sander@ds.ds', 'sander@ds.ds',
'koeinsloot') 'koeinsloot')
u.first_name = 'John'
u.last_name = 'Sander'
u.save()
r = Rower.objects.create(user=u) r = Rower.objects.create(user=u)
res = add_workout_from_strokedata(u,1,workoutsummary,strokedata, res = add_workout_from_strokedata(u,1,workoutsummary,strokedata,

View File

@@ -183,6 +183,7 @@ urlpatterns = [
url(r'^workout/compare/(?P<id>\d+)/(?P<startdatestring>\d+-\d+-\d+)/(?P<enddatestring>\w+.*)$',views.workout_comparison_list), url(r'^workout/compare/(?P<id>\d+)/(?P<startdatestring>\d+-\d+-\d+)/(?P<enddatestring>\w+.*)$',views.workout_comparison_list),
url(r'^workout/(?P<id>\d+)/edit$',views.workout_edit_view), url(r'^workout/(?P<id>\d+)/edit$',views.workout_edit_view),
url(r'^workout/(?P<id>\d+)/setprivate$',views.workout_setprivate_view), url(r'^workout/(?P<id>\d+)/setprivate$',views.workout_setprivate_view),
url(r'^workout/(?P<id>\d+)/updatecp$',views.workout_update_cp_view),
url(r'^workout/(?P<id>\d+)/makepublic$',views.workout_makepublic_view), url(r'^workout/(?P<id>\d+)/makepublic$',views.workout_makepublic_view),
url(r'^workout/(?P<id>\d+)/geeky$',views.workout_geeky_view), url(r'^workout/(?P<id>\d+)/geeky$',views.workout_geeky_view),
url(r'^workout/(?P<id>\d+)/advanced$',views.workout_advanced_view), url(r'^workout/(?P<id>\d+)/advanced$',views.workout_advanced_view),

View File

@@ -74,3 +74,13 @@ def geo_distance(lat1,lon1,lat2,lon2):
bearing = math.degrees(tc1) bearing = math.degrees(tc1)
return [distance,bearing] 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

View File

@@ -274,6 +274,8 @@ from utils import (
str2bool str2bool
) )
import datautils
from rowers.models import checkworkoutuser from rowers.models import checkworkoutuser
# Check if a user is a Coach member # Check if a user is a Coach member
@@ -343,6 +345,10 @@ def rower_register_view(request):
response = dataprep.new_workout_from_file(therower,f2, response = dataprep.new_workout_from_file(therower,f2,
title='New User Sample Data', title='New User Sample Data',
notes='This is an example workout to get you started') 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 # Create and send email
fullemail = first_name + " " + last_name + " " + "<" + email + ">" fullemail = first_name + " " + last_name + " " + "<" + email + ">"
@@ -2774,6 +2780,27 @@ def rankings_view(request,theuser=0,
'teams':get_my_teams(request.user), '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 # Show ranking distances including predicted paces
@user_passes_test(ispromember,login_url="/",redirect_field_name=None) @user_passes_test(ispromember,login_url="/",redirect_field_name=None)
def otwrankings_view(request,theuser=0, def otwrankings_view(request,theuser=0,
@@ -2915,7 +2942,7 @@ def otwrankings_view(request,theuser=0,
dfgrouped = df.groupby(['workoutid']) dfgrouped = df.groupby(['workoutid'])
delta,cpvalue,avgpower = dataprep.getcp(dfgrouped,logarr) delta,cpvalue,avgpower = datautils.getcp(dfgrouped,logarr)
powerdf = pd.DataFrame({ powerdf = pd.DataFrame({
@@ -7409,7 +7436,7 @@ def workout_summary_edit_view(request,id,message="",successmessage=""
iunits = [] iunits = []
itypes = [] itypes = []
iresults = [] iresults = []
for i in range(nrintervals): for i in xrange(nrintervals):
try: try:
t = datetime.datetime.strptime(request.POST['intervalt_%s' % i],"%H:%M:%S.%f") t = datetime.datetime.strptime(request.POST['intervalt_%s' % i],"%H:%M:%S.%f")
except ValueError: except ValueError:
@@ -7462,7 +7489,7 @@ def workout_summary_edit_view(request,id,message="",successmessage=""
iunits = [] iunits = []
itypes = [] itypes = []
iresults = [] iresults = []
for i in range(nrintervals): for i in xrange(nrintervals):
t = cd['intervalt_%s' % i] t = cd['intervalt_%s' % i]
timesecs = t.total_seconds() timesecs = t.total_seconds()
itime += [timesecs] itime += [timesecs]
@@ -7496,7 +7523,7 @@ def workout_summary_edit_view(request,id,message="",successmessage=""
form = SummaryStringForm() form = SummaryStringForm()
initial = {} initial = {}
for i in range(nrintervals): for i in xrange(nrintervals):
initial['intervald_%s' % i] = idist[i] initial['intervald_%s' % i] = idist[i]
initial['intervalt_%s' % i] = get_time(itime[i]) initial['intervalt_%s' % i] = get_time(itime[i])
initial['type_%s' % i] = itype[i] initial['type_%s' % i] = itype[i]
@@ -7775,6 +7802,7 @@ def rower_edit_view(request,message=""):
last_name = ucd['last_name'] last_name = ucd['last_name']
email = ucd['email'] email = ucd['email']
weightcategory = cd['weightcategory'] weightcategory = cd['weightcategory']
getemailnotifications = cd['getemailnotifications']
u = request.user u = request.user
if len(first_name): if len(first_name):
u.first_name = first_name u.first_name = first_name
@@ -7784,6 +7812,7 @@ def rower_edit_view(request,message=""):
u.save() u.save()
r = getrower(u) r = getrower(u)
r.weightcategory = weightcategory r.weightcategory = weightcategory
r.getemailnotifications = getemailnotifications
r.save() r.save()
form = RowerForm(instance=r) form = RowerForm(instance=r)
powerform = RowerPowerForm(instance=r) powerform = RowerPowerForm(instance=r)

Binary file not shown.

View File

@@ -54,7 +54,6 @@ INSTALLED_APPS = [
'django_rq', 'django_rq',
'django_rq_dashboard', 'django_rq_dashboard',
'translation_manager', 'translation_manager',
# 'debug_toolbar',
'django_mailbox', 'django_mailbox',
'rest_framework', 'rest_framework',
'rest_framework_swagger', 'rest_framework_swagger',
@@ -90,7 +89,6 @@ MIDDLEWARE_CLASSES = [
'django.contrib.messages.middleware.MessageMiddleware', 'django.contrib.messages.middleware.MessageMiddleware',
'async_messages.middleware.AsyncMiddleware', 'async_messages.middleware.AsyncMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware',
# 'debug_toolbar.middleware.DebugToolbarMiddleware',
] ]
ROOT_URLCONF = 'rowsandall_app.urls' ROOT_URLCONF = 'rowsandall_app.urls'

View File

@@ -50,6 +50,10 @@ TEMPLATES[0]['OPTIONS']['debug'] = DEBUG
ALLOWED_HOSTS = ['localhost'] ALLOWED_HOSTS = ['localhost']
INSTALLED_APPS += ['debug_toolbar',]
MIDDLEWARE_CLASSES += ['debug_toolbar.middleware.DebugToolbarMiddleware',]
CACHES = { CACHES = {
'default': { 'default': {
'BACKEND': 'django.core.cache.backends.filebased.FileBasedCache', 'BACKEND': 'django.core.cache.backends.filebased.FileBasedCache',