Merge branch 'release/v18.9.7'
This commit is contained in:
+9
-4
@@ -1214,7 +1214,12 @@ def save_workout_database(f2, r, dosmooth=True, workouttype='rower',
|
|||||||
delta = datetime.timedelta(
|
delta = datetime.timedelta(
|
||||||
seconds=totaltime)
|
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
|
# check for duplicate start times and duration
|
||||||
duplicate = checkduplicates(
|
duplicate = checkduplicates(
|
||||||
@@ -1570,7 +1575,7 @@ def get_existing_job(w):
|
|||||||
def workout_trimp(w, reset=False):
|
def workout_trimp(w, reset=False):
|
||||||
r = w.user
|
r = w.user
|
||||||
|
|
||||||
if w.trimp > 0 and not reset:
|
if w.trimp > -1 and not reset:
|
||||||
return w.trimp, w.hrtss
|
return w.trimp, w.hrtss
|
||||||
|
|
||||||
if get_existing_job(w):
|
if get_existing_job(w):
|
||||||
@@ -1615,7 +1620,7 @@ def workout_trimp(w, reset=False):
|
|||||||
|
|
||||||
|
|
||||||
def workout_rscore(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
|
return w.rscore, w.normp
|
||||||
|
|
||||||
if get_existing_job(w):
|
if get_existing_job(w):
|
||||||
@@ -1646,7 +1651,7 @@ def workout_rscore(w, reset=False):
|
|||||||
|
|
||||||
|
|
||||||
def workout_normv(w, pp=4.0):
|
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
|
return w.normv, w.normw
|
||||||
|
|
||||||
if get_existing_job(w):
|
if get_existing_job(w):
|
||||||
|
|||||||
+12
-2
@@ -67,6 +67,7 @@ from rowers.models import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
import pytz
|
import pytz
|
||||||
|
from pytz.exceptions import UnknownTimeZoneError
|
||||||
import collections
|
import collections
|
||||||
import pendulum
|
import pendulum
|
||||||
from rowingdata import rowingdata as rrdata
|
from rowingdata import rowingdata as rrdata
|
||||||
@@ -915,6 +916,12 @@ parsers = {
|
|||||||
|
|
||||||
|
|
||||||
def get_startdate_time_zone(r, row, startdatetime=None):
|
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 != '':
|
if startdatetime is not None and startdatetime != '':
|
||||||
try:
|
try:
|
||||||
timezone_str = pendulum.instance(startdatetime).timezone.name
|
timezone_str = pendulum.instance(startdatetime).timezone.name
|
||||||
@@ -969,8 +976,11 @@ def get_startdate_time_zone(r, row, startdatetime=None):
|
|||||||
else:
|
else:
|
||||||
timezone_str = str(startdatetime.tzinfo)
|
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')
|
startdate = startdatetime.strftime('%Y-%m-%d')
|
||||||
starttime = startdatetime.strftime('%H:%M:%S')
|
starttime = startdatetime.strftime('%H:%M:%S')
|
||||||
|
|
||||||
|
|||||||
@@ -1524,16 +1524,16 @@ def getfatigues(
|
|||||||
for w in ws:
|
for w in ws:
|
||||||
if getattr(w, metricchoice) > 0:
|
if getattr(w, metricchoice) > 0:
|
||||||
weight += getattr(w, metricchoice)
|
weight += getattr(w, metricchoice)
|
||||||
if getattr(w, metricchoice) <= 0:
|
if getattr(w, metricchoice) == 0:
|
||||||
if metricchoice == 'rscore' and w.hrtss > 0: # pragma: no cover
|
if metricchoice == 'rscore' and w.hrtss > 0: # pragma: no cover
|
||||||
weight += w.hrtss
|
weight += w.hrtss
|
||||||
else:
|
if getattr(w, metricchoice) < 0:
|
||||||
trimp, hrtss = dataprep.workout_trimp(w)
|
trimp, hrtss = dataprep.workout_trimp(w)
|
||||||
rscore, normp = dataprep.workout_rscore(w)
|
rscore, normp = dataprep.workout_rscore(w)
|
||||||
if w.rpe and w.rpe > 0: # pragma: no cover
|
if w.rpe and w.rpe > 0: # pragma: no cover
|
||||||
dd = 3600*w.duration.hour+60*w.duration.minute+w.duration.second
|
dd = 3600*w.duration.hour+60*w.duration.minute+w.duration.second
|
||||||
dd = dd/3600
|
dd = dd/3600
|
||||||
weight += rpetotss[w.rpe]*dd
|
weight += rpetotss[w.rpe]*dd
|
||||||
|
|
||||||
impulses.append(weight)
|
impulses.append(weight)
|
||||||
|
|
||||||
|
|||||||
+13
-2
@@ -9,13 +9,15 @@ from rowsandall_app.settings import (
|
|||||||
RP3_CLIENT_ID, RP3_CLIENT_KEY, RP3_REDIRECT_URI, RP3_CLIENT_SECRET,
|
RP3_CLIENT_ID, RP3_CLIENT_KEY, RP3_REDIRECT_URI, RP3_CLIENT_SECRET,
|
||||||
UPLOAD_SERVICE_URL, UPLOAD_SERVICE_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
|
# All the functionality needed to connect to Runkeeper
|
||||||
from rowers.imports import *
|
from rowers.imports import *
|
||||||
|
|
||||||
# Python
|
# Python
|
||||||
import gzip
|
import gzip
|
||||||
|
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
import base64
|
import base64
|
||||||
from io import BytesIO
|
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
|
# Checks if user has UnderArmour token, renews them if they are expired
|
||||||
def rp3_open(user):
|
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)
|
return imports_open(user, oauth_data)
|
||||||
|
|
||||||
# Refresh ST token using refresh token
|
# 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)
|
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]
|
newids = [rp3id for rp3id in rp3ids if rp3id not in knownrp3ids]
|
||||||
|
|
||||||
|
dologging('rp3_import.log',newids)
|
||||||
|
|
||||||
for id in newids:
|
for id in newids:
|
||||||
startdatetime = workouts_list.loc[id, 'executed_at']
|
startdatetime = workouts_list.loc[id, 'executed_at']
|
||||||
dologging('rp3_import.log', startdatetime)
|
dologging('rp3_import.log', startdatetime)
|
||||||
@@ -150,7 +161,7 @@ def get_rp3_workouts(rower, do_async=True): # pragma: no cover
|
|||||||
auth_token,
|
auth_token,
|
||||||
id,
|
id,
|
||||||
startdatetime,
|
startdatetime,
|
||||||
10,
|
20,
|
||||||
)
|
)
|
||||||
|
|
||||||
return 1
|
return 1
|
||||||
|
|||||||
@@ -2967,6 +2967,7 @@ def handle_rp3_async_workout(userid, rp3token, rp3id, startdatetime, max_attempt
|
|||||||
headers=headers,
|
headers=headers,
|
||||||
json={'query': get_download_link}
|
json={'query': get_download_link}
|
||||||
)
|
)
|
||||||
|
dologging('rp3_import.log',response.status_code)
|
||||||
|
|
||||||
if response.status_code != 200: # pragma: no cover
|
if response.status_code != 200: # pragma: no cover
|
||||||
have_link = True
|
have_link = True
|
||||||
@@ -2974,6 +2975,7 @@ def handle_rp3_async_workout(userid, rp3token, rp3id, startdatetime, max_attempt
|
|||||||
try:
|
try:
|
||||||
workout_download_details = pd.json_normalize(
|
workout_download_details = pd.json_normalize(
|
||||||
response.json()['data']['download'])
|
response.json()['data']['download'])
|
||||||
|
dologging('rp3_import.log', response.json())
|
||||||
except: # pragma: no cover
|
except: # pragma: no cover
|
||||||
return 0
|
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]
|
download_url = workout_download_details.iat[0, 2]
|
||||||
have_link = True
|
have_link = True
|
||||||
|
|
||||||
|
dologging('rp3_import.log', download_url)
|
||||||
|
|
||||||
counter += 1
|
counter += 1
|
||||||
|
|
||||||
|
dologging('rp3_import.log', counter)
|
||||||
|
|
||||||
if counter > max_attempts: # pragma: no cover
|
if counter > max_attempts: # pragma: no cover
|
||||||
have_link = True
|
have_link = True
|
||||||
|
|
||||||
@@ -2994,6 +3000,8 @@ def handle_rp3_async_workout(userid, rp3token, rp3id, startdatetime, max_attempt
|
|||||||
filename = 'media/RP3Import_'+str(rp3id)+'.csv'
|
filename = 'media/RP3Import_'+str(rp3id)+'.csv'
|
||||||
|
|
||||||
res = requests.get(download_url, headers=headers)
|
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
|
if not startdatetime: # pragma: no cover
|
||||||
startdatetime = str(timezone.now())
|
startdatetime = str(timezone.now())
|
||||||
|
|||||||
@@ -658,6 +658,90 @@
|
|||||||
fails a test, its values are silently replaced by zeros.</p>
|
fails a test, its values are silently replaced by zeros.</p>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</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>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -180,3 +180,75 @@ class OwnApi(TestCase):
|
|||||||
response = strokedatajson_v2(request,id=w.id)
|
response = strokedatajson_v2(request,id=w.id)
|
||||||
|
|
||||||
self.assertEqual(response.status_code,200)
|
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']
|
||||||
|
|
||||||
|
|||||||
@@ -247,6 +247,8 @@ urlpatterns = [
|
|||||||
name='strokedatajson'),
|
name='strokedatajson'),
|
||||||
re_path(r'^api/v2/workouts/(?P<id>\b[0-9A-Fa-f]+\b)/strokedata/$', views.strokedatajson_v2,
|
re_path(r'^api/v2/workouts/(?P<id>\b[0-9A-Fa-f]+\b)/strokedata/$', views.strokedatajson_v2,
|
||||||
name='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'^500v/$', views.error500_view, name='error500_view'),
|
||||||
re_path(r'^500q/$', views.servererror_view, name='servererror_view'),
|
re_path(r'^500q/$', views.servererror_view, name='servererror_view'),
|
||||||
path('502/', TemplateView.as_view(template_name='502.html'), name='502'),
|
path('502/', TemplateView.as_view(template_name='502.html'), name='502'),
|
||||||
|
|||||||
+257
-131
@@ -3,6 +3,8 @@ from rowers.tasks import handle_calctrimp
|
|||||||
from rowers.opaque import encoder
|
from rowers.opaque import encoder
|
||||||
|
|
||||||
import arrow
|
import arrow
|
||||||
|
import pendulum
|
||||||
|
from rowsandall_app.settings import UPLOAD_SERVICE_SECRET, UPLOAD_SERVICE_URL
|
||||||
|
|
||||||
# Stroke data form to test API upload
|
# Stroke data form to test API upload
|
||||||
|
|
||||||
@@ -37,6 +39,138 @@ def strokedataform(request, id=0):
|
|||||||
'workout': w,
|
'workout': w,
|
||||||
}) # pragma: no cover
|
}) # 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()
|
@login_required()
|
||||||
def strokedataform_v2(request, id=0):
|
def strokedataform_v2(request, id=0):
|
||||||
@@ -69,10 +203,127 @@ def strokedataform_v2(request, id=0):
|
|||||||
}) # pragma: no cover
|
}) # 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
|
# Process the POSTed stroke data according to the API definition
|
||||||
# Return the GET stroke data according to the API definition
|
# Return the GET stroke data according to the API definition
|
||||||
|
|
||||||
|
|
||||||
@csrf_exempt
|
@csrf_exempt
|
||||||
@login_required()
|
@login_required()
|
||||||
@api_view(["GET", "POST"])
|
@api_view(["GET", "POST"])
|
||||||
@@ -161,135 +412,10 @@ def strokedatajson_v2(request, id):
|
|||||||
df.index = df.index.astype(int)
|
df.index = df.index.astype(int)
|
||||||
df.sort_index(inplace=True)
|
df.sort_index(inplace=True)
|
||||||
|
|
||||||
try:
|
status, comment, data = api_get_dataframe(row.startdatetime, df)
|
||||||
time = df['time']/1.e3
|
if status != 200:
|
||||||
except KeyError: # pragma: no cover
|
return HttpResponse(comment, status=status)
|
||||||
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,
|
|
||||||
})
|
|
||||||
|
|
||||||
r = getrower(request.user)
|
r = getrower(request.user)
|
||||||
|
|
||||||
timestr = row.startdatetime.strftime("%Y%m%d-%H%M%S")
|
timestr = row.startdatetime.strftime("%Y%m%d-%H%M%S")
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ from rq.exceptions import NoSuchJobError
|
|||||||
import threading
|
import threading
|
||||||
import redis
|
import redis
|
||||||
import colorsys
|
import colorsys
|
||||||
|
import re
|
||||||
import zipfile
|
import zipfile
|
||||||
import bleach
|
import bleach
|
||||||
import arrow
|
import arrow
|
||||||
|
|||||||
@@ -5234,6 +5234,8 @@ def workout_upload_api(request):
|
|||||||
r = None
|
r = None
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
t = form.cleaned_data['title']
|
t = form.cleaned_data['title']
|
||||||
|
t = re.sub('\r',' ',t)
|
||||||
|
t = re.sub('\n',' ',t)
|
||||||
boattype = form.cleaned_data['boattype']
|
boattype = form.cleaned_data['boattype']
|
||||||
workouttype = form.cleaned_data['workouttype']
|
workouttype = form.cleaned_data['workouttype']
|
||||||
try:
|
try:
|
||||||
@@ -6701,40 +6703,44 @@ def workout_summary_edit_view(request, id, message="", successmessage=""
|
|||||||
vals = None
|
vals = None
|
||||||
# feeling lucky / ruptures
|
# feeling lucky / ruptures
|
||||||
if request.method == 'POST' and "ruptures" in request.POST:
|
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:
|
try:
|
||||||
timeprev = int(df['time'].values[0])
|
df = pd.DataFrame({
|
||||||
timenext = int(df['time'].values[result[0]])
|
'spm': rowdata.df[' Cadence (stokes/min)'],
|
||||||
s = '{delta}sec'.format(delta=timenext-timeprev)
|
'power': rowdata.df[' Power (watts)'],
|
||||||
except IndexError: # pragma: no cover
|
'v': rowdata.df[' AverageBoatSpeed (m/s)']
|
||||||
s = '0sec'
|
})
|
||||||
|
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):
|
for i in range(len(result)-1):
|
||||||
timeprev = int(df['time'].values[result[i]-1])
|
timeprev = int(df['time'].values[result[i]-1])
|
||||||
timenext = int(df['time'].values[result[i+1]-1])
|
timenext = int(df['time'].values[result[i+1]-1])
|
||||||
interval = '+{delta}sec'.format(delta=timenext-timeprev)
|
interval = '+{delta}sec'.format(delta=timenext-timeprev)
|
||||||
s += interval
|
s += interval
|
||||||
|
|
||||||
try:
|
try:
|
||||||
rowdata.updateinterval_string(s)
|
rowdata.updateinterval_string(s)
|
||||||
except: # pragma: no cover
|
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")
|
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
|
# We have submitted the mini language interpreter
|
||||||
if request.method == 'POST' and "intervalstring" in request.POST:
|
if request.method == 'POST' and "intervalstring" in request.POST:
|
||||||
form = SummaryStringForm(request.POST)
|
form = SummaryStringForm(request.POST)
|
||||||
|
|||||||
Reference in New Issue
Block a user