Merge branch 'feature/garminstuff' into develop
This commit is contained in:
115
garminscript.py
115
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())
|
||||
|
||||
@@ -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']
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -18,6 +18,11 @@
|
||||
<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>
|
||||
{% 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>
|
||||
{% endif %}
|
||||
<h1>Session {{ psdict.name.1 }}</h1>
|
||||
|
||||
@@ -63,7 +63,10 @@
|
||||
{% endif %}
|
||||
<p>
|
||||
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>
|
||||
Strava Auto Import also imports activity changes on Strava to Rowsandall, except when you delete
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -769,6 +769,8 @@ urlpatterns = [
|
||||
name='plannedsession_templateedit_view'),
|
||||
re_path(r'^sessions/(?P<id>\d+)/maketemplate/$',views.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/$',
|
||||
views.plannedsession_compare_view,
|
||||
name='plannedsession_compare_view'),
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user