From dc6981999020359b3c981a9eefd42cb40c36b46a Mon Sep 17 00:00:00 2001
From: Sander Roosendaal
Date: Mon, 16 Jan 2023 18:01:39 +0100
Subject: [PATCH 1/3] 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 2/3] 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 3/3] 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.
+ |
+