diff --git a/rowers/integrations/__init__.py b/rowers/integrations/__init__.py
index fff8ac50..06b659fd 100644
--- a/rowers/integrations/__init__.py
+++ b/rowers/integrations/__init__.py
@@ -3,3 +3,5 @@ from .strava import StravaIntegration
from .nk import NKIntegration
from .sporttracks import SportTracksIntegration
from .rp3 import RP3Integration
+from .trainingpeaks import TPIntegration
+
diff --git a/rowers/integrations/rp3.py b/rowers/integrations/rp3.py
index 657176ef..fb1230c4 100644
--- a/rowers/integrations/rp3.py
+++ b/rowers/integrations/rp3.py
@@ -244,8 +244,4 @@ class RP3Integration(SyncIntegration):
return super(RP3Integration, self).token_refresh(*args, **kwargs)
-# just as a quick test during development
-u = User.objects.get(id=1)
-
-integration_1 = RP3Integration(u)
diff --git a/rowers/integrations/trainingpeaks.py b/rowers/integrations/trainingpeaks.py
new file mode 100644
index 00000000..d9cea185
--- /dev/null
+++ b/rowers/integrations/trainingpeaks.py
@@ -0,0 +1,134 @@
+from .integrations import SyncIntegration, NoTokenError
+from rowers.models import User, Rower, Workout, TombStone
+
+import django_rq
+queue = django_rq.get_queue('default')
+queuelow = django_rq.get_queue('low')
+queuehigh = django_rq.get_queue('high')
+
+from rowers.utils import myqueue, dologging, myqueue
+
+import requests
+
+from rowingdata import rowingdata
+
+from rowers.rower_rules import is_workout_user
+import time
+from django_rq import job
+
+from rowers.tasks import check_tp_workout_id, handle_workout_tp_upload
+
+from rowsandall_app.settings import (
+ TP_CLIENT_ID, TP_CLIENT_SECRET,
+ TP_REDIRECT_URI, TP_CLIENT_KEY,TP_API_LOCATION,
+ TP_OAUTH_LOCATION,
+)
+
+import gzip
+
+import base64
+from io import BytesIO
+
+
+tpapilocation = TP_API_LOCATION
+
+
+
+class TPIntegration(SyncIntegration):
+ def __init__(self, *args, **kwargs):
+ super(TPIntegration, self).__init__(*args, **kwargs)
+ self.oauth_data = {
+ 'client_id': TP_CLIENT_ID,
+ 'client_secret': TP_CLIENT_SECRET,
+ 'redirect_uri': TP_REDIRECT_URI,
+ 'autorization_uri': "https://oauth.trainingpeaks.com/oauth/authorize?",
+ 'content_type': 'application/x-www-form-urlencoded',
+ 'tokenname': 'tptoken',
+ 'refreshtokenname': 'tprefreshtoken',
+ 'expirydatename': 'tptokenexpirydate',
+ 'bearer_auth': False,
+ 'base_url': "https://oauth.trainingpeaks.com/oauth/token",
+ 'scope': 'write',
+ }
+
+ def createworkoutdata(self, w, *args, **kwargs):
+ filename = w.csvfilename
+ row = rowingdata(csvfile=filename)
+ tcxfilename = filename[:-4]+'.tcx'
+ try:
+ newnotes = w.notes+'\n from '+w.workoutsource+' via rowsandall.com'
+ except TypeError:
+ newnotes = 'from '+w.workoutsource+' via rowsandall.com'
+
+ row.exporttotcx(tcxfilename, notes=newnotes)
+
+ return tcxfilename
+
+
+ def workout_export(self, workout, *args, **kwargs) -> str:
+ thetoken = self.open()
+ tcxfilename = self.createworkoutdata(workout)
+ job = myqueue(
+ queue,
+ handle_workout_tp_upload,
+ workout,
+ thetoken,
+ tcxfilename
+ )
+ return job.id
+
+
+ def get_workouts(self, *args, **kwargs) -> int:
+ raise NotImplementedError("not implemented")
+
+ def get_workout(self, id) -> int:
+ raise NotImplementedError("not implemented")
+
+ def get_workout_list(self, *args, **kwargs) -> list:
+ raise NotImplementedError("not implemented")
+
+ def make_authorization_url(self, *args, **kwargs) -> str: # pragma: no cover
+ return super(TPIntegration, self).make_authorization_url(self, *args, **kwargs)
+
+ def get_token(self, code, *args, **kwargs) -> (str, int, str):
+ # client_auth = requests.auth.HTTPBasicAuth(TP_CLIENT_KEY, TP_CLIENT_SECRET)
+ post_data = {
+ "client_id": TP_CLIENT_KEY,
+ "grant_type": "authorization_code",
+ "code": code,
+ "redirect_uri": TP_REDIRECT_URI,
+ "client_secret": TP_CLIENT_SECRET,
+ }
+
+ response = requests.post(
+ TP_OAUTH_LOCATION+"/oauth/token/",
+ data=post_data, verify=False,
+ )
+
+ if response.status_code != 200:
+ raise NoTokenError
+
+ try:
+ token_json = response.json()
+ thetoken = token_json['access_token']
+ expires_in = token_json['expires_in']
+ refresh_token = token_json['refresh_token']
+ except KeyError: # pragma: no cover
+ thetoken = ""
+ expires_in = 0
+ refresh_token = ""
+
+ return thetoken, expires_in, refresh_token
+
+
+ def open(self, *args, **kwargs) -> str:
+ return super(TPIntegration, self).open(*args, **kwargs)
+
+ def token_refresh(self, *args, **kwargs) -> str:
+ return super(TPIntegration, self).token_refresh(*args, **kwargs)
+
+# just as a quick test during development
+u = User.objects.get(id=1)
+
+integration_1 = TPIntegration(u)
+
diff --git a/rowers/tasks.py b/rowers/tasks.py
index 5430cf20..6418787a 100644
--- a/rowers/tasks.py
+++ b/rowers/tasks.py
@@ -47,6 +47,8 @@ import sys
import json
import traceback
from time import strftime
+import base64
+from io import BytesIO
from scipy import optimize
from scipy.signal import savgol_filter
@@ -105,7 +107,8 @@ except KeyError: # pragma: no cover
NK_API_LOCATION = CFG["nk_api_location"]
TP_CLIENT_ID = CFG["tp_client_id"]
TP_CLIENT_SECRET = CFG["tp_client_secret"]
-
+TP_API_LOCATION = CFG["tp_api_location"]
+tpapilocation = TP_API_LOCATION
from requests_oauthlib import OAuth1, OAuth1Session
@@ -344,6 +347,46 @@ def handle_add_workouts_team(ws, t, debug=False, **kwargs):
return 1
+def uploadactivity(access_token, filename, description='',
+ name='Rowsandall.com workout'):
+
+ data_gz = BytesIO()
+ with open(filename, 'rb') as inF:
+ s = inF.read()
+ with gzip.GzipFile(fileobj=data_gz, mode="w") as gzf:
+ gzf.write(s)
+
+ headers = {
+ 'Content-Type': 'application/json',
+ 'Accept': 'application/json',
+ 'Authorization': 'Bearer %s' % access_token
+ }
+
+ data = {
+ "UploadClient": "rowsandall",
+ "Filename": filename,
+ "SetWorkoutPublic": True,
+ "Title": name,
+ "Type": "rowing",
+ "Comment": description,
+ "Data": base64.b64encode(data_gz.getvalue()).decode("ascii")
+ }
+
+ resp = requests.post(tpapilocation+"/v3/file",
+ data=json.dumps(data),
+ headers=headers, verify=False)
+
+ if resp.status_code not in (200, 202): # pragma: no cover
+ dologging('tp_export.log',resp.status_code)
+ dologging('tp_export.log',resp.reason)
+ dologging('tp_export.log',json.dumps(data))
+ return 0, resp.reason, resp.status_code, headers
+ else:
+ return 1, "ok", 200, resp.headers
+
+ return 0, 0, 0, 0 # pragma: no cover
+
+
@app.task
def check_tp_workout_id(workout, location, attempts=5, debug=False, **kwargs):
authorizationstring = str('Bearer ' + workout.user.tptoken)
@@ -361,6 +404,37 @@ def check_tp_workout_id(workout, location, attempts=5, debug=False, **kwargs):
return 1
+@app.task
+def handle_workout_tp_upload(w, thetoken, tcxfilename, debug=False, **kwargs):
+ tpid = 0
+ r = w.user
+ if not tcxfilename:
+ return 0
+
+ res, reason, status_code, headers = uploadactivity(
+ thetoken, tcxfilename,
+ name=w.name
+ )
+
+ if res == 0:
+ w.tpid = -1
+ try:
+ os.remove(tcxfilename)
+ except WindowsError:
+ pass
+
+ w.save()
+ return 0
+
+ w.uploadedtotp = res
+ tpid = res
+ w.save()
+ os.remove(tcxfilename)
+
+ check_tp_workout_id(w,headers['Location'])
+
+ return tpid
+
@app.task
def instroke_static(w, metric, debug=False, **kwargs):
f1 = w.csvfilename[6:-4]
diff --git a/rowers/templates/menu_workout.html b/rowers/templates/menu_workout.html
index bba56d91..6d8ff847 100644
--- a/rowers/templates/menu_workout.html
+++ b/rowers/templates/menu_workout.html
@@ -234,10 +234,6 @@
Connect to RP3
- {% else %}
-
- RP3
-
{% endif %}
diff --git a/rowers/tests/test_imports.py b/rowers/tests/test_imports.py
index 0bac3505..8ff6ab46 100644
--- a/rowers/tests/test_imports.py
+++ b/rowers/tests/test_imports.py
@@ -1373,15 +1373,15 @@ class TPObjects(DjangoTestCase):
self.assertEqual(response.status_code, 200)
- @patch('rowers.tpstuff.requests.post', side_effect=mocked_requests)
+ @patch('rowers.integrations.trainingpeaks.requests.post', side_effect=mocked_requests)
def test_tp_token_refresh(self, mock_post):
response = self.c.get('/rowers/me/tprefresh/',follow=True)
self.assertEqual(response.status_code, 200)
- @patch('rowers.tpstuff.requests.post', side_effect=mocked_requests)
- @patch('rowers.tpstuff.requests.get', side_effect=mocked_requests)
+ @patch('rowers.integrations.trainingpeaks.requests.post', side_effect=mocked_requests)
+ @patch('rowers.integrations.trainingpeaks.requests.get', side_effect=mocked_requests)
def test_tp_upload(self, mock_get, mock_post):
url = '/rowers/workout/'+encoded1+'/tpuploadw/'
diff --git a/rowers/tests/testdata/testdata.tcx.gz b/rowers/tests/testdata/testdata.tcx.gz
index 3133a666..dcef8d75 100644
Binary files a/rowers/tests/testdata/testdata.tcx.gz and b/rowers/tests/testdata/testdata.tcx.gz differ
diff --git a/rowers/tpstuff.py b/rowers/tpstuff.py
deleted file mode 100644
index 25ef8616..00000000
--- a/rowers/tpstuff.py
+++ /dev/null
@@ -1,232 +0,0 @@
-from celery import Celery, app
-from rowers.rower_rules import is_workout_user
-import time
-from django_rq import job
-# All the functionality needed to connect to Runkeeper
-from rowers.imports import *
-from rowers.utils import dologging
-from rowers.tasks import check_tp_workout_id
-
-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
-
-# Python
-import gzip
-
-import base64
-from io import BytesIO
-
-
-from rowsandall_app.settings import (
- C2_CLIENT_ID, C2_REDIRECT_URI, C2_CLIENT_SECRET,
- STRAVA_CLIENT_ID, STRAVA_REDIRECT_URI, STRAVA_CLIENT_SECRET,
- TP_CLIENT_ID, TP_CLIENT_SECRET,
- TP_REDIRECT_URI, TP_CLIENT_KEY,TP_API_LOCATION,
- TP_OAUTH_LOCATION,
-)
-
-tpapilocation = TP_API_LOCATION
-
-oauth_data = {
- 'client_id': TP_CLIENT_ID,
- 'client_secret': TP_CLIENT_SECRET,
- 'redirect_uri': TP_REDIRECT_URI,
- 'autorization_uri': "https://oauth.trainingpeaks.com/oauth/authorize?",
- 'content_type': 'application/x-www-form-urlencoded',
- # 'content_type': 'application/json',
- 'tokenname': 'tptoken',
- 'refreshtokenname': 'tprefreshtoken',
- 'expirydatename': 'tptokenexpirydate',
- 'bearer_auth': False,
- 'base_url': "https://oauth.trainingpeaks.com/oauth/token",
- 'scope': 'write',
-}
-
-
-# Checks if user has UnderArmour token, renews them if they are expired
-def tp_open(user):
- return imports_open(user, oauth_data)
-
-# Refresh ST token using refresh token
-
-
-def do_refresh_token(refreshtoken):
- return imports_do_refresh_token(refreshtoken, oauth_data)
-
-# Exchange access code for long-lived access token
-
-def get_token(code):
- # client_auth = requests.auth.HTTPBasicAuth(TP_CLIENT_KEY, TP_CLIENT_SECRET)
- post_data = {
- "client_id": TP_CLIENT_KEY,
- "grant_type": "authorization_code",
- "code": code,
- "redirect_uri": TP_REDIRECT_URI,
- "client_secret": TP_CLIENT_SECRET,
- }
-
- response = requests.post(
- TP_OAUTH_LOCATION+"/oauth/token/",
- data=post_data, verify=False,
- )
-
- if response.status_code != 200:
- return 0,0,0
-
- try:
- token_json = response.json()
- thetoken = token_json['access_token']
- expires_in = token_json['expires_in']
- refresh_token = token_json['refresh_token']
- except KeyError: # pragma: no cover
- thetoken = 0
- expires_in = 0
- refresh_token = 0
-
- return thetoken, expires_in, refresh_token
-
-# Make authorization URL including random string
-
-
-def make_authorization_url(request): # pragma: no cover
- return imports_make_authorization_url(oauth_data)
-
-
-def getidfromresponse(response): # pragma: no cover
- t = json.loads(response.text)
-
- links = t["_links"]
-
- id = links["self"][0]["id"]
-
- return int(id)
-
-
-def createtpworkoutdata(w):
- filename = w.csvfilename
- row = rowingdata(csvfile=filename)
- tcxfilename = filename[:-4]+'.tcx'
- try:
- newnotes = w.notes+'\n from '+w.workoutsource+' via rowsandall.com'
- except TypeError:
- newnotes = 'from '+w.workoutsource+' via rowsandall.com'
-
- row.exporttotcx(tcxfilename, notes=newnotes)
-
- return tcxfilename
-
-
-def tp_check(access_token): # pragma: no cover
- headers = {
- "Content-Type": "application/json",
- 'Accept': 'application/json',
- 'authorization': 'Bearer %s' % access_token
- }
-
- resp = requests.post(tpapilocation+"/v2/info/version",
- headers=headers, verify=False)
-
- return resp
-
-
-def uploadactivity(access_token, filename, description='',
- name='Rowsandall.com workout'):
-
- data_gz = BytesIO()
- with open(filename, 'rb') as inF:
- s = inF.read()
- with gzip.GzipFile(fileobj=data_gz, mode="w") as gzf:
- gzf.write(s)
-
- headers = {
- 'Content-Type': 'application/json',
- 'Accept': 'application/json',
- 'Authorization': 'Bearer %s' % access_token
- }
-
- data = {
- "UploadClient": "rowsandall",
- "Filename": filename,
- "SetWorkoutPublic": True,
- "Title": name,
- "Type": "rowing",
- "Comment": description,
- "Data": base64.b64encode(data_gz.getvalue()).decode("ascii")
- }
-
- #resp = requests.post(tpapilocation+"/v2/file/synchronous",
- # data=json.dumps(data),
- # headers=headers, verify=False)
-
- resp = requests.post(tpapilocation+"/v3/file",
- data=json.dumps(data),
- headers=headers, verify=False)
-
- if resp.status_code not in (200, 202): # pragma: no cover
- dologging('tp_export.log',resp.status_code)
- dologging('tp_export.log',resp.reason)
- dologging('tp_export.log',json.dumps(data))
- return 0, resp.reason, resp.status_code, headers
- else:
- return 1, "ok", 200, resp.headers
-
- return 0, 0, 0, 0 # pragma: no cover
-
-
-def workout_tp_upload(user, w): # pragma: no cover
- message = "Uploading to TrainingPeaks"
- tpid = 0
- r = w.user
-
- thetoken = tp_open(r.user)
-
- # need some code if token doesn't refresh
-
- if (is_workout_user(user, w)):
- tcxfile = createtpworkoutdata(w)
- if tcxfile:
- res, reason, status_code, headers = uploadactivity(
- thetoken, tcxfile,
- name=w.name
- )
- if res == 0:
- message = "Upload to TrainingPeaks failed with status code " + \
- str(status_code)+": "+reason
- w.tpid = -1
- try:
- os.remove(tcxfile)
- except WindowsError:
- pass
-
- return message, tpid
-
- else: # res != 0
- w.uploadedtotp = res
- tpid = res
- w.save()
- os.remove(tcxfile)
-
- job = myqueue(queuelow,
- check_tp_workout_id,
- w,
- headers['Location'])
-
- return 'Successfully synchronized to TrainingPeaks', tpid
-
- else: # no tcxfile
- dologging('tp_export.log','Failed to create tcx file')
- message = "Upload to TrainingPeaks failed"
- w.uploadedtotp = -1
- tpid = -1
- w.save()
- return message, tpid
- else: # not allowed to upload
- message = "You are not allowed to export this workout to TP"
- tpid = 0
- return message, tpid
-
- return message, tpid
diff --git a/rowers/uploads.py b/rowers/uploads.py
index f99501a4..606d5a9e 100644
--- a/rowers/uploads.py
+++ b/rowers/uploads.py
@@ -1,7 +1,6 @@
from rowers.mytypes import workouttypes, boattypes, otwtypes, workoutsources, workouttypes_ordered
from rowers.rower_rules import is_promember
-import rowers.tpstuff as tpstuff
from rowers.integrations import *
from rowers.utils import (
@@ -270,9 +269,8 @@ def do_sync(w, options, quick=False):
upload_to_st = False
if do_tp_export:
try:
- _, id = tpstuff.workout_tp_upload(
- w.user.user, w
- )
+ tp_integration = TPIntegration(w.user.user)
+ id = tp_integration.workout_export(w)
dologging('tp_export.log',
'exported workout {wid} for user {uid}'.format(
wid = w.id,
diff --git a/rowers/views/importviews.py b/rowers/views/importviews.py
index 06f51140..4193d681 100644
--- a/rowers/views/importviews.py
+++ b/rowers/views/importviews.py
@@ -23,12 +23,15 @@ def default(o): # pragma: no cover
# 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):
-
+
message = ""
r = getrower(request.user)
res = -1
+
+ tp_integration = TPIntegration(request.user)
+
try:
- _ = tp_open(r.user)
+ _ = tp_integration.open()
except NoTokenError: # pragma: no cover
return HttpResponseRedirect("/rowers/me/tpauthorize/")
@@ -36,38 +39,9 @@ def workout_tp_upload_view(request, id=0):
w = get_workout_by_opaqueid(request, id)
r = w.user
- tcxfile = tpstuff.createtpworkoutdata(w)
- if tcxfile:
- res, reason, status_code, headers = tpstuff.uploadactivity(
- r.tptoken, tcxfile,
- name=w.name
- )
- if res == 0: # pragma: no cover
- message = "Upload to TrainingPeaks failed with status code " + \
- str(status_code)+": "+reason
- try:
- os.remove(tcxfile)
- except WindowsError:
- pass
+ jobid = tp_integration.workout_export(w)
+ messages.info(request,'Your workout will be exported to TrainingPeaks in the background')
- messages.error(request, message)
-
- else: # res != 0
- w.uploadedtotp = res
- w.save()
- os.remove(tcxfile)
- job = myqueue(queuelow,
- check_tp_workout_id,
- w,
- headers['Location'])
-
- messages.info(request, 'Uploaded to TrainingPeaks')
-
- else: # pragma: no cover # no tcxfile
- message = "Upload to TrainingPeaks failed"
- w.uploadedtotp = -1
- w.save()
- messages.error(request, message)
url = reverse(r.defaultlandingpage,
kwargs={
@@ -301,20 +275,8 @@ def rower_c2_token_refresh(request):
@login_required()
def rower_tp_token_refresh(request):
r = getrower(request.user)
- res = tpstuff.do_refresh_token(
- r.tprefreshtoken,
- )
- access_token = res[0]
- expires_in = res[1]
- refresh_token = res[2]
- expirydatetime = timezone.now()+datetime.timedelta(seconds=expires_in)
-
- r = getrower(request.user)
- r.tptoken = access_token
- r.tptokenexpirydate = expirydatetime
- r.tprefreshtoken = refresh_token
-
- r.save()
+ tp_integration = TPIntegration(request.user)
+ token = tp_integration.token_refresh()
successmessage = "Tokens refreshed. Good to go"
messages.info(request, successmessage)
@@ -759,11 +721,8 @@ def rower_process_tpcallback(request):
url = reverse('rower_exportsettings_view')
return HttpResponseRedirect(url)
- res = tpstuff.get_token(code)
-
- access_token = res[0]
- expires_in = res[1]
- refresh_token = res[2]
+ tp_integration = TPIntegration(request.user)
+ access_token, expires_in, refresh_token = tp_integration.get_token(code)
expirydatetime = timezone.now()+datetime.timedelta(seconds=expires_in)
r = getrower(request.user)
@@ -1560,7 +1519,7 @@ importsources = {
'polar': polarstuff,
'ownapi': ownapistuff,
'sporttracks': SportTracksIntegration,
- 'trainingpeaks': tpstuff,
+ 'trainingpeaks': TPIntegration,
'nk': NKIntegration,
'rp3':RP3Integration,
}
diff --git a/rowers/views/statements.py b/rowers/views/statements.py
index f3f2def6..d0526ca8 100644
--- a/rowers/views/statements.py
+++ b/rowers/views/statements.py
@@ -194,7 +194,6 @@ import datetime
import iso8601
import rowers.rojabo_stuff as rojabo_stuff
-from rowers.tpstuff import tp_open
from iso8601 import ParseError
import rowers.rojabo_stuff as rojabo_stuff
@@ -205,7 +204,6 @@ import rowers.polarstuff as polarstuff
from rowers.integrations import *
-import rowers.tpstuff as tpstuff
import rowers.ownapistuff as ownapistuff
from rowers.ownapistuff import TEST_CLIENT_ID, TEST_CLIENT_SECRET, TEST_REDIRECT_URI
diff --git a/rowers/views/workoutviews.py b/rowers/views/workoutviews.py
index 6e8d0a62..4a72c3e0 100644
--- a/rowers/views/workoutviews.py
+++ b/rowers/views/workoutviews.py
@@ -5636,9 +5636,6 @@ def workout_upload_view(request,
except NoTokenError:
id = 0
message = "Something went wrong with the Concept2 sync"
- if id > 1:
- messages.info(request, message)
- else:
messages.error(request, message)
if (upload_to_strava): # pragma: no cover
@@ -5648,9 +5645,6 @@ def workout_upload_view(request,
except NoTokenError:
id = 0
message = "Please connect to Strava first"
- if id > 1:
- messages.info(request, message)
- else:
messages.error(request, message)
if (upload_to_st): # pragma: no cover
@@ -5660,23 +5654,14 @@ def workout_upload_view(request,
except NoTokenError:
message = "Please connect to SportTracks first"
id = 0
- if id > 1:
- messages.info(request, message)
- else:
messages.error(request, message)
if (upload_to_tp): # pragma: no cover
+ tp_integration = TPIntegration(request.user)
try:
- message, id = tpstuff.workout_tp_upload(
- request.user, w
- )
+ id = tp_integration.workout_export(w)
except NoTokenError:
message = "Please connect to TrainingPeaks first"
- id = 0
-
- if id > 1:
- messages.info(request, message)
- else:
messages.error(request, message)
if int(registrationid) < 0: # pragma: no cover