Private
Public Access
1
0

Merge branch 'feature/garminstuff' into develop

This commit is contained in:
Sander Roosendaal
2021-05-18 08:34:29 +02:00
15 changed files with 418 additions and 90 deletions

View File

@@ -28,23 +28,24 @@ payload = {
'workoutSourceId': 'Rowsandall.com', 'workoutSourceId': 'Rowsandall.com',
'steps': [ 'steps': [
{ {
'type': 'WorkoutStep', 'stepOrder': 0, 'type': 'WorkoutStep',
'stepOrder': 0,
#'repeatType': 'Step', #'repeatType': 'Step',
'repeatValue': 1, #'repeatValue': 1,
'intensity': 'ACTIVE', 'intensity': 'ACTIVE',
'description': '0', 'description': 'At 220W',
'durationType': 'TIME', 'durationType': 'TIME',
'durationValue': 1800, 'durationValue': 1800,
'durationValueType': 'METER', 'durationValueType': None,
'targetType': 'POWER', 'targetType': 'POWER',
'targetValue': 1226, 'targetValue': 226,
'targetValueLow': None, 'targetValueLow': None,
'targetValueHigh': None, 'targetValueHigh': None,
} }
] ]
} }
payload = { payload2 = {
"workoutName": "Test", "workoutName": "Test",
"description": "Test", "description": "Test",
"sport": "CYCLING", "sport": "CYCLING",
@@ -55,16 +56,93 @@ payload = {
"intensity": "INTERVAL", "intensity": "INTERVAL",
"description": "Free Ride", "description": "Free Ride",
"durationType": "TIME", "durationType": "TIME",
"durationValue": 300, "durationValue": 1800,
"durationValueType": None, "durationValueType": None,
"targetType": "POWER", "targetType": "POWER",
"targetValue": None, "targetValue": 226,
"targetValueLow": 0, "targetValueLow": None,
"targetValueHigh": 0.7, "targetValueHigh": None,
"targetValueType": "PERCENT", "targetValueType": None,
"exerciseCategory": 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)) print(json.dumps(payload))
@@ -95,4 +173,17 @@ response = requests.post(url,auth=authheaders,json=payload)
# build base_string # build base_string
print(response.status_code) 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())

View File

