Private
Public Access
1
0

Merge branch 'feature/apiv3' into develop

This commit is contained in:
Sander Roosendaal
2023-01-16 20:40:48 +01:00
4 changed files with 415 additions and 131 deletions

View File

@@ -658,6 +658,90 @@
fails a test, its values are silently replaced by zeros.</p>
</td>
</tr>
<tr>
<td>Stroke Data (v3)</td>
<td><a href="/rowers/api/v3/workouts/">/rowers/api/v3/workouts/</a>
</td>
<td>POST</td>
<td>
<pre>{
"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
}
]
}
}
</pre>
With v3, you can post stroke data and workout metadata in one JSON object.
<br>
<p>For v3, mandatory data fields are:</p>
<p>
<ul class="contentli">
<li><b>time</b>: Time (milliseconds since workout start)</li>
<li><b>distance</b>: Distance (meters)</li>
<li><b>pace</b>: Pace (milliseconds per 500m)</li>
<li><b>spm</b> Stroke rate (strokes per minute)</li>
</ul>
</p>
<p>Optional data fiels are:</p>
<p>
<ul class="contentli">
<li><b>power</b>: Power (Watt)</li>
<li><b>latitude</b>: GPS position (latitude)</li>
<li><b>longitude</b>: GPS position (longitude)</li>
<li><b>drivelength</b>: Drive length (meters)</li>
<li><b>dragfactor</b>: Drag factor</li>
<li><b>drivetime</b>: Drive time (ms)</li>
<li><b>strokerecoverytime</b>: Recovery time (ms)</li>
<li><b>averagedriveforce</b>: Average handle force (lbs)</li>
<li><b>peakdriveforce</b>: Peak handle force (lbs)</li>
<li><b>lapidx</b>: Lap identifier</li>
<li><b>hr</b>: Heart rate (beats per minute)</li>
<li><b>wash</b>: Wash as defined per Empower oarlock (degrees)</li>
<li><b>catch</b>: Catch angle per Empower oarlock (degrees)</li>
<li><b>finish</b>: Finish angle per Empower oarlock (degrees)</li>
<li><b>peakforceangle</b>: Peak Force Angle per Empower oarlock (degrees)</li>
<li><b>slip</b>: Slip as defined per Empower oarlock (degrees)</li>
</ul>
</p>
<p>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.</p>
</td>
</tr>
</tbody>
</table>
</p>

View File

@@ -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']

View File

@@ -247,6 +247,8 @@ urlpatterns = [
name='strokedatajson'),
re_path(r'^api/v2/workouts/(?P<id>\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'),

View File

@@ -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
@@ -37,6 +39,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 +203,127 @@ 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:
{
"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 HttpResponseNotAllowed("Method not supported") # pragma: no cover
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)
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(strokes['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.gz'.format(code=uuid4().hex[:16])
_ = data.to_csv(csvfilename, index_label='index', compression='gzip')
uploadoptions = {
'secret': UPLOAD_SERVICE_SECRET,
'user': request.user.id,
'file': csvfilename,
'title': title,
'workouttype': workouttype,
'boattype': boattype,
'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),
"workout id": 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 +412,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")