diff --git a/rowers/dataprep.py b/rowers/dataprep.py index 3a1debc8..1a7cb73c 100644 --- a/rowers/dataprep.py +++ b/rowers/dataprep.py @@ -10,7 +10,9 @@ from rowingdata import rower as rrower from shutil import copyfile -from rowingdata import get_file_type, get_empower_rigging +from rowingdata import ( + get_file_type, get_empower_rigging,get_empower_firmware + ) from rowers.tasks import handle_sendemail_unrecognized from rowers.tasks import handle_zip_file @@ -1094,11 +1096,12 @@ def parsenonpainsled(fileformat,f2,summary): if (fileformat == 'rp'): row = RowProParser(f2) hasrecognized = True - # handle TCX + + # handle TCX if (fileformat == 'tcx'): row = TCXParser(f2) hasrecognized = True - + # handle Mystery if (fileformat == 'mystery'): row = MysteryParser(f2) @@ -1153,11 +1156,15 @@ def parsenonpainsled(fileformat,f2,summary): if (fileformat == 'speedcoach2'): row = SpeedCoach2Parser(f2) hasrecognized = True - try: - oarlength, inboard = get_empower_rigging(f2) - summary = row.allstats() - except: - pass + + oarlength, inboard = get_empower_rigging(f2) + empowerfirmware = get_empower_firmware(f2) + if empowerfirmware != '': + fileformat = fileformat+'v'+str(empowerfirmware) + else: + fileformat = 'speedcoach2v0' + summary = row.allstats() + # handle ErgStick if (fileformat == 'ergstick'): @@ -1176,7 +1183,7 @@ def parsenonpainsled(fileformat,f2,summary): pass hasrecognized = True - return row,hasrecognized,summary + return row,hasrecognized,summary,fileformat def handle_nonpainsled(f2, fileformat, summary=''): oarlength = 2.89 @@ -1184,17 +1191,17 @@ def handle_nonpainsled(f2, fileformat, summary=''): hasrecognized = False try: - row,hasrecognized,summary = parsenonpainsled(fileformat,f2,summary) + row,hasrecognized,summary,fileformat = parsenonpainsled(fileformat,f2,summary) except: pass # Handle c2log if (fileformat == 'c2log' or fileformat == 'rowprolog'): - return (0,0,0,0) + return (0,0,0,0,0) if not hasrecognized: - return (0,0,0,0) + return (0,0,0,0,0) f_to_be_deleted = f2 # should delete file @@ -1203,7 +1210,7 @@ def handle_nonpainsled(f2, fileformat, summary=''): row2 = rrdata(df = row.df) row2.write_csv(f2, gzip=True) except: - return (0,0,0,0) + return (0,0,0,0,0) # os.remove(f2) try: @@ -1211,7 +1218,7 @@ def handle_nonpainsled(f2, fileformat, summary=''): except: os.remove(f_to_be_deleted + '.gz') - return (f2, summary, oarlength, inboard) + return (f2, summary, oarlength, inboard, fileformat) # Create new workout from file and store it in the database # This routine should be used everywhere in views.py and mailprocessing.py @@ -1297,18 +1304,23 @@ def new_workout_from_file(r, f2, # handle non-Painsled by converting it to painsled compatible CSV if (fileformat != 'csv'): try: - f2, summary, oarlength, inboard = handle_nonpainsled(f2, - fileformat, - summary=summary) + f2, summary, oarlength, inboard, fileformat = handle_nonpainsled( + f2, + fileformat, + summary=summary + ) if not f2: message = 'Something went wrong' return (0, message, '') - except: + except Exception as e: errorstring = str(sys.exc_info()[0]) - message = 'Something went wrong: ' + errorstring + print e.message + message = 'Something went wrong: ' + e.message return (0, message, '') - dosummary = (fileformat != 'fit' and fileformat != 'speedcoach2') + dosummary = (fileformat != 'fit' and 'speedcoach2' not in fileformat) + + id, message = save_workout_database( f2, r, workouttype=workouttype, diff --git a/rowers/dataprepnodjango.py b/rowers/dataprepnodjango.py index 11c8e1e1..f17cbb8b 100644 --- a/rowers/dataprepnodjango.py +++ b/rowers/dataprepnodjango.py @@ -4,6 +4,7 @@ from rowingdata import rowingdata as rrdata from rowingdata import make_cumvalues from rowingdata import rower as rrower from rowingdata import main as rmain +from rowingdata import empower_bug_correction,get_empower_rigging from time import strftime from pandas import DataFrame,Series @@ -659,7 +660,55 @@ def update_strokedata(id,df,debug=False): debug=debug) return rowdata + +def update_empower(id, inboard, oarlength, boattype, df, f1, debug=False): + + corr_factor = 1.0 + if 'x' in boattype: + # sweep + a = 0.06 + b = 0.275 + else: + # scull + a = 0.15 + b = 0.275 + + corr_factor = empower_bug_correction(oarlength,inboard,a,b) + + success = False + + print df[' Power (watts)'].mean() + try: + df['power empower old'] = df[' Power (watts)'] + df[' Power (watts)'] = df[' Power (watts)'] * corr_factor + df['driveenergy empower old'] = df['driveenergy'] + df['driveenergy'] = df['driveenergy'] * corr_factor + success = True + except KeyError: + pass + + if success: + delete_strokedata(id,debug=debug) + if debug: + print "updated ",id + print "correction ",corr_factor + else: + if debug: + print "not updated ",id + + print df[' Power (watts)'].mean() + + + rowdata = dataprep(df,id=id,bands=True,barchart=True,otwpower=True, + debug=debug) + + row = rrdata(df=df) + row.write_csv(f1,gzip=True) + + return success + + def testdata(time,distance,pace,spm): t1 = np.issubdtype(time,np.number) t2 = np.issubdtype(distance,np.number) diff --git a/rowers/mailprocessing.py b/rowers/mailprocessing.py index 08c727fc..e01be4c5 100644 --- a/rowers/mailprocessing.py +++ b/rowers/mailprocessing.py @@ -121,7 +121,7 @@ def make_new_workout_from_email(rower, datafile, name, cntr=0,testing=False): # handle non-Painsled if fileformat != 'csv': - filename_mediadir, summary, oarlength, inboard = dataprep.handle_nonpainsled( + filename_mediadir, summary, oarlength, inboard,fileformat = dataprep.handle_nonpainsled( 'media/' + datafilename, fileformat, summary) if not filename_mediadir: return 0 @@ -148,7 +148,7 @@ def make_new_workout_from_email(rower, datafile, name, cntr=0,testing=False): pass row.write_csv(datafilename, gzip=True) - dosummary = (fileformat != 'fit') + dosummary = (fileformat != 'fit' and 'speedcoach2' not in fileformat) if name == '': name = 'Workout from Background Queue' diff --git a/rowers/models.py b/rowers/models.py index b8c70042..75afa570 100644 --- a/rowers/models.py +++ b/rowers/models.py @@ -1221,7 +1221,7 @@ class Workout(models.Model): name = models.CharField(max_length=150) date = models.DateField() workouttype = models.CharField(choices=workouttypes,max_length=50) - workoutsource = models.CharField(choices=workoutsources,max_length=100, + workoutsource = models.CharField(max_length=100, default='unknown') boattype = models.CharField(choices=boattypes,max_length=50, default='1x', diff --git a/rowers/tasks.py b/rowers/tasks.py index 08f48c49..96c400a2 100644 --- a/rowers/tasks.py +++ b/rowers/tasks.py @@ -41,7 +41,7 @@ from rowers.dataprepnodjango import ( getsmallrowdata_db, updatecpdata_sql, update_agegroup_db,fitnessmetric_to_sql, add_c2_stroke_data_db,totaltime_sec_to_string, - create_c2_stroke_data_db + create_c2_stroke_data_db,update_empower ) @@ -265,6 +265,89 @@ def handle_new_workout_from_file(r, f2, # process and update workouts +@app.task(bind=True) +def handle_update_empower(self, + useremail, + workoutdicts, + debug=False, **kwargs): + + job = self.request + job_id = job.id + + if 'jobkey' in kwargs: + job_id = kwargs.pop('jobkey') + + aantal = len(workoutdicts) + counter = 0 + + for workoutdict in workoutdicts: + wid = workoutdict['id'] + inboard = workoutdict['inboard'] + oarlength = workoutdict['oarlength'] + boattype = workoutdict['boattype'] + f1 = workoutdict['filename'] + + print wid + + # oarlength consistency checks will be done in view + + havedata = 1 + try: + rowdata = rdata(f1) + except IOError: + try: + rowdata = rdata(f1 + '.csv') + except IOError: + try: + rowdata = rdata(f1 + '.gz') + except IOError: + havedata = 0 + + progressurl = SITE_URL + siteurl = SITE_URL + if debug: + progressurl = SITE_URL_DEV + siteurl = SITE_URL_DEV + secret = PROGRESS_CACHE_SECRET + + kwargs['job_id'] = job_id + + progressurl += "/rowers/record-progress/" + progressurl += job_id + + if havedata: + success = update_empower(wid, inboard, oarlength, boattype, + rowdata.df, f1, debug=debug) + + counter += 1 + + progress = 100.*float(counter)/float(aantal) + + post_data = { + "secret":secret, + "value":progress, + } + + s = requests.post(progressurl, data=post_data) + status_code = s.status_code + + subject = "Rowsandall.com Your Old Empower Oarlock data have been corrected" + message = """ +We have updated Power and Work per Stroke data according to the instructions by Nielsen-Kellerman. +""" + + email = EmailMessage(subject, message, + 'Rowsandall ', + [useremail]) + + if 'emailbounced' in kwargs: + emailbounced = kwargs['emailbounced'] + else: + emailbounced = False + + if not emailbounced: + res = email.send() + return 1 @app.task def handle_updatedps(useremail, workoutids, debug=False,**kwargs): diff --git a/rowers/templates/empower_fix.html b/rowers/templates/empower_fix.html new file mode 100644 index 00000000..8e059f80 --- /dev/null +++ b/rowers/templates/empower_fix.html @@ -0,0 +1,144 @@ +{% extends "base.html" %} +{% load staticfiles %} +{% load rowerfilters %} + +{% block title %}Workouts{% endblock %} + +{% block content %} + + + + + + +
+ {% include "teambuttons.html" with teamid=team.id team=team %} +
+
+

