Private
Public Access
1
0

adding strava

This commit is contained in:
Sander Roosendaal
2023-02-11 17:20:02 +01:00
parent da9998eaf1
commit eaedf30369
20 changed files with 474 additions and 626 deletions

View File

@@ -1 +1,2 @@
from .c2 import C2Integration
from .strava import StravaIntegration

View File

@@ -5,7 +5,7 @@ import numpy as np
import datetime
import json
import urllib
from rowers.utils import dologging, uniqify
from rowers.utils import dologging, uniqify, custom_exception_handler
from django.utils import timezone
import requests
from pytz.exceptions import UnknownTimeZoneError
@@ -432,7 +432,7 @@ class C2Integration(SyncIntegration):
else: # pragma: no cover
nnn = 'NEW'
keys = ['id', 'distance', 'duration', 'starttime',
'rowtype', 'source', 'comment', 'new']
'rowtype', 'source', 'name', 'new']
values = [i, d, ttot, s, r, s2, c, nnn]
ress = dict(zip(keys, values))
workouts.append(ress)
@@ -444,7 +444,3 @@ class C2Integration(SyncIntegration):
# just as a quick test during development
u = User.objects.get(id=1)
c2_integration_1 = C2Integration(u)

View File

@@ -38,8 +38,8 @@ class SyncIntegration(metaclass=ABCMeta):
@abstractmethod
def createworkoutdata(w, *args, **kwargs) -> dict:
return {}
def createworkoutdata(w, *args, **kwargs):
return None
@abstractmethod
def workout_export(workout, *args, **kwargs) -> str:
@@ -51,12 +51,12 @@ class SyncIntegration(metaclass=ABCMeta):
@abstractmethod
def get_workout(id) -> int:
pass
return 0
# need to unify workout list
@abstractmethod
def get_workout_list(*args, **kwargs) -> list:
pass
return []
@abstractmethod
def make_authorization_url(self, *args, **kwargs) -> str: # pragma: no cover

View File

