From cc57eb2b821693cebae031989c07d2f7323abb9c Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Wed, 31 May 2017 11:21:47 +0200 Subject: [PATCH 01/11] added workoutsource to email processing --- rowers/mailprocessing.py | 1 + 1 file changed, 1 insertion(+) diff --git a/rowers/mailprocessing.py b/rowers/mailprocessing.py index 2e4c1208..bb346b73 100644 --- a/rowers/mailprocessing.py +++ b/rowers/mailprocessing.py @@ -237,6 +237,7 @@ def make_new_workout_from_email(rr,f2,name,cntr=0): inboard=inboard, oarlength=oarlength, title=name, + workoutsource=fileformat, notes='imported through email') From 79ab50e29a3b61305600ffff14cf05a60b7f31e8 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Thu, 1 Jun 2017 16:19:56 +0200 Subject: [PATCH 02/11] no consistency checks on data fusion --- rowers/dataprep.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/rowers/dataprep.py b/rowers/dataprep.py index 9744c77a..18516db8 100644 --- a/rowers/dataprep.py +++ b/rowers/dataprep.py @@ -398,7 +398,8 @@ def save_workout_database(f2,r,dosmooth=True,workouttype='rower', notes='',totaldist=0,totaltime=0, summary='', makeprivate=False, - oarlength=2.89,inboard=0.88): + oarlength=2.89,inboard=0.88, + consistencychecks=True): message = None powerperc = 100*np.array([r.pw_ut2, r.pw_ut1, @@ -417,9 +418,12 @@ def save_workout_database(f2,r,dosmooth=True,workouttype='rower', for key,value in checks.iteritems(): if not value: allchecks = 0 - a_messages.error(r.user,'Failed consistency check: '+key+', autocorrected') + if consistencychecks: + a_messages.error(r.user,'Failed consistency check: '+key+', autocorrected') + else: + a_messages.error(r.user,'Failed consistency check: '+key+', not corrected') - if not allchecks: + if not allchecks and consistencychecks: row.repair() @@ -780,7 +784,8 @@ def new_workout_from_df(r,df, oarlength=oarlength, inboard=inboard, makeprivate=makeprivate, - dosmooth=False) + dosmooth=False, + consistencychecks=False) return (id,message) From dae5897b6a69cde3fac2f6bab461ccb1e40858f3 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Thu, 1 Jun 2017 16:21:58 +0200 Subject: [PATCH 03/11] duplicates are marked private --- rowers/dataprep.py | 1 + 1 file changed, 1 insertion(+) diff --git a/rowers/dataprep.py b/rowers/dataprep.py index 18516db8..2cf91786 100644 --- a/rowers/dataprep.py +++ b/rowers/dataprep.py @@ -532,6 +532,7 @@ def save_workout_database(f2,r,dosmooth=True,workouttype='rower', user=r) if (len(ws) != 0): message = "Warning: This workout probably already exists in the database" + privacy = 'private' # checking for inf values totaldist = np.nan_to_num(totaldist) From 4e8b7e02b198ee016af602cba62ddf54a5bb3dbb Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Fri, 2 Jun 2017 20:28:12 +0200 Subject: [PATCH 04/11] otw cp stuff not finished --- gpx.xsd | 1 + rowers/.#views.py | 1 + rowers/models.py | 5 +- rowers/stravastuff.py | 28 ++-- rowers/urls.py | 1 + rowers/views.py | 348 +++++++++++++++++++++++++++++++++++++++++- 6 files changed, 369 insertions(+), 15 deletions(-) create mode 100644 gpx.xsd create mode 100644 rowers/.#views.py diff --git a/gpx.xsd b/gpx.xsd new file mode 100644 index 00000000..2237442b --- /dev/null +++ b/gpx.xsd @@ -0,0 +1 @@ + GPX schema version 1.1 - For more information on GPX and this schema, visit http://www.topografix.com/gpx.asp GPX uses the following conventions: all coordinates are relative to the WGS84 datum. All measurements are in metric units. GPX is the root element in the XML file. GPX documents contain a metadata header, followed by waypoints, routes, and tracks. You can add your own elements to the extensions section of the GPX document. Metadata about the file. A list of waypoints. A list of routes. A list of tracks. You can add extend GPX by adding your own elements from another schema here. You must include the version number in your GPX document. You must include the name or URL of the software that created your GPX document. This allows others to inform the creator of a GPX instance document that fails to validate. Information about the GPX file, author, and copyright restrictions goes in the metadata section. Providing rich, meaningful information about your GPX files allows others to search for and use your GPS data. The name of the GPX file. A description of the contents of the GPX file. The person or organization who created the GPX file. Copyright and license information governing use of the file. URLs associated with the location described in the file. The creation date of the file. Keywords associated with the file. Search engines or databases can use this information to classify the data. Minimum and maximum coordinates which describe the extent of the coordinates in the file. You can add extend GPX by adding your own elements from another schema here. wpt represents a waypoint, point of interest, or named feature on a map. Elevation (in meters) of the point. Creation/modification timestamp for element. Date and time in are in Univeral Coordinated Time (UTC), not local time! Conforms to ISO 8601 specification for date/time representation. Fractional seconds are allowed for millisecond timing in tracklogs. Magnetic variation (in degrees) at the point Height (in meters) of geoid (mean sea level) above WGS84 earth ellipsoid. As defined in NMEA GGA message. The GPS name of the waypoint. This field will be transferred to and from the GPS. GPX does not place restrictions on the length of this field or the characters contained in it. It is up to the receiving application to validate the field before sending it to the GPS. GPS waypoint comment. Sent to GPS as comment. A text description of the element. Holds additional information about the element intended for the user, not the GPS. Source of data. Included to give user some idea of reliability and accuracy of data. "Garmin eTrex", "USGS quad Boston North", e.g. Link to additional information about the waypoint. Text of GPS symbol name. For interchange with other programs, use the exact spelling of the symbol as displayed on the GPS. If the GPS abbreviates words, spell them out. Type (classification) of the waypoint. Type of GPX fix. Number of satellites used to calculate the GPX fix. Horizontal dilution of precision. Vertical dilution of precision. Position dilution of precision. Number of seconds since last DGPS update. ID of DGPS station used in differential correction. You can add extend GPX by adding your own elements from another schema here. The latitude of the point. This is always in decimal degrees, and always in WGS84 datum. The longitude of the point. This is always in decimal degrees, and always in WGS84 datum. rte represents route - an ordered list of waypoints representing a series of turn points leading to a destination. GPS name of route. GPS comment for route. Text description of route for user. Not sent to GPS. Source of data. Included to give user some idea of reliability and accuracy of data. Links to external information about the route. GPS route number. Type (classification) of route. You can add extend GPX by adding your own elements from another schema here. A list of route points. trk represents a track - an ordered list of points describing a path. GPS name of track. GPS comment for track. User description of track. Source of data. Included to give user some idea of reliability and accuracy of data. Links to external information about track. GPS track number. Type (classification) of track. You can add extend GPX by adding your own elements from another schema here. A Track Segment holds a list of Track Points which are logically connected in order. To represent a single GPS track where GPS reception was lost, or the GPS receiver was turned off, start a new Track Segment for each continuous span of track data. You can add extend GPX by adding your own elements from another schema here. You can add extend GPX by adding your own elements from another schema here. A Track Segment holds a list of Track Points which are logically connected in order. To represent a single GPS track where GPS reception was lost, or the GPS receiver was turned off, start a new Track Segment for each continuous span of track data. A Track Point holds the coordinates, elevation, timestamp, and metadata for a single point in a track. You can add extend GPX by adding your own elements from another schema here. Information about the copyright holder and any license governing use of this file. By linking to an appropriate license, you may place your data into the public domain or grant additional usage rights. Year of copyright. Link to external file containing license text. Copyright holder (TopoSoft, Inc.) A link to an external resource (Web page, digital photo, video clip, etc) with additional information. Text of hyperlink. Mime type of content (image/jpeg) URL of hyperlink. An email address. Broken into two parts (id and domain) to help prevent email harvesting. id half of email address (billgates2004) domain half of email address (hotmail.com) A person or organization. Name of person or organization. Email address. Link to Web site or other external information about person. A geographic point with optional elevation and time. Available for use by other schemas. The elevation (in meters) of the point. The time that the point was recorded. The latitude of the point. Decimal degrees, WGS84 datum. The latitude of the point. Decimal degrees, WGS84 datum. An ordered sequence of points. (for polygons or polylines, e.g.) Ordered list of geographic points. Two lat/lon pairs defining the extent of an element. The minimum latitude. The minimum longitude. The maximum latitude. The maximum longitude. The latitude of the point. Decimal degrees, WGS84 datum. The longitude of the point. Decimal degrees, WGS84 datum. Used for bearing, heading, course. Units are decimal degrees, true (not magnetic). Type of GPS fix. none means GPS had no fix. To signify "the fix info is unknown, leave out fixType entirely. pps = military signal used Represents a differential GPS station. \ No newline at end of file diff --git a/rowers/.#views.py b/rowers/.#views.py new file mode 100644 index 00000000..1666c3af --- /dev/null +++ b/rowers/.#views.py @@ -0,0 +1 @@ +E408191@CZ27LT9RCGN72.7808:1496304801 \ No newline at end of file diff --git a/rowers/models.py b/rowers/models.py index 528c28b9..05e27344 100644 --- a/rowers/models.py +++ b/rowers/models.py @@ -425,7 +425,8 @@ class Workout(models.Model): summary = models.TextField(blank=True) privacy = models.CharField(default='visible',max_length=30, choices=privacychoices) - + rankingpiece = models.BooleanField(default=False,verbose_name='Ranking Piece') + def __unicode__(self): date = self.date @@ -552,7 +553,7 @@ class WorkoutForm(ModelForm): duration = forms.TimeInput(format='%H:%M:%S.%f') class Meta: model = Workout - fields = ['name','date','starttime','duration','distance','workouttype','notes','privacy','boattype'] + fields = ['name','date','starttime','duration','distance','workouttype','notes','privacy','rankingpiece','boattype'] widgets = { 'date': DateInput(), 'notes': forms.Textarea, diff --git a/rowers/stravastuff.py b/rowers/stravastuff.py index 28900411..eff56909 100644 --- a/rowers/stravastuff.py +++ b/rowers/stravastuff.py @@ -245,7 +245,7 @@ def get_strava_workout(user,stravaid): return [workoutsummary,df] # Generate Workout data for Strava (a TCX file) -def createstravaworkoutdata(w): +def createstravaworkoutdata(w,dozip=True): filename = w.csvfilename row = rowingdata(filename) @@ -256,18 +256,22 @@ def createstravaworkoutdata(w): newnotes = 'from '+w.workoutsource+' via rowsandall.com' row.exporttotcx(tcxfilename,notes=newnotes) - gzfilename = tcxfilename+'.gz' - with file(tcxfilename,'rb') as inF: - s = inF.read() - with gzip.GzipFile(gzfilename,'wb') as outF: - outF.write(s) + if dozip: + gzfilename = tcxfilename+'.gz' + with file(tcxfilename,'rb') as inF: + s = inF.read() + with gzip.GzipFile(gzfilename,'wb') as outF: + outF.write(s) - try: - os.remove(tcxfilename) - except WindowError: - pass - - return gzfilename,"" + try: + os.remove(tcxfilename) + except WindowError: + pass + + return gzfilename,"" + + else: + return tcxfilename,"" # Upload the TCX file to Strava and set the workout activity type diff --git a/rowers/urls.py b/rowers/urls.py index 33f953f7..68495427 100644 --- a/rowers/urls.py +++ b/rowers/urls.py @@ -169,6 +169,7 @@ urlpatterns = [ url(r'^workout/(?P\d+)/export$',views.workout_export_view), url(r'^workout/(?P\d+)/comment$',views.workout_comment_view), url(r'^workout/(?P\d+)/emailtcx$',views.workout_tcxemail_view), + url(r'^workout/(?P\d+)/emailgpx$',views.workout_gpxemail_view), url(r'^workout/(?P\d+)/emailcsv$',views.workout_csvemail_view), url(r'^workout/(?P\d+)/csvtoadmin$',views.workout_csvtoadmin_view), url(r'^workout/compare/(?P\d+)/$',views.workout_comparison_list), diff --git a/rowers/views.py b/rowers/views.py index b8abbc77..8e15110d 100644 --- a/rowers/views.py +++ b/rowers/views.py @@ -1064,7 +1064,7 @@ def workout_tcxemail_view(request,id=0): raise Http404("Workout doesn't exist") if (checkworkoutuser(request.user,w)): try: - tcxfile,tcxmessg = stravastuff.createstravaworkoutdata(w) + tcxfile,tcxmessg = stravastuff.createstravaworkoutdata(w,dozip=False) if tcxfile == 0: message = "Something went wrong (TCX export) "+tcxmessg messages.error(request,message) @@ -1116,6 +1116,51 @@ def workout_tcxemail_view(request,id=0): return response +# Export workout to GPX and send to user's email address +@login_required() +def workout_gpxemail_view(request,id=0): + message = "" + successmessage = "" + r = Rower.objects.get(user=request.user) + try: + w = Workout.objects.get(id=id) + except Workout.DoesNotExist: + raise Http404("Workout doesn't exist") + if (checkworkoutuser(request.user,w)): + filename = w.csvfilename + row = rdata(filename) + gpxfilename = filename[:-4]+'.gpx' + row.exporttogpx(gpxfilename) + if settings.DEBUG: + res = handle_sendemailtcx.delay(r.user.first_name, + r.user.last_name, + r.user.email,gpxfilename) + + else: + res = queuehigh.enqueue(handle_sendemailtcx,r.user.first_name, + r.user.last_name, + r.user.email,gpxfilename) + + successmessage = "The GPX file was sent to you per email" + messages.info(request,successmessage) + url = reverse(workout_export_view, + kwargs = { + 'id':str(w.id), + }) + + response = HttpResponseRedirect(url) + + else: + message = "You are not allowed to export this workout" + messages.error(request,message) + url = reverse(workout_export_view, + kwargs = { + 'id':str(w.id), + }) + response = HttpResponseRedirect(url) + + return response + # Get Workout CSV file and send it to user's email address @login_required() def workout_csvemail_view(request,id=0): @@ -2645,6 +2690,301 @@ def rankings_view(request,theuser=0, + for rankingduration in rankingdurations: + t = 3600.*rankingduration.hour + t += 60.*rankingduration.minute + t += rankingduration.second + t += rankingduration.microsecond/1.e6 + + # Paul's model + ratio = paulintercept/paulslope + + u = ((2**(2+ratio))*(5.**(3+ratio))*t*np.log(10))/paulslope + + d = 500*t*np.log(10.) + d = d/(paulslope*lambertw(u)) + d = d.real + + velo = d/t + p = 500./velo + pwr = 2.8*(velo**3) + a = {'distance':int(d), + 'duration':timedeltaconv(t), + 'pace':timedeltaconv(p), + 'power':int(pwr)} + predictions.append(a) + + # CP model + pwr = p1[0]/(1+t/p1[2]) + pwr += p1[1]/(1+t/p1[3]) + + if pwr <= 0: + pwr = 50. + + velo = (pwr/2.8)**(1./3.) + + if np.isnan(velo) or velo <=0: + velo = 1.0 + + d = t*velo + p = 500./velo + a = {'distance':int(d), + 'duration':timedeltaconv(t), + 'pace':timedeltaconv(p), + 'power':int(pwr)} + cpredictions.append(a) + + + messages.error(request,message) + return render(request, 'rankings.html', + {'rankingworkouts':theworkouts, + 'interactiveplot':script, + 'the_div':div, + 'predictions':predictions, + 'cpredictions':cpredictions, + 'nrdata':len(thedistances), + 'form':form, + 'dateform':dateform, + 'deltaform':deltaform, + 'id': theuser, + 'theuser':uu, + 'startdate':startdate, + 'enddate':enddate, + 'teams':get_my_teams(request.user), + }) + +# Show ranking distances including predicted paces +@login_required() +def otwrankings_view(request,theuser=0, + startdate=timezone.now()-datetime.timedelta(days=365), + enddate=timezone.now(), + deltadays=-1, + startdatestring="", + enddatestring=""): + + if deltadays>0: + startdate = enddate-datetime.timedelta(days=int(deltadays)) + + if startdatestring != "": + startdate = iso8601.parse_date(startdatestring) + + if enddatestring != "": + enddate = iso8601.parse_date(enddatestring) + + if enddate < startdate: + s = enddate + enddate = startdate + startdate = s + + if theuser == 0: + theuser = request.user.id + + promember=0 + if not request.user.is_anonymous(): + r = Rower.objects.get(user=request.user) + result = request.user.is_authenticated() and ispromember(request.user) + if result: + promember=1 + + # get all OTW rows in date range + + # process form + if request.method == 'POST' and "daterange" in request.POST: + dateform = DateRangeForm(request.POST) + deltaform = DeltaDaysForm(request.POST) + if dateform.is_valid(): + startdate = dateform.cleaned_data['startdate'] + enddate = dateform.cleaned_data['enddate'] + if startdate > enddate: + s = enddate + enddate = startdate + startdate = s + elif request.method == 'POST' and "datedelta" in request.POST: + deltaform = DeltaDaysForm(request.POST) + if deltaform.is_valid(): + deltadays = deltaform.cleaned_data['deltadays'] + if deltadays: + enddate = timezone.now() + startdate = enddate-datetime.timedelta(days=deltadays) + if startdate > enddate: + s = enddate + enddate = startdate + startdate = s + dateform = DateRangeForm(initial={ + 'startdate': startdate, + 'enddate': enddate, + }) + else: + dateform = DateRangeForm() + deltaform = DeltaDaysForm() + + else: + dateform = DateRangeForm(initial={ + 'startdate': startdate, + 'enddate': enddate, + }) + deltaform = DeltaDaysForm() + + # get all 2k (if any) - this rower, in date range + try: + r = Rower.objects.get(user=theuser) + except Rower.DoesNotExist: + allergworkouts = [] + r=0 + + + try: + uu = User.objects.get(id=theuser) + except User.DoesNotExist: + uu = '' + + + # test to fix bug + startdate = datetime.datetime.combine(startdate,datetime.time()) + enddate = datetime.datetime.combine(enddate,datetime.time(23,59,59)) + enddate = enddate+datetime.timedelta(days=1) + + + rankingdurations = [] + rankingdurations.append(datetime.time(minute=1)) + rankingdurations.append(datetime.time(minute=4)) + rankingdurations.append(datetime.time(minute=30)) + rankingdurations.append(datetime.time(hour=1)) + + thedistances = [] + theworkouts = [] + thesecs = [] + + + + rankingdistances.sort() + rankingdurations.sort() + + + + theworkouts = Workout.objects.filter(user=r,rankingpiece=True, + workouttype='water', + startdatetime__gte=startdate, + startdatetime__lte=enddate) + + + # get all power data from database (plus workoutid) + theids = [w.id for w in theworkouts] + columns = ['power','workoutid','time'] + df = dataprep.getsmallrowdata_db(columns,ids=theids) + + dfgrouped = df.groupby(['workoutid']) + for id in theids: + tt = df['time'] + + for w in theworkouts: + + thedistances.append(w.distance) + + timesecs = 3600*workouts[0].duration.hour + timesecs += 60*workouts[0].duration.minute + timesecs += workouts[0].duration.second + timesecs += 1.e-5*workouts[0].duration.microsecond + + thesecs.append(timesecs) + + + + thesecs = np.array(thesecs) + + theavpower = 2.8*(thevelos**3) + + + # create interactive plot + if len(thedistances) !=0 : + res = interactive_cpchart(thedistances,thesecs,theavpower, + theworkouts,promember=promember) + script = res[0] + div = res[1] + paulslope = res[2] + paulintercept = res[3] + p1 = res[4] + message = res[5] + else: + script = '' + div = '

No ranking pieces found.

' + paulslope = 1 + paulintercept = 1 + p1 = [1,1,1,1] + message = "" + + + if request.method == 'POST' and "piece" in request.POST: + form = PredictedPieceForm(request.POST) + if form.is_valid(): + value = form.cleaned_data['value'] + hourvalue,value = divmod(value,60) + if hourvalue >= 24: + hourvalue = 23 + pieceunit = form.cleaned_data['pieceunit'] + if pieceunit == 'd': + rankingdistances.append(value) + else: + rankingdurations.append(datetime.time(minute=value,hour=hourvalue)) + else: + form = PredictedPieceForm() + + rankingdistances.sort() + rankingdurations.sort() + + + predictions = [] + cpredictions = [] + + + for rankingdistance in rankingdistances: + # Paul's model + p = paulslope*np.log10(rankingdistance)+paulintercept + velo = 500./p + t = rankingdistance/velo + pwr = 2.8*(velo**3) + a = {'distance':rankingdistance, + 'duration':timedeltaconv(t), + 'pace':timedeltaconv(p), + 'power':int(pwr)} + predictions.append(a) + + # CP model - + pwr2 = p1[0]/(1+t/p1[2]) + pwr2 += p1[1]/(1+t/p1[3]) + + if pwr2 <= 0: + pwr2 = 50. + + velo2 = (pwr2/2.8)**(1./3.) + + if np.isnan(velo2) or velo2 <= 0: + velo2 = 1.0 + + t2 = rankingdistance/velo2 + + pwr3 = p1[0]/(1+t2/p1[2]) + pwr3 += p1[1]/(1+t2/p1[3]) + + if pwr3 <= 0: + pwr3 = 50. + + velo3 = (pwr3/2.8)**(1./3.) + if np.isnan(velo3) or velo3 <= 0: + velo3 = 1.0 + + t3 = rankingdistance/velo3 + p3 = 500./velo3 + + a = {'distance':rankingdistance, + 'duration':timedeltaconv(t3), + 'pace':timedeltaconv(p3), + 'power':int(pwr3)} + cpredictions.append(a) + + + + for rankingduration in rankingdurations: t = 3600.*rankingduration.hour t += 60.*rankingduration.minute @@ -5187,6 +5527,11 @@ def workout_edit_view(request,id=0,message="",successmessage=""): privacy = request.POST['privacy'] except KeyError: privacy = Workout.objects.get(id=id).privacy + try: + rankingpiece = form.cleaned_data['rankingpiece'] + except KeyError: + rankingpiece =- Workout.objects.get(id=id).rankingpiece + startdatetime = (str(date) + ' ' + str(starttime)) startdatetime = datetime.datetime.strptime(startdatetime, "%Y-%m-%d %H:%M:%S") @@ -5203,6 +5548,7 @@ def workout_edit_view(request,id=0,message="",successmessage=""): row.distance = distance row.boattype = boattype row.privacy = privacy + row.rankingpiece = rankingpiece try: row.save() except IntegrityError: From 39eee9c79012d6fcef3be43a78c9987e7ba3a625 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Sun, 4 Jun 2017 19:55:33 +0200 Subject: [PATCH 05/11] gpx export on export page --- rowers/.#views.py | 1 - rowers/templates/export.html | 4 ++++ static/img/gpx.jpg | Bin 0 -> 27965 bytes 3 files changed, 4 insertions(+), 1 deletion(-) delete mode 100644 rowers/.#views.py create mode 100644 static/img/gpx.jpg diff --git a/rowers/.#views.py b/rowers/.#views.py deleted file mode 100644 index 1666c3af..00000000 --- a/rowers/.#views.py +++ /dev/null @@ -1 +0,0 @@ -E408191@CZ27LT9RCGN72.7808:1496304801 \ No newline at end of file diff --git a/rowers/templates/export.html b/rowers/templates/export.html index 52f53480..3953b013 100644 --- a/rowers/templates/export.html +++ b/rowers/templates/export.html @@ -147,6 +147,10 @@ {% endif %} +
+ + GPX Export +
diff --git a/static/img/gpx.jpg b/static/img/gpx.jpg new file mode 100644 index 0000000000000000000000000000000000000000..b81a5ef4d0bad2765824ee469562887aee75f4fa GIT binary patch literal 27965 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 Date: Sun, 4 Jun 2017 20:42:48 +0200 Subject: [PATCH 06/11] otwpower untested, unfinished --- rowers/views.py | 57 ++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 47 insertions(+), 10 deletions(-) diff --git a/rowers/views.py b/rowers/views.py index 8e15110d..1ac5a4ab 100644 --- a/rowers/views.py +++ b/rowers/views.py @@ -138,6 +138,8 @@ from scipy.special import lambertw from dataprep import timedeltaconv +from scipy.interpolate import griddata + LOCALTIMEZONE = tz('Etc/UTC') USER_LANGUAGE = 'en-US' @@ -2873,15 +2875,10 @@ def otwrankings_view(request,theuser=0, columns = ['power','workoutid','time'] df = dataprep.getsmallrowdata_db(columns,ids=theids) - dfgrouped = df.groupby(['workoutid']) - for id in theids: - tt = df['time'] - + thesecs = [] + for w in theworkouts: - - thedistances.append(w.distance) - - timesecs = 3600*workouts[0].duration.hour + timesecs = 3600*workouts[0].duration.hour timesecs += 60*workouts[0].duration.minute timesecs += workouts[0].duration.second timesecs += 1.e-5*workouts[0].duration.microsecond @@ -2889,11 +2886,51 @@ def otwrankings_view(request,theuser=0, thesecs.append(timesecs) + maxt = pd.Series(thesecs).max() - thesecs = np.array(thesecs) + maxlog10 = np.log10(maxt) + logarr = np.arange(100)*maxlog10/100. + logarr = 10.**(logarr) - theavpower = 2.8*(thevelos**3) + delta = [] + cpvalue = [] + + dfgrouped = df.groupby(['workoutid']) + for id,group in dfgrouped: + tt = group['time'] + ww = group['power'] + length = len(ww) + dt = [] + cpw = [] + for i in range(length): + w_roll = ww.rolling(i) + # now goes with # data points - should be fixed seconds + t_0 = tt.ix[w_roll.idxmax(axis=1)] + t_1 = tt.ix[w_roll.idxmax(axis=1)-i] + deltat = t_1-t_0 + wmax = w_roll.ix[w_roll.idxmax(axis=1)] + dt.append(deltat) + cpw.append(wmax) + cpvalues = griddata(dt,cpw,logarr,method='linear',fill_value=0) + + for cpv in cpvalues: + cpvalue.append(cpv) + for d in logarr: + delta.append(d) + + dt = pd.Series(delta,name='Delta') + cpvalue = pd.Series(cpvalue,name='CP') + + + powerdf = pd.DataFrame({ + 'Delta':delta, + 'CP':cpvalue, + }) + + powerdf.sort_value(['Delta','CP'],ascending=[1,0] + powerdf.drop_duplicates(subset='Delta',keep='first') + # create interactive plot if len(thedistances) !=0 : From 5e0d99b72099d0e9bae79e946abc0fa90950b9da Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Mon, 5 Jun 2017 14:52:54 +0200 Subject: [PATCH 07/11] set privacy to private on email imported duplicates --- rowers/dataprepnodjango.py | 1 + 1 file changed, 1 insertion(+) diff --git a/rowers/dataprepnodjango.py b/rowers/dataprepnodjango.py index 95616d9e..40aa638d 100644 --- a/rowers/dataprepnodjango.py +++ b/rowers/dataprepnodjango.py @@ -242,6 +242,7 @@ def save_workout_database(f2,r,dosmooth=True,workouttype='rower', user=r) if (len(ws) != 0): message = "Warning: This workout probably already exists in the database" + privacy = 'private' From 0449ff432fddf2de74532e8e6714d6db97edb450 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Mon, 5 Jun 2017 15:54:25 +0200 Subject: [PATCH 08/11] got to point where chart is generated (L2936) --- rowers/urls.py | 6 +++ rowers/views.py | 117 +++++++++++++++--------------------------------- 2 files changed, 42 insertions(+), 81 deletions(-) diff --git a/rowers/urls.py b/rowers/urls.py index 68495427..e850a4c6 100644 --- a/rowers/urls.py +++ b/rowers/urls.py @@ -135,6 +135,12 @@ urlpatterns = [ url(r'^ote-bests/(?P\d+)$',views.rankings_view), url(r'^ote-bests/$',views.rankings_view), url(r'^(?P\d+)/ote-bests/$',views.rankings_view), + url(r'^(?P\d+)/otw-bests/(?P\w+.*)/(?P\w+.*)$',views.otwrankings_view), + url(r'^(?P\d+)/otw-bests/(?P\d+)$',views.otwrankings_view), + url(r'^otw-bests/(?P\w+.*)/(?P\w+.*)$',views.otwrankings_view), + url(r'^otw-bests/(?P\d+)$',views.otwrankings_view), + url(r'^otw-bests/$',views.otwrankings_view), + url(r'^(?P\d+)/otw-bests/$',views.otwrankings_view), url(r'^(?P\d+)/flexall/(?P\w+.*)/(?P\w+.*)/(?P\w+.*)/(?P\w+.*)/(?P\w+.*)$',views.cum_flex), url(r'^flexall/(?P\w+.*)/(?P\w+.*)/(?P\w+.*)/(?P\w+.*)/(?P\w+.*)$',views.cum_flex), url(r'^flexall/(?P\w+.*)/(?P\w+.*)/(?P\w+.*)$',views.cum_flex), diff --git a/rowers/views.py b/rowers/views.py index 1ac5a4ab..922ba101 100644 --- a/rowers/views.py +++ b/rowers/views.py @@ -2856,13 +2856,6 @@ def otwrankings_view(request,theuser=0, thedistances = [] theworkouts = [] thesecs = [] - - - - rankingdistances.sort() - rankingdurations.sort() - - theworkouts = Workout.objects.filter(user=r,rankingpiece=True, workouttype='water', @@ -2878,10 +2871,10 @@ def otwrankings_view(request,theuser=0, thesecs = [] for w in theworkouts: - timesecs = 3600*workouts[0].duration.hour - timesecs += 60*workouts[0].duration.minute - timesecs += workouts[0].duration.second - timesecs += 1.e-5*workouts[0].duration.microsecond + timesecs = 3600*w.duration.hour + timesecs += 60*w.duration.minute + timesecs += w.duration.second + timesecs += 1.e-5*w.duration.microsecond thesecs.append(timesecs) @@ -2890,7 +2883,10 @@ def otwrankings_view(request,theuser=0, maxlog10 = np.log10(maxt) logarr = np.arange(100)*maxlog10/100. - logarr = 10.**(logarr) + logarr = [int(10.**(la)) for la in logarr] + logarr = pd.Series(logarr) + logarr.drop_duplicates(keep='first',inplace=True) + logarr = logarr.values delta = [] cpvalue = [] @@ -2899,25 +2895,34 @@ def otwrankings_view(request,theuser=0, for id,group in dfgrouped: tt = group['time'] ww = group['power'] - length = len(ww) - dt = [] - cpw = [] - for i in range(length): - w_roll = ww.rolling(i) - # now goes with # data points - should be fixed seconds - t_0 = tt.ix[w_roll.idxmax(axis=1)] - t_1 = tt.ix[w_roll.idxmax(axis=1)-i] - deltat = t_1-t_0 - wmax = w_roll.ix[w_roll.idxmax(axis=1)] - dt.append(deltat) - cpw.append(wmax) + if not np.isnan(ww.mean()): + length = len(ww) + dt = [] + cpw = [] + for i in range(length-2): + w_roll = ww.rolling(i+2,min_periods=2).mean() + # 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-2] + deltat = 1.0e-3*(t_0-t_1) + wmax = w_roll.ix[indexmax] + dt.append(deltat) + cpw.append(wmax) + except KeyError: + pass - cpvalues = griddata(dt,cpw,logarr,method='linear',fill_value=0) + dt = pd.Series(dt) + cpw = pd.Series(cpw) + cpvalues = griddata(dt.values, + cpw.values, + logarr,method='linear',fill_value=0) - 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) dt = pd.Series(delta,name='Delta') cpvalue = pd.Series(cpvalue,name='CP') @@ -2928,9 +2933,9 @@ def otwrankings_view(request,theuser=0, 'CP':cpvalue, }) - powerdf.sort_value(['Delta','CP'],ascending=[1,0] + powerdf.sort_values(['Delta','CP'],ascending=[1,0]) powerdf.drop_duplicates(subset='Delta',keep='first') - + # create interactive plot if len(thedistances) !=0 : @@ -2966,61 +2971,11 @@ def otwrankings_view(request,theuser=0, else: form = PredictedPieceForm() - rankingdistances.sort() - rankingdurations.sort() - predictions = [] cpredictions = [] - for rankingdistance in rankingdistances: - # Paul's model - p = paulslope*np.log10(rankingdistance)+paulintercept - velo = 500./p - t = rankingdistance/velo - pwr = 2.8*(velo**3) - a = {'distance':rankingdistance, - 'duration':timedeltaconv(t), - 'pace':timedeltaconv(p), - 'power':int(pwr)} - predictions.append(a) - - # CP model - - pwr2 = p1[0]/(1+t/p1[2]) - pwr2 += p1[1]/(1+t/p1[3]) - - if pwr2 <= 0: - pwr2 = 50. - - velo2 = (pwr2/2.8)**(1./3.) - - if np.isnan(velo2) or velo2 <= 0: - velo2 = 1.0 - - t2 = rankingdistance/velo2 - - pwr3 = p1[0]/(1+t2/p1[2]) - pwr3 += p1[1]/(1+t2/p1[3]) - - if pwr3 <= 0: - pwr3 = 50. - - velo3 = (pwr3/2.8)**(1./3.) - if np.isnan(velo3) or velo3 <= 0: - velo3 = 1.0 - - t3 = rankingdistance/velo3 - p3 = 500./velo3 - - a = {'distance':rankingdistance, - 'duration':timedeltaconv(t3), - 'pace':timedeltaconv(p3), - 'power':int(pwr3)} - cpredictions.append(a) - - - for rankingduration in rankingdurations: t = 3600.*rankingduration.hour From 4e94a9e2e5d73d257bb68aee29ceb8987bfabbd4 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Tue, 6 Jun 2017 20:54:24 -0500 Subject: [PATCH 09/11] otw-bests somehow working --- rowers/interactiveplots.py | 88 +++++++++++++ rowers/templates/otwrankings.html | 199 ++++++++++++++++++++++++++++++ rowers/views.py | 48 ++----- 3 files changed, 298 insertions(+), 37 deletions(-) create mode 100644 rowers/templates/otwrankings.html diff --git a/rowers/interactiveplots.py b/rowers/interactiveplots.py index 7f640cee..0fdb77c1 100644 --- a/rowers/interactiveplots.py +++ b/rowers/interactiveplots.py @@ -596,6 +596,94 @@ def googlemap_chart(lat,lon,name=""): return [script,div] +def interactive_otwcpchart(powerdf,promember=0): + powerdf = powerdf[~(powerdf == 0).any(axis=1)] + # plot tools + if (promember==1): + TOOLS = 'save,pan,box_zoom,wheel_zoom,reset,tap,hover,resize,crosshair' + else: + TOOLS = 'pan,box_zoom,wheel_zoom,reset,tap,hover,crosshair' + + + x_axis_type = 'log' + y_axis_type = 'linear' + + source = ColumnDataSource( + data = powerdf + ) + + # 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])) + errfunc = lambda pars,x,y: fitfunc(pars,x)-y + + p0 = [500,350,10,8000] + + p1 = p0 + + thesecs = powerdf['Delta'] + theavpower = powerdf['CP'] + + if len(thesecs)>=4: + p1, success = optimize.leastsq(errfunc, p0[:], args = (thesecs,theavpower)) + else: + factor = fitfunc(p0,thesecs.mean())/theavpower.mean() + p1 = [p0[0]/factor,p0[1]/factor,p0[2],p0[3]] + + + fitt = pd.Series(10**(4*np.arange(100)/100.)) + + fitpower = fitfunc(p1,fitt) + + message = "" + #if len(fitpower[fitpower<0]) > 0: + # message = "CP model fit didn't give correct results" + + + sourcecomplex = ColumnDataSource( + data = dict( + power = fitpower, + duration = fitt + ) + ) + + # making the plot + plot = Figure(tools=TOOLS,x_axis_type=x_axis_type, + plot_width=900, + toolbar_location="above", + toolbar_sticky=False) + + # add watermark + plot.extra_y_ranges = {"watermark": watermarkrange} + + plot.image_url([watermarkurl],1.8*max(thesecs),watermarky, + watermarkw,watermarkh, + global_alpha=watermarkalpha, + w_units='screen', + h_units='screen', + anchor=watermarkanchor, + dilate=True, + y_range_name = "watermark", + ) + + plot.circle('Delta','CP',source=source,fill_color='red',size=15, + legend='Power') + plot.xaxis.axis_label = "Duration (seconds)" + plot.yaxis.axis_label = "Power (W)" + + plot.y_range = Range1d(0,1.5*max(theavpower)) + plot.x_range = Range1d(1,2*max(thesecs)) + plot.legend.orientation = "vertical" + + + plot.line('duration','power',source=sourcecomplex,legend="CP Model", + color='green') + + script, div = components(plot) + + return [script,div,p1,message] + def interactive_cpchart(thedistances,thesecs,theavpower, theworkouts,promember=0): diff --git a/rowers/templates/otwrankings.html b/rowers/templates/otwrankings.html new file mode 100644 index 00000000..35b35865 --- /dev/null +++ b/rowers/templates/otwrankings.html @@ -0,0 +1,199 @@ +{% extends "base.html" %} +{% load staticfiles %} +{% load rowerfilters %} + +{% block title %}Workouts{% endblock %} + +{% block content %} + + + + + {{ interactiveplot |safe }} + + + + + +
+
+ {% if theuser %} +

{{ theuser.first_name }}'s Ranking Pieces

+ {% else %} +

{{ user.first_name }}'s Ranking Pieces

+ {% endif %} +
+
+ {% if user.is_authenticated and user|is_manager %} + +
+ +
+

Summary for {{ theuser.first_name }} {{ theuser.last_name }} + between {{ startdate|date }} and {{ enddate|date }}

+ +

Direct link for other users: + https://rowsandall.com/rowers/{{ id }}/otw-bests/{{ startdate|date:"Y-m-d" }}/{{ enddate|date:"Y-m-d" }} +

+ +

The table gives the best efforts achieved on the official Concept2 ranking pieces in the selected date range.

+ +

This page will evolve and try to give you guidance on where to improve.

+
+
+

Use this form to select a different date range:

+

+ Select start and end date for a date range: +

+ +
+ + + {{ dateform.as_table }} +
+ {% csrf_token %} +
+
+ +
+
+
+ Or use the last {{ deltaform }} days. +
+
+ {% csrf_token %} + + +
+
+ +
+ +

Ranking Piece Results

+ + {% if rankingworkouts %} + + + + + + + + + + + + + + {% for workout in rankingworkouts %} + + + + + + + + + + + {% endfor %} + +
Distance Duration Date Avg HR Max HR Edit
{{ workout.distance }} {{ workout.duration |durationprint:"%H:%M:%S.%f" }} {{ workout.date }} {{ workout.averagehr }} {{ workout.maxhr }} + {{ workout.name }}
+ {% else %} +

No ranking workouts found

+ {% endif %} + +
+ + +
+ +

Critical Power Plot

+ + {{ the_div|safe }} + +
+ +
+

Pace predictions for Ranking Pieces

+ +

Add non-ranking piece using the form. The piece will be added in the prediction tables below.

+
+
+ {{ form.value }} {{ form.pieceunit }} + + {% csrf_token %} +
+
+ + +
+ + + +
+ No Paul Data +
+
+

CP Model

+ + + + + + + + + {% for pred in cpredictions %} + + {% for key, value in pred.items %} + {% if key == "power" %} + + {% endif %} + {% if key == "duration" %} + + {% endif %} + {% endfor %} + + {% endfor %} + +
Duration Power
{{ value }} W {{ value |deltatimeprint }}
+ +
+
+ +{% endblock %} diff --git a/rowers/views.py b/rowers/views.py index 922ba101..fc15ea5e 100644 --- a/rowers/views.py +++ b/rowers/views.py @@ -2938,15 +2938,14 @@ def otwrankings_view(request,theuser=0, # create interactive plot - if len(thedistances) !=0 : - res = interactive_cpchart(thedistances,thesecs,theavpower, - theworkouts,promember=promember) + if len(powerdf) !=0 : + res = interactive_otwcpchart(powerdf) script = res[0] div = res[1] - paulslope = res[2] - paulintercept = res[3] - p1 = res[4] - message = res[5] + p1 = res[2] + paulslope = 1 + paulintercept = 1 + message = res[3] else: script = '' div = '

No ranking pieces found.

' @@ -2983,23 +2982,6 @@ def otwrankings_view(request,theuser=0, t += rankingduration.second t += rankingduration.microsecond/1.e6 - # Paul's model - ratio = paulintercept/paulslope - - u = ((2**(2+ratio))*(5.**(3+ratio))*t*np.log(10))/paulslope - - d = 500*t*np.log(10.) - d = d/(paulslope*lambertw(u)) - d = d.real - - velo = d/t - p = 500./velo - pwr = 2.8*(velo**3) - a = {'distance':int(d), - 'duration':timedeltaconv(t), - 'pace':timedeltaconv(p), - 'power':int(pwr)} - predictions.append(a) # CP model pwr = p1[0]/(1+t/p1[2]) @@ -3008,28 +2990,20 @@ def otwrankings_view(request,theuser=0, if pwr <= 0: pwr = 50. - velo = (pwr/2.8)**(1./3.) - - if np.isnan(velo) or velo <=0: - velo = 1.0 - - d = t*velo - p = 500./velo - a = {'distance':int(d), - 'duration':timedeltaconv(t), - 'pace':timedeltaconv(p), - 'power':int(pwr)} + a = { + 'duration':timedeltaconv(t), + 'power':int(pwr)} cpredictions.append(a) + print cpredictions messages.error(request,message) - return render(request, 'rankings.html', + return render(request, 'otwrankings.html', {'rankingworkouts':theworkouts, 'interactiveplot':script, 'the_div':div, 'predictions':predictions, 'cpredictions':cpredictions, - 'nrdata':len(thedistances), 'form':form, 'dateform':dateform, 'deltaform':deltaform, From 2cb4841e8ceec211fb71339415b153edcd0f3440 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Wed, 7 Jun 2017 17:52:00 -0700 Subject: [PATCH 10/11] creates Rower object if user doesn't have it --- rowers/views.py | 222 ++++++++++++++++++++++++++---------------------- 1 file changed, 121 insertions(+), 101 deletions(-) diff --git a/rowers/views.py b/rowers/views.py index b8abbc77..1eaa47b8 100644 --- a/rowers/views.py +++ b/rowers/views.py @@ -277,17 +277,39 @@ from rowers.models import checkworkoutuser # Check if a user is a Coach member def iscoachmember(user): if not user.is_anonymous(): - r = Rower.objects.get(user=user) + try: + r = Rower.objects.get(user=user) + except Rower.DoesNotExist: + r = Rower(user=user) + r.save() + result = user.is_authenticated() and (r.rowerplan=='coach') else: result = False return result +def getrower(user): + if not user.is_anonymous(): + try: + r = Rower.objects.get(user=user) + except Rower.DoesNotExist: + r = Rower(user=user) + r.save() + else: + raise PermissionDenied("You need to log in to use this function") + + return r + # Check if a user is a Pro member def ispromember(user): if not user.is_anonymous(): - r = Rower.objects.get(user=user) + try: + r = Rower.objects.get(user=user) + except Rower.DoesNotExists: + r = Rower(user=user) + r.save() + result = user.is_authenticated() and (r.rowerplan=='pro' or r.rowerplan=='coach') else: result = False @@ -406,7 +428,7 @@ def add_workout_from_strokedata(user,importid,data,strokedata, except: thetimezone = 'UTC' - r = Rower.objects.get(user=user) + r = getrower(user) try: rowdatetime = iso8601.parse_date(data['date_utc']) except KeyError: @@ -570,7 +592,7 @@ def add_workout_from_runkeeperdata(user,importid,data): except: utcoffset = 0 - r = Rower.objects.get(user=user) + r = getrower(user) try: rowdatetime = iso8601.parse_date(data['start_time']) @@ -734,7 +756,7 @@ def add_workout_from_stdata(user,importid,data): except: thetimezone = 'UTC' - r = Rower.objects.get(user=user) + r = getrower(user) try: rowdatetime = iso8601.parse_date(data['start_time']) except iso8601.ParseError: @@ -897,7 +919,7 @@ def add_workout_from_underarmourdata(user,importid,data): except: thetimezone = 'UTC' - r = Rower.objects.get(user=user) + r = getrower(user) try: rowdatetime = iso8601.parse_date(data['start_datetime']) except iso8601.ParseError: @@ -1057,7 +1079,7 @@ def add_workout_from_underarmourdata(user,importid,data): def workout_tcxemail_view(request,id=0): message = "" successmessage = "" - r = Rower.objects.get(user=request.user) + r = getrower(request.user) try: w = Workout.objects.get(id=id) except Workout.DoesNotExist: @@ -1120,7 +1142,7 @@ def workout_tcxemail_view(request,id=0): @login_required() def workout_csvemail_view(request,id=0): message = "" - r = Rower.objects.get(user=request.user) + r = getrower(request.user) try: w = Workout.objects.get(id=id) except Workout.DoesNotExist: @@ -1160,7 +1182,7 @@ def workout_csvemail_view(request,id=0): @login_required() def workout_csvtoadmin_view(request,id=0): message = "" - r = Rower.objects.get(user=request.user) + r = getrower(request.user) try: w = Workout.objects.get(id=id) except Workout.DoesNotExist: @@ -1195,7 +1217,7 @@ def workout_csvtoadmin_view(request,id=0): @login_required() def workout_tp_upload_view(request,id=0): message = "" - r = Rower.objects.get(user=request.user) + r = getrower(request.user) res = -1 try: thetoken = tp_open(r.user) @@ -1253,7 +1275,7 @@ def workout_tp_upload_view(request,id=0): @login_required() def workout_strava_upload_view(request,id=0): message = "" - r = Rower.objects.get(user=request.user) + r = getrower(request.user) res = -1 if (r.stravatoken == '') or (r.stravatoken is None): s = "Token doesn't exist. Need to authorize" @@ -1674,14 +1696,14 @@ def rower_tp_authorize(request): # Concept2 token refresh. URL for manual refresh. Not visible to users @login_required() def rower_c2_token_refresh(request): - r = Rower.objects.get(user=request.user) + r = getrower(request.user) res = c2stuff.do_refresh_token(r.c2refreshtoken) if res[0] != None: access_token = res[0] expires_in = res[1] refresh_token = res[2] expirydatetime = timezone.now()+datetime.timedelta(seconds=expires_in) - r = Rower.objects.get(user=request.user) + r = getrower(request.user) r.c2token = access_token r.tokenexpirydate = expirydatetime r.c2refreshtoken = refresh_token @@ -1699,7 +1721,7 @@ def rower_c2_token_refresh(request): # Underarmour token refresh. URL for manual refresh. Not visible to users @login_required() def rower_underarmour_token_refresh(request): - r = Rower.objects.get(user=request.user) + r = getrower(request.user) res = underarmourstuff.do_refresh_token( r.underarmourrefreshtoken, r.underarmourtoken @@ -1709,7 +1731,7 @@ def rower_underarmour_token_refresh(request): refresh_token = res[2] expirydatetime = timezone.now()+datetime.timedelta(seconds=expires_in) - r = Rower.objects.get(user=request.user) + r = getrower(request.user) r.underarmourtoken = access_token r.underarmourtokenexpirydate = expirydatetime r.underarmourrefreshtoken = refresh_token @@ -1724,7 +1746,7 @@ def rower_underarmour_token_refresh(request): # TrainingPeaks token refresh. URL for manual refresh. Not visible to users @login_required() def rower_tp_token_refresh(request): - r = Rower.objects.get(user=request.user) + r = getrower(request.user) res = tpstuff.do_refresh_token( r.tprefreshtoken, ) @@ -1733,7 +1755,7 @@ def rower_tp_token_refresh(request): refresh_token = res[2] expirydatetime = timezone.now()+datetime.timedelta(seconds=expires_in) - r = Rower.objects.get(user=request.user) + r = getrower(request.user) r.tptoken = access_token r.tptokenexpirydate = expirydatetime r.tprefreshtoken = refresh_token @@ -1748,7 +1770,7 @@ def rower_tp_token_refresh(request): # SportTracks token refresh. URL for manual refresh. Not visible to users @login_required() def rower_sporttracks_token_refresh(request): - r = Rower.objects.get(user=request.user) + r = getrower(request.user) res = sporttracksstuff.do_refresh_token( r.sporttracksrefreshtoken, ) @@ -1757,7 +1779,7 @@ def rower_sporttracks_token_refresh(request): refresh_token = res[2] expirydatetime = timezone.now()+datetime.timedelta(seconds=expires_in) - r = Rower.objects.get(user=request.user) + r = getrower(request.user) r.sporttrackstoken = access_token r.sporttrackstokenexpirydate = expirydatetime r.sporttracksrefreshtoken = refresh_token @@ -1791,7 +1813,7 @@ def rower_process_callback(request): refresh_token = res[2] expirydatetime = timezone.now()+datetime.timedelta(seconds=expires_in) - r = Rower.objects.get(user=request.user) + r = getrower(request.user) r.c2token = access_token r.tokenexpirydate = expirydatetime r.c2refreshtoken = refresh_token @@ -1843,7 +1865,7 @@ def rower_process_stravacallback(request): if res[0]: access_token = res[0] - r = Rower.objects.get(user=request.user) + r = getrower(request.user) r.stravatoken = access_token r.save() @@ -1862,7 +1884,7 @@ def rower_process_runkeepercallback(request): code = request.GET['code'] access_token = runkeeperstuff.get_token(code) - r = Rower.objects.get(user=request.user) + r = getrower(request.user) r.runkeepertoken = access_token r.save() @@ -1883,7 +1905,7 @@ def rower_process_sporttrackscallback(request): refresh_token = res[2] expirydatetime = timezone.now()+datetime.timedelta(seconds=expires_in) - r = Rower.objects.get(user=request.user) + r = getrower(request.user) r.sporttrackstoken = access_token r.sporttrackstokenexpirydate = expirydatetime r.sporttracksrefreshtoken = refresh_token @@ -1906,7 +1928,7 @@ def rower_process_underarmourcallback(request): refresh_token = res[2] expirydatetime = timezone.now()+datetime.timedelta(seconds=expires_in) - r = Rower.objects.get(user=request.user) + r = getrower(request.user) r.underarmourtoken = access_token r.underarmourtokenexpirydate = expirydatetime r.underarmourrefreshtoken = refresh_token @@ -1929,7 +1951,7 @@ def rower_process_tpcallback(request): refresh_token = res[2] expirydatetime = timezone.now()+datetime.timedelta(seconds=expires_in) - r = Rower.objects.get(user=request.user) + r = getrower(request.user) r.tptoken = access_token r.tptokenexpirydate = expirydatetime r.tprefreshtoken = refresh_token @@ -1968,7 +1990,7 @@ def histo_all(request,theuser=0): theuser = request.user.id if not request.user.is_anonymous(): - r = Rower.objects.get(user=request.user) + r = getrower(request.user) result = request.user.is_authenticated() and ispromember(request.user) if result: promember=1 @@ -1979,7 +2001,7 @@ def histo_all(request,theuser=0): # get all indoor rows of past 12 months ayearago = timezone.now()-datetime.timedelta(days=365) try: - r2 = Rower.objects.get(user=theuser) + r2 = getrower(theuser) allergworkouts = Workout.objects.filter(user=r2, workouttype__in=['rower','dynamic','slides'], startdatetime__gte=ayearago) @@ -2052,7 +2074,7 @@ def cum_flex(request,theuser=0, theuser = request.user.id if not request.user.is_anonymous(): - r = Rower.objects.get(user=request.user) + r = getrower(request.user) result = request.user.is_authenticated() and ispromember(request.user) if result: promember=1 @@ -2126,7 +2148,7 @@ def cum_flex(request,theuser=0, deltaform = DeltaDaysForm() optionsform = StatsOptionsForm() try: - r2 = Rower.objects.get(user=theuser) + r2 = getrower(theuser) allworkouts = Workout.objects.filter(user=r2, workouttype__in=workouttypes, startdatetime__gte=startdate, @@ -2210,7 +2232,7 @@ def workout_forcecurve_view(request,id=0,workstrokesonly=False): promember=0 mayedit=0 if not request.user.is_anonymous(): - r = Rower.objects.get(user=request.user) + r = getrower(request.user) result = request.user.is_authenticated() and ispromember(request.user) if result: promember=1 @@ -2271,7 +2293,7 @@ def workout_histo_view(request,id=0): promember=0 mayedit=0 if not request.user.is_anonymous(): - r = Rower.objects.get(user=request.user) + r = getrower(request.user) result = request.user.is_authenticated() and ispromember(request.user) if result: promember=1 @@ -2324,7 +2346,7 @@ def histo(request,theuser=0, theuser = request.user.id if not request.user.is_anonymous(): - r = Rower.objects.get(user=request.user) + r = getrower(request.user) result = request.user.is_authenticated() and ispromember(request.user) if result: promember=1 @@ -2371,7 +2393,7 @@ def histo(request,theuser=0, deltaform = DeltaDaysForm() try: - r2 = Rower.objects.get(user=theuser) + r2 = getrower(theuser) allergworkouts = Workout.objects.filter(user=r2, workouttype__in=['rower','dynamic','slides'], startdatetime__gte=startdate, @@ -2435,7 +2457,7 @@ def rankings_view(request,theuser=0, promember=0 if not request.user.is_anonymous(): - r = Rower.objects.get(user=request.user) + r = getrower(request.user) result = request.user.is_authenticated() and ispromember(request.user) if result: promember=1 @@ -2481,7 +2503,7 @@ def rankings_view(request,theuser=0, # get all 2k (if any) - this rower, in date range try: - r = Rower.objects.get(user=theuser) + r = getrower(theuser) except Rower.DoesNotExist: allergworkouts = [] r=0 @@ -2762,7 +2784,7 @@ def workout_makepublic_view(request,id, row.privacy = 'visible' row.save() - rr = Rower.objects.get(user=request.user) + rr = getrower(request.user) teams = rr.team.all() for team in teams: @@ -2821,7 +2843,7 @@ def team_comparison_select(request, teamid=0): try: - r = Rower.objects.get(user=request.user) + r = getrower(request.user) except Rower.DoesNotExist: raise Http404("Rower doesn't exist") @@ -2906,7 +2928,7 @@ def team_comparison_select(request, def multi_compare_view(request): promember=0 if not request.user.is_anonymous(): - r = Rower.objects.get(user=request.user) + r = getrower(request.user) result = request.user.is_authenticated() and ispromember(request.user) if result: promember=1 @@ -2997,10 +3019,8 @@ def user_boxplot_select(request, else: user = User.objects.get(id=userid) - try: - r = Rower.objects.get(user=user) - except Rower.DoesNotExist: - raise Http404("Rower doesn't exist") + + r = getrower(user) if 'startdate' in request.session: startdate = iso8601.parse_date(request.session['startdate']) @@ -3259,7 +3279,7 @@ def workouts_view(request,message='',successmessage='', enddate=timezone.now()+datetime.timedelta(days=1), teamid=0): try: - r = Rower.objects.get(user=request.user) + r = getrower(request.user) except Rower.DoesNotExist: raise Http404("Rower doesn't exist") @@ -3360,7 +3380,7 @@ def workout_comparison_list(request,id=0,message='',successmessage='', enddate=timezone.now()): try: - r = Rower.objects.get(user=request.user) + r = getrower(request.user) u = User.objects.get(id=r.user.id) if request.method == 'POST': dateform = DateRangeForm(request.POST) @@ -3440,7 +3460,7 @@ def workout_fusion_list(request,id=0,message='',successmessage='', enddate=timezone.now()): try: - r = Rower.objects.get(user=request.user) + r = getrower(request.user) u = User.objects.get(id=r.user.id) if request.method == 'POST': dateform = DateRangeForm(request.POST) @@ -3840,7 +3860,7 @@ def workout_wind_view(request,id=0,message="",successmessage=""): # get data f1 = row.csvfilename u = row.user.user - r = Rower.objects.get(user=u) + r = getrower(u) # create bearing rowdata = rdata(f1) @@ -3961,7 +3981,7 @@ def workout_stream_view(request,id=0,message="",successmessage=""): # create interactive plot f1 = row.csvfilename u = row.user.user - r = Rower.objects.get(user=u) + r = getrower(u) rowdata = rdata(f1) if rowdata == 0: @@ -4128,7 +4148,7 @@ def workout_geeky_view(request,id=0,message="",successmessage=""): # create interactive plot f1 = row.csvfilename u = row.user.user - r = Rower.objects.get(user=u) + r = getrower(u) # create interactive plot try: @@ -4198,7 +4218,7 @@ def cumstats(request,theuser=0, theuser = request.user.id if not request.user.is_anonymous(): - r = Rower.objects.get(user=request.user) + r = getrower(request.user) result = request.user.is_authenticated() and ispromember(request.user) if result: promember=1 @@ -4266,7 +4286,7 @@ def cumstats(request,theuser=0, optionsform = StatsOptionsForm() try: - r2 = Rower.objects.get(user=theuser) + r2 = getrower(theuser) allergworkouts = Workout.objects.filter(user=r2, workouttype__in=workouttypes, startdatetime__gte=startdate, @@ -4407,7 +4427,7 @@ def cumstats(request,theuser=0, @login_required() def workout_stats_view(request,id=0,message="",successmessage=""): - r = Rower.objects.get(user=request.user) + r = getrower(request.user) try: w = Workout.objects.get(id=id) except Workout.DoesNotExist: @@ -4562,7 +4582,7 @@ def workout_advanced_view(request,id=0,message="",successmessage=""): # create interactive plot f1 = row.csvfilename u = row.user.user - r = Rower.objects.get(user=u) + r = getrower(u) # create interactive plot try: @@ -4595,7 +4615,7 @@ def workout_advanced_view(request,id=0,message="",successmessage=""): def workout_comparison_view(request,id1=0,id2=0,xparam='distance',yparam='spm'): promember=0 if not request.user.is_anonymous(): - r = Rower.objects.get(user=request.user) + r = getrower(request.user) result = request.user.is_authenticated() and ispromember(request.user) if result: promember=1 @@ -4630,7 +4650,7 @@ def workout_comparison_view2(request,id1=0,id2=0,xparam='distance', yparam='spm',plottype='line'): promember=0 if not request.user.is_anonymous(): - r = Rower.objects.get(user=request.user) + r = getrower(request.user) result = request.user.is_authenticated() and ispromember(request.user) if result: promember=1 @@ -4706,7 +4726,7 @@ def workout_flexchart3_view(request,*args,**kwargs): promember=0 mayedit=0 if not request.user.is_anonymous(): - r = Rower.objects.get(user=request.user) + r = getrower(request.user) result = request.user.is_authenticated() and ispromember(request.user) if result: promember=1 @@ -4781,7 +4801,7 @@ def workout_flexchart3_view(request,*args,**kwargs): if not request.user.is_anonymous(): workstrokesonly = request.POST['workstrokesonlysave'] reststrokes = not workstrokesonly - r = Rower.objects.get(user=request.user) + r = getrower(request.user) f = FavoriteChart(user=r,xparam=xparam, yparam1=yparam1,yparam2=yparam2, plottype=plottype,workouttype=workouttype, @@ -4889,12 +4909,12 @@ def workout_biginteractive_view(request,id=0,message="",successmessage=""): # create interactive plot f1 = row.csvfilename u = row.user.user - # r = Rower.objects.get(user=u) + # r = getrower(u) promember=0 mayedit=0 if not request.user.is_anonymous(): - r = Rower.objects.get(user=request.user) + r = getrower(request.user) result = request.user.is_authenticated() and ispromember(request.user) if result: promember=1 @@ -4933,12 +4953,12 @@ def workout_otwpowerplot_view(request,id=0,message="",successmessage=""): # create interactive plot f1 = row.csvfilename u = row.user.user - # r = Rower.objects.get(user=u) + # r = getrower(u) promember=0 mayedit=0 if not request.user.is_anonymous(): - r = Rower.objects.get(user=request.user) + r = getrower(request.user) result = request.user.is_authenticated() and ispromember(request.user) if result: promember=1 @@ -5247,7 +5267,7 @@ def workout_edit_view(request,id=0,message="",successmessage=""): # create interactive plot f1 = row.csvfilename u = row.user.user - r = Rower.objects.get(user=u) + r = getrower(u) rowdata = rdata(f1) hascoordinates = 1 if rowdata != 0: @@ -5314,7 +5334,7 @@ def workout_add_otw_powerplot_view(request,id): imagename = f1+timestr+'.png' fullpathimagename = 'static/plots/'+imagename u = w.user.user - r = Rower.objects.get(user=u) + r = getrower(u) powerperc = 100*np.array([r.pw_ut2, r.pw_ut1, r.pw_at, @@ -5375,7 +5395,7 @@ def workout_add_piechart_view(request,id): imagename = f1+timestr+'.png' fullpathimagename = 'static/plots/'+imagename u = w.user.user - r = Rower.objects.get(user=u) + r = getrower(u) powerperc = 100*np.array([r.pw_ut2, r.pw_ut1, @@ -5436,7 +5456,7 @@ def workout_add_power_piechart_view(request,id): imagename = f1+timestr+'.png' fullpathimagename = 'static/plots/'+imagename u = w.user.user - r = Rower.objects.get(user=u) + r = getrower(u) powerperc = 100*np.array([r.pw_ut2, r.pw_ut1, @@ -5495,7 +5515,7 @@ def workout_add_timeplot_view(request,id): imagename = f1+timestr+'.png' fullpathimagename = 'static/plots/'+imagename u = w.user.user - r = Rower.objects.get(user=u) + r = getrower(u) powerperc = 100*np.array([r.pw_ut2, r.pw_ut1, r.pw_at, @@ -5555,7 +5575,7 @@ def workout_add_distanceplot_view(request,id): imagename = f1+timestr+'.png' fullpathimagename = 'static/plots/'+imagename u = w.user.user - r = Rower.objects.get(user=u) + r = getrower(u) powerperc = 100*np.array([r.pw_ut2, r.pw_ut1, r.pw_at, @@ -5613,7 +5633,7 @@ def workout_add_distanceplot2_view(request,id): imagename = f1+timestr+'.png' fullpathimagename = 'static/plots/'+imagename u = w.user.user - r = Rower.objects.get(user=u) + r = getrower(u) powerperc = 100*np.array([r.pw_ut2, r.pw_ut1, r.pw_at, @@ -5673,7 +5693,7 @@ def workout_add_timeplot2_view(request,id): imagename = f1+timestr+'.png' fullpathimagename = 'static/plots/'+imagename u = w.user.user - r = Rower.objects.get(user=u) + r = getrower(u) powerperc = 100*np.array([r.pw_ut2, r.pw_ut1, r.pw_at, @@ -5722,7 +5742,7 @@ def workout_stravaimport_view(request,message=""): res = stravastuff.get_strava_workout_list(request.user) if (res.status_code != 200): if (res.status_code == 401): - r = Rower.objects.get(user=request.user) + r = getrower(request.user) if (r.stravatoken == '') or (r.stravatoken is None): s = "Token doesn't exist. Need to authorize" return HttpResponseRedirect("/rowers/me/stravaauthorize/") @@ -5735,7 +5755,7 @@ def workout_stravaimport_view(request,message=""): return HttpResponseRedirect(url) else: workouts = [] - r = Rower.objects.get(user=request.user) + r = getrower(request.user) stravaids = [int(item['id']) for item in res.json()] knownstravaids = uniqify([ w.uploadedtostrava for w in Workout.objects.filter(user=r) @@ -5772,7 +5792,7 @@ def workout_runkeeperimport_view(request,message=""): res = runkeeperstuff.get_runkeeper_workout_list(request.user) if (res.status_code != 200): if (res.status_code == 401): - r = Rower.objects.get(user=request.user) + r = getrower(request.user) if (r.runkeepertoken == '') or (r.runkeepertoken is None): s = "Token doesn't exist. Need to authorize" return HttpResponseRedirect("/rowers/me/runkeeperauthorize/") @@ -5809,7 +5829,7 @@ def workout_underarmourimport_view(request,message=""): res = underarmourstuff.get_underarmour_workout_list(request.user) if (res.status_code != 200): if (res.status_code == 401): - r = Rower.objects.get(user=request.user) + r = getrower(request.user) if (r.underarmourtoken == '') or (r.underarmourtoken is None): s = "Token doesn't exist. Need to authorize" return HttpResponseRedirect("/rowers/me/underarmourauthorize/") @@ -5857,7 +5877,7 @@ def workout_sporttracksimport_view(request,message=""): res = sporttracksstuff.get_sporttracks_workout_list(request.user) if (res.status_code != 200): if (res.status_code == 401): - r = Rower.objects.get(user=request.user) + r = getrower(request.user) if (r.sporttrackstoken == '') or (r.sporttrackstoken is None): s = "Token doesn't exist. Need to authorize" return HttpResponseRedirect("/rowers/me/sporttracksauthorize/") @@ -5870,7 +5890,7 @@ def workout_sporttracksimport_view(request,message=""): return HttpResponseRedirect(url) else: workouts = [] - r = Rower.objects.get(user=request.user) + r = getrower(request.user) stids = [int(getidfromsturi(item['uri'])) for item in res.json()['items']] knownstids = uniqify([ w.uploadedtosporttracks for w in Workout.objects.filter(user=r) @@ -5906,7 +5926,7 @@ def c2listdebug_view(request,message=""): except C2NoTokenError: return HttpResponseRedirect("/rowers/me/c2authorize/") - r = Rower.objects.get(user=request.user) + r = getrower(request.user) res = c2stuff.get_c2_workout_list(request.user) @@ -5954,7 +5974,7 @@ def workout_getc2workout_all(request,message=""): message = "Something went wrong in workout_c2import_view (C2 token refresh)" messages.error(request,message) else: - r = Rower.objects.get(user=request.user) + r = getrower(request.user) c2ids = [item['id'] for item in res.json()['data']] knownc2ids = uniqify([ w.uploadedtoc2 for w in Workout.objects.filter(user=r) @@ -6011,7 +6031,7 @@ def workout_c2import_view(request,message=""): return HttpResponseRedirect(url) else: workouts = [] - r = Rower.objects.get(user=request.user) + r = getrower(request.user) c2ids = [item['id'] for item in res.json()['data']] knownc2ids = uniqify([ w.uploadedtoc2 for w in Workout.objects.filter(user=r) @@ -6141,7 +6161,7 @@ def workout_getsporttracksworkout_view(request,sporttracksid): def workout_getsporttracksworkout_all(request): res = sporttracksstuff.get_sporttracks_workout_list(request.user) if (res.status_code == 200): - r = Rower.objects.get(user=request.user) + r = getrower(request.user) stids = [int(getidfromsturi(item['uri'])) for item in res.json()['items']] knownstids = uniqify([ w.uploadedtosporttracks for w in Workout.objects.filter(user=r) @@ -6170,7 +6190,7 @@ def workout_getsporttracksworkout_all(request): def workout_getstravaworkout_all(request): res = stravastuff.get_strava_workout_list(request.user) if (res.status_code == 200): - r = Rower.objects.get(user=request.user) + r = getrower(request.user) stravaids = [int(item['id']) for item in res.json()] knownstravaids = uniqify([ w.uploadedtostrava for w in Workout.objects.filter(user=r) @@ -6357,7 +6377,7 @@ def workout_upload_view(request, except KeyError: upload_totp = False - r = Rower.objects.get(user=request.user) + r = getrower(request.user) if request.method == 'POST': form = DocumentsForm(request.POST,request.FILES) optionsform = UploadOptionsForm(request.POST) @@ -6431,7 +6451,7 @@ def workout_upload_view(request, imagename = f1[:-4]+'.png' fullpathimagename = 'static/plots/'+imagename u = request.user - r = Rower.objects.get(user=request.user) + r = getrower(request.user) powerperc = 100*np.array([r.pw_ut2, r.pw_ut1, r.pw_at, @@ -6605,7 +6625,7 @@ def team_workout_upload_view(request,message="", make_plot = uploadoptions['make_plot'] plottype = uploadoptions['plottype'] - r = Rower.objects.get(user=request.user) + r = getrower(request.user) if request.method == 'POST': form = DocumentsForm(request.POST,request.FILES) optionsform = TeamUploadOptionsForm(request.POST) @@ -6620,7 +6640,7 @@ def team_workout_upload_view(request,message="", if rowerform.is_valid(): u = rowerform.cleaned_data['user'] if u: - r = Rower.objects.get(user=u) + r = getrower(u) else: message = 'Please select a rower' messages.error(request,message) @@ -6834,7 +6854,7 @@ def graph_delete_view(request,id=0): @login_required() def graphs_view(request): try: - r = Rower.objects.get(user=request.user) + r = getrower(request.user) workouts = Workout.objects.filter(user=r).order_by("-date", "-starttime") query = request.GET.get('q') if query: @@ -6892,7 +6912,7 @@ def workout_summary_restore_view(request,id,message="",successmessage=""): # still here - this is a workout we may edit f1 = row.csvfilename u = row.user.user - r = Rower.objects.get(user=u) + r = getrower(u) powerperc = 100*np.array([r.pw_ut2, r.pw_ut1, r.pw_at, @@ -7010,7 +7030,7 @@ def workout_summary_edit_view(request,id,message="",successmessage="" # still here - this is a workout we may edit f1 = row.csvfilename u = row.user.user - r = Rower.objects.get(user=u) + r = getrower(u) powerperc = 100*np.array([r.pw_ut2, r.pw_ut1, r.pw_at, @@ -7208,7 +7228,7 @@ def workout_summary_edit_view(request,id,message="",successmessage="" def rower_favoritecharts_view(request): message = '' successmessage = '' - r = Rower.objects.get(user=request.user) + r = getrower(request.user) favorites = FavoriteChart.objects.filter(user=r).order_by('id') aantal = len(favorites) favorites_data = [{'yparam1':f.yparam1, @@ -7272,7 +7292,7 @@ def rower_favoritecharts_view(request): # Add email address to form so user can change his email address @login_required() def rower_edit_view(request,message=""): - r = Rower.objects.get(user=request.user) + r = getrower(request.user) if request.method == 'POST' and "ut2" in request.POST: form = RowerForm(request.POST) if form.is_valid(): @@ -7286,7 +7306,7 @@ def rower_edit_view(request,message=""): an = cd['an'] rest = cd['rest'] try: - r = Rower.objects.get(user=request.user) + r = getrower(request.user) r.max = max(min(hrmax,250),10) r.ut2 = max(min(ut2,250),10) r.ut1 = max(min(ut1,250),10) @@ -7343,7 +7363,7 @@ def rower_edit_view(request,message=""): ftp = cd['ftp'] otwslack = cd['otwslack'] try: - r = Rower.objects.get(user=request.user) + r = getrower(request.user) powerfrac = 100*np.array([r.pw_ut2, r.pw_ut1, r.pw_at, @@ -7401,7 +7421,7 @@ def rower_edit_view(request,message=""): anname = cd['anname'] powerzones = [ut3name,ut2name,ut1name,atname,trname,anname] try: - r = Rower.objects.get(user=request.user) + r = getrower(request.user) r.pw_ut2 = pw_ut2 r.pw_ut1 = pw_ut1 r.pw_at = pw_at @@ -7465,7 +7485,7 @@ def rower_edit_view(request,message=""): if len(email): u.email = email u.save() - r = Rower.objects.get(user=u) + r = getrower(u) r.weightcategory = weightcategory r.save() form = RowerForm(instance=r) @@ -7501,7 +7521,7 @@ def rower_edit_view(request,message=""): else: try: - r = Rower.objects.get(user=request.user) + r = getrower(request.user) form = RowerForm(instance=r) powerform = RowerPowerForm(instance=r) @@ -7536,7 +7556,7 @@ def rower_revokeapp_view(request,id=0): for token in refreshtokens: token.revoke() - r = Rower.objects.get(user=request.user) + r = getrower(request.user) form = RowerForm(instance=r) powerform = RowerPowerForm(instance=r) grants = AccessToken.objects.filter(user=request.user) @@ -7700,7 +7720,7 @@ def strokedatajson(request,id): row.csvfilename = csvfilename row.save() - r = Rower.objects.get(user=request.user) + r = getrower(request.user) powerperc = 100*np.array([r.pw_ut2, r.pw_ut1, r.pw_at, @@ -7733,7 +7753,7 @@ import teams def team_view(request,id=0): ismember = 0 hasrequested = 0 - r = Rower.objects.get(user=request.user) + r = getrower(request.user) myteams = Team.objects.filter(manager=request.user) try: @@ -7799,7 +7819,7 @@ def team_leaveconfirm_view(request,id=0): @login_required() def team_leave_view(request,id=0): - r = Rower.objects.get(user=request.user) + r = getrower(request.user) teams.remove_member(id,r) url = reverse(rower_teams_view) @@ -7822,7 +7842,7 @@ def rower_teams_view(request,message='',successmessage=''): else: form = TeamInviteCodeForm() - r = Rower.objects.get(user=request.user) + r = getrower(request.user) ts = Team.objects.filter(rower=r) myteams = Team.objects.filter(manager=request.user) otherteams = Team.objects.filter(private='open').exclude(rower=r).exclude(manager=request.user).order_by('name') @@ -8050,7 +8070,7 @@ def team_create_view(request): @user_passes_test(iscoachmember,login_url="/",redirect_field_name=None) def team_deleteconfirm_view(request,id): - r = Rower.objects.get(user=request.user) + r = getrower(request.user) try: t = Team.objects.get(id=id) except Team.DoesNotExist: @@ -8066,7 +8086,7 @@ def team_deleteconfirm_view(request,id): @user_passes_test(iscoachmember,login_url="/",redirect_field_name=None) def team_delete_view(request,id): - r = Rower.objects.get(user=request.user) + r = getrower(request.user) try: t = Team.objects.get(id=id) except Team.DoesNotExist: @@ -8082,7 +8102,7 @@ def team_delete_view(request,id): @user_passes_test(iscoachmember,login_url="/",redirect_field_name=None) def team_members_stats_view(request,id): - r = Rower.objects.get(user=request.user) + r = getrower(request.user) try: t = Team.objects.get(id=id) except Team.DoesNotExist: From 832d6f88786471a0534290cf4e3269722cd36400 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Thu, 8 Jun 2017 14:19:21 +0200 Subject: [PATCH 11/11] OTW power CP graph working! --- rowers/interactiveplots.py | 14 +++- rowers/templates/analysis.html | 20 +++++ rowers/templates/otwrankings.html | 118 ++++++++++++++++-------------- rowers/views.py | 62 +++++++++------- 4 files changed, 130 insertions(+), 84 deletions(-) diff --git a/rowers/interactiveplots.py b/rowers/interactiveplots.py index 0fdb77c1..0e2e4d23 100644 --- a/rowers/interactiveplots.py +++ b/rowers/interactiveplots.py @@ -608,6 +608,10 @@ def interactive_otwcpchart(powerdf,promember=0): x_axis_type = 'log' y_axis_type = 'linear' + deltas = powerdf['Delta'].apply(lambda x: timedeltaconv(x)) + powerdf['ftime'] = niceformat(deltas) + + source = ColumnDataSource( data = powerdf ) @@ -668,7 +672,7 @@ def interactive_otwcpchart(powerdf,promember=0): ) plot.circle('Delta','CP',source=source,fill_color='red',size=15, - legend='Power') + legend='Power Data') plot.xaxis.axis_label = "Duration (seconds)" plot.yaxis.axis_label = "Power (W)" @@ -676,6 +680,14 @@ def interactive_otwcpchart(powerdf,promember=0): plot.x_range = Range1d(1,2*max(thesecs)) plot.legend.orientation = "vertical" + hover = plot.select(dict(type=HoverTool)) + + hover.tooltips = OrderedDict([ + ('Duration ','@ftime'), + ('Power (W)','@CP{int}'), + ]) + + hover.mode = 'mouse' plot.line('duration','power',source=sourcecomplex,legend="CP Model", color='green') diff --git a/rowers/templates/analysis.html b/rowers/templates/analysis.html index 2dc2cf43..9fd83289 100644 --- a/rowers/templates/analysis.html +++ b/rowers/templates/analysis.html @@ -77,8 +77,28 @@
+
+
+

 

+
+
+
+

+ {% if user.rower.rowerplan == 'pro' or user.rower.rowerplan == 'coach' %} + OTW Ranking Pieces + {% else %} + OTW Ranking Pieces + {% endif %} +

+

+ Analyse power vs piece duration to make predictions. +

+
+ +
+ {% endblock %} diff --git a/rowers/templates/otwrankings.html b/rowers/templates/otwrankings.html index 35b35865..a78330e4 100644 --- a/rowers/templates/otwrankings.html +++ b/rowers/templates/otwrankings.html @@ -67,9 +67,12 @@ https://rowsandall.com/rowers/{{ id }}/otw-bests/{{ startdate|date:"Y-m-d" }}/{{ enddate|date:"Y-m-d" }}

-

The table gives the best efforts achieved on the official Concept2 ranking pieces in the selected date range.

- -

This page will evolve and try to give you guidance on where to improve.

+

The table gives the OTW efforts you marked as Ranking Piece. + The graph shows the best segments from those pieces, plotted as + average power (over the segment) vs the duration of the segment/ + In other words: How long you can hold that power. +

+

At the bottom of the page, you will find predictions derived from the model.

Use this form to select a different date range:

@@ -98,6 +101,16 @@
+ + +
+ +

Critical Power Plot

+ + {{ the_div|safe }} + +
+

Ranking Piece Results

@@ -109,6 +122,7 @@ Distance Duration + Avg Power Date Avg HR Max HR @@ -118,8 +132,9 @@ {% for workout in rankingworkouts %} - {{ workout.distance }} + {{ workout.distance }} m {{ workout.duration |durationprint:"%H:%M:%S.%f" }} + {{ avgpower|lookup:workout.id }} W {{ workout.date }} {{ workout.averagehr }} {{ workout.maxhr }} @@ -137,63 +152,56 @@
- -
- -

Critical Power Plot

- - {{ the_div|safe }} - -
-
-

Pace predictions for Ranking Pieces

+

Pace predictions for Ranking Pieces

-

Add non-ranking piece using the form. The piece will be added in the prediction tables below.

-
-
- {{ form.value }} {{ form.pieceunit }} - - {% csrf_token %} -
-
- - -
+

Add non-ranking piece using the form. The piece will be added in the prediction tables below.

-
- No Paul Data -
-
-

CP Model

- - - - - - - - - {% for pred in cpredictions %} - - {% for key, value in pred.items %} - {% if key == "power" %} - - {% endif %} - {% if key == "duration" %} - - {% endif %} - {% endfor %} - - {% endfor %} - -
Duration Power
{{ value }} W {{ value |deltatimeprint }}
+
+ + + + + + + + + {% for pred in cpredictions %} + + {% for key, value in pred.items %} + {% if key == "power" %} + + {% endif %} + {% if key == "duration" %} + + {% endif %} + {% endfor %} + + {% endfor %} + +
Duration Power
{{ value }} W {{ value |deltatimeprint }}
-
+
+ +
+
+ {{ form.value }} {{ form.pieceunit }} + + {% csrf_token %} +
+
+ minutes +
+
+ + +
+ +
{% endblock %} diff --git a/rowers/views.py b/rowers/views.py index 85f321aa..0c5ae939 100644 --- a/rowers/views.py +++ b/rowers/views.py @@ -292,14 +292,11 @@ def iscoachmember(user): return result def getrower(user): - if not user.is_anonymous(): - try: - r = Rower.objects.get(user=user) - except Rower.DoesNotExist: - r = Rower(user=user) - r.save() - else: - raise PermissionDenied("You need to log in to use this function") + try: + r = Rower.objects.get(user=user) + except Rower.DoesNotExist: + r = Rower(user=user) + r.save() return r @@ -2900,8 +2897,10 @@ def otwrankings_view(request,theuser=0, thesecs.append(timesecs) - - maxt = pd.Series(thesecs).max() + if len(thesecs) != 0: + maxt = pd.Series(thesecs).max() + else: + maxt = 1000. maxlog10 = np.log10(maxt) logarr = np.arange(100)*maxlog10/100. @@ -2912,11 +2911,16 @@ def otwrankings_view(request,theuser=0, delta = [] cpvalue = [] + avgpower = {} dfgrouped = df.groupby(['workoutid']) for id,group in dfgrouped: tt = group['time'] ww = group['power'] + try: + avgpower[id] = int(ww.mean()) + except ValueError: + avgpower[id] = '---' if not np.isnan(ww.mean()): length = len(ww) dt = [] @@ -2946,6 +2950,7 @@ def otwrankings_view(request,theuser=0, for d in logarr: delta.append(d) + print avgpower dt = pd.Series(delta,name='Delta') cpvalue = pd.Series(cpvalue,name='CP') @@ -2955,13 +2960,15 @@ def otwrankings_view(request,theuser=0, 'CP':cpvalue, }) - powerdf.sort_values(['Delta','CP'],ascending=[1,0]) - powerdf.drop_duplicates(subset='Delta',keep='first') + 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) # create interactive plot if len(powerdf) !=0 : - res = interactive_otwcpchart(powerdf) + res = interactive_otwcpchart(powerdf,promember=promember) script = res[0] div = res[1] p1 = res[2] @@ -2979,16 +2986,12 @@ def otwrankings_view(request,theuser=0, if request.method == 'POST' and "piece" in request.POST: form = PredictedPieceForm(request.POST) - if form.is_valid(): - value = form.cleaned_data['value'] - hourvalue,value = divmod(value,60) - if hourvalue >= 24: - hourvalue = 23 - pieceunit = form.cleaned_data['pieceunit'] - if pieceunit == 'd': - rankingdistances.append(value) - else: - rankingdurations.append(datetime.time(minute=value,hour=hourvalue)) + clean = form.is_valid() + value = form.cleaned_data['value'] + hourvalue,value = divmod(value,60) + if hourvalue >= 24: + hourvalue = 23 + rankingdurations.append(datetime.time(minute=value,hour=hourvalue)) else: form = PredictedPieceForm() @@ -3012,13 +3015,15 @@ def otwrankings_view(request,theuser=0, if pwr <= 0: pwr = 50. - a = { - 'duration':timedeltaconv(t), - 'power':int(pwr)} - cpredictions.append(a) + if not np.isnan(pwr): + a = { + 'duration':timedeltaconv(t), + 'power':int(pwr)} + cpredictions.append(a) - print cpredictions + del form.fields["pieceunit"] + messages.error(request,message) return render(request, 'otwrankings.html', {'rankingworkouts':theworkouts, @@ -3026,6 +3031,7 @@ def otwrankings_view(request,theuser=0, 'the_div':div, 'predictions':predictions, 'cpredictions':cpredictions, + 'avgpower':avgpower, 'form':form, 'dateform':dateform, 'deltaform':deltaform,