diff --git a/garminscript.py b/garminscript.py index b288cd1e..f29698ec 100644 --- a/garminscript.py +++ b/garminscript.py @@ -28,23 +28,24 @@ payload = { 'workoutSourceId': 'Rowsandall.com', 'steps': [ { - 'type': 'WorkoutStep', 'stepOrder': 0, + 'type': 'WorkoutStep', + 'stepOrder': 0, #'repeatType': 'Step', - 'repeatValue': 1, + #'repeatValue': 1, 'intensity': 'ACTIVE', - 'description': '0', + 'description': 'At 220W', 'durationType': 'TIME', 'durationValue': 1800, - 'durationValueType': 'METER', + 'durationValueType': None, 'targetType': 'POWER', - 'targetValue': 1226, + 'targetValue': 226, 'targetValueLow': None, 'targetValueHigh': None, } ] } -payload = { +payload2 = { "workoutName": "Test", "description": "Test", "sport": "CYCLING", @@ -55,16 +56,93 @@ payload = { "intensity": "INTERVAL", "description": "Free Ride", "durationType": "TIME", - "durationValue": 300, + "durationValue": 1800, "durationValueType": None, "targetType": "POWER", - "targetValue": None, - "targetValueLow": 0, - "targetValueHigh": 0.7, - "targetValueType": "PERCENT", + "targetValue": 226, + "targetValueLow": None, + "targetValueHigh": None, + "targetValueType": None, "exerciseCategory": None }]} +payload = {'workoutName': '4x1000m row', + 'sport': 'CARDIO_TRAINING', + 'description': 'Uploaded from Rowsandall.com', + 'estimatedDurationInSecs': 2700, + 'estimatedDistanceInMeters': 8768, + 'workoutProvider': 'Rowsandall.com', + 'workoutSourceId': 'Rowsandall.com', + 'steps': [{'type': 'WorkoutStep', + 'stepOrder': 0, + 'repeatType': None, + 'repeatValue': 1, + 'intensity': 'INTERVAL', + 'description': '0', + 'durationType': 'DISTANCE', + 'durationValue': 2000, + 'durationValueType': 'METER', + 'targetType': None, + 'targetValue': None, + 'targetValueLow': None, + 'targetValueHigh': None}, + {'type': 'WorkoutRepeatStep', + 'stepOrder': 1, + 'repeatType': 'REPEAT_UNTIL_STEPS_CMPLT', + 'repeatValue': 4, + 'intensity': 'INTERVAL', + 'description': '3', + 'durationType': 'REPS', + 'durationValue': None, + 'durationValueType': None, + 'targetType': None, + 'targetValue': None, + 'targetValueLow': None, + 'targetValueHigh': None, + 'steps': [{'type': 'WorkoutStep', + 'stepOrder': 2, + 'repeatType': None, + 'repeatValue': 1, + 'intensity': 'INTERVAL', + 'description': '1', + 'durationType': 'DISTANCE', + 'durationValue': 1000, + 'durationValueType': 'METER', + #'targetType': 'CADENCE', + #'targetValue': 25, + #'targetValueLow': None, + #'targetValueHigh': None + }, + {'type': 'WorkoutStep', + 'stepOrder': 3, + 'repeatType': None, + 'repeatValue': 1, + 'intensity': 'REST', + 'description': '2', + 'durationType': 'TIME', + 'durationValue': 60, + 'durationValueType': None, + 'targetType': None, + 'targetValue': None, + 'targetValueLow': None, + 'targetValueHigh': None}]}, + {'type': 'WorkoutStep', + 'stepOrder': 4, + 'repeatType': None, + 'repeatValue': 1, + 'intensity': 'INTERVAL', + 'description': '4', + 'durationType': 'DISTANCE', + 'durationValue': 2000, + 'durationValueType': 'METER', + 'targetType': None, + 'targetValue': None, + 'targetValueLow': None, + 'targetValueHigh': None}]} + + + + print(json.dumps(payload)) @@ -95,4 +173,17 @@ response = requests.post(url,auth=authheaders,json=payload) # build base_string print(response.status_code) -print(response.text) + +print(response.json()) +garmin_workout_id = response.json()['workoutId'] +url = 'http://apis.garmin.com/training-api/schedule' + +payload = { + 'workoutId': garmin_workout_id, + 'date': '2021-05-16' +} + + +response = requests.post(url,auth=authheaders,json=payload) +print(response.status_code) +print(response.json()) diff --git a/rowers/garmin_stuff.py b/rowers/garmin_stuff.py index 6d255b64..f82334a7 100644 --- a/rowers/garmin_stuff.py +++ b/rowers/garmin_stuff.py @@ -11,6 +11,8 @@ from rowers.mytypes import otwtypes from rowers.rower_rules import is_workout_user,ispromember from iso8601 import ParseError +from rowers.plannedsessions import ps_dict_get_description + import pandas as pd @@ -27,13 +29,6 @@ from rowsandall_app.settings import ( from pytz import timezone as tz, utc -#try: -# import http.client as http_client -#except ImportError: - # Python 2 -# import httplib as http_client -#http_client.HTTPConnection.debuglevel = 1 - # You must initialize logging, otherwise you'll not see debug output. #logging.basicConfig() #logging.getLogger().setLevel(logging.DEBUG) @@ -82,6 +77,36 @@ columns = { 'bikeCadenceInRPM':' Cadence (stokes/min)', } +targettypes = { + "Speed": "SPEED", + "HeartRate": "HEART_RATE", + "Open": "OPEN", + "Cadence": "CADENCE", + "Power": "POWER", + "Grade": "GRADE", + "Resistance": "RESISTANCE", + "Power3s": "POWER_3S", + "Power10s": "POWER_10S", + "Power30s": "POWER_30S", + "PowerLap": "POWER_LAP", + "SwimStroke": "SWIM_STROKE", + "SpeedLap": "SPEED_LAP", + "HeartRateLap": "HEART_RATE_LAP" +} + +repeattypes = { + "RepeatUntilStepsCmplt": "REPEAT_UNTIL_STEPS_CMPLT", + "RepeatUntilTime": "REPEAT_UNTIL_TIME", + "RepeatUntilDistance": "REPEAT_UNTIL_TIME", + "RepeatUntilCalories": "REPEAT_UNTIL_CALORIES" , + "RepeatUntilHrLessThan": "REPEAT_UNTIL_HR_LESS_THAN" , + "RepeatUntilHrGreaterThan": "REPEAT_UNTIL_HR_GREATER_THAN", + "RepeatUntilPowerLessThan": "REPEAT_UNTIL_POWER_LESS_THAN", + "RepeatUntilPowerGreaterThan": "REPEAT_UNTIL_POWER_GREATER_THAN", + "RepeatUntilPowerLapLessThan": "REPEAT_UNTIL_POWER_LAP_LESS_THAN", + "RepeatUntilPowerLapGreaterThan": "REPEAT_UNTIL_POWER_LAP_GREATER_THAN", +} + def garmin_authorize(): # pragma: no cover redirect_uri = oauth_data['redirect_uri'] client_secret = oauth_data['client_secret'] @@ -109,6 +134,7 @@ def garmin_processcallback(redirect_response,resource_owner_key,resource_owner_s token = oauth_response.get('oauth_token') access_token_url = 'https://connectapi.garmin.com/oauth-service/oauth/access_token' + # Using OAuth1Session garmin = OAuth1Session(oauth_data['client_id'], client_secret=oauth_data['client_secret'], @@ -167,28 +193,32 @@ def get_garmin_workout_list(user): # pragma: no cover return result -def garmin_can_export_session(user): # pragma: no cover +def garmin_can_export_session(user): + if user.rower.rowerplan not in ['coach','plan']: + return False # pragma: no cover result = get_garmin_permissions(user) if 'WORKOUT_IMPORT' in result: return True - return False + return False # pragma: no cover from rowers import utils def step_to_garmin(step,order=0): durationtype = step['dict']['durationType'] durationvalue = step['dict']['durationValue'] - durationvaluetype = '' + durationvaluetype = None try: - intensity = step['dict']['intensity'] + intensity = step['dict']['intensity'].upper() + if intensity.lower() == 'active': + intensity = 'INTERVAL' except KeyError: intensity = None #durationvaluetype = '' - if durationtype == 'Time': # pragma: no cover + if durationtype == 'Time': durationtype = 'TIME' durationvalue = int(durationvalue/1000.) - elif durationtype == 'Distance': # pragma: no cover + elif durationtype == 'Distance': durationtype = 'DISTANCE' durationvalue = int(durationvalue/100) durationvaluetype = 'METER' @@ -197,56 +227,102 @@ def step_to_garmin(step,order=0): if durationvalue <= 100: durationvaluetype = 'PERCENT' else: - durationvaluetype = '' + durationvaluetype = None durationvalue -= 100 elif durationtype == 'HrGreaterThan': # pragma: no cover durationtype = 'HR_GREATER_THAN' if durationvalue <= 100: durationvaluetype = 'PERCENT' else: - durationvaluetype = '' + durationvaluetype = None durationvalue -= 100 elif durationtype == 'PowerLessThan': # pragma: no cover durationtype = 'POWER_LESS_THAN' if durationvalue <= 1000: durationvaluetype = 'PERCENT' else: - durationvaluetype = '' + durationvaluetype = None durationvalue -= 1000 elif durationtype == 'PowerGreaterThan': # pragma: no cover durationtype = 'POWER_GREATER_THAN' if durationvalue <= 1000: durationvaluetype = 'PERCENT' else: - durationvaluetype = '' + durationvaluetype = None durationvalue -= 1000 elif durationtype == 'Reps': # pragma: no cover durationtype = 'REPS' try: targetType = step['dict']['targetType'] + targetType = targettypes[targetType] except KeyError: targetType = None - try: targetValue = step['dict']['targetValue'] + if targetValue == 0: # pragma: no cover + targetValue = None except KeyError: targetValue = None + + if targetType is not None and targetType.lower() == "power": + targetType = 'POWER' + if targetValue is not None and targetValue <= 1000: + targetValueType = 'PERCENT' # pragma: no cover + else: + targetValueType = None + targetValue -= 1000 + + try: targetValueLow = step['dict']['targetValueLow'] + if targetValueLow == 0 and targetValue is not None and targetValue > 0: # pragma: no cover + targetValueLow = targetValue + targetValue = None + elif targetValueLow == 0: # pragma: no cover + targetValueLow = None + elif targetValueLow <= 1000 and targetType == 'POWER': # pragma: no cover + targetValueType = 'PERCENT' + elif targetValueLow > 1000 and targetType == 'POWER': # pragma: no cover + targetValueLow -= 1000 except KeyError: targetValueLow = None try: - targetValueHigh = step['dict']['targetValueHigh'], + targetValueHigh = step['dict']['targetValueHigh'] + if targetValue is not None and targetValue > 0 and targetValueHigh == 0: # pragma: no cover + targetValueHigh = targetValue + targetValue = 0 + elif targetValueHigh <= 1000 and targetType == 'POWER': # pragma: no cover + targetValueType = 'PERCENT' + elif targetValueHigh > 1000 and targetType == 'POWER': # pragma: no cover + targetValueHigh -= 1000 + elif targetValueHigh == 0: # pragma: no cover + targetValueHigh = None except KeyError: targetValueHigh = None + if targetValue is None and targetValueLow is None and targetValueHigh is None: + targetType = None + + steptype = 'WorkoutRepeatStep' + if step['type'].lower() == 'step': + steptype = 'WorkoutStep' + + repeattype = None + if steptype == 'WorkoutRepeatStep': + repeattype = repeattypes[step['dict']['durationType']] + durationtype = 'REPS' + durationvalue = None + durationvaluetype = None + targetType = None + targetValue = None + out = { - 'type': step['type'], + 'type': steptype, 'stepOrder':order, - 'repeatType':step['type'], + 'repeatType':repeattype, 'repeatValue':step['repeatValue'], 'intensity':intensity, 'description':step['dict']['wkt_step_name'], @@ -277,8 +353,8 @@ def step_to_garmin(step,order=0): def ps_to_garmin(ps,r): payload = { 'workoutName': ps.name, - 'sport': 'GENERIC', - 'description':'Uploaded from Rowsandall.com', + 'sport':r.garminactivity, + 'description':ps_dict_get_description(ps.steps), 'estimatedDurationInSecs':60*ps.approximate_duration, 'estimatedDistanceInMeters': ps.approximate_distance, 'workoutProvider': 'Rowsandall.com', @@ -303,79 +379,88 @@ def ps_to_garmin(ps,r): lijst.append(gstep) payload['steps'] = lijst - url = 'https://apis.garmin.com/training-api/workout/' - garmin = OAuth1Session(oauth_data['client_id'], - client_secret=oauth_data['client_secret'], - resource_owner_key=r.garmintoken, - resource_owner_secret=r.garminrefreshtoken, - signature_method='HMAC-SHA1', - encoding='base64' - ) + return payload - response = garmin.post(url,data=payload) - #POST /training-api/workout?undefined HTTP/1.1 - #Authorization: OAuth oauth_nonce="3347376452", oauth_signature="jM8%2BCsflDfmB6SGYFIEFa%2BKRBOU%3D", oauth_token="673806b7-aa7b-4064-8290-2dd1b0236ae6", oauth_consumer_key="ca29ba5e-6868-4468-987d-4ee60a1f04bf", oauth_timestamp="1616050850", oauth_signature_method="HMAC-SHA1", oauth_version="1.0" - #Host: apis.garmin.com - #Accept: */* - - #curl -v --header 'Authorization: OAuth oauth_nonce="3347376452", oauth_signature="jM8%2BCsflDfmB6SGYFIEFa%2BKRBOU%3D", oauth_token="673806b7-aa7b-4064-8290-2dd1b0236ae6", oauth_consumer_key="ca29ba5e-6868-4468-987d-4ee60a1f04bf", oauth_timestamp="1616050850", oauth_signature_method="HMAC-SHA1", oauth_version="1.0"' 'https://apis.garmin.com/training-api/workout' - - #Authorization: OAuth oauth_nonce="3347376452", oauth_signature="jM8%2BCsflDfmB6SGYFIEFa%2BKRBOU%3D", oauth_token="673806b7-aa7b-4064-8290-2dd1b0236ae6", oauth_consumer_key="ca29ba5e-6868-4468-987d-4ee60a1f04bf", oauth_timestamp="1616050850", oauth_signature_method="HMAC-SHA1", oauth_version="1.0" - - return response - - -def get_garmin_permissions(user): # pragma: no cover +def get_garmin_permissions(user): r = Rower.objects.get(user=user) - if (r.garmintoken == '') or (r.garmintoken is None): + if (r.garmintoken == '') or (r.garmintoken is None): # pragma: no cover s = "Token doesn't exist. Need to authorize" return custom_exception_handler(401,s) - garmin = OAuth1Session(oauth_data['client_id'], - client_secret=oauth_data['client_secret'], - resource_owner_key=r.garmintoken, - resource_owner_secret=r.garminrefreshtoken, - ) + garminheaders = OAuth1( + client_key = oauth_data['client_id'], + client_secret=oauth_data['client_secret'], + resource_owner_key=r.garmintoken, + resource_owner_secret=r.garminrefreshtoken, + signature_method='HMAC-SHA1', + ) - url = 'https://apis.garmin.com/userPermissions/' - result = garmin.get(url) + url = 'https://apis.garmin.com/userPermissions' + + result = requests.get(url,auth=garminheaders) if result.status_code == 200: return result.json() - return [] + return [] # pragma: no cover -def garmin_session_create(ps,user): # pragma: no cover +def garmin_session_create(ps,user): if not ps.steps: - return 0 + return 0 # pragma: no cover if not garmin_can_export_session(user): - return 0 + return 0 # pragma: no cover - garmindict = ps_to_garmin(ps) + if ps.garmin_schedule_id != 0: + return ps.garmin_schedule_id # pragma: no cover r = Rower.objects.get(user=user) - if (r.garmintoken == '') or (r.garmintoken is None): + if (r.garmintoken == '') or (r.garmintoken is None): # pragma: no cover s = "Token doesn't exist. Need to authorize" return custom_exception_handler(401,s) - garmin = OAuth1Session(oauth_data['client_id'], - client_secret=oauth_data['client_secret'], - resource_owner_key=r.garmintoken, - resource_owner_secret=r.garminrefreshtoken, - ) + payload = ps_to_garmin(ps,r) - url = 'http://apis.garmin.com/training-api/schedule/' + url = 'https://apis.garmin.com/training-api/workout' - response = garmin.post(url,data=garmindict) + garminheaders = OAuth1( + client_key = oauth_data['client_id'], + client_secret=oauth_data['client_secret'], + resource_owner_key=r.garmintoken, + resource_owner_secret=r.garminrefreshtoken, + signature_method='HMAC-SHA1', + ) - if response.status_code != 200: + response = requests.post(url,auth=garminheaders,json=payload) + + if response.status_code != 200: # pragma: no cover return 0 - return response.json()['workoutId'] + garmin_workout_id = response.json()['workoutId'] + + + url = 'https://apis.garmin.com/training-api/schedule' + + payload = { + 'workoutId': garmin_workout_id, + 'date': ps.preferreddate.strftime('%Y-%m-%d') + } + + + response = requests.post(url,auth=garminheaders,json=payload) + + + if response.status_code != 200: # pragma: no cover + return 0 + + ps.garmin_schedule_id = response.json() + ps.garmin_workout_id = garmin_workout_id + ps.save() + + return garmin_workout_id def garmin_getworkout(garminid,r,activity): starttime = activity['startTimeInSeconds'] diff --git a/rowers/models.py b/rowers/models.py index efac1c2d..d5449391 100644 --- a/rowers/models.py +++ b/rowers/models.py @@ -837,6 +837,15 @@ class Rower(models.Model): ('None','None'), ) + garminsports = ( + ('GENERIC','Custom'), + ('RUNNING','Running'), + ('CYCLING','Cycling'), + ('LAP_SWIMMING','Lap Swimming'), + ('STRENGTH_TRAINING','Strength Training'), + ('CARDIO_TRAINING','Cardio Training'), + ) + user = models.OneToOneField(User,on_delete=models.CASCADE) #billing details @@ -1015,6 +1024,10 @@ class Rower(models.Model): garminrefreshtoken = models.CharField(default='',max_length=1000, blank=True,null=True) + garminactivity = models.CharField(default='RUNNING',max_length=200, + verbose_name='Garmin Activity for Structured Workouts', + choices=garminsports) + stravatoken = models.CharField(default='',max_length=200,blank=True,null=True) stravatokenexpirydate = models.DateTimeField(blank=True,null=True) stravarefreshtoken = models.CharField(default='',max_length=1000, @@ -2399,6 +2412,8 @@ class PlannedSession(models.Model): steps = PlannedSessionStepField(default={},null=True) interval_string = models.TextField(max_length=1000,default=None,blank=True,null=True, verbose_name='Interval String (optional)') + garmin_workout_id = models.BigIntegerField(default=0) + garmin_schedule_id = models.BigIntegerField(default=0) tags = TaggableManager(blank=True) @@ -3851,6 +3866,7 @@ class RowerExportForm(ModelForm): model = Rower fields = [ 'stravaexportas', + 'garminactivity', 'polar_auto_import', 'c2_auto_export', 'c2_auto_import', diff --git a/rowers/plannedsessions.py b/rowers/plannedsessions.py index 687ac4a4..60e82e56 100644 --- a/rowers/plannedsessions.py +++ b/rowers/plannedsessions.py @@ -86,7 +86,7 @@ from rowers.tasks import ( from rowers.utils import totaltime_sec_to_string def ps_dict_get_description(d,short=False): # pragma: no cover - sdict,totalmeters,totalseconds,totalrscore = ps_dict_order(d,short=short) + sdict,totalmeters,totalseconds,totalrscore = ps_dict_order(d,short=short,html=False) s = '' for item in sdict: s += item['string']+'\n' diff --git a/rowers/stravastuff.py b/rowers/stravastuff.py index 5ff2fda8..7e13afbd 100644 --- a/rowers/stravastuff.py +++ b/rowers/stravastuff.py @@ -383,7 +383,7 @@ def async_get_workout(user,stravaid): # Get a Strava workout summary data and stroke data by ID def get_workout(user,stravaid,do_async=False): - if do_async: + if do_async: # pragma: no cover res = async_get_workout(user,stravaid) return {},pd.DataFrame() try: diff --git a/rowers/tasks.py b/rowers/tasks.py index 2972937e..5ce2807d 100644 --- a/rowers/tasks.py +++ b/rowers/tasks.py @@ -2993,7 +2993,7 @@ def handle_c2_async_workout(alldata,userid,c2token,c2id,delaysec,defaulttimezone verified = data['verified'] try: startdatetime = iso8601.parse_date(data['date_utc']) - except: + except: # pragma: no cover startdatetime = iso8601.parse_date(data['date']) weightclass = data['weight_class'] @@ -3367,7 +3367,7 @@ def fetch_strava_workout(stravatoken,oauth_data,stravaid,csvfilename,userid,debu try: thetimezone = workoutsummary['timezone'] - except: + except: # pragma: no cover thetimezone = 'UTC' try: diff --git a/rowers/templates/plannedsessionview.html b/rowers/templates/plannedsessionview.html index 21d57028..2389ffcc 100644 --- a/rowers/templates/plannedsessionview.html +++ b/rowers/templates/plannedsessionview.html @@ -18,6 +18,11 @@ Edit Session / Save to Library + {% if psdict.garmin_schedule_id.1 %} + Exported to Garmin + {% else %} + Export to Garmin + {% endif %}

{% endif %}

Session {{ psdict.name.1 }}

diff --git a/rowers/templates/rower_exportsettings.html b/rowers/templates/rower_exportsettings.html index 79332c87..e631af88 100644 --- a/rowers/templates/rower_exportsettings.html +++ b/rowers/templates/rower_exportsettings.html @@ -63,7 +63,10 @@ {% endif %}

Garmin Connnect has no manual sync, so connecting your account to your Garmin account will - automatically auto-sync workouts from Garmin to Rowsandall (but not in the other direction). + automatically auto-sync workouts from Garmin to Rowsandall (but not in the other direction). If you + want to export our structured workout sessions to your Garmin device, you have to set the "Garmin Activity" + to a activity type that is supported by your watch. Not all watches support "Custom" activities, so + you may have to set your activity to Run or Ride while rowing.

Strava Auto Import also imports activity changes on Strava to Rowsandall, except when you delete diff --git a/rowers/tests/mocks.py b/rowers/tests/mocks.py index eddf1473..7dea2622 100644 --- a/rowers/tests/mocks.py +++ b/rowers/tests/mocks.py @@ -876,6 +876,10 @@ def mocked_requests(*args, **kwargs): return MockResponse(json_data,200) + if len(args)==1 and 'userPermissions' in args[0]: + json_data = ['WORKOUT_IMPORT','ACTIVITY_EXPORT'] + return MockResponse(json_data,200) + if 'garmin' in args: return MockOAuth1Session() @@ -884,6 +888,8 @@ def mocked_requests(*args, **kwargs): args = [kwargs['url']] if "tofit" in kwargs['url']: args = [kwargs['url']] + if "tojson" in kwargs['url']: + args = [kwargs['url']] if not args: return MockSession() @@ -999,10 +1005,23 @@ def mocked_requests(*args, **kwargs): garmindownloadregex = '.*?garmin\.com\/mockfile?id=1' garmindownloadtester = re.compile(garmindownloadregex) + garmintrainingregex = '.*?garmin\.com\/training-api\/workout' + garmintrainingtester = re.compile(garmintrainingregex) + + garmintrainingscheduleregex = '.*?garmin\.com\/training-api\/schedule' + garmintrainingscheduletester = re.compile(garmintrainingscheduleregex) if garmintester.match(args[0]): if garmindownloadtester.match(args[0]): return MockStreamResponse('rowers/tests/testdata/3x250m.fit',200) + if garmintrainingtester.match(args[0]): + json_data = { + 'workoutId':1212, + } + return MockResponse(json_data,200) + if garmintrainingscheduletester.match(args[0]): + json_data = 1234 + return MockResponse(json_data,200) if stravaathletetester.match(args[0]): json_data = stravaathletejson @@ -1258,6 +1277,8 @@ class MockResponse: def json(self): return self.json_data + + class MockOAuth1Session: def __init__(self,*args, **kwargs): pass @@ -1268,5 +1289,14 @@ class MockOAuth1Session: def post(*args, **kwargs): return MockResponse({},200) + def fetch_request_token(*args, **kwargs): + return { + 'oauth_token':'aap', + 'oauth_token_secret':'noot', + } + + def authorization_url(*args, **kwargs): + return 'url' + def mocked_invoiceid(*args,**kwargs): return 1 diff --git a/rowers/tests/test_imports.py b/rowers/tests/test_imports.py index 7bd662ef..dfc65665 100644 --- a/rowers/tests/test_imports.py +++ b/rowers/tests/test_imports.py @@ -18,6 +18,9 @@ from rowers import stravastuff import urllib import json +from django.db import transaction +import rowers.garmin_stuff as gs + @pytest.mark.django_db @override_settings(TESTING=True) class GarminObjects(DjangoTestCase): @@ -35,11 +38,31 @@ class GarminObjects(DjangoTestCase): ) self.r.garmintoken = 'dfdzf' self.r.garminrefreshtoken = 'fsls' + self.r.rowerplan = 'plan' self.r.save() self.c.login(username='john',password='koeinsloot') self.nu = datetime.datetime.now() + startdate = nu.date() + enddate = (nu+datetime.timedelta(days=3)).date() + preferreddate = startdate + + self.ps_trimp = SessionFactory( + startdate=startdate,enddate=enddate, + sessiontype='test', + sessionmode = 'TRIMP', + criterium = 'none', + sessionvalue = 77, + sessionunit='none', + preferreddate=preferreddate, + manager=self.u, + ) + + self.ps_trimp.interval_string = '10min+4x1000m@200W/20sec+2000m@24spm+10min' + self.ps_trimp.save() + + def tearDown(self): ws = Workout.objects.filter(user=self.r) for w in ws: @@ -137,7 +160,35 @@ class GarminObjects(DjangoTestCase): self.assertEqual(res,1) + @patch('rowers.garmin_stuff.OAuth1Session') + def notest_garmin_callback(self,MockOAuth1Session): + with transaction.atomic(): + response = self.c.get('/garmin_callback/?oauth_token=528ea5d9-1163-434d-b172-f428c5d9f522&oauth_verifier=LW33ZMBP8H') + self.assertEqual(response.status_code, 200) + @patch('rowers.garmin_stuff.requests.get',side_effect=mocked_requests) + def test_garmin_can_export_session(self,mock_get): + result = gs.garmin_can_export_session(self.u) + self.assertTrue(result) + + def test_ps_to_garmin(self): + res = gs.ps_to_garmin(self.ps_trimp,self.r) + self.assertTrue(len(json.dumps(res))>500) + + @patch('rowers.garmin_stuff.requests.get',side_effect=mocked_requests) + @patch('rowers.garmin_stuff.requests.post',side_effect=mocked_requests) + def test_garmin_session_create(self,mock_get,mock_post): + res = gs.garmin_session_create(self.ps_trimp,self.u) + self.assertEqual(res,1212) + + @patch('rowers.garmin_stuff.requests.get',side_effect=mocked_requests) + @patch('rowers.garmin_stuff.requests.post',side_effect=mocked_requests) + def test_togarmin_view(self,mock_get,mock_post): + url = reverse('plannedsession_togarmin_view',kwargs={'id':self.ps_trimp.id}) + response = self.c.get(url,follow=True) + + self.assertEqual(response.status_code,200) + @pytest.mark.django_db @override_settings(TESTING=True) diff --git a/rowers/tests/test_plans.py b/rowers/tests/test_plans.py index ecb90802..c30433af 100644 --- a/rowers/tests/test_plans.py +++ b/rowers/tests/test_plans.py @@ -13,6 +13,8 @@ from rowers import garmin_stuff import rowers.plannedsessions as plannedsessions from django.db import transaction +import json + from rowers.views.workoutviews import plannedsession_compare_view from rowers.views.otherviews import download_fit from rowers.opaque import encoder @@ -1899,7 +1901,7 @@ description: "" self.assertEqual(len(stepsdict),2) response = garmin_stuff.ps_to_garmin(self.ps_trimp,self.r) - self.assertEqual(response.status_code,200) + self.assertTrue(len(json.dumps(response))>800) url = '0' request = self.factory.get(url) diff --git a/rowers/tests/test_user.py b/rowers/tests/test_user.py index 47ed565b..cf604395 100644 --- a/rowers/tests/test_user.py +++ b/rowers/tests/test_user.py @@ -143,6 +143,7 @@ class UserPreferencesTest(TestCase): form_data = { 'stravaexportas':'Rowing', + 'garminactivity': 'RUNNING', 'polar_auto_import':True, 'c2_auto_export':False, 'c2_auto_import':False, diff --git a/rowers/urls.py b/rowers/urls.py index 0377375f..7f002f21 100644 --- a/rowers/urls.py +++ b/rowers/urls.py @@ -769,6 +769,8 @@ urlpatterns = [ name='plannedsession_templateedit_view'), re_path(r'^sessions/(?P\d+)/maketemplate/$',views.plannedsession_totemplate_view, name='plannedsession_totemplate_view'), + re_path(r'^sessions/(?P\d+)/togarmin/$',views.plannedsession_togarmin_view, + name='plannedsession_togarmin_view'), re_path(r'^sessions/(?P\d+)/compare/$', views.plannedsession_compare_view, name='plannedsession_compare_view'), diff --git a/rowers/utils.py b/rowers/utils.py index defdd0af..0c1a35d4 100644 --- a/rowers/utils.py +++ b/rowers/utils.py @@ -775,7 +775,7 @@ def ps_dict_order_dict(d,short=False): return sdict2 -def ps_dict_order(d,short=False,rower=None): +def ps_dict_order(d,short=False,rower=None,html=True): sdict = collections.OrderedDict({}) steps = d['steps'] @@ -836,7 +836,10 @@ def ps_dict_order(d,short=False,rower=None): holduntil.append(item['repeatID']) multiplier.append(item['repeatValue']) factor *= item['repeatValue'] - spaces += '   ' + if html: + spaces += '   ' + else: + spaces += ' ' if item['type'] == 'Step': item['string'] = spaces+item['string'] sdict3.append(item) @@ -847,7 +850,10 @@ def ps_dict_order(d,short=False,rower=None): if item['stepID'] == holduntil[-1]: sdict3.append(hold.pop()) factor /= multiplier.pop() - spaces = spaces[:-18] + if html: + spaces = spaces[:-18] + else: + spaces = spaces[:-3] holduntil.pop() else: # pragma: no cover prevstep = sdict3.pop() @@ -861,7 +867,10 @@ def ps_dict_order(d,short=False,rower=None): factor /= multiplier.pop() sdict3.append(prevstep) holduntil.pop() - spaces = spaces[:-18] + if html: + spaces = spaces[:-18] + else: + spaces = spaces[:-3] sdict = list(reversed(sdict3)) diff --git a/rowers/views/planviews.py b/rowers/views/planviews.py index dc117e6b..9d02627a 100644 --- a/rowers/views/planviews.py +++ b/rowers/views/planviews.py @@ -8,6 +8,7 @@ from rowingdata import trainingparser import json from taggit.models import Tag +import rowers.garmin_stuff as gs @login_required @permission_required('plannedsession.view_session',fn=get_session_by_pk,raise_exception=True) @@ -1979,6 +1980,37 @@ def plannedsession_templateedit_view(request,id=0): }) +@permission_required('plannedsession.change_session',fn=get_session_by_pk,raise_exception=True) +@user_passes_test(can_plan, login_url="/rowers/paidplans/", + message="This functionality requires a Coach or Self-Coach plan", + redirect_field_name=None) +def plannedsession_togarmin_view(request,id=0): + + r = getrequestplanrower(request) + + startdate, enddate = get_dates_timeperiod(request) + + ps = get_object_or_404(PlannedSession,pk=id) + + result = gs.garmin_session_create(ps,r.user) + + if not result: # pragma: no cover + messages.error(request,'You failed to export your session to Garmin Connect') # pragma: no cover + else: # pragma: no cover + messages.info(request,'Session is now on Garmin Connect. Sync your Garmin watch') + + url = reverse(plannedsession_view,kwargs={'userid':r.user.id, + 'id':ps.id,}) + + startdatestring = startdate.strftime('%Y-%m-%d') + enddatestring = enddate.strftime('%Y-%m-%d') + url += '?when='+startdatestring+'/'+enddatestring + + next = request.GET.get('next', url) + + return HttpResponseRedirect(next) + + @permission_required('plannedsession.change_session',fn=get_session_by_pk,raise_exception=True) @user_passes_test(can_plan, login_url="/rowers/paidplans/", message="This functionality requires a Coach or Self-Coach plan", @@ -2385,6 +2417,7 @@ def plannedsession_view(request,id=0,userid=0): steps = ps_dict_get_description_html(d,short=False) + return render(request,'plannedsessionview.html', { 'psdict': psdict,