Private
Public Access
1
0

Merge branch 'release/v16.2.4'

This commit is contained in:
Sander Roosendaal
2021-05-21 08:11:15 +02:00
22 changed files with 594 additions and 100 deletions
+103 -12
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())
+28 -2
View File
@@ -3,7 +3,6 @@ from __future__ import division
from __future__ import print_function from __future__ import print_function
from __future__ import unicode_literals from __future__ import unicode_literals
# The interactions with the Concept2 logbook API # The interactions with the Concept2 logbook API
# All C2 related functions should be defined here # All C2 related functions should be defined here
# (There is still some stuff defined directly in views.py. Need to # (There is still some stuff defined directly in views.py. Need to
@@ -24,6 +23,15 @@ from json.decoder import JSONDecodeError
from pytz.exceptions import UnknownTimeZoneError from pytz.exceptions import UnknownTimeZoneError
def dologging(s):
tstamp = time.localtime()
timestamp = time.strftime('%b-%d-%Y %H:%M:%S', tstamp)
with open('debuglog.log','a') as f:
f.write('\n')
f.write(timestamp)
f.write(' ')
f.write(s)
from rowsandall_app.settings import ( from rowsandall_app.settings import (
C2_CLIENT_ID, C2_REDIRECT_URI, C2_CLIENT_SECRET, C2_CLIENT_ID, C2_REDIRECT_URI, C2_CLIENT_SECRET,
UPLOAD_SERVICE_URL, UPLOAD_SERVICE_SECRET UPLOAD_SERVICE_URL, UPLOAD_SERVICE_SECRET
@@ -696,7 +704,7 @@ def createc2workoutdata(w):
workouttype = 'water' workouttype = 'water'
wendtime = w.startdatetime-datetime.timedelta(seconds=makeseconds(durationstr)) wendtime = w.startdatetime+datetime.timedelta(seconds=makeseconds(durationstr))
data = { data = {
"type": mytypes.c2mapping[workouttype], "type": mytypes.c2mapping[workouttype],
@@ -849,11 +857,26 @@ def get_workout(user,c2id,do_async=False):
return custom_exception_handler(401,s), 0 return custom_exception_handler(401,s), 0
data = s.json()['data'] data = s.json()['data']
alldata = {c2id:data}
splitdata = None splitdata = None
#with open('c2temp.json','w') as f: #with open('c2temp.json','w') as f:
# f.write(json.dumps(s.json())) # f.write(json.dumps(s.json()))
# print(s.json()) # print(s.json())
if do_async:
print('aap',alldata)
job = myqueue(queuehigh,
handle_c2_async_workout,
alldata,
r.user.id,
r.c2token,
c2id,
0,
r.defaulttimezone)
return data, pd.DataFrame()
if 'workout' in data: if 'workout' in data:
if 'splits' in data['workout']: # pragma: no cover if 'splits' in data['workout']: # pragma: no cover
splitdata = data['workout']['splits'] splitdata = data['workout']['splits']
@@ -873,6 +896,7 @@ def get_workout(user,c2id,do_async=False):
else: # pragma: no cover else: # pragma: no cover
strokedata = pd.DataFrame() strokedata = pd.DataFrame()
return data,strokedata return data,strokedata
# Get stroke data belonging to C2 ID # Get stroke data belonging to C2 ID
@@ -1003,7 +1027,9 @@ def workout_c2_upload(user,w,asynchron=False):
if not c2userid: # pragma: no cover if not c2userid: # pragma: no cover
raise NoTokenError("User has no token") raise NoTokenError("User has no token")
dologging('Upload to C2 user {userid}'.format(userid=user.id))
data = createc2workoutdata(w) data = createc2workoutdata(w)
dologging(json.dumps(data))
if data == 0: # pragma: no cover if data == 0: # pragma: no cover
return "Error: No data file. Contact info@rowsandall.com if the problem persists",0 return "Error: No data file. Contact info@rowsandall.com if the problem persists",0
+153 -68
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']
+16
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',
+1 -1
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'
+1 -1
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:
+25 -3
View File
@@ -35,6 +35,7 @@ import iso8601
from iso8601 import ParseError from iso8601 import ParseError
from json.decoder import JSONDecodeError from json.decoder import JSONDecodeError
from pytz.exceptions import UnknownTimeZoneError
from matplotlib.backends.backend_agg import FigureCanvas from matplotlib.backends.backend_agg import FigureCanvas
#from matplotlib.backends.backend_cairo import FigureCanvasCairo as FigureCanvas #from matplotlib.backends.backend_cairo import FigureCanvasCairo as FigureCanvas
@@ -124,6 +125,14 @@ from rowers.courseutils import (
InvalidTrajectoryError InvalidTrajectoryError
) )
def dologging(s):
tstamp = time.localtime()
timestamp = time.strftime('%b-%d-%Y %H:%M:%S', tstamp)
with open('debuglog.log','a') as f:
f.write('\n')
f.write(timestamp)
f.write(' ')
f.write(s)
# Concept2 logbook sends over split data for each interval # Concept2 logbook sends over split data for each interval
# We use it here to generate a custom summary # We use it here to generate a custom summary
@@ -2993,10 +3002,14 @@ 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']
s = 'User {userid}, C2 ID {c2id}'.format(userid=userid,c2id=c2id)
dologging(s)
dologging(json.dumps(data))
try: try:
title = data['name'] title = data['name']
except KeyError: except KeyError:
@@ -3019,9 +3032,18 @@ def handle_c2_async_workout(alldata,userid,c2token,c2id,delaysec,defaulttimezone
starttimeunix = arrow.get(startdatetime).timestamp()-totaltime starttimeunix = arrow.get(startdatetime).timestamp()-totaltime
startdatetime = arrow.get(starttimeunix) startdatetime = arrow.get(starttimeunix)
timezone = pytz.timezone(data['timezone']) try:
timezone = pytz.timezone(data['timezone'])
except UnknownTimeZoneError:
timezone = 'UTC'
startdatetime = startdatetime.astimezone(timezone) startdatetime = startdatetime.astimezone(timezone)
s = 'Time zone {timezone}, stardatetime {startdatetime}, duration {duration}'.format(
timezone=timezone,startdatetime=startdatetime,
duration=duration)
dologging(s)
workoutdate = startdatetime.astimezone( workoutdate = startdatetime.astimezone(
timezone timezone
@@ -3367,7 +3389,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:
+2
View File
@@ -637,7 +637,9 @@ def process_invite_code(user,code): # pragma: no cover
return (0,'The invitation has expired') return (0,'The invitation has expired')
t = invitation.team t = invitation.team
result, comment = add_member(t.id,r) result, comment = add_member(t.id,r)
if not result: if not result:
return (result,"The member couldn't be added") return (result,"The member couldn't be added")
+1 -1
View File
@@ -34,7 +34,7 @@
<td>{{ value }}</td> <td>{{ value }}</td>
{% endif %} {% endif %}
{% if key == "id" %} {% if key == "id" %}
<td><a href="/rowers/workout/c2import/{{ value }}/">Import</a></td> <td><a href="/rowers/workout/c2import/{{ value }}/async/">Import</a></td>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
</tr> </tr>
+1 -1
View File
@@ -47,7 +47,7 @@
{% for workout in workouts %} {% for workout in workouts %}
<tr> <tr>
<td> <td>
<a href="/rowers/workout/c2import/{{ workout|lookup:'id' }}/">Import</a></td> <a href="/rowers/workout/c2import/{{ workout|lookup:'id' }}/async/">Import</a></td>
<td>{{ workout|lookup:'starttime' }}</td> <td>{{ workout|lookup:'starttime' }}</td>
<td>{{ workout|lookup:'duration' }}</td> <td>{{ workout|lookup:'duration' }}</td>
<td>{{ workout|lookup:'distance' }}</td> <td>{{ workout|lookup:'distance' }}</td>
+5
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>
+4 -1
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
+30
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
+59 -2
View File
@@ -9,6 +9,7 @@ from django.db import transaction
from .statements import * from .statements import *
nu = datetime.datetime.now() nu = datetime.datetime.now()
import numpy as np
import rowers import rowers
from rowers import dataprep from rowers import dataprep
@@ -18,6 +19,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 +39,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,6 +161,34 @@ 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
@@ -198,10 +250,15 @@ class C2Objects(DjangoTestCase):
def test_timezone_c2(self): def test_timezone_c2(self):
data = c2stuff.createc2workoutdata(self.w) data = c2stuff.createc2workoutdata(self.w)
wenddtime = self.w.startdatetime-datetime.timedelta(seconds=self.totaltime) wenddtime = self.w.startdatetime+datetime.timedelta(seconds=self.totaltime)
t1 = arrow.get(wenddtime).timestamp()
t2 = arrow.get(data['date']).timestamp()
diff = np.abs(t1-t2)
self.assertEqual(data['timezone'],'Europe/Amsterdam') self.assertEqual(data['timezone'],'Europe/Amsterdam')
self.assertEqual(data['date'],wenddtime.strftime('%Y-%m-%d %H:%M:%S')) self.assertTrue(diff<2)
#self.assertEqual(data['date'],wenddtime.strftime('%Y-%m-%d %H:%M:%S'))
@patch('rowers.c2stuff.Session', side_effect=mocked_requests) @patch('rowers.c2stuff.Session', side_effect=mocked_requests)
+3 -1
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)
+92
View File
@@ -350,6 +350,98 @@ class TeamTest(TestCase):
response = self.c.post(url, form_data) response = self.c.post(url, form_data)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
def test_team_invite_convoluted(self):
uA = User.objects.get(username='user1')
uB = User.objects.get(username='user2')
passwordA = self.user_passwords[0]
passwordB = self.user_passwords[1]
# login User A
login = self.c.login(username=uA.username, password = passwordA)
self.assertTrue(login)
# start a group
teamA = Team(
manager = uA,
name = 'teamA',
private = 'open',
viewing = 'allmembers'
)
teamA.save()
uA.rower.team.add(teamA)
# invite B to the group
inviteA = TeamInvite(
team = teamA,
user = uB,
code = 'ABCDEF',
email = self.users[1].email,
)
inviteA.save()
invite = TeamInvite.objects.get(code=inviteA.code)
# B accepts the invite
login = self.c.login(username = uB.username, password = passwordB)
self.assertTrue(login)
url = reverse('rower_invitations_view')+inviteA.code
response = self.c.get(url,follow=True)
expected_url = reverse('team_view', kwargs={'team_id':teamA.id})
self.assertRedirects(response, expected_url=expected_url, status_code=301, target_status_code=200)
# A requests B to become their coach
coachrequest = CoachRequest(
coach = uB.rower,
user = uA,
code = 'ABCDEFG'
)
coachrequest.save()
# B accepts coaching request
login = self.c.login(username = uB.username, password = passwordB)
self.assertTrue(login)
url = reverse('coach_accept_coachrequest_view', kwargs={'code':coachrequest.code})
response = self.c.get(url,follow=True)
expected_url = reverse('rower_teams_view')
self.assertRedirects(response, expected_url=expected_url, status_code=302, target_status_code=200)
# Now check a few things
self.assertTrue(is_team_member(uA,teamA))
self.assertTrue(is_team_member(uB,teamA))
self.assertTrue(is_coach_user(uB,uA))
# B leaves A's group
url = reverse('team_leave_view',kwargs={'id':teamA.id})
response = self.c.get(url)
expected_url = reverse('rower_teams_view')
self.assertRedirects(response, expected_url=expected_url, status_code=302, target_status_code=200)
self.assertFalse(is_team_member(uB,teamA))
self.assertTrue(is_coach_user(uB,uA))
# B should see A's workouts on the list
url = reverse('workouts_view',kwargs={'userid':uA.id})
response = self.c.get(url)
self.assertEqual(response.status_code, 200)
# B should see A's Plan
url = '/rowers/sessions/'
response = self.c.get(url)
self.assertEqual(response.status_code, 200)
#self.assertTrue(uA.first_name in str(response.content))
url = reverse('plannedsessions_view',kwargs={'userid':uA.id})
response = self.c.get(url)
self.assertEqual(response.status_code, 200)
#self.assertTrue(uA.first_name in str(response.content))
@override_settings(TESTING=True) @override_settings(TESTING=True)
class TeamTestLowLevel(TestCase): class TeamTestLowLevel(TestCase):
def setUp(self): def setUp(self):
+1
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,
+2
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'),
+13 -4
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))
+16
View File
@@ -14,6 +14,15 @@ def default(o): # pragma: no cover
from rowsandall_app.settings import NK_OAUTH_LOCATION from rowsandall_app.settings import NK_OAUTH_LOCATION
def dologging(s):
tstamp = time.localtime()
timestamp = time.strftime('%b-%d-%Y %H:%M:%S', tstamp)
with open('debuglog.log','a') as f:
f.write('\n')
f.write(timestamp)
f.write(' ')
f.write(s)
# Send workout to TP # Send workout to TP
@permission_required('workout.change_workout',fn=get_workout_by_opaqueid,raise_exception=True) @permission_required('workout.change_workout',fn=get_workout_by_opaqueid,raise_exception=True)
def workout_tp_upload_view(request,id=0): def workout_tp_upload_view(request,id=0):
@@ -188,6 +197,13 @@ def workout_c2_upload_view(request,id=0):
w = get_workout(id) w = get_workout(id)
r = w.user r = w.user
s = 'C2 Upload Workout starttime {startdatetime} timezone {timezone} user id {userid}'.format(
startdatetime = w.startdatetime,
timezone = w.timezone,
userid = w.user.user.id
)
dologging(s)
try: try:
message,c2id = c2stuff.workout_c2_upload(request.user,w) message,c2id = c2stuff.workout_c2_upload(request.user,w)
except NoTokenError: # pragma: no cover except NoTokenError: # pragma: no cover
+34
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)
@@ -1386,6 +1387,7 @@ from rowers.plannedsessions import cratiocolors,checkscores
def plannedsessions_view(request, def plannedsessions_view(request,
userid=0,startdatestring='',enddatestring=''): userid=0,startdatestring='',enddatestring=''):
try: try:
r = getrequestplanrower(request,userid=userid) r = getrequestplanrower(request,userid=userid)
except PermissionDenied: # pragma: no cover except PermissionDenied: # pragma: no cover
@@ -1979,6 +1981,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 +2418,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,
+2 -1
View File
@@ -564,9 +564,10 @@ def invitation_reject_view(request,id=0):
@login_required() @login_required()
def rower_invitations_view(request,code=None,message='',successmessage=''): def rower_invitations_view(request,code=None,message='',successmessage=''):
if code: if code:
teams.remove_expired_invites() teams.remove_expired_invites()
res,text = teams.process_invite_code(request.user,code) res,text = teams.process_invite_code(request.user,code)
if res: # pragma: no cover if res: # pragma: no cover
messages.info(request,text) messages.info(request,text)