Empower Workouts

+

This functionality is aimed at users who have uploaded workouts from + the Nielsen-Kellerman Empower Oarlock/SpeedCoach combination before the + power inflation bug was known (May 4, 2018).

+ +

Workouts recorded with a SpeedCoach running NK Firmware version 2.17 or + lower have Power and Work per Stroke values that are approximately + 9% (sculling) or 5% too high. The exact value of the error depends on your + inboard and oar length.

+ +

Currently, we autocorrect workouts recorded with old Firmware upon + their upload, but workouts that were present on the site before + the bug was known still have incorrect values for Power and Work per Stroke. +

+ +

+ You can use this page to correct those workouts. +

+ + +
+
+
+ +
+
+ + {{ dateform.as_table }} +
+ {% csrf_token %} +
+
+ +
+
+
+ +
+ + +
+
+ + + {% if workouts %} + + Toggle All
+ + + {{ form.as_table }} +
+ +{% else %} +

No workouts found

+{% endif %} +
+
+

Select workouts on the left, + and press submit

+
+

+ {% csrf_token %} + +

+
+
+

You can use the date form above to reduce the selection

+
+
+
+ + +{% endblock %} diff --git a/rowers/templates/list_workouts.html b/rowers/templates/list_workouts.html index 93c148e1..a3e07cbb 100644 --- a/rowers/templates/list_workouts.html +++ b/rowers/templates/list_workouts.html @@ -266,21 +266,24 @@
{% if rankingonly and not team %} -
+ {% elif not team %} -
+ {% endif %} -
+
{% if user|is_promember %} Glue Workouts {% else %} Glue {% endif %}
+

 

