diff --git a/rowers/integrations/__init__.py b/rowers/integrations/__init__.py index 56112937..89ea5f85 100644 --- a/rowers/integrations/__init__.py +++ b/rowers/integrations/__init__.py @@ -1 +1,2 @@ from .c2 import C2Integration +from .strava import StravaIntegration diff --git a/rowers/integrations/c2.py b/rowers/integrations/c2.py index 09af9f4a..1d5d3ea4 100644 --- a/rowers/integrations/c2.py +++ b/rowers/integrations/c2.py @@ -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) diff --git a/rowers/integrations/integrations.py b/rowers/integrations/integrations.py index d1fd8646..95bae300 100644 --- a/rowers/integrations/integrations.py +++ b/rowers/integrations/integrations.py @@ -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 diff --git a/rowers/integrations/strava.py b/rowers/integrations/strava.py new file mode 100644 index 00000000..cedebb8b --- /dev/null +++ b/rowers/integrations/strava.py @@ -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) diff --git a/rowers/interactiveplots.py b/rowers/interactiveplots.py index ff5d1464..8235fc74 100644 --- a/rowers/interactiveplots.py +++ b/rowers/interactiveplots.py @@ -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 diff --git a/rowers/management/commands/processemail.py b/rowers/management/commands/processemail.py index c9ff2cb1..a89c1eb9 100644 --- a/rowers/management/commands/processemail.py +++ b/rowers/management/commands/processemail.py @@ -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 * diff --git a/rowers/stravastuff.py b/rowers/stravastuff.py deleted file mode 100644 index 4eab0961..00000000 --- a/rowers/stravastuff.py +++ /dev/null @@ -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 diff --git a/rowers/templates/list_import.html b/rowers/templates/list_import.html index 27e1e36c..1a0f21c2 100644 --- a/rowers/templates/list_import.html +++ b/rowers/templates/list_import.html @@ -9,11 +9,12 @@
    {% if workouts %} + {% if integration == 'C2 Logbook' %}
  • Import all NEW

    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. -

    +

  • @@ -29,11 +30,12 @@

  • + {% endif %}
  • {% csrf_token %} - Select All New + Select All New @@ -43,7 +45,7 @@ - + @@ -62,7 +64,7 @@ - + diff --git a/rowers/templatetags/rowerfilters.py b/rowers/templatetags/rowerfilters.py index 21ca77ad..6fc2fee0 100644 --- a/rowers/templatetags/rowerfilters.py +++ b/rowers/templatetags/rowerfilters.py @@ -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] diff --git a/rowers/tests/test_api.py b/rowers/tests/test_api.py index bf7d74e4..f0f8eab6 100644 --- a/rowers/tests/test_api.py +++ b/rowers/tests/test_api.py @@ -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 diff --git a/rowers/tests/test_braintree.py b/rowers/tests/test_braintree.py index 9933bd15..ca540e2b 100644 --- a/rowers/tests/test_braintree.py +++ b/rowers/tests/test_braintree.py @@ -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 diff --git a/rowers/tests/test_imports.py b/rowers/tests/test_imports.py index 46bc63af..5d1596a9 100644 --- a/rowers/tests/test_imports.py +++ b/rowers/tests/test_imports.py @@ -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 diff --git a/rowers/tests/test_permissions2.py b/rowers/tests/test_permissions2.py index 8baa8c28..52d495aa 100644 --- a/rowers/tests/test_permissions2.py +++ b/rowers/tests/test_permissions2.py @@ -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, diff --git a/rowers/tests/test_unit_tests.py b/rowers/tests/test_unit_tests.py index aad12551..a4984a1a 100644 --- a/rowers/tests/test_unit_tests.py +++ b/rowers/tests/test_unit_tests.py @@ -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 diff --git a/rowers/tests/viewnames.csv b/rowers/tests/viewnames.csv index 34c2b7cd..02e35056 100644 --- a/rowers/tests/viewnames.csv +++ b/rowers/tests/viewnames.csv @@ -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, diff --git a/rowers/uploads.py b/rowers/uploads.py index bbf63aab..7d699303 100644 --- a/rowers/uploads.py +++ b/rowers/uploads.py @@ -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( diff --git a/rowers/urls.py b/rowers/urls.py index ea9448b3..29b13906 100644 --- a/rowers/urls.py +++ b/rowers/urls.py @@ -664,8 +664,6 @@ urlpatterns = [ re_path(r'^workout/(?P\w+.*)import/(?P\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\d+)/$', diff --git a/rowers/views/importviews.py b/rowers/views/importviews.py index 90a160e6..b454e031 100644 --- a/rowers/views/importviews.py +++ b/rowers/views/importviews.py @@ -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) diff --git a/rowers/views/statements.py b/rowers/views/statements.py index 118fc2c5..b56e8dfa 100644 --- a/rowers/views/statements.py +++ b/rowers/views/statements.py @@ -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 diff --git a/rowers/views/workoutviews.py b/rowers/views/workoutviews.py index 1e7cf862..9bafbd6e 100644 --- a/rowers/views/workoutviews.py +++ b/rowers/views/workoutviews.py @@ -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"
    Total Distance Type Source Comment Name New
    {{ workout|lookup:'distance' }} {{ workout|lookup:'rowtype' }} {{ workout|lookup:'source' }}{{ workout|lookup:'comment' }}{{ workout|lookup:'name' }} {{ workout|lookup:'new' }}