From 56b307eb64318f9bf6767b9934b6e7b6eb0c2c79 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Sat, 18 Nov 2023 11:31:28 +0100 Subject: [PATCH 1/2] first version TCX API --- rowers/urls.py | 2 + rowers/views/apiviews.py | 114 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 116 insertions(+) diff --git a/rowers/urls.py b/rowers/urls.py index 42924ec1..a784096c 100644 --- a/rowers/urls.py +++ b/rowers/urls.py @@ -249,6 +249,8 @@ urlpatterns = [ name='strokedatajson_v2'), re_path(r'^api/v3/workouts/$', views.strokedatajson_v3, name='strokedatajson_v3'), + re_path(r'^api/TCX/workouts/$', views.strokedata_tcx, + name='strokedata_tcx'), 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 c04f605a..99bd7227 100644 --- a/rowers/views/apiviews.py +++ b/rowers/views/apiviews.py @@ -1,10 +1,25 @@ from rowers.views.statements import * from rowers.tasks import handle_calctrimp from rowers.opaque import encoder +from xml.etree import ElementTree as ET import arrow import pendulum from rowsandall_app.settings import UPLOAD_SERVICE_SECRET, UPLOAD_SERVICE_URL +from rowers.dataroutines import get_workouttype_from_tcx, get_startdate_time_zone + +from rest_framework.decorators import parser_classes +from rest_framework.parsers import BaseParser + +import rowingdata.tcxtools as tcxtools +from rowingdata import TCXParser, rowingdata +from datetime import timedelta, datetime + +class XMLParser(BaseParser): + media_type = "application/xml" + + def parse(self, stream, media_type=None, parser_context=None): + return ET.parse(stream).getroot() # Stroke data form to test API upload @@ -202,6 +217,105 @@ def strokedataform_v2(request, id=0): 'workout': w, }) # pragma: no cover +def part_of_day(hour): + if 5 <= hour < 12: + return "Morning" + elif 12 <= hour < 18: + return "Afternoon" + elif 18 <= hour < 24: + return "Evening" + else: + return "Night" + +@csrf_exempt +@login_required() +@api_view(["POST"]) +@permission_classes([IsAuthenticated]) +@parser_classes([XMLParser]) +def strokedata_tcx(request): + """ + Upload a TCX file through API + """ + if request.method != 'POST': + return HttpResponseNotAllowed("Method not supported") # pragma: no cover + + if request.content_type.lower() != 'application/xml': + return HttpResponseNotAllowed("Need application/xml") + + dologging('apilog.log', request.user.username+" (strokedatajson_TCX POST)") + + try: + tcxdata = request.data + activity_node = tcxdata.find(".//{http://www.garmin.com/xmlschemas/TrainingCenterDatabase/v2}Activity") + + # Extract the activity start time + start_time_node = activity_node.find(".//{http://www.garmin.com/xmlschemas/TrainingCenterDatabase/v2}Id") + start_time_str = start_time_node.text + + # Calculate the total duration of the entire activity + total_duration = 0 + + # Find all Lap nodes + lap_nodes = activity_node.findall(".//{http://www.garmin.com/xmlschemas/TrainingCenterDatabase/v2}Lap") + + # Sum up the durations of all laps + for lap_node in lap_nodes: + lap_duration_node = lap_node.find(".//{http://www.garmin.com/xmlschemas/TrainingCenterDatabase/v2}TotalTimeSeconds") + lap_duration_seconds = float(lap_duration_node.text) + total_duration += lap_duration_seconds + + except: + return HttpResponseNotAllowed("Could not parse TCX data") + + + tcxfilename = 'media/{code}.tcx'.format(code=uuid4().hex[:16]) + xml_string = ET.tostring(tcxdata, encoding='utf-8', method='xml').decode('utf-8') + + with open(tcxfilename, 'w', encoding='utf-8') as xml_file: + xml_file.write(xml_string) + + + duration = totaltime_sec_to_string(total_duration) + startdatetime = datetime.strptime(start_time_str, "%Y-%m-%dT%H:%M:%S%z") + startdate = startdatetime.date() + partofday = part_of_day(startdatetime.hour) + title = '{partofday} water'.format(partofday=partofday) + + w = Workout(user=request.user.rower, + date=startdate, + name=title, + duration=duration) + w.save() + + # need workouttype, duration + + uploadoptions = { + 'secret': UPLOAD_SERVICE_SECRET, + 'user': request.user.id, + 'file': tcxfilename, + 'id': w.id, + 'title': title, + 'rpe': 0, + 'workouttype': 'water', + 'boattype': '1x', + 'notes': '', + 'offline': False, + } + + + _ = myqueue(queuehigh, + handle_post_workout_api, + uploadoptions) + + workoutid = w.id + + return JsonResponse( + {"workout public id": encoder.encode_hex(workoutid), + "workout id": workoutid, + "status": "success", + }) + + @csrf_exempt @login_required() From b3af3d128035bebfe85ab6bf07d3c4f491a5fe3d Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Sat, 18 Nov 2023 14:59:18 +0100 Subject: [PATCH 2/2] fix --- rowers/models.py | 9 ++++++--- rowers/tests/test_api.py | 24 ++++++++++++++++++++++++ rowers/tests/testdata/testdata.tcx.gz | Bin 4000 -> 4000 bytes rowers/views/apiviews.py | 9 ++++++--- 4 files changed, 36 insertions(+), 6 deletions(-) diff --git a/rowers/models.py b/rowers/models.py index 4433bd5c..4c9318cb 100644 --- a/rowers/models.py +++ b/rowers/models.py @@ -3778,9 +3778,12 @@ def update_duplicates_on_delete(sender, instance, **kwargs): t = ww.duration delta = datetime.timedelta( hours=t.hour, minutes=t.minute, seconds=t.second) - enddatetime = ww.startdatetime+delta - if enddatetime > d.startdatetime: - ws2.append(ww) + try: + enddatetime = ww.startdatetime+delta + if enddatetime > d.startdatetime: + ws2.append(ww) + except TypeError: + pass if len(ws2) == 0: d.duplicate = False diff --git a/rowers/tests/test_api.py b/rowers/tests/test_api.py index f0f8eab6..69279f53 100644 --- a/rowers/tests/test_api.py +++ b/rowers/tests/test_api.py @@ -181,6 +181,30 @@ class OwnApi(TestCase): self.assertEqual(response.status_code,200) + def test_strokedataform_tcx(self): + login = self.c.login(username=self.u.username, password=self.password) + self.assertTrue(login) + + w = self.user_workouts[1] + + url = reverse('strokedata_tcx') + + with open('rowers/tests/testdata/crewnerddata.tcx') as f: + tcxdata_str = f.read() + + result = get_random_file(filename='rowers/tests/testdata/thyro.csv') + + request = self.factory.post(url, data = tcxdata_str, content_type='application/xml') + request.user = self.u + request.content_type = 'application/xml' + + force_authenticate(request, user=self.u) + with patch('rowers.dataprep.getrowdata_db') as mock_getrowdata: + mock_getrowdata.return_value = (pd.DataFrame(),None) + response = strokedata_tcx(request) + 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) diff --git a/rowers/tests/testdata/testdata.tcx.gz b/rowers/tests/testdata/testdata.tcx.gz index 57d35b560f7e473f04096f69f4e95b67aa8f5e61..3dcc1ddfb68207b48edf8b145094b979b25cfde6 100644 GIT binary patch literal 4000 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 GfB^t4ZzJmf literal 4000 zcmV;R4`1*fiwFpzo=Rl`|8!+@bYx+4VJ>uIcmVC4NpBoC7J%>m6@m`QVG#CP__!#F zf^35^U}u8F$mBLCOC7mqq?XWK-v0fN-Ht5VDkM*uB3K3bV5qBW>-y@EZ|CvXZ!Rwm zUM)A<)z$i|Lo{%B@b$@~<7b<0wO*~yzgw>R<>vdY@1A$tuukyPnYSa?doWM-X~Y5UBCOa_qu@t{ZX&T zyxE@p`GJ4$`tQ1ntIcY;UA)3S-~acnKi%)Yytx$qwMz{Gy6>U;-*|ZP=-}Y^-2+HJ zBY%9q;qAdM*H@eVtHXcxFP59b{g>(M$7d%5=)M5?g5VkQk;Nl&COYB>e*-uII9{Zk z{@DLXcYW|n-);J5tIOqAhkthL&4tpT&#wCJA|Lk4^7LwbwmlK{$BR$4zCYmm)&327 zx=cTIo9;8;%SRjE@BIh96U1=5xcT&chi{Uob?3`}EYscp-1W&NTetJ=Rk*}WCZMD}LO?FV_bOb_^kFh9oq-?u1kR_EvG)|UN?tFx1* z-TGyBktlw!_4eM~>3=`C``wo7tJS){y*Yo8-hsdHF79^w@W_u|2{Cr~HQ62TKmxZZ zkpG0jp2Xb_{V2V=loaK_epHx@nZki$Jbn> zi+=p`?!LwRBHV>nOP#?aX&0-Yj%;um*z<#~h&#E8xF?7RE-1KD(who$5BKLzb;-Mx zinu#NjH*#S4G~Ayfe&(yRV45rQJC16x?kvRFFG9fIC*j zJwQd=C2>zerP1cSaTDQQm%InAh`TqzgVX5q&XqLdnC8RX*TY@F_<&X;?_P{dgnMP< zd_{Jgu!&%!hI^2b_WPXVJygWq83zO!y)H^oeSSXjj!=F+^q>}k${Fn6H`T5NA0u_;GV;O}tYIWHQI-=x= z=6SMZ$=ex`uP9=}!2p044S9i*(Ruq2zdbjrZisx6U+YknhD8!f`tW8&z9JuFx6_FQXg;_PvyP~zUvm!4Q zkq;(uS7l(@vK5sAP3FfSZ_y-;(~+v>Y0+M{GNUSLu@#YLYO04Y>ka?y8RgB5llp4jrZ08w%pt4%RI7?K%|M%I-Cx9 zZl0V$@-Amxt2!Y%!lJtUoXFQS+D8M3K@*>MOhv1xeT&bJK|bf+1`L|Mx6V@0+}k;k zugIP!Ysew1=6RvX_PM*9NZyjo4bw23^1!HxBGqUF^iqG3%EHY|xa(sGQ z>_o`d=EDp1kQdCgw5l~NycDgUpOehjq~Qr0FN7K&o_DUK-+or}d`0$rl3$LbsBIA}=qlv1l6k3!yfqd?CG!D_u0=g7 znRgYD_XdGLW7P!=lacvGewchTE)DjY(LPWpYR}JUv=8-=M>5u8)MQ-;j!ow8Nj_FY zUJL}~Xm5+IR5&Y7PDP_VdgDb@-BN-px;p8cytkg}Ay1HdW3k4Q6UwepDb&c1LY@th zt_NlaMOQ7G6Zx9vc@h%@qb370L@b(_K{J}?eMRJhktFk)N{a~8WPa+gIyquAmPwVF z5s@4DsYh%Qd21;BQ<0~l>)7P_)0UYr0(qbF@KpWqA{Sl9M(m8bpVmY^n&2?1WWI1V zmTi4+#~^RXIMG#C#lqQGb6S_Sq9|>WUty(g?_5!c)En#-b_r4?z!=p_hu#OKz9~x&hzPWYA;lcRh$c z7(qf(p--wIPsFHiQa^}3w=*SV9eQVr+IG_B`ccqZV+B<{Tm&yo>hFo3Dnjpbi!6^S z^ubL=>MKLft{(KsPP2i)s?bMYRLz^yfR7cS=QP(7sOEZvFd6i<4fr)ly)z6*s<|Gq zY`M*x&~ruTW7hDaPU_39TG-H!gFbgcb*#$F!Zc$w4SxpcB@g(jkS(H6(P;gg)Vzv} zI}V0C29<`7u6gtBYutr;(5JB*T<(vjLGO$9!F^ChCN$%25WO`3#5$=Db~5O(8Fz!| zeL}BmqLH(?ilXO+eiZc4M5}b^5V)eo9W`&>80eF-9#L0zgOnZrP3k9)UX1fv^i;N0 z+MJ{wDncKO2&o!iBPEN_H}unX>G<3k(>f8AaYso(kcqU)Pk!g)t`6 zIjPz8!|1)CAVD|adnj61k4?=R13e}IRu>PhR?R_Iycm6?#4i$Gb{yl_4O=e+o@1f^F~3>MgXHu>Rp)3 zfR8OfYY@FN7IkYGBKughaT+zdeiZc4P(-WK@I%?~4L5Jz80anK{#iP29>>_!yy*}4 zRt$);@X8`gCSG4#GIm8lTbndCq!?A`iP~I0ee?n*Did-qXYTUK18snf3S?axR!1p{Esc!@RAbLguH=fC=g0>FaWKuPnnm33(*=g3HE=$LuYXkn&bMq2pgPOnmcI5-6-gz@ut}#I8cEijPU@qdj9pJn%^O0`WW-us_$EMTcKr;{ zOD?*7dxAkhF?pTj=*ju zYe27ZtuD(&CXFr+ME!$i_h(51m%}6S5 zUbMbw^$oSYd844`-6G~H<1R{Ut6NUoo0rkMTzI8K?_yD@Z;evNK_7GBmCmk@0b294 ziF@-B`rS&_%E(=e*g9j+0KGM7rdKV1LIABt?tRhwvCzNU`p1Uj#gp#(=EILy{c?Nq z*+u{54;RbJ<+?xke7EWIgRVb-H~#nK^E2YkKM(NPdH?0{Vt>G+{h9vwjrsG8Ibk7&qtsWFP|^dodJ9Su>b#faqElylixk2lSez$t95z~-zAjG&G+ePJWrQCdGtRA3Rhvo GfB^slvPg>n diff --git a/rowers/views/apiviews.py b/rowers/views/apiviews.py index 99bd7227..360f0f0e 100644 --- a/rowers/views/apiviews.py +++ b/rowers/views/apiviews.py @@ -11,9 +11,11 @@ from rowers.dataroutines import get_workouttype_from_tcx, get_startdate_time_zon from rest_framework.decorators import parser_classes from rest_framework.parsers import BaseParser +from datetime import datetime as dt + import rowingdata.tcxtools as tcxtools from rowingdata import TCXParser, rowingdata -from datetime import timedelta, datetime + class XMLParser(BaseParser): media_type = "application/xml" @@ -264,7 +266,8 @@ def strokedata_tcx(request): lap_duration_seconds = float(lap_duration_node.text) total_duration += lap_duration_seconds - except: + except Exception as e: + dologging('apilog.log',e) return HttpResponseNotAllowed("Could not parse TCX data") @@ -276,7 +279,7 @@ def strokedata_tcx(request): duration = totaltime_sec_to_string(total_duration) - startdatetime = datetime.strptime(start_time_str, "%Y-%m-%dT%H:%M:%S%z") + startdatetime = dt.strptime(start_time_str, "%Y-%m-%dT%H:%M:%S%z") startdate = startdatetime.date() partofday = part_of_day(startdatetime.hour) title = '{partofday} water'.format(partofday=partofday)