{% if team %}
-

Multi Flex Chart

+

Trend Flex Chart

{{ the_div|safe }} diff --git a/rowers/tests.py b/rowers/tests.py index 8eef0ab1..5e64f2e4 100644 --- a/rowers/tests.py +++ b/rowers/tests.py @@ -846,7 +846,7 @@ class ViewTest(TestCase): f_to_be_deleted = w.csvfilename os.remove(f_to_be_deleted+'.gz') - def test_upload_view_TCX_SpeedCoach2(self): + def test_upload_view_TCX_SpeedCoach2a(self): self.c.login(username='john',password='koeinsloot') filename = 'rowers/testdata/Speedcoach2example.csv' @@ -876,7 +876,7 @@ class ViewTest(TestCase): f_to_be_deleted = w.csvfilename os.remove(f_to_be_deleted+'.gz') - def test_upload_view_TCX_SpeedCoach2(self): + def test_upload_view_TCX_SpeedCoach2b(self): self.c.login(username='john',password='koeinsloot') filename = 'rowers/testdata/Speedcoach2example.csv' @@ -908,7 +908,7 @@ class ViewTest(TestCase): - def test_upload_view_TCX_SpeedCoach2(self): + def test_upload_view_TCX_SpeedCoach2c(self): self.c.login(username='john',password='koeinsloot') filename = 'rowers/testdata/speedcoach3test3.csv' diff --git a/rowers/urls.py b/rowers/urls.py index d37bf26b..b207fa47 100644 --- a/rowers/urls.py +++ b/rowers/urls.py @@ -122,6 +122,7 @@ urlpatterns = [ url(r'^403/$', TemplateView.as_view(template_name='403.html'),name='403'), url(r'^imports/$', TemplateView.as_view(template_name='imports.html'), name='imports'), url(r'^exportallworkouts/?$',views.workouts_summaries_email_view), + url(r'^update_empower$',views.rower_update_empower_view), url(r'^agegroupcp/(?P\d+)$',views.agegroupcpview), url(r'^agegroupcp/(?P\d+)/(?P\d+)$',views.agegroupcpview), url(r'^ajax_agegroup/(?P\d+)/(?P\w+.*)/(?P\w+.*)/(?P\d+)$', diff --git a/rowers/views.py b/rowers/views.py index ccb2b10c..37d09098 100644 --- a/rowers/views.py +++ b/rowers/views.py @@ -128,6 +128,7 @@ from rowers.tasks import ( handle_updatecp,long_test_task,long_test_task2, handle_zip_file,handle_getagegrouprecords, handle_updatefitnessmetric, + handle_update_empower, handle_sendemail_userdeleted, ) @@ -369,6 +370,7 @@ verbose_job_status = { 'make_plot': 'Create static chart', 'long_test_task': 'Long Test Task', 'long_test_task2': 'Long Test Task 2', + 'update_empower': 'Correct Empower Inflated Power Bug', } def get_job_status(jobid): @@ -11812,6 +11814,113 @@ def rower_calcdps_view(request): url = reverse(workouts_view) return HttpResponseRedirect(url) +@login_required() +def rower_update_empower_view( + request, + startdate=timezone.now()-datetime.timedelta(days=365), + enddate=timezone.now() +): + try: + r = getrower(request.user) + except Rower.DoesNotExist: + raise Http404("Rower doesn't exist") + + if 'startdate' in request.session: + startdate = iso8601.parse_date(request.session['startdate']) + + + if 'enddate' in request.session: + enddate = iso8601.parse_date(request.session['enddate']) + + if request.method == 'POST' and 'daterange' in request.POST: + dateform = DateRangeForm(request.POST) + if dateform.is_valid(): + startdate = dateform.cleaned_data['startdate'] + enddate = dateform.cleaned_data['enddate'] + startdatestring = startdate.strftime('%Y-%m-%d') + enddatestring = enddate.strftime('%Y-%m-%d') + request.session['startdate'] = startdatestring + request.session['enddate'] = enddatestring + else: + dateform = DateRangeForm(initial={ + 'startdate':startdate, + 'enddate':enddate, + }) + + + if request.method == 'POST' and 'workouts' in request.POST: + form = WorkoutMultipleCompareForm(request.POST) + if form.is_valid(): + cd = form.cleaned_data + workouts = cd['workouts'] + workoutdicts = [] + + for w in workouts: + if w.user != r: + message = "You can only alter your own workouts" + messages.error(request,message) + if 'x' in w.boattype and w.oarlength is not None and w.oarlength > 3.30: + message = "Oarlength and boat type mismatch for workout "+str(w.id)+". Skipping workout" + messages.error(request,message) + elif 'x' not in w.boattype and w.oarlength is not None and w.oarlength <= 3.30: + message = "Oarlength and boat type mismatch for workout "+str(w.id)+". Skipping workout" + messages.error(request,message) + elif w.oarlength is None: + message = "Incorrect oarlength in workout "+str(w.id)+". Skipping workout" + messages.error(request,message) + else: + + + workoutdict = { + 'id':w.id, + 'boattype':w.boattype, + 'filename':w.csvfilename, + 'inboard':w.inboard, + 'oarlength':w.oarlength + } + + workoutdicts.append(workoutdict) + + w.workoutsource = 'speedcoach2corrected' + w.save() + + + job = myqueue(queuelow,handle_update_empower, + request.user.email,workoutdicts, + debug=False, + emailbounced=r.emailbounced) + + try: + request.session['async_tasks'] += [(job.id,'update_empower')] + except KeyError: + request.session['async_tasks'] = [(job.id,'update_empower')] + + successmessage = 'Your workouts are being updated in the background. You will receive email when this is done. You can check the status of your calculations here' + + messages.info(request,successmessage) + + url = reverse(workouts_view) + return HttpResponseRedirect(url) + else: + + workouts = Workout.objects.filter( + startdatetime__gte=startdate, + startdatetime__lte=enddate, + workoutsource='speedcoach2', + user=r, + ).order_by("-date","-starttime") + + form = WorkoutMultipleCompareForm() + form.fields["workouts"].queryset = workouts + # GET request = prepare form + + return render(request, 'empower_fix.html', + {'workouts':workouts, + 'dateform':dateform, + 'form':form, + 'rower':r + }) + @login_required() def team_leave_view(request,id=0): r = getrower(request.user)