diff --git a/rowers/dataprep.py b/rowers/dataprep.py index 61116cea..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') + 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 49a3e6dc..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 @@ -623,7 +624,8 @@ def new_workout_from_file(r,f2, - dosummary = (fileformat != 'fit') + dosummary = (fileformat != 'fit' and fileformat != 'speedcoach2') + id,message = save_workout_database(f2,r, workouttype=workouttype, makeprivate=makeprivate, @@ -658,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/templates/physics.html b/rowers/templates/physics.html index 4ed2bd7f..efc5757d 100644 --- a/rowers/templates/physics.html +++ b/rowers/templates/physics.html @@ -107,16 +107,27 @@ GPS. For now, export the data to Strava and then import them here.
  1. Click on the workout. This will bring you to the workout Edit view
  2. Click on the "Advanced" button
  3. -
  4. Click on Edit Wind Data (optional) to edit wind data. Go back to Advanced when you're done.
  5. -
  6. Click on Edit Stream Data (optional) to edit stream data. Go back to Advanced when you're done.
  7. +
  8. Click on "Geeky Stuff"
  9. +
  10. Look for three buttons labelled "Edit Wind Data", "Edit Stream + Data" and "OTW Power"
  11. +
  12. If you have wind or stream data, click on the appropriate + button and enter your data.
  13. +
  14. If you have both wind and stream, click the shortcut button + on the respective page to take you to the other parameter
  15. Click on OTW Power
  16. -
  17. Select the boat type and enter the average weight per crew member. - Do not use the crew total weight.
  18. +
  19. Select the boat type and enter the average weight + per crew member. + Do not use the crew total weight.
  20. Click "Update & Run"
  21. Go do something else. You will receive an email when the calculations are finished. The calculation itself will take about 10 minutes for an hour long row, but there may be other people's calculations in the queue, so -it may take longer.
  22. -
  23. From the "Advanced" view, click on "Corrected Pace Plot" to see the result. From here, you can re-run the calculation with different parameters.
  24. + it may take longer. +
  25. Progress can be monitored by clicking on "here" in the message + at the top of the page advising that the calculation has + begun.
  26. +
  27. + When the calculation is complete, go back to the "Geeky Stuff" page + and click on "Corrected Pace Plot" to see the result. From here, you can re-run the calculation with different parameters.

@@ -152,4 +163,4 @@ well validated. For that, I need to collect data, so keep the workouts coming!
- {% endblock content %} \ No newline at end of file + {% endblock content %} 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)