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 57d35b56..3dcc1ddf 100644 Binary files a/rowers/tests/testdata/testdata.tcx.gz and b/rowers/tests/testdata/testdata.tcx.gz differ 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..360f0f0e 100644 --- a/rowers/views/apiviews.py +++ b/rowers/views/apiviews.py @@ -1,10 +1,27 @@ 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 + +from datetime import datetime as dt + +import rowingdata.tcxtools as tcxtools +from rowingdata import TCXParser, rowingdata + + +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 +219,106 @@ 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 Exception as e: + dologging('apilog.log',e) + 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 = 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) + + 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()