@@ -0,0 +1,344 @@
from .integrations import SyncIntegration, NoTokenError
from rowers.models import User, Rower, Workout, TombStone
from rowingdata import rowingdata
from rowers import mytypes
from rowers.tasks import handle_strava_sync, fetch_strava_workout
from stravalib.exc import ActivityUploadFailed, TimeoutExceeded
from rowers.rower_rules import is_workout_user, ispromember
from rowers.utils import get_strava_stream
from rowers.utils import myqueue, dologging
from rowers.imports import *
import gzip
import time
import requests
import arrow
import datetime
from rowsandall_app.settings import (
STRAVA_CLIENT_ID, STRAVA_REDIRECT_URI, STRAVA_CLIENT_SECRET,
SITE_URL
)
import django_rq
queue = django_rq.get_queue('default')
queuelow = django_rq.get_queue('low')
queuehigh = django_rq.get_queue('high')
webhookverification = "kudos_to_rowing"
webhooklink = SITE_URL+'/rowers/strava/webhooks/'
headers = {'Accept': 'application/json',
'Api-Key': STRAVA_CLIENT_ID,
'Content-Type': 'application/json',
'user-agent': 'sanderroosendaal'}
from json.decoder import JSONDecodeError
from rowers.dataprep import columndict
def strava_establish_push(): # pragma: no cover
url = "https://www.strava.com/api/v3/push_subscriptions"
post_data = {
'client_id': STRAVA_CLIENT_ID,
'client_secret': STRAVA_CLIENT_SECRET,
'callback_url': webhooklink,
'verify_token': webhookverification,
}
response = requests.post(url, data=post_data)
return response.status_code
def strava_list_push(): # pragma: no cover
url = "https://www.strava.com/api/v3/push_subscriptions"
params = {
'client_id': STRAVA_CLIENT_ID,
'client_secret': STRAVA_CLIENT_SECRET,
}
response = requests.get(url, params=params)
if response.status_code == 200:
data = response.json()
return [w['id'] for w in data]
return []
def strava_push_delete(id): # pragma: no cover
url = "https://www.strava.com/api/v3/push_subscriptions/{id}".format(id=id)
params = {
'client_id': STRAVA_CLIENT_ID,
'client_secret': STRAVA_CLIENT_SECRET,
}
response = requests.delete(url, json=params)
return response.status_code
class StravaIntegration(SyncIntegration):
def __init__(self, *args, **kwargs):
super(StravaIntegration, self).__init__(self, *args, **kwargs)
self.oauth_data = {
'client_id': STRAVA_CLIENT_ID,
'client_secret': STRAVA_CLIENT_SECRET,
'redirect_uri': STRAVA_REDIRECT_URI,
'autorization_uri': "https://www.strava.com/oauth/authorize",
'content_type': 'application/json',
'tokenname': 'stravatoken',
'refreshtokenname': 'stravarefreshtoken',
'expirydatename': 'stravatokenexpirydate',
'bearer_auth': True,
'base_url': "https://www.strava.com/oauth/token",
'grant_type': 'refresh_token',
'headers': headers,
'scope': 'activity:write,activity:read_all',
}
def get_token(self, code, *args, **kwargs):
return super(StravaIntegration, self).get_token(code, *args, **kwargs)
def open(self, *args, **kwargs):
dologging('strava_log.log','Getting token for user {id}'.format(id=self.rower.id))
token = super(StravaIntegration, self).open(*args, **kwargs)
if self.rower.strava_owner_id == 0:
_ = self.set_strava_athlete_id()
return token
# createworkoutdata
def createworkoutdata(self, w, *args, **kwargs) -> str:
dozip = kwargs.get('dozip', True)
filename = w.csvfilename
try:
row = rowingdata(csvfile=filename)
except IOError: # pragma: no cover
data = dataprep.read_df_sql(w.id)
try:
datalength = len(data)
except AttributeError:
datalength = 0
if datalength != 0:
data.rename(columns=columndict, inplace=True)
_ = data.to_csv(w.csvfilename+'.gz',
index_label='index',
compression='gzip')
try:
row = rowingdata(csvfile=filename)
except IOError:
return ''
else:
return ''
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)
if dozip:
gzfilename = tcxfilename+'.gz'
with open(tcxfilename, 'rb') as inF:
s = inF.read()
with gzip.GzipFile(gzfilename, 'wb') as outF:
outF.write(s)
try:
os.remove(tcxfilename)
except WindowError: # pragma: no cover
pass
return gzfilename
return tcxfilename
# workout_export
def workout_export(self, workout, *args, **kwargs) -> str:
description = kwargs.get('description','')
quick = kwargs.get('quick',False)
try:
_ = self.open()
except NoTokenError:
return 0
if (self.rower.stravatoken == '') or (self.rower.stravatoken is None):
raise NoTokenError("Your hovercraft is full of eels")
if not (is_workout_user(self.user, workout)):
return 0
tcxfile = self.createworkoutdata(workout)
if not tcxfile:
return 0
activity_type = self.rower.stravaexportas
if activity_type == 'match':
try:
activity_type = mytypes.stravamapping[workout.workouttype]
except KeyError:
activity_type = 'Rowing'
_ = myqueue(queue,
handle_strava_sync,
self.rower.stravatoken,
workout.id,
tcxfile, workout.name, activity_type,
workout.notes)
dologging('strava_export_log.log', 'Exporting as {t} from {w}'.format(
t=activity_type, w=workout.workouttype))
return 1
# get_workouts
def get_workouts(workout, *args, **kwargs) -> int:
return NotImplemented
# get_workout
def get_workout(self, id) -> int:
try:
_ = self.open()
except NoTokenError:
return 0
csvfilename = 'media/{code}_{stravaid}.csv'.format(
code=uuid4().hex[:16], stravaid=stravaid)
job = myqueue(queue,
fetch_strava_workout,
self.rower.stravatoken,
oauth_data,
id,
csvfilename,
self.user.id,
)
return job.id
# get_workout_list
def get_workout_list(self, *args, **kwargs) -> list:
limit_n = kwargs.get('limit_n',0)
if (self.rower.stravatoken == '') or (self.rower.stravatoken is None): # pragma: no cover
raise NoTokenError
elif (self.rower.stravatokenexpirydate is None or timezone.now()+timedelta(seconds=3599) > self.rower.stravatokenexpirydate): # pragma: no cover
raise NoTokenError
# ready to fetch. Hurray
authorizationstring = str('Bearer ' + self.rower.stravatoken)
headers = {'Authorization': authorizationstring,
'user-agent': 'sanderroosendaal',
'Content-Type': 'application/json'}
url = "https://www.strava.com/api/v3/athlete/activities"
if limit_n == 0:
params = {}
else: # pragma: no cover
params = {'per_page': limit_n}
res = requests.get(url, headers=headers, params=params)
if (res.status_code != 200): # pragma: no cover
return []
workouts = []
rower = self.rower
stravaids = [int(item['id']) for item in res.json()]
stravadata = [{
'id': int(item['id']),
'elapsed_time':item['elapsed_time'],
'start_date':item['start_date'],
} for item in res.json()]
wfailed = Workout.objects.filter(user=rower, uploadedtostrava=-1)
for w in wfailed: # pragma: no cover
for item in stravadata:
elapsed_time = item['elapsed_time']
start_date = item['start_date']
stravaid = item['id']
if arrow.get(start_date) == arrow.get(w.startdatetime):
elapsed_td = datetime.timedelta(seconds=int(elapsed_time))
elapsed_time = datetime.datetime.strptime(
str(elapsed_td),
"%H:%M:%S"
)
if str(elapsed_time)[-7:] == str(w.duration)[-7:]:
w.uploadedtostrava = int(stravaid)
w.save()
knownstravaids = uniqify([
w.uploadedtostrava for w in Workout.objects.filter(user=self.rower)
])
for item in res.json():
d = int(float(item['distance']))
i = item['id']
if i in knownstravaids: # pragma: no cover
nnn = ''
else:
nnn = 'NEW'
n = item['name']
ttot = str(datetime.timedelta(
seconds=int(float(item['elapsed_time']))))
s = item['start_date']
r = item['type']
s2 = None
keys = ['id', 'distance', 'duration',
'starttime', 'rowtype', 'source', 'name', 'new']
values = [i, d, ttot, s, r, s2, n, nnn]
res2 = dict(zip(keys, values))
workouts.append(res2)
return workouts
# make_authorization_url
def make_authorization_url(self, *args, **kwargs):
params = {"client_id": STRAVA_CLIENT_ID,
"response_type": "code",
"redirect_uri": STRAVA_REDIRECT_URI,
"scope": "activity:write,activity:read_all"}
url = "https://www.strava.com/oauth/authorize?" + \
urllib.parse.urlencode(params)
return url
# token_refresh
def token_refresh(self, *args, **kwargs):
return super(StravaIntegration).token_refresh(*args, **kwargs)
def set_strava_athlete_id():
r = self.rower()
if (r.stravatoken == '') or (r.stravatoken is None): # pragma: no cover
s = "Token doesn't exist. Need to authorize"
return custom_exception_handler(401, s)
elif (r.stravatokenexpirydate is None or timezone.now()+timedelta(seconds=3599) > r.stravatokenexpirydate):
_ = self.open()
authorizationstring = str('Bearer ' + r.stravatoken)
headers = {'Authorization': authorizationstring,
'user-agent': 'sanderroosendaal',
'Content-Type': 'application/json'}
url = "https://www.strava.com/api/v3/athlete"
response = requests.get(url, headers=headers, params={})
if response.status_code == 200: # pragma: no cover
r.strava_owner_id = response.json()['id']
r.save()
return response.json()['id']
return 0
# just as a quick test during development
u = User.objects.get(id=1)
strava_integration_1 = StravaIntegration(u)

View File