@@ -11,6 +11,8 @@ from rowers.mytypes import otwtypes
from rowers.rower_rules import is_workout_user,ispromember from rowers.rower_rules import is_workout_user,ispromember
from iso8601 import ParseError from iso8601 import ParseError
from rowers.plannedsessions import ps_dict_get_description
import pandas as pd import pandas as pd
@@ -27,13 +29,6 @@ from rowsandall_app.settings import (
from pytz import timezone as tz, utc 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. # You must initialize logging, otherwise you'll not see debug output.
#logging.basicConfig() #logging.basicConfig()
#logging.getLogger().setLevel(logging.DEBUG) #logging.getLogger().setLevel(logging.DEBUG)
@@ -82,6 +77,36 @@ columns = {
'bikeCadenceInRPM':' Cadence (stokes/min)', '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 def garmin_authorize(): # pragma: no cover
redirect_uri = oauth_data['redirect_uri'] redirect_uri = oauth_data['redirect_uri']
client_secret = oauth_data['client_secret'] 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') token = oauth_response.get('oauth_token')
access_token_url = 'https://connectapi.garmin.com/oauth-service/oauth/access_token' access_token_url = 'https://connectapi.garmin.com/oauth-service/oauth/access_token'
# Using OAuth1Session # Using OAuth1Session
garmin = OAuth1Session(oauth_data['client_id'], garmin = OAuth1Session(oauth_data['client_id'],
client_secret=oauth_data['client_secret'], client_secret=oauth_data['client_secret'],
@@ -167,28 +193,32 @@ def get_garmin_workout_list(user): # pragma: no cover
return result 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) result = get_garmin_permissions(user)
if 'WORKOUT_IMPORT' in result: if 'WORKOUT_IMPORT' in result:
return True return True
return False return False # pragma: no cover
from rowers import utils from rowers import utils
def step_to_garmin(step,order=0): def step_to_garmin(step,order=0):
durationtype = step['dict']['durationType'] durationtype = step['dict']['durationType']
durationvalue = step['dict']['durationValue'] durationvalue = step['dict']['durationValue']
durationvaluetype = '' durationvaluetype = None
try: try:
intensity = step['dict']['intensity'] intensity = step['dict']['intensity'].upper()
if intensity.lower() == 'active':
intensity = 'INTERVAL'
except KeyError: except KeyError:
intensity = None intensity = None
#durationvaluetype = '' #durationvaluetype = ''
if durationtype == 'Time': # pragma: no cover if durationtype == 'Time':
durationtype = 'TIME' durationtype = 'TIME'
durationvalue = int(durationvalue/1000.) durationvalue = int(durationvalue/1000.)
elif durationtype == 'Distance': # pragma: no cover elif durationtype == 'Distance':
durationtype = 'DISTANCE' durationtype = 'DISTANCE'
durationvalue = int(durationvalue/100) durationvalue = int(durationvalue/100)
durationvaluetype = 'METER' durationvaluetype = 'METER'
@@ -197,56 +227,102 @@ def step_to_garmin(step,order=0):
if durationvalue <= 100: if durationvalue <= 100:
durationvaluetype = 'PERCENT' durationvaluetype = 'PERCENT'
else: else:
durationvaluetype = '' durationvaluetype = None
durationvalue -= 100 durationvalue -= 100
elif durationtype == 'HrGreaterThan': # pragma: no cover elif durationtype == 'HrGreaterThan': # pragma: no cover
durationtype = 'HR_GREATER_THAN' durationtype = 'HR_GREATER_THAN'
if durationvalue <= 100: if durationvalue <= 100:
durationvaluetype = 'PERCENT' durationvaluetype = 'PERCENT'
else: else:
durationvaluetype = '' durationvaluetype = None
durationvalue -= 100 durationvalue -= 100
elif durationtype == 'PowerLessThan': # pragma: no cover elif durationtype == 'PowerLessThan': # pragma: no cover
durationtype = 'POWER_LESS_THAN' durationtype = 'POWER_LESS_THAN'
if durationvalue <= 1000: if durationvalue <= 1000:
durationvaluetype = 'PERCENT' durationvaluetype = 'PERCENT'
else: else:
durationvaluetype = '' durationvaluetype = None
durationvalue -= 1000 durationvalue -= 1000
elif durationtype == 'PowerGreaterThan': # pragma: no cover elif durationtype == 'PowerGreaterThan': # pragma: no cover
durationtype = 'POWER_GREATER_THAN' durationtype = 'POWER_GREATER_THAN'
if durationvalue <= 1000: if durationvalue <= 1000:
durationvaluetype = 'PERCENT' durationvaluetype = 'PERCENT'
else: else:
durationvaluetype = '' durationvaluetype = None
durationvalue -= 1000 durationvalue -= 1000
elif durationtype == 'Reps': # pragma: no cover elif durationtype == 'Reps': # pragma: no cover
durationtype = 'REPS' durationtype = 'REPS'
try: try:
targetType = step['dict']['targetType'] targetType = step['dict']['targetType']
targetType = targettypes[targetType]
except KeyError: except KeyError:
targetType = None targetType = None
try: try:
targetValue = step['dict']['targetValue'] targetValue = step['dict']['targetValue']
if targetValue == 0: # pragma: no cover
targetValue = None
except KeyError: except KeyError:
targetValue = None 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: try:
targetValueLow = step['dict']['targetValueLow'] 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: except KeyError:
targetValueLow = None targetValueLow = None
try: 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: except KeyError:
targetValueHigh = None 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 = { out = {
'type': step['type'], 'type': steptype,
'stepOrder':order, 'stepOrder':order,
'repeatType':step['type'], 'repeatType':repeattype,
'repeatValue':step['repeatValue'], 'repeatValue':step['repeatValue'],
'intensity':intensity, 'intensity':intensity,
'description':step['dict']['wkt_step_name'], 'description':step['dict']['wkt_step_name'],
@@ -277,8 +353,8 @@ def step_to_garmin(step,order=0):
def ps_to_garmin(ps,r): def ps_to_garmin(ps,r):
payload = { payload = {
'workoutName': ps.name, 'workoutName': ps.name,
'sport': 'GENERIC', 'sport':r.garminactivity,
'description':'Uploaded from Rowsandall.com', 'description':ps_dict_get_description(ps.steps),
'estimatedDurationInSecs':60*ps.approximate_duration, 'estimatedDurationInSecs':60*ps.approximate_duration,
'estimatedDistanceInMeters': ps.approximate_distance, 'estimatedDistanceInMeters': ps.approximate_distance,
'workoutProvider': 'Rowsandall.com', 'workoutProvider': 'Rowsandall.com',
@@ -303,79 +379,88 @@ def ps_to_garmin(ps,r):
lijst.append(gstep) lijst.append(gstep)
payload['steps'] = lijst payload['steps'] = lijst
url = 'https://apis.garmin.com/training-api/workout/'
garmin = OAuth1Session(oauth_data['client_id'], return payload
client_secret=oauth_data['client_secret'],
resource_owner_key=r.garmintoken,
resource_owner_secret=r.garminrefreshtoken,
signature_method='HMAC-SHA1',
encoding='base64'
)
response = garmin.post(url,data=payload)
#POST /training-api/workout?undefined HTTP/1.1 def get_garmin_permissions(user):
#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
r = Rower.objects.get(user=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" s = "Token doesn't exist. Need to authorize"
return custom_exception_handler(401,s) return custom_exception_handler(401,s)
garmin = OAuth1Session(oauth_data['client_id'], garminheaders = OAuth1(
client_secret=oauth_data['client_secret'], client_key = oauth_data['client_id'],
resource_owner_key=r.garmintoken, client_secret=oauth_data['client_secret'],
resource_owner_secret=r.garminrefreshtoken, 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: if result.status_code == 200:
return result.json() 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: if not ps.steps:
return 0 return 0 # pragma: no cover
if not garmin_can_export_session(user): 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) 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" s = "Token doesn't exist. Need to authorize"
return custom_exception_handler(401,s) return custom_exception_handler(401,s)
garmin = OAuth1Session(oauth_data['client_id'], payload = ps_to_garmin(ps,r)
client_secret=oauth_data['client_secret'],
resource_owner_key=r.garmintoken,
resource_owner_secret=r.garminrefreshtoken,
)
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 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): def garmin_getworkout(garminid,r,activity):
starttime = activity['startTimeInSeconds'] starttime = activity['startTimeInSeconds']

View File

@@ -837,6 +837,15 @@ class Rower(models.Model):
('None','None'), ('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) user = models.OneToOneField(User,on_delete=models.CASCADE)
#billing details #billing details
@@ -1015,6 +1024,10 @@ class Rower(models.Model):
garminrefreshtoken = models.CharField(default='',max_length=1000, garminrefreshtoken = models.CharField(default='',max_length=1000,
blank=True,null=True) 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) stravatoken = models.CharField(default='',max_length=200,blank=True,null=True)
stravatokenexpirydate = models.DateTimeField(blank=True,null=True) stravatokenexpirydate = models.DateTimeField(blank=True,null=True)
stravarefreshtoken = models.CharField(default='',max_length=1000, stravarefreshtoken = models.CharField(default='',max_length=1000,
@@ -2399,6 +2412,8 @@ class PlannedSession(models.Model):
steps = PlannedSessionStepField(default={},null=True) steps = PlannedSessionStepField(default={},null=True)
interval_string = models.TextField(max_length=1000,default=None,blank=True,null=True, interval_string = models.TextField(max_length=1000,default=None,blank=True,null=True,
verbose_name='Interval String (optional)') verbose_name='Interval String (optional)')
garmin_workout_id = models.BigIntegerField(default=0)
garmin_schedule_id = models.BigIntegerField(default=0)
tags = TaggableManager(blank=True) tags = TaggableManager(blank=True)
@@ -3851,6 +3866,7 @@ class RowerExportForm(ModelForm):
model = Rower model = Rower
fields = [ fields = [
'stravaexportas', 'stravaexportas',
'garminactivity',
'polar_auto_import', 'polar_auto_import',
'c2_auto_export', 'c2_auto_export',
'c2_auto_import', 'c2_auto_import',

View File

@@ -86,7 +86,7 @@ from rowers.tasks import (
from rowers.utils import totaltime_sec_to_string from rowers.utils import totaltime_sec_to_string
def ps_dict_get_description(d,short=False): # pragma: no cover 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 = '' s = ''
for item in sdict: for item in sdict:
s += item['string']+'\n' s += item['string']+'\n'

View File

@@ -383,7 +383,7 @@ def async_get_workout(user,stravaid):
# Get a Strava workout summary data and stroke data by ID # Get a Strava workout summary data and stroke data by ID
def get_workout(user,stravaid,do_async=False): def get_workout(user,stravaid,do_async=False):
if do_async: if do_async: # pragma: no cover
res = async_get_workout(user,stravaid) res = async_get_workout(user,stravaid)
return {},pd.DataFrame() return {},pd.DataFrame()
try: try:

View File

@@ -2993,7 +2993,7 @@ def handle_c2_async_workout(alldata,userid,c2token,c2id,delaysec,defaulttimezone
verified = data['verified'] verified = data['verified']
try: try:
startdatetime = iso8601.parse_date(data['date_utc']) startdatetime = iso8601.parse_date(data['date_utc'])
except: except: # pragma: no cover
startdatetime = iso8601.parse_date(data['date']) startdatetime = iso8601.parse_date(data['date'])
weightclass = data['weight_class'] weightclass = data['weight_class']
@@ -3367,7 +3367,7 @@ def fetch_strava_workout(stravatoken,oauth_data,stravaid,csvfilename,userid,debu
try: try:
thetimezone = workoutsummary['timezone'] thetimezone = workoutsummary['timezone']
except: except: # pragma: no cover
thetimezone = 'UTC' thetimezone = 'UTC'
try: try:

View File

@@ -18,6 +18,11 @@
<i class="fas fa-pencil-alt fa-fw"></i> Edit Session</a> <i class="fas fa-pencil-alt fa-fw"></i> Edit Session</a>
/ /
<a href="/rowers/sessions/{{ psdict.id.1 }}/maketemplate/?next={{ request.path|urlencode }}"><i class="fas fa-books fa-fw"></i> Save to Library</a> <a href="/rowers/sessions/{{ psdict.id.1 }}/maketemplate/?next={{ request.path|urlencode }}"><i class="fas fa-books fa-fw"></i> Save to Library</a>
{% if psdict.garmin_schedule_id.1 %}
<i class="fas fa-watch-fitness fa-fw"></i>Exported to Garmin
{% else %}
<a href="/rowers/sessions/{{ psdict.id.1 }}/togarmin/?next={{ request.path|urlencode }}"><i class="fas fa-watch-fitness fa-fw"></i> Export to Garmin</a>
{% endif %}
</p> </p>
{% endif %} {% endif %}
<h1>Session {{ psdict.name.1 }}</h1> <h1>Session {{ psdict.name.1 }}</h1>

View File

@@ -63,7 +63,10 @@
{% endif %} {% endif %}
<p> <p>
Garmin Connnect has no manual sync, so connecting your account to your Garmin account will 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.
</p> </p>
<p> <p>
Strava Auto Import also imports activity changes on Strava to Rowsandall, except when you delete Strava Auto Import also imports activity changes on Strava to Rowsandall, except when you delete

View File

@@ -876,6 +876,10 @@ def mocked_requests(*args, **kwargs):
return MockResponse(json_data,200) 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: if 'garmin' in args:
return MockOAuth1Session() return MockOAuth1Session()
@@ -884,6 +888,8 @@ def mocked_requests(*args, **kwargs):
args = [kwargs['url']] args = [kwargs['url']]
if "tofit" in kwargs['url']: if "tofit" in kwargs['url']:
args = [kwargs['url']] args = [kwargs['url']]
if "tojson" in kwargs['url']:
args = [kwargs['url']]
if not args: if not args:
return MockSession() return MockSession()
@@ -999,10 +1005,23 @@ def mocked_requests(*args, **kwargs):
garmindownloadregex = '.*?garmin\.com\/mockfile?id=1' garmindownloadregex = '.*?garmin\.com\/mockfile?id=1'
garmindownloadtester = re.compile(garmindownloadregex) 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 garmintester.match(args[0]):
if garmindownloadtester.match(args[0]): if garmindownloadtester.match(args[0]):
return MockStreamResponse('rowers/tests/testdata/3x250m.fit',200) 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]): if stravaathletetester.match(args[0]):
json_data = stravaathletejson json_data = stravaathletejson
@@ -1258,6 +1277,8 @@ class MockResponse:
def json(self): def json(self):
return self.json_data return self.json_data
class MockOAuth1Session: class MockOAuth1Session:
def __init__(self,*args, **kwargs): def __init__(self,*args, **kwargs):
pass pass
@@ -1268,5 +1289,14 @@ class MockOAuth1Session:
def post(*args, **kwargs): def post(*args, **kwargs):
return MockResponse({},200) 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): def mocked_invoiceid(*args,**kwargs):
return 1 return 1

View File

@@ -18,6 +18,9 @@ from rowers import stravastuff
import urllib import urllib
import json import json
from django.db import transaction
import rowers.garmin_stuff as gs
@pytest.mark.django_db @pytest.mark.django_db
@override_settings(TESTING=True) @override_settings(TESTING=True)
class GarminObjects(DjangoTestCase): class GarminObjects(DjangoTestCase):
@@ -35,11 +38,31 @@ class GarminObjects(DjangoTestCase):
) )
self.r.garmintoken = 'dfdzf' self.r.garmintoken = 'dfdzf'
self.r.garminrefreshtoken = 'fsls' self.r.garminrefreshtoken = 'fsls'
self.r.rowerplan = 'plan'
self.r.save() self.r.save()
self.c.login(username='john',password='koeinsloot') self.c.login(username='john',password='koeinsloot')
self.nu = datetime.datetime.now() 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): def tearDown(self):
ws = Workout.objects.filter(user=self.r) ws = Workout.objects.filter(user=self.r)
for w in ws: for w in ws:
@@ -137,7 +160,35 @@ class GarminObjects(DjangoTestCase):
self.assertEqual(res,1) 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 @pytest.mark.django_db
@override_settings(TESTING=True) @override_settings(TESTING=True)

View File

@@ -13,6 +13,8 @@ from rowers import garmin_stuff
import rowers.plannedsessions as plannedsessions import rowers.plannedsessions as plannedsessions
from django.db import transaction from django.db import transaction
import json
from rowers.views.workoutviews import plannedsession_compare_view from rowers.views.workoutviews import plannedsession_compare_view
from rowers.views.otherviews import download_fit from rowers.views.otherviews import download_fit
from rowers.opaque import encoder from rowers.opaque import encoder
@@ -1899,7 +1901,7 @@ description: ""
self.assertEqual(len(stepsdict),2) self.assertEqual(len(stepsdict),2)
response = garmin_stuff.ps_to_garmin(self.ps_trimp,self.r) 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' url = '0'
request = self.factory.get(url) request = self.factory.get(url)

View File

@@ -143,6 +143,7 @@ class UserPreferencesTest(TestCase):
form_data = { form_data = {
'stravaexportas':'Rowing', 'stravaexportas':'Rowing',
'garminactivity': 'RUNNING',
'polar_auto_import':True, 'polar_auto_import':True,
'c2_auto_export':False, 'c2_auto_export':False,
'c2_auto_import':False, 'c2_auto_import':False,

View File

@@ -769,6 +769,8 @@ urlpatterns = [
name='plannedsession_templateedit_view'), name='plannedsession_templateedit_view'),
re_path(r'^sessions/(?P<id>\d+)/maketemplate/$',views.plannedsession_totemplate_view, re_path(r'^sessions/(?P<id>\d+)/maketemplate/$',views.plannedsession_totemplate_view,
name='plannedsession_totemplate_view'), name='plannedsession_totemplate_view'),
re_path(r'^sessions/(?P<id>\d+)/togarmin/$',views.plannedsession_togarmin_view,
name='plannedsession_togarmin_view'),
re_path(r'^sessions/(?P<id>\d+)/compare/$', re_path(r'^sessions/(?P<id>\d+)/compare/$',
views.plannedsession_compare_view, views.plannedsession_compare_view,
name='plannedsession_compare_view'), name='plannedsession_compare_view'),

View File

@@ -775,7 +775,7 @@ def ps_dict_order_dict(d,short=False):
return sdict2 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({}) sdict = collections.OrderedDict({})
steps = d['steps'] steps = d['steps']
@@ -836,7 +836,10 @@ def ps_dict_order(d,short=False,rower=None):
holduntil.append(item['repeatID']) holduntil.append(item['repeatID'])
multiplier.append(item['repeatValue']) multiplier.append(item['repeatValue'])
factor *= item['repeatValue'] factor *= item['repeatValue']
spaces += '&nbsp;&nbsp;&nbsp;' if html:
spaces += '&nbsp;&nbsp;&nbsp;'
else:
spaces += ' '
if item['type'] == 'Step': if item['type'] == 'Step':
item['string'] = spaces+item['string'] item['string'] = spaces+item['string']
sdict3.append(item) sdict3.append(item)
@@ -847,7 +850,10 @@ def ps_dict_order(d,short=False,rower=None):
if item['stepID'] == holduntil[-1]: if item['stepID'] == holduntil[-1]:
sdict3.append(hold.pop()) sdict3.append(hold.pop())
factor /= multiplier.pop() factor /= multiplier.pop()
spaces = spaces[:-18] if html:
spaces = spaces[:-18]
else:
spaces = spaces[:-3]
holduntil.pop() holduntil.pop()
else: # pragma: no cover else: # pragma: no cover
prevstep = sdict3.pop() prevstep = sdict3.pop()
@@ -861,7 +867,10 @@ def ps_dict_order(d,short=False,rower=None):
factor /= multiplier.pop() factor /= multiplier.pop()
sdict3.append(prevstep) sdict3.append(prevstep)
holduntil.pop() holduntil.pop()
spaces = spaces[:-18] if html:
spaces = spaces[:-18]
else:
spaces = spaces[:-3]
sdict = list(reversed(sdict3)) sdict = list(reversed(sdict3))

View File

@@ -8,6 +8,7 @@ from rowingdata import trainingparser
import json import json
from taggit.models import Tag from taggit.models import Tag
import rowers.garmin_stuff as gs
@login_required @login_required
@permission_required('plannedsession.view_session',fn=get_session_by_pk,raise_exception=True) @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) @permission_required('plannedsession.change_session',fn=get_session_by_pk,raise_exception=True)
@user_passes_test(can_plan, login_url="/rowers/paidplans/", @user_passes_test(can_plan, login_url="/rowers/paidplans/",
message="This functionality requires a Coach or Self-Coach plan", 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) steps = ps_dict_get_description_html(d,short=False)
return render(request,'plannedsessionview.html', return render(request,'plannedsessionview.html',
{ {
'psdict': psdict, 'psdict': psdict,