From e9e7326505a26cc732d7b6be7425b6de91a8aed6 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Sun, 15 Jan 2023 14:47:34 +0100 Subject: [PATCH 1/6] fix --- rowers/views/statements.py | 2 +- rowers/views/workoutviews.py | 64 ++++++++++++++++++++---------------- 2 files changed, 36 insertions(+), 30 deletions(-) diff --git a/rowers/views/statements.py b/rowers/views/statements.py index 745118e3..e0fdc6f1 100644 --- a/rowers/views/statements.py +++ b/rowers/views/statements.py @@ -33,7 +33,7 @@ from rq.exceptions import NoSuchJobError import threading import redis import colorsys - +import re import zipfile import bleach import arrow diff --git a/rowers/views/workoutviews.py b/rowers/views/workoutviews.py index 9d1f7538..03802078 100644 --- a/rowers/views/workoutviews.py +++ b/rowers/views/workoutviews.py @@ -5234,6 +5234,8 @@ def workout_upload_api(request): r = None if form.is_valid(): t = form.cleaned_data['title'] + t = re.sub('\r',' ',t) + t = re.sub('\n',' ',t) boattype = form.cleaned_data['boattype'] workouttype = form.cleaned_data['workouttype'] try: @@ -6701,40 +6703,44 @@ def workout_summary_edit_view(request, id, message="", successmessage="" vals = None # feeling lucky / ruptures if request.method == 'POST' and "ruptures" in request.POST: - df = pd.DataFrame({ - 'spm': rowdata.df[' Cadence (stokes/min)'], - 'power': rowdata.df[' Power (watts)'], - 'v': rowdata.df[' AverageBoatSpeed (m/s)'] - }) - algo = rpt.Pelt(model="rbf").fit(df.values) - result = algo.predict(pen=10) - - df['time'] = rowdata.df['TimeStamp (sec)'].values try: - timeprev = int(df['time'].values[0]) - timenext = int(df['time'].values[result[0]]) - s = '{delta}sec'.format(delta=timenext-timeprev) - except IndexError: # pragma: no cover - s = '0sec' + df = pd.DataFrame({ + 'spm': rowdata.df[' Cadence (stokes/min)'], + 'power': rowdata.df[' Power (watts)'], + 'v': rowdata.df[' AverageBoatSpeed (m/s)'] + }) + algo = rpt.Pelt(model="rbf").fit(df.values) + result = algo.predict(pen=10) + + df['time'] = rowdata.df['TimeStamp (sec)'].values + try: + timeprev = int(df['time'].values[0]) + timenext = int(df['time'].values[result[0]]) + s = '{delta}sec'.format(delta=timenext-timeprev) + except IndexError: # pragma: no cover + s = '0sec' - for i in range(len(result)-1): - timeprev = int(df['time'].values[result[i]-1]) - timenext = int(df['time'].values[result[i+1]-1]) - interval = '+{delta}sec'.format(delta=timenext-timeprev) - s += interval + for i in range(len(result)-1): + timeprev = int(df['time'].values[result[i]-1]) + timenext = int(df['time'].values[result[i+1]-1]) + interval = '+{delta}sec'.format(delta=timenext-timeprev) + s += interval - try: - rowdata.updateinterval_string(s) - except: # pragma: no cover + try: + rowdata.updateinterval_string(s) + except: # pragma: no cover + messages.error(request, "Nope, you were not lucky") + + intervalstats = rowdata.allstats() + itime, idist, itype = rowdata.intervalstats_values() + nrintervals = len(idist) + savebutton = 'savestringform' + intervalString = s + form = SummaryStringForm(initial={'intervalstring': intervalString}) + + except KeyError: messages.error(request, "Nope, you were not lucky") - intervalstats = rowdata.allstats() - itime, idist, itype = rowdata.intervalstats_values() - nrintervals = len(idist) - savebutton = 'savestringform' - intervalString = s - form = SummaryStringForm(initial={'intervalstring': intervalString}) - # We have submitted the mini language interpreter if request.method == 'POST' and "intervalstring" in request.POST: form = SummaryStringForm(request.POST) From dc6981999020359b3c981a9eefd42cb40c36b46a Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Mon, 16 Jan 2023 18:01:39 +0100 Subject: [PATCH 2/6] first attempt, untested --- rowers/views/apiviews.py | 394 ++++++++++++++++++++++++++------------- 1 file changed, 263 insertions(+), 131 deletions(-) diff --git a/rowers/views/apiviews.py b/rowers/views/apiviews.py index 06745200..fc1226f8 100644 --- a/rowers/views/apiviews.py +++ b/rowers/views/apiviews.py @@ -37,6 +37,138 @@ def strokedataform(request, id=0): 'workout': w, }) # pragma: no cover +def api_get_dataframe(startdatetime, df): + try: + time = df['time']/1.e3 + except KeyError: # pragma: no cover + try: + time = df['t']/10. + except KeyError: + return 400, "Missing time", pd.DataFrame() + + try: + spm = df['spm'] + except KeyError: # pragma: no cover + return 400, "Missing spm", pd.DataFrame() + try: + distance = df['distance'] + except KeyError: # pragma: no cover + try: + distance = df['d']/10. + except KeyError: + return 400, "Missing distance", pd.DataFrame() + + try: + pace = df['pace']/1.e3 + except KeyError: # pragma: no cover + try: + pace = df['p']/10. + except KeyError: + return 400, "Missing pace", pd.DataFrame() + + + try: + power = df['power'] + except KeyError: # pragma: no cover + power = 0*time + try: + drivelength = df['drivelength'] + except KeyError: + drivelength = 0*time + try: + dragfactor = df['dragfactor'] + except KeyError: + dragfactor = 0*time + try: + drivetime = df['drivetime'] + except KeyError: + drivetime = 0*time + try: + strokerecoverytime = df['strokerecoverytime'] + except KeyError: + strokerecoverytime = 0*time + try: + averagedriveforce = df['averagedriveforce'] + except KeyError: + averagedriveforce = 0*time + try: + peakdriveforce = df['peakdriveforce'] + except KeyError: + peakdriveforce = 0*time + try: + wash = df['wash'] + except KeyError: + wash = 0*time + try: + catch = df['catch'] + except KeyError: + catch = 0*time + try: + finish = df['finish'] + except KeyError: + finish = 0*time + try: + peakforceangle = df['peakforceangle'] + except KeyError: + peakforceangle = 0*time + try: + driveenergy = df['driveenergy'] + except KeyError: + driveenergy = 60.*power/spm + try: + slip = df['slip'] + except KeyError: + slip = 0*time + try: + lapidx = df['lapidx'] + except KeyError: + lapidx = 0*time + try: + hr = df['hr'] + except KeyError: # pragma: no cover + hr = 0*df['time'] + + try: + latitude = df['latitude'] + except KeyError: + latitude = 0*df['time'] + + try: + longitude = df['longitude'] + except KeyError: + longitude = 0*df['time'] + + starttime = totimestamp(startdatetime)+time[0] + unixtime = starttime+time + + dologging('apilog.log',"(strokedatajson_v2/3 POST - data parsed)") + + data = pd.DataFrame({'TimeStamp (sec)': unixtime, + ' Horizontal (meters)': distance, + ' Cadence (stokes/min)': spm, + ' HRCur (bpm)': hr, + ' DragFactor': dragfactor, + ' Stroke500mPace (sec/500m)': pace, + ' Power (watts)': power, + ' DriveLength (meters)': drivelength, + ' DriveTime (ms)': drivetime, + ' StrokeRecoveryTime (ms)': strokerecoverytime, + ' AverageDriveForce (lbs)': averagedriveforce, + ' PeakDriveForce (lbs)': peakdriveforce, + ' lapIdx': lapidx, + ' ElapsedTime (sec)': time, + 'catch': catch, + 'slip': slip, + 'finish': finish, + 'wash': wash, + 'driveenergy': driveenergy, + 'peakforceangle': peakforceangle, + ' latitude': latitude, + ' longitude': longitude, + }) + + return 200, "", data + @login_required() def strokedataform_v2(request, id=0): @@ -69,10 +201,135 @@ def strokedataform_v2(request, id=0): }) # pragma: no cover +@csrf_exempt +@login_required() +@api_view(["POST"]) +@permission_classes([IsAuthenticated]) +def strokedatajson_v3(request): + """ + POST: Add Stroke data to workout + GET: Get stroke data of workout + This v2 API works on stroke based data dict: + {"startdatetime":"2023-01-16 12:34:45", + "totalDistance":11233, + "elapsedTime":"65:23.10", + "summary":"Some summary", + "timezone":"UTC", + "workouttype":"rower", + "boattype":"1x", + "notes":"some notes", + "title":"Workout title", + "rpe":4, + "data": + [ + {"hr": 110, "p": 3600, "spm": 53, "d": 6, "t": 12, "latitude": 52.343, "longitude": -6.342}, + {"hr": 111, "p": 3600, "spm": 53, "d": 6, "t": 12, "latitude": 52.343, "longitude": -6.342}, + {"hr": 111, "p": 3600, "spm": 64, "d": 6, "t": 22, "latitude": 52.343, "longitude": -6.342}, + {"hr": 110, "p": 3600, "spm": 16, "d": 14, "t": 55, "latitude": 52.343, "longitude": -6.342}, + {"hr": 110, "p": 3600, "spm": 16, "d": 14, "t": 82, "latitude": 52.343, "longitude": -6.342}, + {"hr": 107, "p": 3600, "spm": 12, "d": 22, "t": 109, "latitude": 52.343, "longitude": -6.342}, + {"hr": 107, "p": 3600, "spm": 12, "d": 22, "t": 133, "latitude": 52.343, "longitude": -6.342}, + {"hr": 108, "p": 3600, "spm": 12, "d": 32, "t": 157, "latitude": 52.343, "longitude": -6.342}, + {"hr": 108, "p": 3577, "spm": 12, "d": 32, "t": 157, "latitude": 52.343, "longitude": -6.342}, + {"hr": 108, "p": 3411, "spm": 12, "d": 32, "t": 157, "latitude": 52.343, "longitude": -6.342}, + {"hr": 108, "p": 2649, "spm": 12, "d": 32, "t": 157, "latitude": 52.343, "longitude": -6.342}, + {"hr": 108, "p": 3099, "spm": 12, "d": 32, "t": 157, "latitude": 52.343, "longitude": -6.342}, + {"hr": 108, "p": 3600, "spm": 12, "d": 32, "t": 157, "latitude": 52.343, "longitude": -6.342}, + {"hr": 100, "p": 3600, "spm": 44, "d": 115, "t": 292, "latitude": 52.343, "longitude": -6.342}, + {"hr": 99, "p": 3600, "spm": 27, "d": 129, "t": 305, "latitude": 52.343, "longitude": -6.342}, + {"hr": 97, "p": 3600, "spm": 34, "d": 161, "t": 330, "latitude": 52.343, "longitude": -6.342}, + {"hr": 96, "p": 3600, "spm": 25, "d": 177, "t": 344, "latitude": 52.343, "longitude": -6.342}, + {"hr": 96, "p": 3494, "spm": 43, "d": 196, "t": 357, "latitude": 52.343, "longitude": -6.342}, + {"hr": 98, "p": 2927, "spm": 26, "d": 235, "t": 377, "latitude": 52.343, "longitude": -6.342}, + {"hr": 102, "p": 2718, "spm": 27, "d": 380, "t": 455, "latitude": 52.343, "longitude": -6.342}, + {"hr": 102, "p": 2753, "spm": 9, "d": 398, "t": 472, "latitude": 52.343, "longitude": -6.342}, + {"hr": 102, "p": 2864, "spm": 61, "d": 406, "t": 477, "latitude": 52.343, "longitude": -6.342}, + {"hr": 101, "p": 2780, "spm": 15, "d": 484, "t": 515, "latitude": 52.343, "longitude": -6.342}, + {"hr": 101, "p": 2365, "spm": 16, "d": 583, "t": 554, "latitude": 52.343, "longitude": -6.342}, + {"hr": 103, "p": 1965, "spm": 16, "d": 681, "t": 592, "latitude": 52.343, "longitude": -6.342}, + ] + } + """ + + if request.method != 'POST': + return HttpResponse("Not allowed", status=409) + + dologging('apilog.log', request.user.username+" (strokedatajson_v3 POST)") + + title = request.data.get('title','') + try: + elapsedTime = request.data['elapsedTime'] + except KeyError: + return HttpResponse("Missing Elapsed Time", status=400) + try: + totalDistance = request.data['totalDistance'] + except KeyError: + return HttpResponse("Missing Total Distance", status=400) + summary = request.data.get('summary','') + timezone = request.data.get('timezone','UTC') + workouttype = request.data.get('workouttype','rower') + boattype = request.data.get('boattype','1x') + notes = request.data.get('notes','') + rpe = request.data.get('rpe',0) + + df = pd.DataFrame() + + try: + df = pd.DataFrame(request.data['data']) + except KeyError: + try: + df = pd.DataFrame(request.data['strokedata']) + except: + return HttpResponse("No JSON Object could be decoded", status=400) + + df.index = df.index.astype(int) + df.sort_index(inplace=True) + + status, comment, data = api_get_dataframe(startdatetime, df) + + if status != 200: + return HttpResponse(comment, status=status) + + + csvfilename = 'media/{code}.csv'.format(code=uuid4().hex[:16]) + _ = data.to_csv(csvfilename+'.gz', index_label='index', compression='gzip') + + uploadoptions = { + 'secret': UPLOAD_SERVICE_SECRET, + 'user': userid, + 'file': csvfilename, + 'title': title, + 'workouttype': workouttype, + 'boattype': boattype, + 'summary': summary, + 'elapsedTime': elapsedTime/1000., # in seconds + 'totalDistance': totalDistance, + 'rpe': rpe, + 'notes': notes, + 'timezone': timezone, + } + session = requests.session() + newHeaders = {'Content-type': 'application/json', 'Accept': 'text/plain'} + session.headers.update(newHeaders) + + response = session.post(UPLOAD_SERVICE_URL, json=uploadoptions) + + if response.status_code != 200: + return HttpResponse(response.text, response.status_code) + + try: + workoutid = response.json()['id'] + except KeyError: + workoutid = 1 + + return JsonResponse( + {"workout public id": encoder.encode_hex(workoutid), + "status": "success", + }) + + # Process the POSTed stroke data according to the API definition # Return the GET stroke data according to the API definition - - @csrf_exempt @login_required() @api_view(["GET", "POST"]) @@ -161,135 +418,10 @@ def strokedatajson_v2(request, id): df.index = df.index.astype(int) df.sort_index(inplace=True) - try: - time = df['time']/1.e3 - except KeyError: # pragma: no cover - try: - time = df['t']/10. - except KeyError: - return HttpResponse("Missing time", status=400) - - try: - spm = df['spm'] - except KeyError: # pragma: no cover - return HttpResponse("Missing spm", status=400) - - try: - distance = df['distance'] - except KeyError: # pragma: no cover - try: - distance = df['d']/10. - except KeyError: - return HttpResponse("Missing distance", status=400) - - try: - pace = df['pace']/1.e3 - except KeyError: # pragma: no cover - try: - pace = df['p']/10. - except KeyError: - return HttpResponse("Missing pace", status=400) - - try: - power = df['power'] - except KeyError: # pragma: no cover - power = 0*time - try: - drivelength = df['drivelength'] - except KeyError: - drivelength = 0*time - try: - dragfactor = df['dragfactor'] - except KeyError: - dragfactor = 0*time - try: - drivetime = df['drivetime'] - except KeyError: - drivetime = 0*time - try: - strokerecoverytime = df['strokerecoverytime'] - except KeyError: - strokerecoverytime = 0*time - try: - averagedriveforce = df['averagedriveforce'] - except KeyError: - averagedriveforce = 0*time - try: - peakdriveforce = df['peakdriveforce'] - except KeyError: - peakdriveforce = 0*time - try: - wash = df['wash'] - except KeyError: - wash = 0*time - try: - catch = df['catch'] - except KeyError: - catch = 0*time - try: - finish = df['finish'] - except KeyError: - finish = 0*time - try: - peakforceangle = df['peakforceangle'] - except KeyError: - peakforceangle = 0*time - try: - driveenergy = df['driveenergy'] - except KeyError: - driveenergy = 60.*power/spm - try: - slip = df['slip'] - except KeyError: - slip = 0*time - try: - lapidx = df['lapidx'] - except KeyError: - lapidx = 0*time - try: - hr = df['hr'] - except KeyError: # pragma: no cover - hr = 0*df['time'] - - try: - latitude = df['latitude'] - except KeyError: - latitude = 0*df['time'] - - try: - longitude = df['longitude'] - except KeyError: - longitude = 0*df['time'] - - starttime = totimestamp(row.startdatetime)+time[0] - unixtime = starttime+time - - dologging('apilog.log',"(strokedatajson_v2 POST - data parsed)") - - data = pd.DataFrame({'TimeStamp (sec)': unixtime, - ' Horizontal (meters)': distance, - ' Cadence (stokes/min)': spm, - ' HRCur (bpm)': hr, - ' DragFactor': dragfactor, - ' Stroke500mPace (sec/500m)': pace, - ' Power (watts)': power, - ' DriveLength (meters)': drivelength, - ' DriveTime (ms)': drivetime, - ' StrokeRecoveryTime (ms)': strokerecoverytime, - ' AverageDriveForce (lbs)': averagedriveforce, - ' PeakDriveForce (lbs)': peakdriveforce, - ' lapIdx': lapidx, - ' ElapsedTime (sec)': time, - 'catch': catch, - 'slip': slip, - 'finish': finish, - 'wash': wash, - 'driveenergy': driveenergy, - 'peakforceangle': peakforceangle, - ' latitude': latitude, - ' longitude': longitude, - }) - + status, comment, data = api_get_dataframe(row.startdatetime, df) + if status != 200: + return HttpResponse(comment, status=status) + r = getrower(request.user) timestr = row.startdatetime.strftime("%Y%m%d-%H%M%S") From fd5ce16e4b6acddcd296556e422046a0747690db Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Mon, 16 Jan 2023 19:58:26 +0100 Subject: [PATCH 3/6] now working and with a test --- rowers/tests/test_api.py | 72 ++++++++++++++++++++++++++++++++ rowers/urls.py | 2 + rowers/views/apiviews.py | 90 +++++++++++++++++++--------------------- 3 files changed, 116 insertions(+), 48 deletions(-) diff --git a/rowers/tests/test_api.py b/rowers/tests/test_api.py index f1a6f73b..32fc2af6 100644 --- a/rowers/tests/test_api.py +++ b/rowers/tests/test_api.py @@ -180,3 +180,75 @@ class OwnApi(TestCase): response = strokedatajson_v2(request,id=w.id) self.assertEqual(response.status_code,200) + + def test_strokedataform_v3(self): + login = self.c.login(username=self.u.username, password=self.password) + self.assertTrue(login) + + w = self.user_workouts[1] + + url = reverse('strokedatajson_v3') + + request = self.factory.get(url) + request.user = self.u + force_authenticate(request, user=self.u) + response = strokedatajson_v3(request) + self.assertEqual(response.status_code,405) + + + strokedata = { + "data": [ + { + "time": 3200.0000476837, + "pace": 155068.4885951763, + "hr": 85.7857142857, + "power": 84.6531131591, + "distance": 23, + "spm": 16.380952381 + }, + { + "time": 6700.0000476837, + "pace" : 144402.6407586741, + "hr": 91.2142857143, + "power": 117.458827834, + "distance": 36, + "spm": 21.1666666667 + }, + { + "time": 10099.9999046326, + "pace": 138830.8712654931, + "hr": 95.7142857143, + "power": 141.31057207, + "distance": 48, + "spm": 19.8095238095 + } + ] + } + + form_data = { + "totalDistance": 100, + "elapsedTime": 29000, + "title": "Test Workout (GO)", + "startdatetime": "2023-01-16 17:54:35.588838+00:00", + "workouttype": "water", + "boattype": "1x", + "notes": "some\nnotes", + "strokes": strokedata, + } + + + url = reverse('strokedatajson_v3') + + request = self.factory.post(url,form_data,format='json') + request.user = self.u + request.data = json.dumps(form_data) + + force_authenticate(request, user=self.u) + with patch('rowers.dataprep.getrowdata_db') as mock_getrowdata: + mock_getrowdata.return_value = (pd.DataFrame(),None) + response = strokedatajson_v3(request) + + self.assertEqual(response.status_code,200) + response = json.loads(response.content) + x = response['workout id'] + diff --git a/rowers/urls.py b/rowers/urls.py index 9c59e802..c9760f9a 100644 --- a/rowers/urls.py +++ b/rowers/urls.py @@ -247,6 +247,8 @@ urlpatterns = [ name='strokedatajson'), re_path(r'^api/v2/workouts/(?P\b[0-9A-Fa-f]+\b)/strokedata/$', views.strokedatajson_v2, name='strokedatajson_v2'), + re_path(r'^api/v3/workouts/$', views.strokedatajson_v3, + name='strokedatajson_v3'), re_path(r'^500v/$', views.error500_view, name='error500_view'), re_path(r'^500q/$', views.servererror_view, name='servererror_view'), path('502/', TemplateView.as_view(template_name='502.html'), name='502'), diff --git a/rowers/views/apiviews.py b/rowers/views/apiviews.py index fc1226f8..88efe374 100644 --- a/rowers/views/apiviews.py +++ b/rowers/views/apiviews.py @@ -3,6 +3,8 @@ from rowers.tasks import handle_calctrimp from rowers.opaque import encoder import arrow +import pendulum +from rowsandall_app.settings import UPLOAD_SERVICE_SECRET, UPLOAD_SERVICE_URL # Stroke data form to test API upload @@ -210,49 +212,35 @@ def strokedatajson_v3(request): POST: Add Stroke data to workout GET: Get stroke data of workout This v2 API works on stroke based data dict: - {"startdatetime":"2023-01-16 12:34:45", - "totalDistance":11233, - "elapsedTime":"65:23.10", - "summary":"Some summary", - "timezone":"UTC", - "workouttype":"rower", - "boattype":"1x", - "notes":"some notes", - "title":"Workout title", - "rpe":4, - "data": - [ - {"hr": 110, "p": 3600, "spm": 53, "d": 6, "t": 12, "latitude": 52.343, "longitude": -6.342}, - {"hr": 111, "p": 3600, "spm": 53, "d": 6, "t": 12, "latitude": 52.343, "longitude": -6.342}, - {"hr": 111, "p": 3600, "spm": 64, "d": 6, "t": 22, "latitude": 52.343, "longitude": -6.342}, - {"hr": 110, "p": 3600, "spm": 16, "d": 14, "t": 55, "latitude": 52.343, "longitude": -6.342}, - {"hr": 110, "p": 3600, "spm": 16, "d": 14, "t": 82, "latitude": 52.343, "longitude": -6.342}, - {"hr": 107, "p": 3600, "spm": 12, "d": 22, "t": 109, "latitude": 52.343, "longitude": -6.342}, - {"hr": 107, "p": 3600, "spm": 12, "d": 22, "t": 133, "latitude": 52.343, "longitude": -6.342}, - {"hr": 108, "p": 3600, "spm": 12, "d": 32, "t": 157, "latitude": 52.343, "longitude": -6.342}, - {"hr": 108, "p": 3577, "spm": 12, "d": 32, "t": 157, "latitude": 52.343, "longitude": -6.342}, - {"hr": 108, "p": 3411, "spm": 12, "d": 32, "t": 157, "latitude": 52.343, "longitude": -6.342}, - {"hr": 108, "p": 2649, "spm": 12, "d": 32, "t": 157, "latitude": 52.343, "longitude": -6.342}, - {"hr": 108, "p": 3099, "spm": 12, "d": 32, "t": 157, "latitude": 52.343, "longitude": -6.342}, - {"hr": 108, "p": 3600, "spm": 12, "d": 32, "t": 157, "latitude": 52.343, "longitude": -6.342}, - {"hr": 100, "p": 3600, "spm": 44, "d": 115, "t": 292, "latitude": 52.343, "longitude": -6.342}, - {"hr": 99, "p": 3600, "spm": 27, "d": 129, "t": 305, "latitude": 52.343, "longitude": -6.342}, - {"hr": 97, "p": 3600, "spm": 34, "d": 161, "t": 330, "latitude": 52.343, "longitude": -6.342}, - {"hr": 96, "p": 3600, "spm": 25, "d": 177, "t": 344, "latitude": 52.343, "longitude": -6.342}, - {"hr": 96, "p": 3494, "spm": 43, "d": 196, "t": 357, "latitude": 52.343, "longitude": -6.342}, - {"hr": 98, "p": 2927, "spm": 26, "d": 235, "t": 377, "latitude": 52.343, "longitude": -6.342}, - {"hr": 102, "p": 2718, "spm": 27, "d": 380, "t": 455, "latitude": 52.343, "longitude": -6.342}, - {"hr": 102, "p": 2753, "spm": 9, "d": 398, "t": 472, "latitude": 52.343, "longitude": -6.342}, - {"hr": 102, "p": 2864, "spm": 61, "d": 406, "t": 477, "latitude": 52.343, "longitude": -6.342}, - {"hr": 101, "p": 2780, "spm": 15, "d": 484, "t": 515, "latitude": 52.343, "longitude": -6.342}, - {"hr": 101, "p": 2365, "spm": 16, "d": 583, "t": 554, "latitude": 52.343, "longitude": -6.342}, - {"hr": 103, "p": 1965, "spm": 16, "d": 681, "t": 592, "latitude": 52.343, "longitude": -6.342}, - ] - } + { + "totalDistance": 100, + "elapsedTime": 592, + "title": "Test Workout (GO)", + "startdatetime": "2023-01-16 17:54:35.588838+00:00", + "workouttype": "water", + "boattype": "1x", + "notes": "some\nnotes", + "strokes": {"data": [ + {"distance":5, "power": 112, "hr": 132, "pace": 145800, "spm": 11, "time": 0, "latitude":52.2264097,"longitude":6.8493638}, + {"distance":12, "power": 221, "hr": 131, "pace": 116400, "spm": 41, "time": 2200, "latitude":52.2263474,"longitude":6.8495814}, + {"distance":19, "power": 511, "hr": 131, "pace": 88100, "spm": 56, "time": 4599, "latitude":52.2262715,"longitude":6.8496975}, + {"distance":27, "power": 673, "hr": 132, "pace": 80400, "spm": 59, "time": 7000, "latitude":52.2262003,"longitude":6.8498095}, + {"distance":35, "power": 744, "hr": 133, "pace": 77700, "spm": 55, "time": 9599, "latitude":52.2261312, "longitude":6.8499267}, + {"distance":43, "power": 754, "hr": 136, "pace": 77400, "spm": 48, "time": 12000, "latitude":52.2260576,"longitude":6.8500366}, + {"distance":51, "power": 754, "hr": 139, "pace": 77400, "spm": 48, "time": 14400, "latitude":52.2259872,"longitude":6.8501561}, + {"distance":59, "power": 749, "hr": 142, "pace": 77600, "spm": 48, "time": 16799, "latitude":52.2259154,"longitude":6.8502769}, + {"distance":67, "power": 729, "hr": 145, "pace": 78300, "spm": 48, "time": 19400, "latitude":52.2257749,"longitude":6.8503918}, + {"distance":75, "power": 729, "hr": 147, "pace": 78300, "spm": 48, "time": 21799, "latitude":52.2259154,"longitude":6.8502769}, + {"distance":82, "power": 726, "hr": 150, "pace": 78400, "spm": 48, "time": 24200, "latitude":52.2259872,"longitude":6.8501561}, + {"distance":90, "power": 709, "hr": 152, "pace": 79000, "spm": 48, "time": 26599, "latitude":52.2260576,"longitude":6.8500366}, + {"distance":100, "power": 707, "hr": 153, "pace": 79100, "spm": 49, "time": 29000, "latitude":52.2261312,"longitude":6.8499267}]} + +} + """ if request.method != 'POST': - return HttpResponse("Not allowed", status=409) + return HttpResponseNotAllowed("Method not supported") # pragma: no cover dologging('apilog.log', request.user.username+" (strokedatajson_v3 POST)") @@ -265,17 +253,23 @@ def strokedatajson_v3(request): totalDistance = request.data['totalDistance'] except KeyError: return HttpResponse("Missing Total Distance", status=400) - summary = request.data.get('summary','') - timezone = request.data.get('timezone','UTC') + timeZone = request.data.get('timezone','UTC') workouttype = request.data.get('workouttype','rower') boattype = request.data.get('boattype','1x') notes = request.data.get('notes','') rpe = request.data.get('rpe',0) + startdatetime = request.data.get('startdatetime',"%s" % timezone.now()) + + startdatetime = pendulum.parse(startdatetime) df = pd.DataFrame() + try: + strokes = request.data['strokes'] + except KeyError: + return HttpResponse("No Stroke Data in JSON", status=400) try: - df = pd.DataFrame(request.data['data']) + df = pd.DataFrame(strokes['data']) except KeyError: try: df = pd.DataFrame(request.data['strokedata']) @@ -291,22 +285,21 @@ def strokedatajson_v3(request): return HttpResponse(comment, status=status) - csvfilename = 'media/{code}.csv'.format(code=uuid4().hex[:16]) - _ = data.to_csv(csvfilename+'.gz', index_label='index', compression='gzip') + csvfilename = 'media/{code}.csv.gz'.format(code=uuid4().hex[:16]) + _ = data.to_csv(csvfilename, index_label='index', compression='gzip') uploadoptions = { 'secret': UPLOAD_SERVICE_SECRET, - 'user': userid, + 'user': request.user.id, 'file': csvfilename, 'title': title, 'workouttype': workouttype, 'boattype': boattype, - 'summary': summary, 'elapsedTime': elapsedTime/1000., # in seconds 'totalDistance': totalDistance, 'rpe': rpe, 'notes': notes, - 'timezone': timezone, + 'timezone': timeZone, } session = requests.session() newHeaders = {'Content-type': 'application/json', 'Accept': 'text/plain'} @@ -324,6 +317,7 @@ def strokedatajson_v3(request): return JsonResponse( {"workout public id": encoder.encode_hex(workoutid), + "workout id": workoutid, "status": "success", }) From 6c71ff38ebd523f62d1c7ab92bcf49017ab56e9c Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Mon, 16 Jan 2023 20:38:31 +0100 Subject: [PATCH 4/6] added help --- rowers/templates/developers.html | 84 ++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/rowers/templates/developers.html b/rowers/templates/developers.html index b6529e1a..3e5ed958 100644 --- a/rowers/templates/developers.html +++ b/rowers/templates/developers.html @@ -658,6 +658,90 @@ fails a test, its values are silently replaced by zeros.

+ + Stroke Data (v3) + /rowers/api/v3/workouts/ + + POST + +
{
+    "totalDistance": 100,
+    "elapsedTime": 29000,
+    "title": "Test Workout (GO)",
+    "startdatetime": "2023-01-16 17:54:35.588838+00:00",
+    "workouttype": "water",
+    "boattype": "1x",
+    "notes": "some\nnotes",
+    "strokes": {
+	"data": [
+	{
+          "time": 3200.0000476837,
+          "pace": 155068.4885951763,
+          "hr": 85.7857142857,
+          "power": 84.6531131591,
+          "distance": 23,
+          "spm": 16.380952381
+	},
+	{
+	  "time": 6700.0000476837,
+          "pace" : 144402.6407586741,
+          "hr": 91.2142857143,
+          "power": 117.458827834,
+          "distance": 36,
+          "spm": 21.1666666667
+	},
+	{
+          "time": 10099.9999046326,
+          "pace": 138830.8712654931,
+          "hr": 95.7142857143,
+          "power": 141.31057207,
+          "distance": 48,
+          "spm": 19.8095238095
+	}
+	]
+     }
+}
+              
+ With v3, you can post stroke data and workout metadata in one JSON object. +
+

For v3, mandatory data fields are:

+

+

    +
  • time: Time (milliseconds since workout start)
  • +
  • distance: Distance (meters)
  • +
  • pace: Pace (milliseconds per 500m)
  • +
  • spm Stroke rate (strokes per minute)
  • +
+

+

Optional data fiels are:

+

+

    +
  • power: Power (Watt)
  • +
  • latitude: GPS position (latitude)
  • +
  • longitude: GPS position (longitude)
  • +
  • drivelength: Drive length (meters)
  • +
  • dragfactor: Drag factor
  • +
  • drivetime: Drive time (ms)
  • +
  • strokerecoverytime: Recovery time (ms)
  • +
  • averagedriveforce: Average handle force (lbs)
  • +
  • peakdriveforce: Peak handle force (lbs)
  • +
  • lapidx: Lap identifier
  • +
  • hr: Heart rate (beats per minute)
  • +
  • wash: Wash as defined per Empower oarlock (degrees)
  • +
  • catch: Catch angle per Empower oarlock (degrees)
  • +
  • finish: Finish angle per Empower oarlock (degrees)
  • +
  • peakforceangle: Peak Force Angle per Empower oarlock (degrees)
  • +
  • slip: Slip as defined per Empower oarlock (degrees)
  • + +
+

+

Also for this API version, consistency checks will be done and the stroke data will be + refused if the mandatory data fields don't pass the checks. + All mandatory data fields + must have the same number of records. If an optional data field + fails a test, its values are silently replaced by zeros.

+ +

From 22385d606ffa149caade696771d22dca11829139 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Tue, 17 Jan 2023 22:28:32 +0100 Subject: [PATCH 5/6] trimp not recalc if 0, same for tss, hrtss --- rowers/dataprep.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rowers/dataprep.py b/rowers/dataprep.py index a5972d17..6373446c 100644 --- a/rowers/dataprep.py +++ b/rowers/dataprep.py @@ -1570,7 +1570,7 @@ def get_existing_job(w): def workout_trimp(w, reset=False): r = w.user - if w.trimp > 0 and not reset: + if w.trimp > -1 and not reset: return w.trimp, w.hrtss if get_existing_job(w): @@ -1615,7 +1615,7 @@ def workout_trimp(w, reset=False): def workout_rscore(w, reset=False): - if w.rscore > 0 and not reset: + if w.rscore > -1 and not reset: return w.rscore, w.normp if get_existing_job(w): @@ -1646,7 +1646,7 @@ def workout_rscore(w, reset=False): def workout_normv(w, pp=4.0): - if w.normv > 0: # pragma: no cover + if w.normv > -1: # pragma: no cover return w.normv, w.normw if get_existing_job(w): From b5d1090f6aa20d556e663daf40825ebef57e0fb0 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Wed, 18 Jan 2023 19:43:32 +0100 Subject: [PATCH 6/6] fixes in rp3, performance chart --- rowers/dataprep.py | 7 ++++++- rowers/dataroutines.py | 14 ++++++++++++-- rowers/interactiveplots.py | 16 ++++++++-------- rowers/rp3stuff.py | 15 +++++++++++++-- rowers/tasks.py | 8 ++++++++ 5 files changed, 47 insertions(+), 13 deletions(-) diff --git a/rowers/dataprep.py b/rowers/dataprep.py index 6373446c..721a4aa9 100644 --- a/rowers/dataprep.py +++ b/rowers/dataprep.py @@ -1214,7 +1214,12 @@ def save_workout_database(f2, r, dosmooth=True, workouttype='rower', delta = datetime.timedelta( seconds=totaltime) - workoutenddatetime = workoutstartdatetime+delta + try: + workoutenddatetime = workoutstartdatetime+delta + except AttributeError as e: + workoutstartdatetime = pendulum.parse(str(workoutstartdatetime)) + workoutenddatetime = workoutstartdatetime+delta + # check for duplicate start times and duration duplicate = checkduplicates( diff --git a/rowers/dataroutines.py b/rowers/dataroutines.py index 2e8e4c5c..c942b47a 100644 --- a/rowers/dataroutines.py +++ b/rowers/dataroutines.py @@ -67,6 +67,7 @@ from rowers.models import ( ) import pytz +from pytz.exceptions import UnknownTimeZoneError import collections import pendulum from rowingdata import rowingdata as rrdata @@ -915,6 +916,12 @@ parsers = { def get_startdate_time_zone(r, row, startdatetime=None): + if isinstance(startdatetime, str): + try: + startdatetime = pendulum.parse(startdatetime) + except: + dologging('debuglog.log','Could not parse start date time '+startdatetime) + if startdatetime is not None and startdatetime != '': try: timezone_str = pendulum.instance(startdatetime).timezone.name @@ -969,8 +976,11 @@ def get_startdate_time_zone(r, row, startdatetime=None): else: timezone_str = str(startdatetime.tzinfo) - startdatetime = startdatetime.astimezone(pytz.timezone(timezone_str)) - + try: + startdatetime = startdatetime.astimezone(pytz.timezone(timezone_str)) + except UnknownTimeZoneError: + startdatetime = startdatetime.astimezone(pytz.utc) + startdate = startdatetime.strftime('%Y-%m-%d') starttime = startdatetime.strftime('%H:%M:%S') diff --git a/rowers/interactiveplots.py b/rowers/interactiveplots.py index 836568e5..69020a45 100644 --- a/rowers/interactiveplots.py +++ b/rowers/interactiveplots.py @@ -1524,16 +1524,16 @@ def getfatigues( for w in ws: if getattr(w, metricchoice) > 0: weight += getattr(w, metricchoice) - if getattr(w, metricchoice) <= 0: + if getattr(w, metricchoice) == 0: if metricchoice == 'rscore' and w.hrtss > 0: # pragma: no cover weight += w.hrtss - else: - trimp, hrtss = dataprep.workout_trimp(w) - rscore, normp = dataprep.workout_rscore(w) - if w.rpe and w.rpe > 0: # pragma: no cover - dd = 3600*w.duration.hour+60*w.duration.minute+w.duration.second - dd = dd/3600 - weight += rpetotss[w.rpe]*dd + if getattr(w, metricchoice) < 0: + trimp, hrtss = dataprep.workout_trimp(w) + rscore, normp = dataprep.workout_rscore(w) + if w.rpe and w.rpe > 0: # pragma: no cover + dd = 3600*w.duration.hour+60*w.duration.minute+w.duration.second + dd = dd/3600 + weight += rpetotss[w.rpe]*dd impulses.append(weight) diff --git a/rowers/rp3stuff.py b/rowers/rp3stuff.py index 9a50e0cd..d3e5aea2 100644 --- a/rowers/rp3stuff.py +++ b/rowers/rp3stuff.py @@ -9,13 +9,15 @@ from rowsandall_app.settings import ( RP3_CLIENT_ID, RP3_CLIENT_KEY, RP3_REDIRECT_URI, RP3_CLIENT_SECRET, UPLOAD_SERVICE_URL, UPLOAD_SERVICE_SECRET ) -from rowers.utils import myqueue +from rowers.utils import myqueue, NoTokenError # All the functionality needed to connect to Runkeeper from rowers.imports import * # Python import gzip +from datetime import timedelta + import base64 from io import BytesIO @@ -48,6 +50,11 @@ graphql_url = "https://rp3rowing-app.com/graphql" # Checks if user has UnderArmour token, renews them if they are expired def rp3_open(user): + tokenexpirydate = user.rower.rp3tokenexpirydate + if timezone.now()-timedelta(days=120)>tokenexpirydate: + user.rower.rp3tokenexpirydate = timezone.now()-timedelta(days=1) + user.rower.save() + raise NoTokenError return imports_open(user, oauth_data) # Refresh ST token using refresh token @@ -137,8 +144,12 @@ def get_rp3_workouts(rower, do_async=True): # pragma: no cover w.uploadedtorp3 for w in Workout.objects.filter(user=rower) ]) + dologging('rp3_import.log',rp3ids) + newids = [rp3id for rp3id in rp3ids if rp3id not in knownrp3ids] + dologging('rp3_import.log',newids) + for id in newids: startdatetime = workouts_list.loc[id, 'executed_at'] dologging('rp3_import.log', startdatetime) @@ -150,7 +161,7 @@ def get_rp3_workouts(rower, do_async=True): # pragma: no cover auth_token, id, startdatetime, - 10, + 20, ) return 1 diff --git a/rowers/tasks.py b/rowers/tasks.py index 94fc6993..1e4f8a0d 100644 --- a/rowers/tasks.py +++ b/rowers/tasks.py @@ -2967,6 +2967,7 @@ def handle_rp3_async_workout(userid, rp3token, rp3id, startdatetime, max_attempt headers=headers, json={'query': get_download_link} ) + dologging('rp3_import.log',response.status_code) if response.status_code != 200: # pragma: no cover have_link = True @@ -2974,6 +2975,7 @@ def handle_rp3_async_workout(userid, rp3token, rp3id, startdatetime, max_attempt try: workout_download_details = pd.json_normalize( response.json()['data']['download']) + dologging('rp3_import.log', response.json()) except: # pragma: no cover return 0 @@ -2981,8 +2983,12 @@ def handle_rp3_async_workout(userid, rp3token, rp3id, startdatetime, max_attempt download_url = workout_download_details.iat[0, 2] have_link = True + dologging('rp3_import.log', download_url) + counter += 1 + dologging('rp3_import.log', counter) + if counter > max_attempts: # pragma: no cover have_link = True @@ -2994,6 +3000,8 @@ def handle_rp3_async_workout(userid, rp3token, rp3id, startdatetime, max_attempt filename = 'media/RP3Import_'+str(rp3id)+'.csv' res = requests.get(download_url, headers=headers) + dologging('rp3_import.log','tasks.py '+str(rp3id)) + dologging('rp3_import.log',startdatetime) if not startdatetime: # pragma: no cover startdatetime = str(timezone.now())