Private
Public Access
1
0

Merge branch 'feature/garmin' into develop

This commit is contained in:
Sander Roosendaal
2020-07-06 15:19:23 +02:00
28 changed files with 775 additions and 210 deletions
+2
View File
@@ -1,5 +1,7 @@
# cache
/django_cache/
testcert.crt
testcert.key
# Compiled python modules.
*.pyc
+2 -1
View File
@@ -57,7 +57,8 @@ class RowerInline(admin.StackedInline):
'stravatoken','stravatokenexpirydate','stravarefreshtoken',
'stravaexportas','strava_auto_export',
'strava_auto_import',
'runkeepertoken','runkeeper_auto_export',)}),
'runkeepertoken','runkeeper_auto_export',
'garmintoken','garminrefreshtoken')}),
('Team',
{'fields':('friends','privacy','team')}),
)
-4
View File
@@ -157,10 +157,6 @@ def add_stroke_data(user,c2id,workoutid,startdatetime,csvfilename,
return 1
def get_c2_workouts(rower):
if not ispromember(rower.user):
return 0
try:
thetoken = c2_open(rower.user)
except NoTokenError:
+231
View File
@@ -0,0 +1,231 @@
from rowers.imports import *
import datetime
import requests
from requests_oauthlib import OAuth1,OAuth1Session
from requests import Request, Session
import rowers.mytypes as mytypes
from rowers.mytypes import otwtypes
from rowers.rower_rules import is_workout_user,ispromember
from iso8601 import ParseError
import pandas as pd
import numpy
import json
from json.decoder import JSONDecodeError
from rowsandall_app.settings import (
GARMIN_CLIENT_KEY, GARMIN_REDIRECT_URI, GARMIN_CLIENT_SECRET
)
from rowers.tasks import handle_c2_import_stroke_data, handle_c2_sync
import django_rq
queue = django_rq.get_queue('default')
queuelow = django_rq.get_queue('low')
queuehigh = django_rq.get_queue('low')
from rowers.utils import myqueue
from rowers.models import C2WorldClassAgePerformance,Rower,Workout,TombStone
from django.core.exceptions import PermissionDenied
from rowers.utils import custom_exception_handler,NoTokenError
from rowingdata import rowingdata
oauth_data = {
'client_id': GARMIN_CLIENT_KEY,
'client_secret': GARMIN_CLIENT_SECRET,
'redirect_uri': GARMIN_REDIRECT_URI,
'authorization_uri': "https://connectapi.garmin.com/oauth-service/oauth/request_token",
'content_type': 'application/x-www-form-urlencoded',
'tokenname': 'garmintoken',
'refreshtokenname': 'garminrefreshtoken',
'expirydatename': 'garmintokenexpirydate',
'bearer_auth': True,
'base_url': "https://connect.garmin.com/oauthConfirm",
'scope':'write',
'headers': 'Authorization: OAuth oauth_version="1.0"'
}
columns = {
'startTimeInSeconds':'TimeStamp (sec)',
'latitudeInDegree':' latitude',
'longitudeInDegree':' longitude',
'heartRate':' HRCur (bpm)',
'speedMetersPerSecond':' AverageBoatSpeed (m/s)',
'totalDistanceInMeters':' Horizontal (meters)',
'clockDurationInSeconds':' ElapsedTime (sec)',
'powerInWatts':' Power (watts)',
'bikeCadenceInRPM':' Cadence (stokes/min)',
}
def garmin_authorize():
redirect_uri = oauth_data['redirect_uri']
client_secret = oauth_data['client_secret']
client_id = oauth_data['client_id']
base_uri = oauth_data['base_url']
garmin = OAuth1Session(oauth_data['client_id'],
client_secret=oauth_data['client_secret'],
)
fetch_response = garmin.fetch_request_token(oauth_data['authorization_uri'])
resource_owner_key = fetch_response.get('oauth_token')
resource_owner_secret = fetch_response.get('oauth_token_secret')
authorization_url = garmin.authorization_url(base_uri)
return authorization_url,resource_owner_key,resource_owner_secret
def garmin_processcallback(redirect_response,resource_owner_key,resource_owner_secret):
garmin = OAuth1Session(oauth_data['client_id'],
client_secret=oauth_data['client_secret'],
)
oauth_response = garmin.parse_authorization_response(redirect_response)
verifier = oauth_response.get('oauth_verifier')
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'],
resource_owner_key=resource_owner_key,
resource_owner_secret=resource_owner_secret,
verifier=verifier,)
oauth_tokens = garmin.fetch_access_token(access_token_url)
garmintoken = oauth_tokens.get('oauth_token')
garminrefreshtoken = oauth_tokens.get('oauth_token_secret')
return garmintoken,garminrefreshtoken
def garmin_open(user):
r = Rower.objects.get(user=user)
token = Rower.garmintoken
if (token == '') or (token is None):
raise NoTokenError("User has no garmin token")
return token
def get_garmin_workout_list(user):
r = Rower.objects.get(user=user)
if (r.garmintoken == '') or (r.stravatoken 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://healthapi.garmin.com/wellness-api/rest/activities?uploadStartTimeInSeconds=1593113760&uploadEndTimeInSeconds=1593279360'
result = garmin.get(url)
return result
def garmin_getworkout(garminid,r,activity):
starttime = activity['startTimeInSeconds']
startdatetime = arrow.get(starttime)
durationseconds = activity['durationInSeconds']
duration = dataprep.totaltime_sec_to_string(durationseconds)
activitytype = activity['activityType']
name = 'Imported from Garmin'
date = startdatetime.date()
try:
distance = activity['distanceInMeters']
except KeyError:
distance = 0
try:
averagehr = activity['averageHeartRateInBeatsPerMinute']
maxhr = activity['maxHeartRateInBeatsPerMinute']
except KeyError:
averagehr = 0
maxhr = 0
try:
w = Workout.objects.get(uploadedtogarmin=garminid)
except Workout.DoesNotExist:
newcsvfile='media/garmin{code}_{importid}.csv'
w = Workout(user=r,csvfilename=newcsvfile)
w.startdatetime = datetime.datetime(
year=startdatetime.year,
month=startdatetime.month,
day=startdatetime.day,
hour=startdatetime.hour,
minute=startdatetime.minute,
second=startdatetime.second,
tzinfo=startdatetime.tzinfo)
w.starttime = startdatetime.time()
try:
w.duration = datetime.datetime.strptime(duration,"%H:%M:%S.%f").time()
except ValueError:
w.duration = datetime.datetime.strptime(duration,"%H:%M:%S")
try:
w.workouttype = mytypes.garminmappinginv[activitytype]
except KeyError:
w.workouttype = 'other'
w.name = name
w.date = date
w.distance = distance
w.uploadedtogarmin = garminid
w.save()
return w
def garmin_workouts_from_details(activities):
for activity in activities:
garmintoken = activity['userAccessToken']
try:
r = Rower.objects.get(garmintoken=garmintoken)
garminid = activity['summaryId'][:-7]
summary = activity['summary']
w = garmin_getworkout(garminid,r,summary)
samples = activity['samples']
df = pd.DataFrame(samples)
df.rename(columns=columns,inplace=True)
try:
pace = 500./df[' AverageBoatSpeed (m/s)']
except KeyError:
pace = 0
df[' AverageBoatSpeed (m/s)'] = 0
df[' Stroke500mPace (sec/500m)'] = pace
try:
spm = df[' Cadence (stokes/min)']
except KeyError:
df[' Cadence (stokes/min)'] = 0
df['cum_dist'] = df[' Horizontal (meters)']
try:
power = df[' Power (watts)']
except KeyError:
df[' Power (watts)'] = 0
df[' AverageDriveForce (lbs)'] = 0
df[' DriveLength (meters)'] = 0
df[' PeakDriveForce (lbs)'] = 1
df[' DriveTime (ms)'] = 0
rowdata = rowingdata(df=df)
rowdata.write_csv(w.csvfilename,gzip=True)
data = dataprep.dataprep(rowdata.df,id=w.id)
summary = rowdata.allstats()
w.summary=summary
w.uploadedtogarmin = garminid
w.save()
except Rower.DoesNotExist:
pass
return 1
def garmin_workouts_from_summaries(activities):
for activity in activities:
garmintoken = activity['userAccessToken']
try:
r = Rower.objects.get(garmintoken=garmintoken)
id = activity['summaryId']
w = garmin_getworkout(id,r,activity)
except Rower.DoesNotExist:
pass
return 1
+1 -1
View File
@@ -296,7 +296,7 @@ class Command(BaseCommand):
message.delete()
# Strava
rowers = Rower.objects.filter(strava_auto_import=True).exclude(rowerplan='basic')
rowers = Rower.objects.filter(strava_auto_import=True)
for r in rowers:
stravastuff.get_strava_workouts(r)
+8 -2
View File
@@ -837,6 +837,10 @@ class Rower(models.Model):
polaruserid = models.IntegerField(default=0)
polar_auto_import = models.BooleanField(default=False)
garmintoken = models.CharField(default='',max_length=200,blank=True,null=True)
garminrefreshtoken = models.CharField(default='',max_length=1000,
blank=True,null=True)
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,
@@ -2797,6 +2801,7 @@ class Workout(models.Model):
uploadedtounderarmour = models.BigIntegerField(default=0)
uploadedtotp = models.BigIntegerField(default=0)
uploadedtorunkeeper = models.BigIntegerField(default=0)
uploadedtogarmin = models.BigIntegerField(default=0)
forceunit = models.CharField(default='lbs',
choices = (
('lbs','lbs'),
@@ -2837,12 +2842,13 @@ class Workout(models.Model):
boattype = self.boattype
workouttype = self.workouttype
if workouttype != 'water':
stri = u'{d} {n} {dist}m {duration:%H:%M:%S} {workouttype} {ownerfirst} {ownerlast}'.format(
stri = u'{d} {n} {dist}m {duration} {workouttype} {ownerfirst} {ownerlast}'.format(
d = date.strftime('%Y-%m-%d'),
n = name,
dist = distance,
duration = duration,
duration = duration.strftime("%H:%M:%S"),
workouttype = workouttype,
ownerfirst = ownerfirst,
ownerlast = ownerlast,
+231 -186
View File
@@ -42,207 +42,252 @@ workouttypes_ordered = collections.OrderedDict({
workouttypes = tuple((key, value) for key, value in workouttypes_ordered.items())
def Reverse(tuples):
new_tup = tuples[::-1]
return new_tup
stravacollection = (
('water','Rowing'),
('rower','Rowing'),
('skierg','NordicSki'),
('Bike','Ride'),
('bikeerg','Ride'),
('dynamic','Rowing'),
('slides','Rowing'),
('paddle','StandUpPaddling'),
('snow','NordicSki'),
('coastal','Rowing'),
('c-boat','Rowing'),
('churchboat','Rowing'),
('Ride','Ride'),
('Run','Run'),
('NordicSki','NordicSki'),
('Swim','Swim'),
('Hike','Hike'),
('Walk','Walk'),
('Canoeing','Canoeing'),
('Crossfit','Crossfit'),
('StandUpPaddling','StandUpPaddling'),
('IceSkate','IceSkate'),
('WeightTraining','WeightTraining'),
('InlineSkate','InlineSkate'),
('Kayaking','Kayaking'),
('Workout','Workout'),
('Yoga','Yoga'),
('other','Workout'),
)
stravamapping = collections.OrderedDict({
'water':'Rowing',
'rower':'Rowing',
'skierg':'NordicSki',
'Bike':'Ride',
'bikeerg':'Ride',
'dynamic':'Rowing',
'slides':'Rowing',
'paddle':'StandUpPaddling',
'snow':'NordicSki',
'coastal':'Rowing',
'c-boat':'Rowing',
'churchboat':'Rowing',
'Ride':'Ride',
'Run':'Run',
'NordicSki':'NordicSki',
'Swim':'Swim',
'Hike':'Hike',
'Walk':'Walk',
'Canoeing':'Canoeing',
'Crossfit':'Crossfit',
'StandUpPaddling':'StandUpPaddling',
'IceSkate':'IceSkate',
'WeightTraining':'WeightTraining',
'InlineSkate':'InlineSkate',
'Kayaking':'Kayaking',
'Workout':'Workout',
'Yoga':'Yoga',
'other':'Workout',
stravamapping = {key:value for key,value in Reverse(stravacollection)}
})
garmincollection = (
('water','ROWING'),
('rower','INDOOR_ROWING'),
('skierg','CROSS_COUNTRY_SKIING'),
('Bike','ROAD_BIKING'),
('bikeerg','INDOOR_CYCLING'),
('dynamic','INDOOR_ROWING'),
('slides','INDOOR_ROWING'),
('paddle','PADDLING'),
('snow','CROSS_COUNTRY_SKIING'),
('coastal','ROWING'),
('c-boat','ROWING'),
('churchboat','ROWING'),
('Ride','ROAD_BIKING'),
('Run','RUNNING'),
('NordicSki','CROSS_COUNTRY_SKIING'),
('Swim','SWIMMING'),
('Hike','HIKING'),
('Walk','WALKING'),
('Canoeing','PADDLING'),
('Crossfit','FITNESS_EQUIPMENT'),
('StandUpPaddling','STAND_UP_PADDLEBOARDING'),
('IceSkate','SKATING'),
('WeightTraining','STRENGTH_TRAINING'),
('InlineSkate','INLINE_SKATING'),
('Kayaking','PADDLING'),
('Workout','OTHER'),
('Yoga','OTHER'),
('other','OTHER'),
)
stmapping = collections.OrderedDict({
'water':'Rowing',
'rower':'Rowing',
'skierg':'Skiing:Nordic',
'Bike':'Cycling',
'bikeerg':'Cycling',
'dynamic':'Rowing',
'slides':'Rowing',
'paddle':'Other:Paddling',
'snow':'Skiing:Nordic',
'coastal':'Rowing',
'c-boat':'Rowing',
'churchboat':'Rowing',
'Ride':'Cycling',
'Run':'Running',
'NordicSki':'Skiing:Nordic',
'Swim':'Swimming',
'Hike':'Hiking',
'RollerSki':'Other:RollerSki',
'Walk':'Other:Walk',
'Canoeing':'Other:Canoeing',
'Crossfit':'Other:Crossfit',
'StandUpPaddling':'Other:StandUpPaddling',
'IceSkate':'Skating',
'WeightTraining':'Other:WeightTraining',
'InlineSkate':'Skating:InlineSkate',
'Kayaking':'Other:Kayaking',
'Workout':'Other:Workout',
'Yoga':'Other',
'other':'Other',
garminmapping = {key:value for key,value in Reverse(garmincollection)}
})
stcollection = (
('water','Rowing'),
('rower','Rowing'),
('skierg','Skiing:Nordic'),
('Bike','Cycling'),
('bikeerg','Cycling'),
('dynamic','Rowing'),
('slides','Rowing'),
('paddle','Other:Paddling'),
('snow','Skiing:Nordic'),
('coastal','Rowing'),
('c-boat','Rowing'),
('churchboat','Rowing'),
('Ride','Cycling'),
('Run','Running'),
('NordicSki','Skiing:Nordic'),
('Swim','Swimming'),
('Hike','Hiking'),
('RollerSki','Other:RollerSki'),
('Walk','Other:Walk'),
('Canoeing','Other:Canoeing'),
('Crossfit','Other:Crossfit'),
('StandUpPaddling','Other:StandUpPaddling'),
('IceSkate','Skating'),
('WeightTraining','Other:WeightTraining'),
('InlineSkate','Skating:InlineSkate'),
('Kayaking','Other:Kayaking'),
('Workout','Other:Workout'),
('Yoga','Other'),
('other','Other'),
)
rkmapping = collections.OrderedDict({
'water':'Rowing',
'rower':'Rowing',
'skierg':'Cross-Country Skiing',
'Bike':'Cycling',
'bikeerg':'Cycling',
'dynamic':'Rowing',
'slides':'Rowing',
'paddle':'Other:Paddling',
'snow':'Cross-Country Skiing',
'coastal':'Rowing',
'c-boat':'Rowing',
'churchboat':'Rowing',
'Ride':'Cycling',
'Run':'Running',
'NordicSki':'Cross-Country Skiing',
'Swim':'Swimming',
'Hike':'Hiking',
'Walk':'Walking',
'Canoeing':'Other',
'Crossfit':'CrossFit',
'StandUpPaddling':'Other',
'IceSkate':'Skating',
'WeightTraining':'Other',
'InlineSkate':'Skating',
'Kayaking':'Other',
'Workout':'Other',
'other':'Other',
'Yoga':'Other',
})
stmapping = {key:value for key,value in Reverse(stcollection)}
polarmapping = collections.OrderedDict({
'water':'Rowing',
'rower':'Rowing',
'skierg':'Skiing',
'Bike':'Cycling',
'bikeerg':'Cycling',
'dynamic':'Rowing',
'slides':'Rowing',
'paddle':'Other Outdoor',
'snow':'Skiing',
'coastal':'Rowing',
'c-boat':'Rowing',
'churchboat':'Rowing',
'Ride':'Cycling',
'Run':'Running',
'NordicSki':'Skiing',
'Swim':'Swimming',
'Hike':'Hiking',
'Walk':'Walking',
'Canoeing':'Canoeing',
'Crossfit':'Crossfit',
'StandUpPaddling':'Other Outdoor',
'IceSkate':'Skating',
'WeightTraining':'Strength training',
'InlineSkate':'Skating',
'Kayaking':'Kayaking',
'Workout':'Other Indoor',
'other':'Other Indoor',
'Yoga':'Yoga',
})
rkcollection = (
('water','Rowing'),
('rower','Rowing'),
('skierg','Cross-Country Skiing'),
('Bike','Cycling'),
('bikeerg','Cycling'),
('dynamic','Rowing'),
('slides','Rowing'),
('paddle','Other:Paddling'),
('snow','Cross-Country Skiing'),
('coastal','Rowing'),
('c-boat','Rowing'),
('churchboat','Rowing'),
('Ride','Cycling'),
('Run','Running'),
('NordicSki','Cross-Country Skiing'),
('Swim','Swimming'),
('Hike','Hiking'),
('Walk','Walking'),
('Canoeing','Other'),
('Crossfit','CrossFit'),
('StandUpPaddling','Other'),
('IceSkate','Skating'),
('WeightTraining','Other'),
('InlineSkate','Skating'),
('Kayaking','Other'),
('Workout','Other'),
('other','Other'),
('Yoga','Other'),
)
tpmapping = collections.OrderedDict({
'water':'rowing',
'rower':'rowing',
'skierg':'xc-ski',
'Bike':'bike',
'Bikeerg':'bike',
'dynamic':'rowing',
'slides':'rowing',
'paddle':'other',
'snow':'xc-ski',
'coastal':'rowing',
'c-boat':'rowing',
'churchboat':'rowing',
'Ride':'cycling',
'Run':'run',
'NordicSki':'xc-ski',
'Swim':'swim',
'Hike':'other',
'Walk':'walk',
'Canoeing':'other',
'Crossfit':'other',
'StandUpPaddling':'other',
'IceSkate':'other',
'WeightTraining':'strength',
'InlineSkate':'other',
'Kayaking':'other',
'Workout':'other',
'other':'other',
'Yoga':'other',
})
rkmapping = {key:value for key,value in Reverse(rkcollection)}
c2mapping = collections.OrderedDict({
'water':'water',
'rower':'rower',
'skierg':'skierg',
'Bike':'bike',
'bikeerg':'bike',
'dynamic':'dynamic',
'slides':'slides',
'paddle':'paddle',
'snow':'snow',
'coastal':'water',
'c-boat':'water',
'churchboat':'water',
'Ride':'bike',
'Run':None,
'NordicSki':'snow',
'Swim':None,
'Hike':None,
'Walk':None,
'Canoeing':'paddle',
'Crossfit':None,
'StandUpPaddling':None,
'IceSkate':None,
'WeightTraining':None,
'InlineSkate':None,
'Kayaking':None,
'Workout':None,
'other':None,
'Yoga':None,
polarcollection = (
('water','Rowing'),
('rower','Rowing'),
('skierg','Skiing'),
('Bike','Cycling'),
('bikeerg','Cycling'),
('dynamic','Rowing'),
('slides','Rowing'),
('paddle','Other Outdoor'),
('snow','Skiing'),
('coastal','Rowing'),
('c-boat','Rowing'),
('churchboat','Rowing'),
('Ride','Cycling'),
('Run','Running'),
('NordicSki','Skiing'),
('Swim','Swimming'),
('Hike','Hiking'),
('Walk','Walking'),
('Canoeing','Canoeing'),
('Crossfit','Crossfit'),
('StandUpPaddling','Other Outdoor'),
('IceSkate','Skating'),
('WeightTraining','Strength training'),
('InlineSkate','Skating'),
('Kayaking','Kayaking'),
('Workout','Other Indoor'),
('other','Other Indoor'),
('Yoga','Yoga'),
)
})
polarmapping = {key:value for key,value in Reverse(polarcollection)}
c2mappinginv = {value:key for key,value in reversed(c2mapping.items()) if value is not None}
tpcollection = (
('water','rowing'),
('rower','rowing'),
('skierg','xc-ski'),
('Bike','bike'),
('Bikeerg','bike'),
('dynamic','rowing'),
('slides','rowing'),
('paddle','other'),
('snow','xc-ski'),
('coastal','rowing'),
('c-boat','rowing'),
('churchboat','rowing'),
('Ride','cycling'),
('Run','run'),
('NordicSki','xc-ski'),
('Swim','swim'),
('Hike','other'),
('Walk','walk'),
('Canoeing','other'),
('Crossfit','other'),
('StandUpPaddling','other'),
('IceSkate','other'),
('WeightTraining','strength'),
('InlineSkate','other'),
('Kayaking','other'),
('Workout','other'),
('other','other'),
('Yoga','other'),
)
stravamappinginv = {value:key for key,value in reversed(stravamapping.items()) if value is not None}
tpmapping = {key:value for key,value in Reverse(tpcollection)}
stmappinginv = {value:key for key,value in reversed(stmapping.items()) if value is not None}
c2collection = (
('water','water'),
('rower','rower'),
('skierg','skierg'),
('Bike','bike'),
('bikeerg','bike'),
('dynamic','dynamic'),
('slides','slides'),
('paddle','paddle'),
('snow','snow'),
('coastal','water'),
('c-boat','water'),
('churchboat','water'),
('Ride','bike'),
('Run',None),
('NordicSki','snow'),
('Swim',None),
('Hike',None),
('Walk',None),
('Canoeing','paddle'),
('Crossfit',None),
('StandUpPaddling',None),
('IceSkate',None),
('WeightTraining',None),
('InlineSkate',None),
('Kayaking',None),
('Workout',None),
('other',None),
('Yoga',None),
)
rkmappinginv = {value:key for key,value in reversed(rkmapping.items()) if value is not None}
c2mapping = {key:value for key,value in Reverse(c2collection)}
c2mappinginv = {value:key for key,value in Reverse(c2collection) if value is not None}
polarmappinginv = {value:key for key,value in reversed(polarmapping.items()) if value is not None}
stravamappinginv = {value:key for key,value in Reverse(stravacollection) if value is not None}
stmappinginv = {value:key for key,value in Reverse(stcollection) if value is not None}
rkmappinginv = {value:key for key,value in Reverse(rkcollection) if value is not None}
polarmappinginv = {value:key for key,value in Reverse(polarcollection) if value is not None}
garminmappinginv = {value:key for key, value in Reverse(garmincollection) if value is not None}
otwtypes = (
'water',
-4
View File
@@ -126,10 +126,6 @@ def get_strava_workout_list(user,limit_n=0):
# gets all new Strava workouts for a rower
def get_strava_workouts(rower):
if not ispromember(rower.user):
return 0
try:
thetoken = strava_open(rower.user)
except NoTokenError:
+11
View File
@@ -220,6 +220,17 @@
</a>
{% endif %}
</li>
<li id="export-garmin">
{% if workout.uploadedtogarmin %}
<a href="https://connect.garmin.com/modern/activity/{{ workout.uploadedtogarmin }}">
Garmin <i class="fas fa-check"></i>
</a>
{% elif user.rower.garmintoken == None or user.rower.garmintoken == '' %}
<a href="/rowers/me/garminauthorize/">
Connect to Garmin
</a>
{% endif %}
</li>
<li id="export-csv">
<a href="/rowers/workout/{{ workout.id|encode }}/emailcsv/">
CSV
+1 -1
View File
@@ -65,7 +65,7 @@
</tr>
<tr>
<td>Automatic Synchronization with other fitness sites</td>
<td>&nbsp;</td>
<td>&#10004;</td>
<td>&#10004;</td>
<td>&#10004;</td>
<td>&#10004;</td>
+7 -5
View File
@@ -5,10 +5,6 @@
{% block main %}
<h1>Import and Export Settings for {{ rower.user.first_name }} {{ rower.user.last_name }}</h1>
{% if user.rower.rowerplan == 'basic' %}
The auto import and export settings only work on <a href="/rowers/paidplans/">a paid plan</a>.
{% endif %}
{% if form.errors %}
<p style="color: red;">
Please correct the error{{ form.errors|pluralize }} below.
@@ -23,8 +19,12 @@ The auto import and export settings only work on <a href="/rowers/paidplans/">a
<input type="submit" value="Save">
</form>
</p>
<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).
</p>
<p>Click on one of the icons below to connect to the service of your
choice or to renew the authorization</p>
choice or to renew the authorization.</p>
<p><a href="/rowers/me/stravaauthorize/"><img src="/static/img/ConnectWithStrava.png" alt="connect with strava" width="120"></a></p>
<p><a href="/rowers/me/c2authorize/"><img src="/static/img/blueC2logo.png" alt="connect with Concept2" width="120"></a></p>
<p><a href="/rowers/me/sporttracksauthorize/"><img src="/static/img/sporttracks-button.png" alt="connect with SportTracks" width="120"></a></p>
@@ -34,6 +34,8 @@ The auto import and export settings only work on <a href="/rowers/paidplans/">a
alt="connect with Polar" width="130"></a></p>
<p><a href="/rowers/me/tpauthorize/"><img src="/static/img/TP_logo_horz_2_color.png"
alt="connect with Polar" width="130"></a></p>
<p><a href="/rowers/me/garminauthorize"><img src="/static/img/garmin_badge_130.png"
alt="connect with Garmin" with="130"></a></p>
+86
View File
@@ -3,12 +3,98 @@ from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
from django.db import transaction
#from __future__ import print_function
from .statements import *
nu = datetime.datetime.now()
import rowers
from rowers import dataprep
@pytest.mark.django_db
@override_settings(TESTING=True)
class GarminObjects(DjangoTestCase):
def setUp(self):
self.c = Client()
self.u = User.objects.create_user('john',
'sander@ds.ds',
'koeinsloot')
self.u.first_name = 'John'
self.u.last_name = 'Sander'
self.u.save()
self.r = Rower.objects.create(user=self.u,gdproptin=True,surveydone=True,
gdproptindate=timezone.now()
)
self.r.garmintoken = 'dfdzf'
self.r.garminrefreshtoken = 'fsls'
self.r.save()
self.c.login(username='john',password='koeinsloot')
self.nu = datetime.datetime.now()
def tearDown(self):
ws = Workout.objects.filter(user=self.r)
for w in ws:
w.delete()
def test_garmin_push_summaries(self):
data = json.load(open('rowers/tests/testdata/garminsummarydata.txt','r'))
response = self.c.post('/rowers/garmin/summaries/',json.dumps(data),
content_type="application/json")
self.assertEqual(response.status_code, 200)
#response = self.c.get('/rowers/workout/'+encoded1+'/', follow=True)
#self.assertEqual(response.status_code, 200)
ws = Workout.objects.filter(user=self.r)
self.assertEqual(ws.count(),3)
def test_garmin_push_details3(self):
data = json.load(open('rowers/tests/testdata/garmindetail3.txt','r'))
response = self.c.post('/rowers/garmin/activities/',json.dumps(data),
content_type='application/json')
self.assertEqual(response.status_code, 200)
ws = Workout.objects.filter(user=self.r)
self.assertEqual(ws.count(),1)
data,w = dataprep.getrowdata_db(id=ws[0].id)
self.assertEqual(len(data),515)
def test_garmin_push_details2(self):
data = json.load(open('rowers/tests/testdata/garmindetail2.txt','r'))
response = self.c.post('/rowers/garmin/activities/',json.dumps(data),
content_type='application/json')
self.assertEqual(response.status_code, 200)
ws = Workout.objects.filter(user=self.r)
self.assertEqual(ws.count(),3)
data,w = dataprep.getrowdata_db(id=ws[0].id)
self.assertEqual(len(data),451)
def test_garmin_push_details1(self):
data = json.load(open('rowers/tests/testdata/garmindetail1.txt','r'))
response = self.c.post('/rowers/garmin/activities/',json.dumps(data),
content_type='application/json')
self.assertEqual(response.status_code, 200)
response = self.c.get('/rowers/workout/'+encoded1+'/', follow=True)
self.assertEqual(response.status_code, 200)
ws = Workout.objects.filter(user=self.r)
self.assertEqual(ws.count(),2)
data,w = dataprep.getrowdata_db(id=ws[0].id)
self.assertEqual(len(data),2)
@pytest.mark.django_db
@override_settings(TESTING=True)
+74
View File
@@ -0,0 +1,74 @@
[ { "userId": "25858854-f086-4026-83ac-a3bfc97dcbb1",
"userAccessToken": "dfdzf",
"summaryId" : "14098044-detail",
"summary" : {
"durationInSeconds" : 4828,
"startTimeInSeconds" : 1593691200,
"startTimeOffsetInSeconds" : -18000,
"activityType" : "WALKING",
"averageHeartRateInBeatsPerMinute" : 90,
"averageRunCadenceInStepsPerMinute" : 38.0,
"averageSpeedInMetersPerSecond" : 0.8794985,
"averagePaceInMinutesPerKilometer" : 18.247978,
"activeKilocalories" : 233,
"distanceInMeters" : 4103.25,
"maxHeartRateInBeatsPerMinute" : 124,
"maxPaceInMinutesPerKilometer" : 3.2660666,
"maxRunCadenceInStepsPerMinute" : 120.0,
"maxSpeedInMetersPerSecond" : 4.289388,
"steps" : 1623,
"totalElevationGainInMeters" : 20.09
},
"samples" : [ {
"startTimeInSeconds" : 1593691200,
"speedMetersPerSecond" : 0.0,
"totalDistanceInMeters" : 25.0,
"timerDurationInSeconds" : 23,
"clockDurationInSeconds" : 30,
"movingDurationInSeconds" : 0
}, {
"startTimeInSeconds" : 1593691200,
"speedMetersPerSecond" : 0.0,
"totalDistanceInMeters" : 27.0,
"timerDurationInSeconds" : 23,
"clockDurationInSeconds" : 27,
"movingDurationInSeconds" : 0
} ]
}, {
"userId": "25858854-f086-4026-83ac-a3bfc97dcbb1",
"userAccessToken": "dfdzf",
"summaryId" : "14033650-detail",
"summary" : {
"durationInSeconds" : 4778,
"startTimeInSeconds" : 1593777600,
"startTimeOffsetInSeconds" : -18000,
"activityType" : "WALKING",
"averageHeartRateInBeatsPerMinute" : 86,
"averageRunCadenceInStepsPerMinute" : 30.0,
"averageSpeedInMetersPerSecond" : 0.6164423,
"averagePaceInMinutesPerKilometer" : 18.036388,
"activeKilocalories" : 182,
"distanceInMeters" : 3742.63,
"maxHeartRateInBeatsPerMinute" : 120,
"maxPaceInMinutesPerKilometer" : 3.5400813,
"maxRunCadenceInStepsPerMinute" : 118.0,
"maxSpeedInMetersPerSecond" : 4.5140486,
"steps" : 1623,
"totalElevationGainInMeters" : 20.9
},
"samples" : [ {
"startTimeInSeconds" : 1593691200,
"speedMetersPerSecond" : 1.0,
"totalDistanceInMeters" : 27.0,
"timerDurationInSeconds" : 27,
"clockDurationInSeconds" : 27,
"movingDurationInSeconds" : 0
}, {
"startTimeInSeconds" : 1593691200,
"speedMetersPerSecond" : 1.0,
"totalDistanceInMeters" : 24.0,
"timerDurationInSeconds" : 23,
"clockDurationInSeconds" : 29,
"movingDurationInSeconds" : 0
} ]
} ]
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+43
View File
@@ -0,0 +1,43 @@
{"activities":
[
{
"userId": "25858854-f086-4026-83ac-a3bfc97dcbb1",
"userAccessToken": "dfdzf",
"summaryId" : "5180795946",
"durationInSeconds" : 640,
"startTimeInSeconds" : 1593758587,
"startTimeOffsetInSeconds" : 7200,
"activityType" :
"INDOOR_ROWING",
"averageHeartRateInBeatsPerMinute" : 96,
"activeKilocalories" : 55,
"deviceName" : "vivoactive3",
"maxHeartRateInBeatsPerMinute" : 105},
{
"userId": "25858854-f086-4026-83ac-a3bfc97dcbb1",
"userAccessToken": "dfdzf",
"summaryId" : "5180798644",
"durationInSeconds" : 3959,
"startTimeInSeconds" : 1593759504,
"startTimeOffsetInSeconds" : 7200,
"activityType" : "STRENGTH_TRAINING",
"averageHeartRateInBeatsPerMinute" : 97,
"activeKilocalories" : 296,
"deviceName" : "vivoactive3",
"maxHeartRateInBeatsPerMinute" : 132,
"steps" : 630},
{
"userId": "25858854-f086-4026-83ac-a3bfc97dcbb1",
"userAccessToken": "dfdzf",
"summaryId" : "5180799021",
"durationInSeconds" : 309,
"startTimeInSeconds" : 1593763505,
"startTimeOffsetInSeconds" : 7200,
"activityType" : "INDOOR_CYCLING",
"averageHeartRateInBeatsPerMinute" : 109,
"activeKilocalories" : 38,
"deviceName" : "vivoactive3",
"maxHeartRateInBeatsPerMinute" : 119
}
]
}
+6 -6
View File
@@ -535,7 +535,7 @@ def do_sync(w,options, quick=False):
pass
if ('upload_to_C2' in options and options['upload_to_C2']) or (w.user.c2_auto_export and ispromember(w.user.user)):
if ('upload_to_C2' in options and options['upload_to_C2']) or (w.user.c2_auto_export):
try:
message,id = c2stuff.workout_c2_upload(w.user.user,w,asynchron=True)
except NoTokenError:
@@ -544,7 +544,7 @@ def do_sync(w,options, quick=False):
except:
pass
if ('upload_to_Strava' in options and upload_to_strava) or (w.user.strava_auto_export and ispromember(w.user.user)):
if ('upload_to_Strava' in options and upload_to_strava) or (w.user.strava_auto_export):
try:
message,id = stravastuff.workout_strava_upload(
w.user.user,w,quick=quick,asynchron=True,
@@ -563,7 +563,7 @@ def do_sync(w,options, quick=False):
if ('upload_to_SportTracks' in options and options['upload_to_SportTracks']) or (w.user.sporttracks_auto_export and ispromember(w.user.user)):
if ('upload_to_SportTracks' in options and options['upload_to_SportTracks']) or (w.user.sporttracks_auto_export):
try:
message,id = sporttracksstuff.workout_sporttracks_upload(
w.user.user,w,asynchron=True,
@@ -573,7 +573,7 @@ def do_sync(w,options, quick=False):
id = 0
if ('upload_to_RunKeeper' in options and options['upload_to_RunKeeper']) or (w.user.runkeeper_auto_export and ispromember(w.user.user)):
if ('upload_to_RunKeeper' in options and options['upload_to_RunKeeper']) or (w.user.runkeeper_auto_export):
try:
message,id = runkeeperstuff.workout_runkeeper_upload(
w.user.user,w,asynchron=True,
@@ -582,7 +582,7 @@ def do_sync(w,options, quick=False):
message = "Please connect to Runkeeper first"
id = 0
if ('upload_to_MapMyFitness' in options and options['upload_to_MapMyFitness']) or (w.user.mapmyfitness_auto_export and ispromember(w.user.user)):
if ('upload_to_MapMyFitness' in options and options['upload_to_MapMyFitness']) or (w.user.mapmyfitness_auto_export):
try:
message,id = underarmourstuff.workout_ua_upload(
w.user.user,w
@@ -592,7 +592,7 @@ def do_sync(w,options, quick=False):
id = 0
if ('upload_to_TrainingPeaks' in options and options['upload_to_TrainingPeaks']) or (w.user.trainingpeaks_auto_export and ispromember(w.user.user)):
if ('upload_to_TrainingPeaks' in options and options['upload_to_TrainingPeaks']) or (w.user.trainingpeaks_auto_export):
try:
message,id = tpstuff.workout_tp_upload(
w.user.user,w
+3
View File
@@ -415,6 +415,8 @@ urlpatterns = [
re_path(r'^workout/(?P<pk>\b[0-9A-Fa-f]+\b)/delete/$',login_required(
views.WorkoutDelete.as_view()),
name='workout_delete'),
re_path(r'^garmin/summaries/',views.garmin_summaries_view,name='garmin_summaries_view'),
re_path(r'^garmin/activities/',views.garmin_details_view,name='garmin_details_view'),
# re_path(r'^workout/(?P<id>\b[0-9A-Fa-f]+\b)/delete/$',login_required(
# views.workout_code_delete_view),name='workout_code_delete'),
re_path(r'^workout/(?P<id>\b[0-9A-Fa-f]+\b)/smoothenpace/$',views.workout_smoothenpace_view,name='workout_smoothenpace_view'),
@@ -532,6 +534,7 @@ urlpatterns = [
re_path(r'^me/polarauthorize/$',views.rower_polar_authorize,name='rower_polar_authorize'),
re_path(r'^me/revokeapp/(?P<id>\d+)/$',views.rower_revokeapp_view,name='rower_revokeapp_view'),
re_path(r'^me/stravaauthorize/$',views.rower_strava_authorize,name='rower_strava_authorize'),
re_path(r'^me/garminauthorize/$',views.rower_garmin_authorize,name='rower_garmin_authorize'),
re_path(r'^me/sporttracksauthorize/$',views.rower_sporttracks_authorize,name='rower_sporttracks_authorize'),
re_path(r'^me/underarmourauthorize/$',views.rower_underarmour_authorize,name='rower_underarmour_authorize'),
re_path(r'^me/tpauthorize/$',views.rower_tp_authorize,name='rower_tp_authorize'),
+56
View File
@@ -401,6 +401,14 @@ def rower_c2_authorize(request):
url += "&scope="+scope
return HttpResponseRedirect(url)
# Garmin authorization
@login_required()
def rower_garmin_authorize(request):
authorization_url,token,secret = garmin_stuff.garmin_authorize()
request.session['garmin_owner_key'] = token
request.session['garmin_owner_secret'] = secret
return HttpResponseRedirect(authorization_url)
# Strava Authorization
@login_required()
def rower_strava_authorize(request):
@@ -705,7 +713,23 @@ def rower_process_polarcallback(request):
return HttpResponseRedirect(url)
# process Garmin callback
@login_required()
def rower_process_garmincallback(request):
r = getrower(request.user)
absoluteurl = request.build_absolute_uri()
key = request.session['garmin_owner_key']
secret = request.session['garmin_owner_secret']
garmintoken,garminrefreshtoken = garmin_stuff.garmin_processcallback(absoluteurl,key,secret)
r.garmintoken = garmintoken
r.garminrefreshtoken = garminrefreshtoken
r.save()
successmessage = "Tokens stored. Good to go"
messages.info(request,successmessage)
url = reverse('workouts_view')
return HttpResponseRedirect(url)
# Process Strava Callback
@login_required()
@@ -984,6 +1008,38 @@ def workout_stravaimport_view(request,message="",userid=0):
return HttpResponse(res)
# For push notifications from Garmin
@csrf_exempt
def garmin_summaries_view(request):
if request.method != 'POST':
raise Http404("Not allowed")
# POST request
data = json.loads(request.body)
activities = data['activities']
result = garmin_stuff.garmin_workouts_from_summaries(activities)
if result:
return HttpResponse(status=200)
return HttpResponse(status=400)
@csrf_exempt
def garmin_details_view(request):
if request.method != 'POST':
raise Http404("not allowed")
# POST request
data = json.loads(request.body)
result = garmin_stuff.garmin_workouts_from_details(data)
if result:
return HttpResponse(status=200)
return HttpResponse(status=400)
# The page where you select which RunKeeper workout to import
@login_required()
@permission_required('rower.is_coach',fn=get_user_by_userid,raise_exception=True)
+1
View File
@@ -157,6 +157,7 @@ from rowers.sporttracksstuff import sporttracks_open
from rowers.tpstuff import tp_open
from iso8601 import ParseError
import rowers.stravastuff as stravastuff
import rowers.garmin_stuff as garmin_stuff
from rowers.stravastuff import strava_open
import rowers.polarstuff as polarstuff
import rowers.sporttracksstuff as sporttracksstuff
+4
View File
@@ -282,6 +282,10 @@ STRAVA_CLIENT_ID = CFG['strava_client_id']
STRAVA_CLIENT_SECRET = CFG['strava_client_secret']
STRAVA_REDIRECT_URI = CFG['strava_callback']
# Garmin
GARMIN_CLIENT_KEY = CFG["garmin_client_key"]
GARMIN_CLIENT_SECRET = CFG['garmin_client_secret']
GARMIN_REDIRECT_URI = CFG['garmin_callback']
# SportTracks
+1
View File
@@ -63,6 +63,7 @@ TEMPLATES[0]['OPTIONS']['debug'] = DEBUG
ALLOWED_HOSTS = ['localhost','127.0.0.1']
# INSTALLED_APPS += ['debug_toolbar',]
#INSTALLED_APPS += ["sslserver"]
# MIDDLEWARE_CLASSES += ['debug_toolbar.middleware.DebugToolbarMiddleware',]
+1
View File
@@ -71,6 +71,7 @@ urlpatterns += [
# re_path(r'^admin/rq/',include('django_rq_dashboard.urls')),
re_path(r'^call\_back',rowersviews.rower_process_callback),
re_path(r'^stravacall\_back',rowersviews.rower_process_stravacallback),
re_path(r'^garmin\_callback',rowersviews.rower_process_garmincallback),
re_path(r'^sporttracks\_callback',rowersviews.rower_process_sporttrackscallback),
re_path(r'^underarmour\_callback',rowersviews.rower_process_underarmourcallback),
re_path(r'^polarflowcallback',rowersviews.rower_process_polarcallback),
Binary file not shown.

After

Width:  |  Height:  |  Size: 262 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB