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',
'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())
+28 -2
View File
@@ -3,7 +3,6 @@ from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
# The interactions with the Concept2 logbook API
# All C2 related functions should be defined here
# (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
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 (
C2_CLIENT_ID, C2_REDIRECT_URI, C2_CLIENT_SECRET,
UPLOAD_SERVICE_URL, UPLOAD_SERVICE_SECRET
@@ -696,7 +704,7 @@ def createc2workoutdata(w):
workouttype = 'water'
wendtime = w.startdatetime-datetime.timedelta(seconds=makeseconds(durationstr))
wendtime = w.startdatetime+datetime.timedelta(seconds=makeseconds(durationstr))
data = {
"type": mytypes.c2mapping[workouttype],
@@ -849,11 +857,26 @@ def get_workout(user,c2id,do_async=False):
return custom_exception_handler(401,s), 0
data = s.json()['data']
alldata = {c2id:data}
splitdata = None
#with open('c2temp.json','w') as f:
# f.write(json.dumps(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 'splits' in data['workout']: # pragma: no cover
splitdata = data['workout']['splits']
@@ -873,6 +896,7 @@ def get_workout(user,c2id,do_async=False):
else: # pragma: no cover
strokedata = pd.DataFrame()
return data,strokedata
# 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
raise NoTokenError("User has no token")
dologging('Upload to C2 user {userid}'.format(userid=user.id))
data = createc2workoutdata(w)
dologging(json.dumps(data))
if data == 0: # pragma: no cover
return "Error: No data file. Contact info@rowsandall.com if the problem persists",0
+151 -66
View File
@@ -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'],
return payload
def get_garmin_permissions(user):
r = Rower.objects.get(user=user)
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)
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',
encoding='base64'
)
response = garmin.post(url,data=payload)
url = 'https://apis.garmin.com/userPermissions'
#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
r = Rower.objects.get(user=user)
if (r.garmintoken == '') or (r.garmintoken is None):
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,
)
url = 'https://apis.garmin.com/userPermissions/'
result = garmin.get(url)
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'],
payload = ps_to_garmin(ps,r)
url = 'https://apis.garmin.com/training-api/workout'
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 = 'http://apis.garmin.com/training-api/schedule/'
response = requests.post(url,auth=garminheaders,json=payload)
response = garmin.post(url,data=garmindict)
if response.status_code != 200:
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']
+16
View File
@@ -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',
+1 -1
View File
@@ -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'
+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
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:
+24 -2
View File
@@ -35,6 +35,7 @@ import iso8601
from iso8601 import ParseError
from json.decoder import JSONDecodeError
from pytz.exceptions import UnknownTimeZoneError
from matplotlib.backends.backend_agg import FigureCanvas
#from matplotlib.backends.backend_cairo import FigureCanvasCairo as FigureCanvas
@@ -124,6 +125,14 @@ from rowers.courseutils import (
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
# 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']
try:
startdatetime = iso8601.parse_date(data['date_utc'])
except:
except: # pragma: no cover
startdatetime = iso8601.parse_date(data['date'])
weightclass = data['weight_class']
s = 'User {userid}, C2 ID {c2id}'.format(userid=userid,c2id=c2id)
dologging(s)
dologging(json.dumps(data))
try:
title = data['name']
except KeyError:
@@ -3019,9 +3032,18 @@ def handle_c2_async_workout(alldata,userid,c2token,c2id,delaysec,defaulttimezone
starttimeunix = arrow.get(startdatetime).timestamp()-totaltime
startdatetime = arrow.get(starttimeunix)
try:
timezone = pytz.timezone(data['timezone'])
except UnknownTimeZoneError:
timezone = 'UTC'
startdatetime = startdatetime.astimezone(timezone)
s = 'Time zone {timezone}, stardatetime {startdatetime}, duration {duration}'.format(
timezone=timezone,startdatetime=startdatetime,
duration=duration)
dologging(s)
workoutdate = startdatetime.astimezone(
timezone
@@ -3367,7 +3389,7 @@ def fetch_strava_workout(stravatoken,oauth_data,stravaid,csvfilename,userid,debu
try:
thetimezone = workoutsummary['timezone']
except:
except: # pragma: no cover
thetimezone = 'UTC'
try:
+2
View File
@@ -637,7 +637,9 @@ def process_invite_code(user,code): # pragma: no cover
return (0,'The invitation has expired')
t = invitation.team
result, comment = add_member(t.id,r)
if not result:
return (result,"The member couldn't be added")
+1 -1
View File
@@ -34,7 +34,7 @@
<td>{{ value }}</td>
{% endif %}
{% 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 %}
{% endfor %}
</tr>
+1 -1
View File
@@ -47,7 +47,7 @@
{% for workout in workouts %}
<tr>
<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:'duration' }}</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>
/
<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>
+4 -1
View File
@@ -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
+30
View File
@@ -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
+59 -2
View File
@@ -9,6 +9,7 @@ from django.db import transaction
from .statements import *
nu = datetime.datetime.now()
import numpy as np
import rowers
from rowers import dataprep
@@ -18,6 +19,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 +39,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,6 +161,34 @@ 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
@@ -198,10 +250,15 @@ class C2Objects(DjangoTestCase):
def test_timezone_c2(self):
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['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)
+3 -1
View File
@@ -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)
+92
View File
@@ -350,6 +350,98 @@ class TeamTest(TestCase):
response = self.c.post(url, form_data)
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)
class TeamTestLowLevel(TestCase):
def setUp(self):
+1
View File
@@ -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,
+2
View File
@@ -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'),
+10 -1
View File
@@ -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']
if html:
spaces += '&nbsp;&nbsp;&nbsp;'
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()
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()
if html:
spaces = spaces[:-18]
else:
spaces = spaces[:-3]
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
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
@permission_required('workout.change_workout',fn=get_workout_by_opaqueid,raise_exception=True)
def workout_tp_upload_view(request,id=0):
@@ -188,6 +197,13 @@ def workout_c2_upload_view(request,id=0):
w = get_workout(id)
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:
message,c2id = c2stuff.workout_c2_upload(request.user,w)
except NoTokenError: # pragma: no cover
+34
View File
@@ -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)
@@ -1386,6 +1387,7 @@ from rowers.plannedsessions import cratiocolors,checkscores
def plannedsessions_view(request,
userid=0,startdatestring='',enddatestring=''):
try:
r = getrequestplanrower(request,userid=userid)
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)
@user_passes_test(can_plan, login_url="/rowers/paidplans/",
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)
return render(request,'plannedsessionview.html',
{
'psdict': psdict,
+2 -1
View File
@@ -564,9 +564,10 @@ def invitation_reject_view(request,id=0):
@login_required()
def rower_invitations_view(request,code=None,message='',successmessage=''):
if code:
teams.remove_expired_invites()
res,text = teams.process_invite_code(request.user,code)
if res: # pragma: no cover
messages.info(request,text)