From 768f2ba2bce1014540eaffbb5d0a6e90ab413a98 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Mon, 31 Oct 2022 18:56:36 +0100 Subject: [PATCH 1/5] starting rojabo v2 --- rowers/interactiveplots.py | 2 +- rowers/views/importviews.py | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/rowers/interactiveplots.py b/rowers/interactiveplots.py index 87a7ec9c..93adcfb1 100644 --- a/rowers/interactiveplots.py +++ b/rowers/interactiveplots.py @@ -4209,7 +4209,7 @@ def forcecurve_multi_interactive_chart(selected): return (script, div) def instroke_multi_interactive_chart(selected, *args, **kwargs): - + df_plot = pd.DataFrame() ids = [analysis.id for analysis in selected] metrics = list(set([analysis.metric for analysis in selected])) diff --git a/rowers/views/importviews.py b/rowers/views/importviews.py index 0dea2569..bbdb9aa7 100644 --- a/rowers/views/importviews.py +++ b/rowers/views/importviews.py @@ -1111,6 +1111,15 @@ def workout_rojaboimport_view(request, message="", userid=0): ps.rower.add(r) ps.tags.add('ROJABO') ps.save() + # get steps if there are any + steps = [] + if item['warm_up']: + pass + if item['primary']: + pass + if item['cool_down']: + pass + messages.info(request,'Saved planned session {id}'.format(id=ps.id)) except KeyError: pass From cd8a0456c87dfda554ff3ec0ad0eea8944c91b55 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Mon, 31 Oct 2022 20:58:06 +0100 Subject: [PATCH 2/5] rojabo v2 --- rowers/forms.py | 4 +- rowers/interactiveplots.py | 3 ++ rowers/rojabo_stuff.py | 62 +++++++++++++++++++++- rowers/templates/instroke_interactive.html | 1 + rowers/views/importviews.py | 16 ++++-- 5 files changed, 80 insertions(+), 6 deletions(-) diff --git a/rowers/forms.py b/rowers/forms.py index 805abc7b..d1e9d116 100644 --- a/rowers/forms.py +++ b/rowers/forms.py @@ -154,8 +154,8 @@ class InstrokeForm(forms.Form): metric = forms.ChoiceField(label='metric',choices=(('a','a'),('b','b'))) individual_curves = forms.BooleanField(label='individual curves',initial=False, required=False) - spm_min = forms.IntegerField(initial=15,label='SPM Min',widget=HiddenInput) - spm_max = forms.IntegerField(initial=45,label='SPM Max',widget=HiddenInput) + spm_min = forms.FloatField(initial=15,label='SPM Min',widget=HiddenInput) + spm_max = forms.FloatField(initial=45,label='SPM Max',widget=HiddenInput) activeminutesmin = forms.FloatField( required=False, initial=0, widget=forms.HiddenInput()) activeminutesmax = forms.FloatField( diff --git a/rowers/interactiveplots.py b/rowers/interactiveplots.py index a239928a..a3dc27bf 100644 --- a/rowers/interactiveplots.py +++ b/rowers/interactiveplots.py @@ -4318,6 +4318,9 @@ def instroke_interactive_chart(df,metric, workout, spm_min, spm_max, df_pos = (df+abs(df))/2. df_min = -(-df+abs(-df))/2. + if df.empty: + return "", "No data in selection" + mean_vals = df.median().replace(0, np.nan) q75 = df_pos.quantile(q=0.75).replace(0,np.nan) q25 = df_pos.quantile(q=0.25).replace(0,np.nan) diff --git a/rowers/rojabo_stuff.py b/rowers/rojabo_stuff.py index af3029b6..5fb74a28 100644 --- a/rowers/rojabo_stuff.py +++ b/rowers/rojabo_stuff.py @@ -169,6 +169,66 @@ aweekago = timezone.now()-timedelta(days=7) today = timezone.now() a_week_from_now = timezone.now()+timedelta(days=7) +def stepsconvert(rojabo_steps, startid = 0, warmup = False, cooldown = False): + workout_steps = [] + for step in rojabo_steps: + print(step) + durationtype = 'Time' + durationvalue = 10 + if step['duration_type'] == 'seconds': + durationvalue = 1000*durationvalue # milliseconds + if step['duration_type'] == 'meters': + durationtype = 'Distance' + durationvalue = step['duration_value']*100 # centimeters + elif step['duration_type'] == 'strokes': + durationtype = 'Time' + try: + durationvalue = int(60.*step['duration_value']/step['stroke_rate']) + except TypeError: + try: + durationvalue = step['time']*1000 + except KeyError: + durationvalue = 1000 + + intensity = 'Active' + if warmup: + intensity = 'Warmup' + if cooldown: + intensity = 'Cooldown' + targettype = 'Power' + targetvalue = step['target_value'] + if targetvalue is None: + targettype = 'Cadence' + targetvalue = step['stroke_rate'] + + if step['target_type'] == 'rest': + targettype = '' + intensity = 'Rest' + targetvalue = 0 + + description = step['description'] + + if step['stroke_rate'] is not None: + description = description +' Stroke Rate {cadence} SPM'.format( + cadence = step['stroke_rate'] + ) + + newstep = { + 'stepId': startid, + 'wkt_step_name': step['id'], + 'durationType': durationtype, + 'durationValue': durationvalue, + 'targetType': targettype, + 'targetvalue': targetvalue, + 'intensity': intensity, + 'description': description + } + startid += 1 + workout_steps.append(newstep) + + return workout_steps + + def get_rojabo_workout_list(user,startdate=aweekago,enddate=a_week_from_now): r = Rower.objects.get(user=user) if (r.rojabo_token == '') or (r.rojabo_token is None): # pragma: no cover @@ -190,7 +250,7 @@ def get_rojabo_workout_list(user,startdate=aweekago,enddate=a_week_from_now): date2 = enddate.strftime('%Y-%m-%d') - url = ROJABO_OAUTH_LOCATION+'api/v1/training_sessions?from={date1}&to={date2}'.format(date1=date1,date2=date2) + url = ROJABO_OAUTH_LOCATION+'api/v2/training_sessions?from={date1}&to={date2}'.format(date1=date1,date2=date2) response = requests.get(url, headers=headers) diff --git a/rowers/templates/instroke_interactive.html b/rowers/templates/instroke_interactive.html index cad3bd0e..79f15834 100644 --- a/rowers/templates/instroke_interactive.html +++ b/rowers/templates/instroke_interactive.html @@ -62,6 +62,7 @@ $( function() { range: true, min: 0, max: 60, + step: 0.1, values: [ {{ spm_min }}, {{ spm_max }} ], slide: function( event, ui ) { $( "#amountspm" ).val(ui.values[ 0 ] + " - " + ui.values[ 1 ] ); diff --git a/rowers/views/importviews.py b/rowers/views/importviews.py index bbdb9aa7..a680db8d 100644 --- a/rowers/views/importviews.py +++ b/rowers/views/importviews.py @@ -1113,13 +1113,23 @@ def workout_rojaboimport_view(request, message="", userid=0): ps.save() # get steps if there are any steps = [] - if item['warm_up']: + try: + steps.append(rojabostuff.stepsconvert(item['warm_up'])) + except KeyError: pass - if item['primary']: + try: + steps.append(rojabostuff.stepsconvert(item['primary'])) + except KeyError: pass - if item['cool_down']: + try: + steps.append(rojabostuff.stepsconvert(item['cool_down'])) + except KeyError: pass + if steps: + ps.steps = steps + ps.save() + messages.info(request,'Saved planned session {id}'.format(id=ps.id)) except KeyError: pass From 055725eef94357c07ea76bf10de0219f58a51622 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Tue, 1 Nov 2022 18:36:08 +0100 Subject: [PATCH 3/5] rojabo v2 --- rowers/models.py | 1 + rowers/rojabo_stuff.py | 8 ++++-- rowers/tests/mocks.py | 6 ++++ rowers/tests/statements.py | 1 + rowers/tests/test_imports.py | 53 ++++++++++++++++++++++++++++++++++++ rowers/views/importviews.py | 24 ++++++++++++---- 6 files changed, 85 insertions(+), 8 deletions(-) diff --git a/rowers/models.py b/rowers/models.py index 1ecccfac..53f5034d 100644 --- a/rowers/models.py +++ b/rowers/models.py @@ -2729,6 +2729,7 @@ class PlannedSession(models.Model): dct = trainingparser.parsetodict(self.interval_string) dct = [item for item in dct if item['value'] != 0] dct = trainingparser.tofitdict(dct) + for step in dct['steps']: try: step['targetValue'] = int(step['targetValue']) diff --git a/rowers/rojabo_stuff.py b/rowers/rojabo_stuff.py index 5fb74a28..7f76195d 100644 --- a/rowers/rojabo_stuff.py +++ b/rowers/rojabo_stuff.py @@ -172,11 +172,10 @@ a_week_from_now = timezone.now()+timedelta(days=7) def stepsconvert(rojabo_steps, startid = 0, warmup = False, cooldown = False): workout_steps = [] for step in rojabo_steps: - print(step) durationtype = 'Time' durationvalue = 10 if step['duration_type'] == 'seconds': - durationvalue = 1000*durationvalue # milliseconds + durationvalue = 1000*step['duration_value'] # milliseconds if step['duration_type'] == 'meters': durationtype = 'Distance' durationvalue = step['duration_value']*100 # centimeters @@ -200,6 +199,8 @@ def stepsconvert(rojabo_steps, startid = 0, warmup = False, cooldown = False): if targetvalue is None: targettype = 'Cadence' targetvalue = step['stroke_rate'] + if targettype == 'Power': + targetvalue += 1000 if step['target_type'] == 'rest': targettype = '' @@ -219,10 +220,11 @@ def stepsconvert(rojabo_steps, startid = 0, warmup = False, cooldown = False): 'durationType': durationtype, 'durationValue': durationvalue, 'targetType': targettype, - 'targetvalue': targetvalue, + 'targetValue': targetvalue, 'intensity': intensity, 'description': description } + startid += 1 workout_steps.append(newstep) diff --git a/rowers/tests/mocks.py b/rowers/tests/mocks.py index 4e8bfc34..89ac6292 100644 --- a/rowers/tests/mocks.py +++ b/rowers/tests/mocks.py @@ -734,6 +734,9 @@ def mocked_requests(*args, **kwargs): with open('rowers/tests/testdata/c2_timezone.json','r') as infile: c2timezoneworkoutdata = json.load(infile) + with open('rowers/tests/testdata/rojabolist.json','r') as infile: + rojabodata = json.load(infile) + with open('rowers/tests/testdata/c2_timezone2.json','r') as infile: c2timezoneworkoutdata2 = json.load(infile) @@ -1045,6 +1048,9 @@ def mocked_requests(*args, **kwargs): return MockResponse(jsonresponse,200) + if 'rojabo' in args[0]: + return MockResponse(rojabodata, 200) + polartester = re.compile(r'.*?polaraccesslink\.com') polarremotetester = re.compile(r'.*?polarremote\.com') diff --git a/rowers/tests/statements.py b/rowers/tests/statements.py index eae18b4b..aa4821ea 100644 --- a/rowers/tests/statements.py +++ b/rowers/tests/statements.py @@ -66,6 +66,7 @@ from mock import Mock, patch import pandas as pd import rowers.c2stuff as c2stuff import rowers.sporttracksstuff as sporttracksstuff +import rowers.rojabo_stuff as rojabo_stuff from django.urls import reverse, reverse_lazy diff --git a/rowers/tests/test_imports.py b/rowers/tests/test_imports.py index 6ef6c6e0..5534ea5d 100644 --- a/rowers/tests/test_imports.py +++ b/rowers/tests/test_imports.py @@ -21,11 +21,64 @@ import urllib import json import rowers.utils as utils +import rowers.rojabo_stuff as rojabo_stuff from django.db import transaction import rowers.garmin_stuff as gs +@pytest.mark.django_db +@override_settings(TESTING=True) +class RojaboObjects(DjangoTestCase): + def setUp(self): + self.c = Client() + self.u = User.objects.create_user('john', + 'sander@ds.ds', + 'koeinsloot') + + self.u.first_name = 'John' + self.u.last_name = 'Sander' + self.u.save() + self.r = Rower.objects.create(user=self.u,gdproptin=True,surveydone=True, + gdproptindate=timezone.now() + ) + + self.r.rojabo_token = '12' + self.r.rojabo_refreshtoken = 'ab' + self.r.rojabo_tokenexpirydate = arrow.get(datetime.datetime.now()+datetime.timedelta(days=1)).datetime + self.r.defaulttimezone = 'Europe/Prague' + self.r.rowerplan = 'plan' + self.r.save() + self.c.login(username='john',password='koeinsloot') + + self.nu = datetime.datetime.now() + + @patch('rowers.rojabo_stuff.requests.get', side_effect=mocked_requests) + @patch('rowers.rojabo_stuff.requests.post', side_effect=mocked_requests) + def test_rojabo_import(self, mock_get, mock_post): + url = '/rowers/session/rojaboimport/' + response = self.c.get(url) + self.assertEqual(response.status_code, 200) + + form_data = { + 'csrfmiddlewaretoken': ['xalRrf8y7P2Hhobges9QC4aKCWg4l06gexDc8g2DeuVExgadfk4YhV8oSE1Yu43U'], + 'sessions': ['Import selected sessions'], + 'sessionid': ['862172'] + } + + + response = self.c.post(url,form_data) + self.assertEqual(response.status_code,200) + + pss = PlannedSession.objects.filter(manager=self.r.user) + self.assertEqual(pss.count(),1) + + ps = pss[0] + self.assertEqual(ps.approximate_duration,57) + + self.assertEqual(len(ps.steps['steps']),20) + + @pytest.mark.django_db @override_settings(TESTING=True) class GarminObjects(DjangoTestCase): diff --git a/rowers/views/importviews.py b/rowers/views/importviews.py index a680db8d..6d7b2a65 100644 --- a/rowers/views/importviews.py +++ b/rowers/views/importviews.py @@ -1062,7 +1062,7 @@ def workout_rojaboimport_view(request, message="", userid=0): if (res.status_code != 200): # pragma: no cover if (res.status_code == 401): r = getrower(request.user) - if (r.stravatoken == '') or (r.stravatoken is None): + if (r.rojabo_token == '') or (r.rojabo_token is None): s = "Token doesn't exist. Need to authorize" return HttpResponseRedirect("/rowers/me/rojaboauthorize/") message = "Something went wrong in workout_rojaboimport_view" @@ -1114,20 +1114,34 @@ def workout_rojaboimport_view(request, message="", userid=0): # get steps if there are any steps = [] try: - steps.append(rojabostuff.stepsconvert(item['warm_up'])) + steps = steps+rojabo_stuff.stepsconvert( + item['warm_up']['steps'], + warmup=True + ) except KeyError: pass try: - steps.append(rojabostuff.stepsconvert(item['primary'])) + steps = steps + rojabo_stuff.stepsconvert( + item['primary']['steps'], + startid=len(steps) + ) except KeyError: pass try: - steps.append(rojabostuff.stepsconvert(item['cool_down'])) + steps = steps + rojabo_stuff.stepsconvert( + item['cool_down']['steps'], + cooldown=True, + startid=len(steps)) except KeyError: pass if steps: - ps.steps = steps + ps.steps = { + 'name':'', + 'sport':'rowing', + 'filename':'', + 'steps': steps, + } ps.save() messages.info(request,'Saved planned session {id}'.format(id=ps.id)) From adce60d19a0a3673c1466161b402b983f290bedc Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Tue, 1 Nov 2022 19:05:37 +0100 Subject: [PATCH 4/5] fix --- rowers/tests/testdata/testdata.tcx.gz | Bin 0 -> 4001 bytes rowsandall_app/settings_test.py | 3 +++ 2 files changed, 3 insertions(+) create mode 100644 rowers/tests/testdata/testdata.tcx.gz diff --git a/rowers/tests/testdata/testdata.tcx.gz b/rowers/tests/testdata/testdata.tcx.gz new file mode 100644 index 0000000000000000000000000000000000000000..785e2c8e0fc7b7514b24a3cb0928b5212b1fc63d GIT binary patch literal 4001 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 Date: Tue, 1 Nov 2022 19:09:18 +0100 Subject: [PATCH 5/5] removing obsolete --- rowers/tests/testdata/testdata.tcx.gz | Bin 4001 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 rowers/tests/testdata/testdata.tcx.gz diff --git a/rowers/tests/testdata/testdata.tcx.gz b/rowers/tests/testdata/testdata.tcx.gz deleted file mode 100644 index 785e2c8e0fc7b7514b24a3cb0928b5212b1fc63d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4001 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