@@ -17,7 +17,8 @@ import rowers.c2stuff as c2stuff
import rowers.metrics as metrics
import rowers.dataprep as dataprep
from rowers.dataprep import rdata
import rowers.stravastuff as stravastuff
import rowers.utils as utils
from scipy.interpolate import griddata
from scipy.signal import savgol_filter
from scipy import optimize
@@ -5401,7 +5402,7 @@ def interactive_flexchart_stacked(id, r, xparam='time',
if metricsdicts[column]['maysmooth']:
nrsteps = int(log2(r.usersmooth))
for i in range(nrsteps):
rowdata[column] = stravastuff.ewmovingaverage(
rowdata[column] = utils.ewmovingaverage(
rowdata[column], 5)
except KeyError:
pass
@@ -5767,7 +5768,7 @@ def interactive_flex_chart2(id, r, promember=0,
if metricsdicts[column]['maysmooth']:
nrsteps = int(log2(r.usersmooth))
for i in range(nrsteps):
rowdata[column] = stravastuff.ewmovingaverage(
rowdata[column] = utils.ewmovingaverage(
rowdata[column], 5)
except KeyError:
pass

View File

@@ -30,7 +30,7 @@ import rowers.uploads as uploads
import rowers.polarstuff as polarstuff
import rowers.rp3stuff as rp3stuff
import rowers.stravastuff as stravastuff
import rowers.nkstuff as nkstuff
from rowers.opaque import encoder
from rowers.integrations import *

View File

@@ -1,404 +0,0 @@
from rowers.tasks import handle_strava_sync, fetch_strava_workout
from stravalib.exc import ActivityUploadFailed, TimeoutExceeded
from rowers.rower_rules import is_workout_user, ispromember
from rowers.utils import get_strava_stream
from rowers.utils import dologging
from rowers.imports import *
from rowsandall_app.settings import (
C2_CLIENT_ID, C2_REDIRECT_URI, C2_CLIENT_SECRET,
STRAVA_CLIENT_ID, STRAVA_REDIRECT_URI, STRAVA_CLIENT_SECRET,
SITE_URL
)
import gzip
import rowers.mytypes as mytypes
from rowers.utils import myqueue
from iso8601 import ParseError
import stravalib
from rowers.dataprep import columndict
# All the functionality needed to connect to Strava
from scipy import optimize
from scipy.signal import savgol_filter
import time
from time import strftime
import django_rq
queue = django_rq.get_queue('default')
queuelow = django_rq.get_queue('low')
queuehigh = django_rq.get_queue('low')
try:
from json.decoder import JSONDecodeError
except ImportError: # pragma: no cover
JSONDecodeError = ValueError
webhookverification = "kudos_to_rowing"
webhooklink = SITE_URL+'/rowers/strava/webhooks/'
headers = {'Accept': 'application/json',
'Api-Key': STRAVA_CLIENT_ID,
'Content-Type': 'application/json',
'user-agent': 'sanderroosendaal'}
oauth_data = {
'client_id': STRAVA_CLIENT_ID,
'client_secret': STRAVA_CLIENT_SECRET,
'redirect_uri': STRAVA_REDIRECT_URI,
'autorization_uri': "https://www.strava.com/oauth/authorize",
'content_type': 'application/json',
'tokenname': 'stravatoken',
'refreshtokenname': 'stravarefreshtoken',
'expirydatename': 'stravatokenexpirydate',
'bearer_auth': True,
'base_url': "https://www.strava.com/oauth/token",
'grant_type': 'refresh_token',
'headers': headers,
'scope': 'activity:write,activity:read_all',
}
# Exchange access code for long-lived access token
def get_token(code):
return imports_get_token(code, oauth_data)
def strava_open(user):
t = time.localtime()
timestamp = time.strftime('%b-%d-%Y_%H%M', t)
with open('strava_open.log', 'a') as f:
f.write('\n')
f.write(timestamp)
f.write(' ')
f.write('Getting token for user ')
f.write(str(user.rower.id))
f.write(' token expiry ')
f.write(str(user.rower.stravatokenexpirydate))
f.write(' ')
f.write(json.dumps(oauth_data))
f.write('\n')
token = imports_open(user, oauth_data)
if user.rower.strava_owner_id == 0: # pragma: no cover
_ = set_strava_athlete_id(user)
return token
def do_refresh_token(refreshtoken):
return imports_do_refresh_token(refreshtoken, oauth_data)
def rower_strava_token_refresh(user):
r = Rower.objects.get(user=user)
res = do_refresh_token(r.stravarefreshtoken)
access_token = res[0]
expires_in = res[1]
refresh_token = res[2]
expirydatetime = timezone.now()+timedelta(seconds=expires_in)
r.stravatoken = access_token
r.stravatokenexpirydate = expirydatetime
r.stravarefreshtoken = refresh_token
r.save()
return r.stravatoken
# Make authorization URL including random string
def make_authorization_url(request): # pragma: no cover
return imports_make_authorization_url(oauth_data)
def strava_establish_push(): # pragma: no cover
url = "https://www.strava.com/api/v3/push_subscriptions"
post_data = {
'client_id': STRAVA_CLIENT_ID,
'client_secret': STRAVA_CLIENT_SECRET,
'callback_url': webhooklink,
'verify_token': webhookverification,
}
response = requests.post(url, data=post_data)
return response.status_code
def strava_list_push(): # pragma: no cover
url = "https://www.strava.com/api/v3/push_subscriptions"
params = {
'client_id': STRAVA_CLIENT_ID,
'client_secret': STRAVA_CLIENT_SECRET,
}
response = requests.get(url, params=params)
if response.status_code == 200:
data = response.json()
return [w['id'] for w in data]
return []
def strava_push_delete(id): # pragma: no cover
url = "https://www.strava.com/api/v3/push_subscriptions/{id}".format(id=id)
params = {
'client_id': STRAVA_CLIENT_ID,
'client_secret': STRAVA_CLIENT_SECRET,
}
response = requests.delete(url, json=params)
return response.status_code
def set_strava_athlete_id(user):
r = Rower.objects.get(user=user)
if (r.stravatoken == '') or (r.stravatoken is None): # pragma: no cover
s = "Token doesn't exist. Need to authorize"
return custom_exception_handler(401, s)
elif (r.stravatokenexpirydate is None or timezone.now()+timedelta(seconds=3599) > r.stravatokenexpirydate):
_ = imports_open(user, oauth_data)
authorizationstring = str('Bearer ' + r.stravatoken)
headers = {'Authorization': authorizationstring,
'user-agent': 'sanderroosendaal',
'Content-Type': 'application/json'}
url = "https://www.strava.com/api/v3/athlete"
response = requests.get(url, headers=headers, params={})
if response.status_code == 200: # pragma: no cover
r.strava_owner_id = response.json()['id']
r.save()
return response.json()['id']
return 0
# Get list of workouts available on Strava
def get_strava_workout_list(user, limit_n=0):
r = Rower.objects.get(user=user)
if (r.stravatoken == '') or (r.stravatoken is None): # pragma: no cover
s = "Token doesn't exist. Need to authorize"
return custom_exception_handler(401, s)
elif (
r.stravatokenexpirydate is None or timezone.now()+timedelta(seconds=3599) > r.stravatokenexpirydate
): # pragma: no cover
s = "Token expired. Needs to refresh."
return custom_exception_handler(401, s)
else:
# ready to fetch. Hurray
authorizationstring = str('Bearer ' + r.stravatoken)
headers = {'Authorization': authorizationstring,
'user-agent': 'sanderroosendaal',
'Content-Type': 'application/json'}
url = "https://www.strava.com/api/v3/athlete/activities"
if limit_n == 0:
params = {}
else: # pragma: no cover
params = {'per_page': limit_n}
s = requests.get(url, headers=headers, params=params)
return s
def async_get_workout(user, stravaid):
try:
_ = strava_open(user)
except NoTokenError: # pragma: no cover
return 0
csvfilename = 'media/{code}_{stravaid}.csv'.format(
code=uuid4().hex[:16], stravaid=stravaid)
job = myqueue(queue,
fetch_strava_workout,
user.rower.stravatoken,
oauth_data,
stravaid,
csvfilename,
user.id,
)
return job
# Get a Strava workout summary data and stroke data by ID
def get_workout(user, stravaid, do_async=True):
return async_get_workout(user, stravaid)
# Generate Workout data for Strava (a TCX file)
def createstravaworkoutdata(w, dozip=True):
filename = w.csvfilename
try:
row = rowingdata(csvfile=filename)
except IOError: # pragma: no cover
data = dataprep.read_df_sql(w.id)
try:
datalength = len(data)
except AttributeError:
datalength = 0
if datalength != 0:
data.rename(columns=columndict, inplace=True)
_ = data.to_csv(w.csvfilename+'.gz',
index_label='index',
compression='gzip')
try:
row = rowingdata(csvfile=filename)
except IOError:
return '', 'Error - could not find rowing data'
else:
return '', 'Error - could not find rowing data'
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)
if dozip:
gzfilename = tcxfilename+'.gz'
with open(tcxfilename, 'rb') as inF:
s = inF.read()
with gzip.GzipFile(gzfilename, 'wb') as outF:
outF.write(s)
try:
os.remove(tcxfilename)
except WindowError: # pragma: no cover
pass
return gzfilename, ""
return tcxfilename, ""
# Upload the TCX file to Strava and set the workout activity type
# to rowing on Strava
def handle_stravaexport(f2, workoutname, stravatoken, description='',
activity_type='Rowing', quick=False, asynchron=False):
# w = Workout.objects.get(id=workoutid)
client = stravalib.Client(access_token=stravatoken)
act = client.upload_activity(f2, 'tcx.gz', name=workoutname)
try:
if quick: # pragma: no cover
res = act.wait(poll_interval=2.0, timeout=10)
message = 'Workout successfully synchronized to Strava'
else:
res = act.wait(poll_interval=5.0, timeout=30)
message = 'Workout successfully synchronized to Strava'
except: # pragma: no cover
res = 0
message = 'Strava upload timed out'
# description doesn't work yet. Have to wait for stravalib to update
if res:
try:
act = client.update_activity(
res.id, activity_type=activity_type, description=description, device_name='Rowsandall.com')
except TypeError: # pragma: no cover
act = client.update_activity(
res.id, activity_type=activity_type, description=description)
else: # pragma: no cover
message = 'Strava activity update timed out.'
return (0, message)
return (res.id, message)
def workout_strava_upload(user, w, quick=False, asynchron=True):
try:
_ = strava_open(user)
except NoTokenError: # pragma: no cover
return "Please connect to Strava first", 0
message = "Uploading to Strava"
stravaid = -1
r = Rower.objects.get(user=user)
res = -1
if (r.stravatoken == '') or (r.stravatoken is None): # pragma: no cover
raise NoTokenError("Your hovercraft is full of eels")
if (is_workout_user(user, w)):
if asynchron:
tcxfile, tcxmesg = createstravaworkoutdata(w)
if not tcxfile: # pragma: no cover
return "Failed to create workout data", 0
activity_type = r.stravaexportas
if r.stravaexportas == 'match':
try:
activity_type = mytypes.stravamapping[w.workouttype]
except KeyError: # pragma: no cover
activity_type = 'Rowing'
_ = myqueue(queue,
handle_strava_sync,
r.stravatoken,
w.id,
tcxfile, w.name, activity_type,
w.notes)
dologging('strava_export_log.log', 'Exporting as {t} from {w}'.format(
t=activity_type, w=w.workouttype))
return "Asynchronous sync", -1
try:
tcxfile, tcxmesg = createstravaworkoutdata(w)
if tcxfile:
activity_type = r.stravaexportas
if r.stravaexportas == 'match':
try:
activity_type = mytypes.stravamapping[w.workouttype]
except KeyError: # pragma: no cover
activity_type = 'Rowing'
with open(tcxfile, 'rb') as f:
try:
description = w.notes+'\n from '+w.workoutsource+' via rowsandall.com'
except TypeError:
description = ' via rowsandall.com'
res, mes = handle_stravaexport(
f, w.name,
r.stravatoken,
description=description,
activity_type=activity_type, quick=quick, asynchron=asynchron)
if res == 0: # pragma: no cover
message = mes
w.uploadedtostrava = -1
stravaid = -1
w.save()
try:
os.remove(tcxfile)
except WindowsError:
pass
return message, stravaid
w.uploadedtostrava = res
w.save()
try:
os.remove(tcxfile)
except WindowsError: # pragma: no cover
pass
message = mes
stravaid = res
return message, stravaid
else: # pragma: no cover
message = "Strava TCX data error "+tcxmesg
w.uploadedtostrava = -1
stravaid = -1
w.save()
return message, stravaid
except ActivityUploadFailed as e: # pragma: no cover
message = "Strava Upload error: %s" % e
w.uploadedtostrava = -1
stravaid = -1
w.save()
os.remove(tcxfile)
return message, stravaid
return message, stravaid # pragma: no cover

View File

@@ -9,11 +9,12 @@
<ul class="main-content">
{% if workouts %}
{% if integration == 'C2 Logbook' %}
<li class="grid_2">
<a href="/rowers/workout/c2import/all/{{ page }}">Import all NEW</a>
<p>This imports all workouts that have not been imported to rowsandall.com.
The action may take a longer time to process, so please be patient. Click on Import in the list below to import an individual workout.
</p>
</p>
</li>
<li class="grid_2">
<p>
@@ -29,11 +30,12 @@
</span>
</p>
</li>
{% endif %}
<li class="grid_4">
<form enctype="multipart/form-data" method="post">
{% csrf_token %}
<input name='workouts' type="submit" value="Import selected workouts">
<a href="/rowers/workout/c2list/?selectallnew=true">Select All New</a>
<a href="?selectallnew=true">Select All New</a>
<table width="70%" class="listtable">
<thead>
<tr>
@@ -43,7 +45,7 @@
<th> Total Distance</th>
<th> Type</th>
<th> Source</th>
<th> Comment</th>
<th> Name</th>
<th> New</th>
</tr>
</thead>
@@ -62,7 +64,7 @@
<td>{{ workout|lookup:'distance' }}</td>
<td>{{ workout|lookup:'rowtype' }}</td>
<td>{{ workout|lookup:'source' }}</td>
<td>{{ workout|lookup:'comment' }}</td>
<td>{{ workout|lookup:'name' }}</td>
<td>
{{ workout|lookup:'new' }}
</td>

View File

@@ -522,7 +522,7 @@ def deltatimeprint(d): # pragma: no cover
def c2userid(user): # pragma: no cover
c2integration = C2Integration(user)
c2userid = c2integration.get_userid(thetoken)
c2userid = c2integration.get_userid(user)
return c2userid
@@ -566,6 +566,8 @@ def lookup(dict, key):
s = dict.get(key)
except KeyError: # pragma: no cover
return None
except AttributeError:
return None
if isinstance(s, string_types) and len(s) > 22:
s = s[:22]

View File

@@ -14,7 +14,7 @@ import rowers
from rowers import dataprep
from rowers import tasks
from rowers import c2stuff
from rowers import stravastuff
import urllib
import json
import pandas as pd

View File

@@ -14,7 +14,7 @@ import rowers
from rowers import dataprep
from rowers import tasks
from rowers import c2stuff
from rowers import stravastuff
import urllib
import json

View File

@@ -15,7 +15,6 @@ import rowers
from rowers import dataprep
from rowers import tasks
from rowers import stravastuff
from rowers import polarstuff
import urllib
import json
@@ -26,6 +25,7 @@ from rowers.integrations import *
from django.db import transaction
import rowers.garmin_stuff as gs
import rowers.integrations.strava as strava
@pytest.mark.django_db
@override_settings(TESTING=True)
@@ -1059,14 +1059,14 @@ class StravaObjects(DjangoTestCase):
csvfilename=filename,uploadedtostrava=123,
)
@patch('rowers.stravastuff.requests.post', side_effect=mocked_requests)
@patch('rowers.stravastuff.requests.get', side_effect=mocked_requests)
@patch('rowers.integrations.strava.requests.post', side_effect=mocked_requests)
@patch('rowers.integrations.strava.requests.get', side_effect=mocked_requests)
def test_strava_webhook(self, mock_get, mock_post):
url = reverse('strava_webhook_view')
params = {
'hub.challenge':'aap',
'hub.verify_token':stravastuff.webhookverification,
'hub.verify_token':strava.webhookverification,
}
url2 = url+'?'+urllib.parse.urlencode(params)
@@ -1117,21 +1117,18 @@ class StravaObjects(DjangoTestCase):
response = self.c.generic('POST', url, raw_data)
self.assertEqual(response.status_code,200)
@patch('rowers.stravastuff.requests.post', side_effect=mocked_requests)
@patch('rowers.stravastuff.requests.get', side_effect=mocked_requests)
@patch('rowers.stravastuff.stravalib.Client',side_effect=MockStravalibClient)
def test_workout_strava_upload(self, mock_get, mock_post,MockStravalibClient):
@patch('rowers.integrations.strava.requests.post', side_effect=mocked_requests)
@patch('rowers.integrations.strava.requests.get', side_effect=mocked_requests)
def test_workout_strava_upload(self, mock_get, mock_post):
w = Workout.objects.get(id=1)
res = stravastuff.workout_strava_upload(self.r.user,w,asynchron=True)
self.assertEqual(res[1],-1)
res = stravastuff.workout_strava_upload(self.r.user,w,asynchron=False)
integration = StravaIntegration(self.r.user)
res = integration.workout_export(w)
self.assertEqual(len(res[0]),43)
self.assertEqual(res,1)
@patch('rowers.stravastuff.requests.post', side_effect=mocked_requests)
@patch('rowers.stravastuff.requests.get', side_effect=mocked_requests)
@patch('rowers.stravastuff.stravalib.Client',side_effect=MockStravalibClient)
def test_strava_upload(self, mock_get, mock_post,MockStravalibClient):
@patch('rowers.integrations.strava.requests.post', side_effect=mocked_requests)
@patch('rowers.integrations.strava.requests.get', side_effect=mocked_requests)
def test_strava_upload(self, mock_get, mock_post):
response = self.c.get('/rowers/workout/'+encoded1+'/stravauploadw/')
self.assertRedirects(response,
@@ -1142,17 +1139,18 @@ class StravaObjects(DjangoTestCase):
self.assertEqual(response.status_code, 302)
@patch('rowers.stravastuff.requests.get', side_effect=mocked_requests)
@patch('rowers.stravastuff.requests.post', side_effect=mocked_requests)
@patch('rowers.integrations.strava.requests.get', side_effect=mocked_requests)
@patch('rowers.integrations.strava.requests.post', side_effect=mocked_requests)
def test_strava_list(self, mock_get, mockpost):
result = rowers.stravastuff.rower_strava_token_refresh(self.u)
integration = StravaIntegration(self.u)
result = integration.token_refresh()
self.assertEqual(result,"987654321234567898765432123456789")
response = self.c.get('/rowers/workout/stravaimport/')
self.assertEqual(response.status_code,200)
@patch('rowers.utils.requests.get', side_effect=mocked_requests)
@patch('rowers.stravastuff.requests.post', side_effect=mocked_requests)
@patch('rowers.integrations.strava.requests.post', side_effect=mocked_requests)
@patch('rowers.dataprep.getsmallrowdata_db')
def test_strava_import(self, mock_get, mock_post,
mocked_getsmallrowdata_db):
@@ -1166,16 +1164,11 @@ class StravaObjects(DjangoTestCase):
self.assertEqual(response.status_code, 200)
@patch('rowers.stravastuff.requests.post', side_effect=mocked_requests)
@patch('rowers.integrations.strava.requests.post', side_effect=mocked_requests)
def test_strava_callback(self, mock_post):
response = self.c.get('/stravacall_back?code=absdef23&scope=read',follow=True)
self.assertEqual(response.status_code, 200)
@patch('rowers.stravastuff.requests.post', side_effect=mocked_requests)
def test_strava_token_refresh(self, mock_post):
result = rowers.stravastuff.rower_strava_token_refresh(self.u)
self.assertEqual(result,"987654321234567898765432123456789")
#@pytest.mark.django_db

View File

@@ -424,7 +424,7 @@ class PermissionsViewTests(TestCase):
@patch('rowers.dataprep.get_video_data',side_effect=mocked_get_video_data)
def test_permissions_coachee(
self,view,permissions,
mock_Session,
mocked_session,
mock_c2open,
mocked_sqlalchemy,
mocked_read_df_sql,

View File

@@ -20,7 +20,7 @@ from rowers import dataprep
from rowers import tasks
from rowers import plannedsessions
from rowers.views.workoutviews import get_video_id
from rowers import stravastuff
import rowingdata
from rowers.c2stuff import getagegrouprecord

View File

@@ -84,7 +84,7 @@
95,112,WorkoutDelete,delete workout,TRUE,403,basic,200,302,basic,403,403,coach,403,403,FALSE,FALSE,TRUE,FALSE,FALSE,
96,113,workout_smoothenpace_view,smoothen pace,TRUE,403,pro,302,302,pro,403,403,coach,302,403,FALSE,FALSE,TRUE,TRUE,TRUE,
97,114,workout_undo_smoothenpace_view,unsmoothen pace,TRUE,403,pro,302,302,pro,403,403,coach,302,403,FALSE,FALSE,TRUE,TRUE,TRUE,
98,115,workout_c2import_view,list workouts to be imported (test stops at notokenerror),TRUE,302,basic,302,302,basic,403,403,coach,302,403,FALSE,TRUE,FALSE,TRUE,TRUE,
98,115,workout_c2import_view,list workouts to be imported (test stops at notokenerror),TRUE,302,basic,200,302,basic,403,403,coach,302,403,FALSE,TRUE,FALSE,TRUE,TRUE,
99,120,workout_stravaimport_view,list workouts to be imported (test stops at notokenerror),TRUE,302,basic,302,302,basic,403,403,coach,302,403,FALSE,TRUE,FALSE,TRUE,TRUE,
101,124,workout_getimportview,imports a workout from third party,TRUE,200,basic,200,302,FALSE,200,302,FALSE,200,302,FALSE,FALSE,FALSE,FALSE,FALSE,
103,126,workout_getstravaworkout_next,gets all strava workouts,TRUE,302,basic,302,302,FALSE,200,302,FALSE,200,302,FALSE,FALSE,FALSE,FALSE,FALSE,
1 id view function anonymous anonymous_response own own_response own_nonperm member member_response member_nonperm coachee coachee_response coachee_nonperm is_staff userid workoutid dotest realtest kwargs
84 95 112 WorkoutDelete delete workout TRUE 403 basic 200 302 basic 403 403 coach 403 403 FALSE FALSE TRUE FALSE FALSE
85 96 113 workout_smoothenpace_view smoothen pace TRUE 403 pro 302 302 pro 403 403 coach 302 403 FALSE FALSE TRUE TRUE TRUE
86 97 114 workout_undo_smoothenpace_view unsmoothen pace TRUE 403 pro 302 302 pro 403 403 coach 302 403 FALSE FALSE TRUE TRUE TRUE
87 98 115 workout_c2import_view list workouts to be imported (test stops at notokenerror) TRUE 302 basic 302 200 302 basic 403 403 coach 302 403 FALSE TRUE FALSE TRUE TRUE
88 99 120 workout_stravaimport_view list workouts to be imported (test stops at notokenerror) TRUE 302 basic 302 302 basic 403 403 coach 302 403 FALSE TRUE FALSE TRUE TRUE
89 101 124 workout_getimportview imports a workout from third party TRUE 200 basic 200 302 FALSE 200 302 FALSE 200 302 FALSE FALSE FALSE FALSE FALSE
90 103 126 workout_getstravaworkout_next gets all strava workouts TRUE 302 basic 302 302 FALSE 200 302 FALSE 200 302 FALSE FALSE FALSE FALSE FALSE

View File

@@ -3,7 +3,7 @@ from rowers.mytypes import workouttypes, boattypes, otwtypes, workoutsources, wo
from rowers.rower_rules import is_promember
import rowers.tpstuff as tpstuff
import rowers.sporttracksstuff as sporttracksstuff
import rowers.stravastuff as stravastuff
from rowers.integrations import *
from rowers.utils import (
geo_distance, serialize_list, deserialize_list, uniqify,
@@ -213,10 +213,9 @@ def do_sync(w, options, quick=False):
pass
if do_strava_export: # pragma: no cover
strava_integration = StravaIntegration(w.user.user)
try:
message, id = stravastuff.workout_strava_upload(
w.user.user, w, quick=quick, asynchron=True,
)
id = strava_integration.workout_export(w)
dologging(
'strava_export_log.log',
'exporting workout {id} as {type}'.format(

View File

@@ -664,8 +664,6 @@ urlpatterns = [
re_path(r'^workout/(?P<source>\w+.*)import/(?P<externalid>\d+)/async/$',
views.workout_getimportview, {'do_async': True}, name='workout_getimportview'),
# re_path(r'^workout/stravaimport/all/$',views.workout_getstravaworkout_all,name='workout_getstravaworkout_all'),
re_path(r'^workout/stravaimport/next/$', views.workout_getstravaworkout_next,
name='workout_getstravaworkout_next'),
re_path(r'^workout/sporttracksimport/$', views.workout_sporttracksimport_view,
name='workout_sporttracksimport_view'),
re_path(r'^workout/sporttracksimport/user/(?P<userid>\d+)/$',

View File

@@ -7,7 +7,9 @@ from rowers.views.statements import *
from rowers.plannedsessions import get_dates_timeperiod
from rowers.tasks import fetch_strava_workout
from rowers.integrations.c2 import C2Integration
from rowers.integrations import C2Integration, StravaIntegration
import rowers.integrations.strava as strava
import numpy
@@ -82,8 +84,12 @@ def workout_strava_upload_view(request, id=0):
r = getrower(request.user)
w = get_workout_by_opaqueid(request, id)
result = -1
comment, result = stravastuff.workout_strava_upload(
r.user, w, asynchron=True)
strava_integration = StravaIntegration(request.user)
try:
stravaid = strava_integration.workout_export(w)
except NoTokenError:
return HttpResponseRedirect("/rowers/me/stravaauthorize")
messages.info(
request, 'Your workout will be synchronized to Strava in the background')
@@ -210,21 +216,12 @@ def rower_garmin_authorize(request): # pragma: no cover
@login_required()
def rower_strava_authorize(request): # pragma: no cover
# Generate a random string for the state parameter
# Save it for use later to prevent xsrf attacks
# state = str(uuid4())
params = {"client_id": STRAVA_CLIENT_ID,
"response_type": "code",
"redirect_uri": STRAVA_REDIRECT_URI,
"scope": "activity:write,activity:read_all"}
url = "https://www.strava.com/oauth/authorize?" + \
urllib.parse.urlencode(params)
strava_integration = StravaIntegration(request.user)
url = strava_integration.make_authorization_url()
return HttpResponseRedirect(url)
# Polar Authorization
@@ -673,7 +670,7 @@ def workout_nkimport_view(request, userid=0, after=0, before=0):
if (res.status_code != 200): # pragma: no cover
if (res.status_code == 401):
r = getrower(request.user)
if (r.stravatoken == '') or (r.stravatoken is None):
if (r.nktoken == '') or (r.nktoken is None):
s = "Token doesn't exist. Need to authorize"
return HttpResponseRedirect("/rowers/me/nkauthorize/")
message = "Something went wrong in workout_nkimport_view"
@@ -786,6 +783,7 @@ def workout_nkimport_view(request, userid=0, after=0, before=0):
@login_required()
def rower_process_stravacallback(request):
strava_integration = StravaIntegration(request.user)
try:
code = request.GET['code']
_ = request.GET['scope']
@@ -800,7 +798,7 @@ def rower_process_stravacallback(request):
return HttpResponseRedirect(url)
res = stravastuff.get_token(code)
res = strava_integration.get_token(code)
if res[0]:
access_token = res[0]
@@ -815,7 +813,7 @@ def rower_process_stravacallback(request):
r.stravarefreshtoken = refresh_token
r.save()
_ = stravastuff.set_strava_athlete_id(r.user)
_ = strava_integration.set_strava_athlete_id()
successmessage = "Tokens stored. Good to go. Please check your import/export settings"
messages.info(request, successmessage)
@@ -1169,7 +1167,7 @@ def workout_rojaboimport_view(request, message="", userid=0):
},
{
'url': reverse('workout_rojaboimport_view'),
'name': 'Strava'
'name': 'Rojabo'
},
]
@@ -1197,130 +1195,69 @@ def workout_stravaimport_view(request, message="", userid=0):
url = reverse('workout_stravaimport_view',
kwargs={'userid': request.user.id})
return HttpResponseRedirect(url)
# if r.user != request.user:
# messages.info(request,"You cannot import other people's workouts from Strava")
strava_integration = StravaIntegration(request.user)
try:
_ = strava_open(request.user)
_ = strava_integration.open()
except NoTokenError: # pragma: no cover
return HttpResponseRedirect("/rowers/me/stravaauthorize/")
res = stravastuff.get_strava_workout_list(request.user)
workouts = strava_integration.get_workout_list()
if (res.status_code != 200): # pragma: no cover
if (res.status_code == 401):
r = getrower(request.user)
if (r.stravatoken == '') or (r.stravatoken is None):
s = "Token doesn't exist. Need to authorize"
return HttpResponseRedirect("/rowers/me/stravaauthorize/")
message = "Something went wrong in workout_stravaimport_view"
messages.error(request, message)
url = reverse('workouts_view')
return HttpResponseRedirect(url)
else:
workouts = []
r = getrower(request.user)
rower = r
stravaids = [int(item['id']) for item in res.json()]
stravadata = [{
'id': int(item['id']),
'elapsed_time':item['elapsed_time'],
'start_date':item['start_date'],
} for item in res.json()]
if request.method == "POST":
try: # pragma: no cover
tdict = dict(request.POST.lists())
ids = tdict['workoutid']
stravaids = [int(id) for id in ids]
alldata = {}
wfailed = Workout.objects.filter(user=r, uploadedtostrava=-1)
for stravaid in stravaids:
csvfilename = 'media/{code}_{stravaid}.csv'.format(
code=uuid4().hex[:16], stravaid=stravaid)
_ = myqueue(
queue,
fetch_strava_workout,
r.stravatoken,
strava_integration.oauth_data,
stravaid,
csvfilename,
r.user.id
)
# done, redirect to workouts list
messages.info(request,
'Your Strava workouts will be imported in the background.'
' It may take a few minutes before they appear.')
url = reverse('workouts_view')
return HttpResponseRedirect(url)
except KeyError: # pragma: no cover
pass
for w in wfailed: # pragma: no cover
for item in stravadata:
elapsed_time = item['elapsed_time']
start_date = item['start_date']
stravaid = item['id']
if arrow.get(start_date) == arrow.get(w.startdatetime):
elapsed_td = datetime.timedelta(seconds=int(elapsed_time))
elapsed_time = datetime.datetime.strptime(
str(elapsed_td),
"%H:%M:%S"
)
if str(elapsed_time)[-7:] == str(w.duration)[-7:]:
w.uploadedtostrava = int(stravaid)
w.save()
breadcrumbs = [
{
'url': '/rowers/list-workouts/',
'name': 'Workouts'
},
{
'url': reverse('workout_stravaimport_view'),
'name': 'Strava'
},
]
knownstravaids = uniqify([
w.uploadedtostrava for w in Workout.objects.filter(user=r)
])
checknew = request.GET.get('selectallnew', False)
# 2022-10-24 sorting the results
workouts = sorted(workouts, key = lambda d:d['starttime'], reverse=True)
return render(request, 'list_import.html',
{'workouts': workouts,
'rower': r,
'active': 'nav-workouts',
'breadcrumbs': breadcrumbs,
'teams': get_my_teams(request.user),
'checknew': checknew,
'integration': 'Strava'
})
for item in res.json():
d = int(float(item['distance']))
i = item['id']
if i in knownstravaids: # pragma: no cover
nnn = ''
else:
nnn = 'NEW'
n = item['name']
ttot = str(datetime.timedelta(
seconds=int(float(item['elapsed_time']))))
s = item['start_date']
r = item['type']
keys = ['id', 'distance', 'duration',
'starttime', 'type', 'name', 'new']
values = [i, d, ttot, s, r, n, nnn]
res2 = dict(zip(keys, values))
workouts.append(res2)
if request.method == "POST":
try: # pragma: no cover
tdict = dict(request.POST.lists())
ids = tdict['workoutid']
stravaids = [int(id) for id in ids]
alldata = {}
for item in res.json():
alldata[item['id']] = item
for stravaid in stravaids:
csvfilename = 'media/{code}_{stravaid}.csv'.format(
code=uuid4().hex[:16], stravaid=stravaid)
_ = myqueue(
queue,
fetch_strava_workout,
rower.stravatoken,
stravastuff.oauth_data,
stravaid,
csvfilename,
rower.user.id
)
# done, redirect to workouts list
messages.info(request,
'Your Strava workouts will be imported in the background.'
' It may take a few minutes before they appear.')
url = reverse('workouts_view')
return HttpResponseRedirect(url)
except KeyError: # pragma: no cover
pass
breadcrumbs = [
{
'url': '/rowers/list-workouts/',
'name': 'Workouts'
},
{
'url': reverse('workout_stravaimport_view'),
'name': 'Strava'
},
]
checknew = request.GET.get('selectallnew', False)
# 2022-10-24 sorting the results
workouts = sorted(workouts, key = lambda d:d['starttime'], reverse=True)
return render(request, 'strava_list_import.html',
{'workouts': workouts,
'rower': rower,
'active': 'nav-workouts',
'breadcrumbs': breadcrumbs,
'teams': get_my_teams(request.user),
'checknew': checknew,
})
return HttpResponse(res) # pragma: no cover
# for Strava webhook request validation
@@ -1330,7 +1267,7 @@ def strava_webhook_view(request):
if request.method == 'GET':
challenge = request.GET.get('hub.challenge')
verificationtoken = request.GET.get('hub.verify_token')
if verificationtoken != stravastuff.webhookverification: # pragma: no cover
if verificationtoken != strava.webhookverification: # pragma: no cover
return HttpResponse(status=403)
data = {"hub.challenge": challenge}
return JSONResponse(data)
@@ -1374,8 +1311,9 @@ def strava_webhook_view(request):
ws = Workout.objects.filter(uploadedtostrava=stravaid)
if ws.count() == 0 and r.strava_auto_import:
job = stravastuff.async_get_workout(r.user, stravaid)
if job == 0: # pragma: no cover
strava_integration = StravaIntegration(r.user)
jobid = strava_integration.get_workout(stravaid)
if jobid == 0: # pragma: no cover
dologging('strava_webhooks.log',
'Strava strava_open yielded NoTokenError')
else: # pragma: no cover
@@ -1728,7 +1666,7 @@ def workout_c2import_view(request, page=1, userid=0, message=""):
workouts = c2_integration.get_workout_list(page=1)
if request.method == "POST":
try: # pragma: no cover
tdict = dict(request.POST.lists())
@@ -1801,7 +1739,7 @@ importauthorizeviews = {
importsources = {
'c2': C2Integration,
'strava': stravastuff,
'strava': StravaIntegration,
'polar': polarstuff,
'ownapi': ownapistuff,
'sporttracks': sporttracksstuff,
@@ -1960,24 +1898,3 @@ def workout_getsporttracksworkout_all(request):
url = reverse('workouts_view')
return HttpResponseRedirect(url)
# Imports all new workouts from SportTracks
@login_required()
def workout_getstravaworkout_next(request): # pragma: no cover
r = Rower.objects.get(user=request.user)
res = stravastuff.get_strava_workout_list(r.user)
if (res.status_code != 200):
return 0
else:
alldata = {}
for item in res.json():
alldata[item['id']] = item
_ = stravastuff.create_async_workout(
alldata, r.user, stravaid, debug=True)
url = reverse('workouts_view')
return HttpResponseRedirect(url)

View File

@@ -199,10 +199,10 @@ from rowers.rp3stuff import rp3_open
from rowers.sporttracksstuff import sporttracks_open
from rowers.tpstuff import tp_open
from iso8601 import ParseError
import rowers.stravastuff as stravastuff
import rowers.rojabo_stuff as rojabo_stuff
import rowers.garmin_stuff as garmin_stuff
from rowers.stravastuff import strava_open
from rowers.rojabo_stuff import rojabo_open
import rowers.polarstuff as polarstuff
import rowers.sporttracksstuff as sporttracksstuff

View File

@@ -2566,7 +2566,7 @@ def workout_smoothenpace_view(request, id=0, message="", successmessage=""):
if 'originalvelo' not in row.df:
row.df['originalvelo'] = velo
velo2 = stravastuff.ewmovingaverage(velo, 5)
velo2 = utils.ewmovingaverage(velo, 5)
pace2 = 500./abs(velo2)
@@ -5641,10 +5641,9 @@ def workout_upload_view(request,
messages.error(request, message)
if (upload_to_strava): # pragma: no cover
strava_integration = StravaIntegration(request.user)
try:
message, id = stravastuff.workout_strava_upload(
request.user, w,
)
id = strava_integration.workout_export(w)
except NoTokenError:
id = 0
message = "Please connect to Strava first"