diff --git a/rowers/dataprep.py b/rowers/dataprep.py index 8e907486..3d177657 100644 --- a/rowers/dataprep.py +++ b/rowers/dataprep.py @@ -515,6 +515,23 @@ def paceformatsecs(values): return out +def fitnessmetric_to_sql(m,table='powertimefitnessmetric',debug=False): + engine = create_engine(database_url, echo=False) + columns = ', '.join(m.keys()) + placeholders = ", ".join(["?"] * len(m)) + + query = "INSERT into %s ( %s ) Values (%s)" % (table, columns, placeholders) + + values = tuple(m[key] for key in m.keys()) + with engine.connect() as conn, conn.begin(): + result = conn.execute(query,values) + + conn.close() + engine.dispose() + + return 1 + + def getcpdata_sql(rower_id,table='cpdata'): engine = create_engine(database_url, echo=False) query = sa.text('SELECT * from {table} WHERE user={rower_id};'.format( diff --git a/rowers/dataprepnodjango.py b/rowers/dataprepnodjango.py index 3ce3e6fb..56da1378 100644 --- a/rowers/dataprepnodjango.py +++ b/rowers/dataprepnodjango.py @@ -495,6 +495,26 @@ def getsmallrowdata_db(columns,ids=[],debug=False): return data +def fitnessmetric_to_sql(m,table='powertimefitnessmetric',debug=False): + if debug: + engine = create_engine(database_url_debug, echo=False) + else: + engine = create_engine(database_url, echo=False) + + columns = ', '.join(m.keys()) + placeholders = ", ".join(["?"] * len(m)) + + query = "INSERT into %s ( %s ) Values (%s)" % (table, columns, placeholders) + + values = tuple(m[key] for key in m.keys()) + with engine.connect() as conn, conn.begin(): + result = conn.execute(query,values) + + conn.close() + engine.dispose() + + return 1 + diff --git a/rowers/datautils.py b/rowers/datautils.py index 5aba0141..1358e0d6 100644 --- a/rowers/datautils.py +++ b/rowers/datautils.py @@ -300,7 +300,8 @@ def getmaxwattinterval(tt,ww,i): wmax = 0 deltat = 0 except KeyError: - pass + wmax = 0 + deltat = 0 else: wmax = 0 deltat = 0 diff --git a/rowers/middleware.py b/rowers/middleware.py new file mode 100644 index 00000000..70837ada --- /dev/null +++ b/rowers/middleware.py @@ -0,0 +1,70 @@ +from django.utils import timezone +from rowers.models import Workout, PowerTimeFitnessMetric, Rower +import datetime +from utils import myqueue +import django_rq +queue = django_rq.get_queue('default') +from rowers.tasks import handle_updatefitnessmetric + +def getrower(user): + try: + r = Rower.objects.get(user=user) + except Rower.DoesNotExist: + r = Rower(user=user) + r.save() + + return r + +def do_update(user,mode='rower',days=42): + r = getrower(user) + + startdate = timezone.now()-datetime.timedelta(days=days) + + # test if not something already done + now_date = timezone.now().strftime('%Y-%m-%d') + ms = PowerTimeFitnessMetric.objects.filter( + user=user, + workoutmode=mode) + + if len(ms) == 0: + max_workout_id = 0 + last_update_date = '1972-01-01' + else: + max_workout_id = max([m.last_workout for m in ms]) + last_update_date = max([m.date.strftime('%Y-%m-%d') for m in ms]) + + if mode == 'rower': + workouts = Workout.objects.filter( + user=r, + workouttype__in=['rower','dynamic','slides'], + startdatetime__gte=startdate) + else: + workouts = Workout.objects.filter( + user=r, + workouttype__in=['water','coastal'], + startdatetime__gte=startdate) + + theids = [int(w.id) for w in workouts] + try: + max_id = max(theids) + except ValueError: + max_id = 0 + + if last_update_date < now_date and max_workout_id < max_id: + job = myqueue(queue, + handle_updatefitnessmetric, + user.id,mode,theids, + ) + + + return 1 + +class PowerTimeFitnessMetricMiddleWare(object): + def process_request(self, request): + # Code to be executed before the view is called + if request.user.is_authenticated(): + result = do_update(request.user,mode='rower') + result = do_update(request.user,mode='water') + + + diff --git a/rowers/models.py b/rowers/models.py index db2fb086..e37e35e9 100644 --- a/rowers/models.py +++ b/rowers/models.py @@ -248,13 +248,16 @@ class PowerTimeFitnessMetric(models.Model): ('water','On the water') ) - data = models.DateField(default=timezone.now) + date = models.DateField(default=timezone.now) + last_workout = models.IntegerField(default=0) user = models.ForeignKey(User) PowerFourMin = models.FloatField(default=0) PowerTwoK = models.FloatField(default=0) PowerOneHour = models.FloatField(default=0) workoutmode = models.CharField(default='rower',choices=modechoices, max_length=40) + class Meta: + db_table = 'powertimefitnessmetric' class C2WorldClassAgePerformance(models.Model): weightcategories = ( diff --git a/rowers/tasks.py b/rowers/tasks.py index 6cddaa6e..9a5d90a3 100644 --- a/rowers/tasks.py +++ b/rowers/tasks.py @@ -25,13 +25,14 @@ from rowsandall_app.settings import PROGRESS_CACHE_SECRET import pandas as pd from django_rq import job +from django.utils import timezone from utils import deserialize_list from rowers.dataprepnodjango import ( update_strokedata, new_workout_from_file, getsmallrowdata_db, updatecpdata_sql, - update_agegroup_db, + update_agegroup_db,fitnessmetric_to_sql ) from django.core.mail import send_mail, EmailMessage @@ -612,6 +613,82 @@ def handle_updateergcp(rower_id,workoutfilenames,debug=False,**kwargs): return 1 +@app.task +def handle_updatefitnessmetric(user_id,mode,workoutids,debug=False, + **kwargs): + + columns = ['power','workoutid','time'] + df = getsmallrowdata_db(columns,ids=workoutids,debug=debug) + df.dropna(inplace=True,axis=0) + + if df.empty: + # change this + return 2 + + # df is not empty. We continue + dfgrouped = df.groupby(['workoutid']) + maxt = 1.05*df['time'].max()/1000. + + logarr = datautils.getlogarr(maxt) + + delta,cpvalue,avgpower = datautils.getcp(dfgrouped,logarr) + + powerdf = pd.DataFrame({ + 'Delta':delta, + 'CP':cpvalue, + }) + + powerdf = powerdf[powerdf['CP']>0] + powerdf.dropna(axis=0,inplace=True) + powerdf.sort_values(['Delta','CP'],ascending=[1,0],inplace=True) + powerdf.drop_duplicates(subset='Delta',keep='first',inplace=True) + + p1,fitt,fitpower,ratio = datautils.cpfit(powerdf) + # This is code duplication from datautils -- correct asap + fitfunc = lambda pars,x: abs(pars[0])/(1+(x/abs(pars[2]))) + abs(pars[1])/(1+(x/abs(pars[3]))) + + powerfourmin = fitfunc(p1,240.) + powerhour = fitfunc(p1,3600.) + + # 2k power + velofourmin = (powerfourmin/2.8)**(1./3.) + dfourmin = 240.*velofourmin + dratio = 2000./dfourmin + pacefourmin = 500./velofourmin + + # assume 5 sec per doubling drop + pace2k = pacefourmin + 5.*np.log10(dratio)/np.log10(2.) + velo2k = 500./pace2k + t2k = 2000./velo2k + pwr2k = fitfunc(p1,t2k) + velo2 = (pwr2k/2.8)**(1./3.) + if np.isnan(velo2) or velo2 <= 0: + velo2 = 1.0 + + t2 = 2000./velo2 + + pwr2k = fitfunc(p1,t2) + + velo3 = (pwr2k/2.8)**(1./3.) + + t3 = 2000./velo3 + + power2k = fitfunc(p1,t3) + + + mdict = { + 'user_id': user_id, + 'PowerFourMin': powerfourmin, + 'PowerTwoK': power2k, + 'PowerOneHour': powerhour, + 'workoutmode': mode, + 'last_workout': max(workoutids), + 'date': timezone.now().strftime('%Y-%m-%d'), + } + + result = fitnessmetric_to_sql(mdict,debug=debug) + + return result @app.task def handle_updatecp(rower_id,workoutids,debug=False,table='cpdata',**kwargs): diff --git a/rowers/urls.py b/rowers/urls.py index 7d345028..0b93f9b2 100644 --- a/rowers/urls.py +++ b/rowers/urls.py @@ -125,6 +125,9 @@ urlpatterns = [ url(r'^agegroupcp/(?P\d+)/(?P\d+)$',views.agegroupcpview), url(r'^ajax_agegroup/(?P\d+)/(?P\w+.*)/(?P\w+.*)/(?P\d+)$', views.ajax_agegrouprecords), + url(r'^updatefitness/(?P\w+.*)/(?P\d+)$',views.fitness_metric_view), + url(r'^updatefitness/(?P\w+.*)$',views.fitness_metric_view), + url(r'^updatefitness$',views.fitness_metric_view), url(r'^agegrouprecords/(?P\w+.*)/(?P\w+.*)/(?P\d+)m$', views.agegrouprecordview), url(r'^agegrouprecords/(?P\w+.*)/(?P\w+.*)/(?P\d+)min$', diff --git a/rowers/utils.py b/rowers/utils.py index 26f74d3d..300e95fa 100644 --- a/rowers/utils.py +++ b/rowers/utils.py @@ -15,6 +15,7 @@ landingpages = ( ) + workflowmiddlepanel = ( ('panel_statcharts.html','Static Charts'), ('flexthumbnails.html','Flex Charts'), diff --git a/rowers/views.py b/rowers/views.py index 09571a41..2e3b967e 100644 --- a/rowers/views.py +++ b/rowers/views.py @@ -56,7 +56,7 @@ from rowers.models import ( RowerPowerZonesForm,AccountRowerForm,UserForm,StrokeData, Team,TeamForm,TeamInviteForm,TeamInvite,TeamRequest, WorkoutComment,WorkoutCommentForm,RowerExportForm, - CalcAgePerformance + CalcAgePerformance,PowerTimeFitnessMetric, ) from rowers.models import FavoriteForm,BaseFavoriteFormSet,SiteAnnouncement from rowers.metrics import rowingmetrics,defaultfavoritecharts @@ -109,7 +109,8 @@ from rowers.tasks import ( handle_sendemail_unrecognized,handle_sendemailnewcomment, handle_sendemailnewresponse, handle_updatedps, handle_updatecp,long_test_task,long_test_task2, - handle_zip_file,handle_getagegrouprecords + handle_zip_file,handle_getagegrouprecords, + handle_updatefitnessmetric ) from scipy.signal import savgol_filter @@ -159,6 +160,20 @@ class JSONResponse(HttpResponse): kwargs['content_type'] = 'application/json' super(JSONResponse, self).__init__(content, **kwargs) + +def getrower(user): + try: + r = Rower.objects.get(user=user) + except Rower.DoesNotExist: + r = Rower(user=user) + r.save() + + return r + + + + + def getvalue(data): perc = 0 total = 1 @@ -238,6 +253,7 @@ from io import BytesIO from scipy.special import lambertw from dataprep import timedeltaconv +from dataprep import getsmallrowdata_db from scipy.interpolate import griddata @@ -736,6 +752,8 @@ def iscoachmember(user): return result + + def getrower(user): try: r = Rower.objects.get(user=user) @@ -745,6 +763,8 @@ def getrower(user): return r + + # Check if a user is a Pro member def ispromember(user): if not user.is_anonymous(): @@ -3206,6 +3226,46 @@ def addmanual_view(request): {'form':form, }) +@login_required() +def fitness_metric_view(request,mode='rower',days=42): + r = getrower(request.user) + startdate = timezone.now()-datetime.timedelta(days=days) + + # test if not something already done + ms = PowerTimeFitnessMetric.objects.filter(user=request.user) + max_workout_id = max([m.last_workout for m in ms]) + last_update_date = max([m.date.strftime('%Y-%m-%d') for m in ms]) + + + now_date = timezone.now().strftime('%Y-%m-%d') + + + if mode == 'rower': + workouts = Workout.objects.filter( + user=r, + workouttype__in=['rower','dynamic','slides'], + startdatetime__gte=startdate) + else: + workouts = Workout.objects.filter( + user=r, + workouttype__in=['water','coastal'], + startdatetime__gte=startdate) + + theids = [int(w.id) for w in workouts] + max_id = max(theids) + + if last_update_date >= now_date or max_workout_id >= max_id: + return HttpResponse("already done today or no new workouts") + + + job = myqueue(queue, + handle_updatefitnessmetric, + request.user.id,mode,theids, + ) + + return HttpResponse("job queued") + + # Show ranking distances including predicted paces @login_required() def rankings_view(request,theuser=0, diff --git a/rowsandall_app/settings.py b/rowsandall_app/settings.py index f8da172b..400f4cc1 100644 --- a/rowsandall_app/settings.py +++ b/rowsandall_app/settings.py @@ -93,6 +93,7 @@ MIDDLEWARE_CLASSES = [ 'async_messages.middleware.AsyncMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', 'tz_detect.middleware.TimezoneMiddleware', + 'rowers.middleware.PowerTimeFitnessMetricMiddleWare', ] ROOT_URLCONF = 'rowsandall_app.urls'