From 2981c59a5d1f28b60546db43b6fb7baa39a1b845 Mon Sep 17 00:00:00 2001
From: Sander Roosendaal
Date: Fri, 10 Feb 2023 16:55:47 +0100
Subject: [PATCH 01/27] first attempt on c2
---
rowers/c2stuff.py | 920 ---------------------
rowers/imports.py | 6 +-
rowers/integrations/__init__.py | 1 +
rowers/integrations/c2.py | 450 ++++++++++
rowers/integrations/integrations.py | 250 ++++++
rowers/management/commands/processemail.py | 8 +-
rowers/ownapistuff.py | 1 -
rowers/templates/list_import.html | 84 ++
rowers/templatetags/rowerfilters.py | 10 +-
rowers/tests/mocks.py | 3 +-
rowers/tests/statements.py | 1 -
rowers/tests/test_imports.py | 47 +-
rowers/tests/test_permissions2.py | 16 +-
rowers/tests/test_unit_tests.py | 7 +-
rowers/uploads.py | 7 +-
rowers/urls.py | 5 -
rowers/views/importviews.py | 179 +---
rowers/views/statements.py | 4 +-
rowers/views/workoutviews.py | 3 +-
19 files changed, 873 insertions(+), 1129 deletions(-)
create mode 100644 rowers/integrations/__init__.py
create mode 100644 rowers/integrations/c2.py
create mode 100644 rowers/integrations/integrations.py
create mode 100644 rowers/templates/list_import.html
diff --git a/rowers/c2stuff.py b/rowers/c2stuff.py
index 92aed939..0f933238 100644
--- a/rowers/c2stuff.py
+++ b/rowers/c2stuff.py
@@ -101,923 +101,3 @@ def getagegrouprecord(age, sex='male', weightcategory='hwt',
return power
-
-oauth_data = {
- 'client_id': C2_CLIENT_ID,
- 'client_secret': C2_CLIENT_SECRET,
- 'redirect_uri': C2_REDIRECT_URI,
- 'autorization_uri': "https://log.concept2.com/oauth/authorize",
- 'content_type': 'application/x-www-form-urlencoded',
- 'tokenname': 'c2token',
- 'refreshtokenname': 'c2refreshtoken',
- 'expirydatename': 'tokenexpirydate',
- 'bearer_auth': True,
- 'base_url': "https://log.concept2.com/oauth/access_token",
- 'scope': 'write',
-}
-
-
-# Checks if user has Concept2 tokens, resets tokens if they are
-# expired.
-def c2_open(user):
- r = Rower.objects.get(user=user)
- if (r.c2token == '') or (r.c2token is None):
- raise NoTokenError("User has no token")
- else:
- if (timezone.now() > r.tokenexpirydate):
- res = rower_c2_token_refresh(user)
- if res is None: # pragma: no cover
- raise NoTokenError("User has no token")
- if res[0] is not None:
- thetoken = res[0]
- else: # pragma: no cover
- raise NoTokenError("User has no token")
- else:
- thetoken = r.c2token
-
- return thetoken
-
-
-def get_c2_workouts(rower, page=1, do_async=True):
- try:
- _ = c2_open(rower.user)
- except NoTokenError: # pragma: no cover
- return 0
-
- res = get_c2_workout_list(rower.user, page=page)
-
- if (res.status_code != 200): # pragma: no cover
- return 0
- else:
- c2ids = [item['id'] for item in res.json()['data']]
- alldata = {}
- for item in res.json()['data']:
- alldata[item['id']] = item
-
- knownc2ids = [
- w.uploadedtoc2 for w in Workout.objects.filter(user=rower)
- ]
-
- tombstones = [
- t.uploadedtoc2 for t in TombStone.objects.filter(user=rower)
- ]
-
- # get "blocked" c2ids
- parkedids = []
- try:
- with open('c2blocked.json', 'r') as c2blocked:
- jsondata = json.load(c2blocked)
- parkedids = jsondata['ids']
- except FileNotFoundError: # pragma: no cover
- pass
-
- knownc2ids = uniqify(knownc2ids+tombstones+parkedids)
-
- newids = [c2id for c2id in c2ids if c2id not in knownc2ids]
- if settings.TESTING:
- newids = c2ids
-
- newparkedids = uniqify(newids+parkedids)
-
- with open('c2blocked.json', 'wt') as c2blocked:
- data = {'ids': newparkedids}
- json.dump(data, c2blocked)
-
- counter = 0
- for c2id in newids:
- if do_async: # pragma: no cover
- _ = myqueue(queuehigh,
- handle_c2_async_workout,
- alldata,
- rower.user.id,
- rower.c2token,
- c2id,
- counter,
- rower.defaulttimezone)
-
- counter = counter+1
- else:
- _ = create_async_workout(alldata, rower.user, c2id)
-
- return 1
-
-# get workout metrics, then relay stroke data to an asynchronous task
-
-
-def create_async_workout(alldata, user, c2id):
- data = alldata[c2id]
- splitdata = None
-
- c2id = data['id']
- workouttype = data['type']
- startdatetime = iso8601.parse_date(data['date'])
-
- try:
- title = data['name']
- except KeyError:
- title = ""
- try:
- t = data['comments'].split('\n', 1)[0]
- title += t[:40]
- except:
- title = ''
-
- # Create CSV file name and save data to CSV file
- csvfilename = 'media/Import_'+str(c2id)+'.csv.gz'
-
- r = Rower.objects.get(user=user)
-
- authorizationstring = str('Bearer ' + r.c2token)
- headers = {'Authorization': authorizationstring,
- 'user-agent': 'sanderroosendaal',
- 'Content-Type': 'application/json'}
- # url2 = "https://log.concept2.com/api/users/me/results"+str(c2id)
- url = "https://log.concept2.com/api/users/me/results/"+str(c2id)+"/strokes"
- try:
- s = requests.get(url, headers=headers)
- except ConnectionError: # pragma: no cover
- return 0
-
- if s.status_code != 200: # pragma: no cover
- return 0
-
- strokedata = pd.DataFrame.from_dict(s.json()['data'])
-
- res = make_cumvalues(0.1*strokedata['t'])
- cum_time = res[0]
- lapidx = res[1]
-
- starttimeunix = arrow.get(startdatetime).timestamp()
- starttimeunix = starttimeunix-cum_time.max()
-
- unixtime = cum_time+starttimeunix
- # unixtime[0] = starttimeunix
- seconds = 0.1*strokedata.loc[:, 't']
-
- nr_rows = len(unixtime)
-
- try: # pragma: no cover
- latcoord = strokedata.loc[:, 'lat']
- loncoord = strokedata.loc[:, 'lon']
- except:
- latcoord = np.zeros(nr_rows)
- loncoord = np.zeros(nr_rows)
-
- try:
- strokelength = strokedata.loc[:, 'strokelength']
- except:
- strokelength = np.zeros(nr_rows)
-
- dist2 = 0.1*strokedata.loc[:, 'd']
-
- try:
- spm = strokedata.loc[:, 'spm']
- except KeyError: # pragma: no cover
- spm = 0*dist2
-
- try:
- hr = strokedata.loc[:, 'hr']
- except KeyError: # pragma: no cover
- hr = 0*spm
-
- pace = strokedata.loc[:, 'p']/10.
- pace = np.clip(pace, 0, 1e4)
- pace = pace.replace(0, 300)
-
- velo = 500./pace
- power = 2.8*velo**3
- if workouttype == 'bike': # pragma: no cover
- velo = 1000./pace
-
- df = pd.DataFrame({'TimeStamp (sec)': unixtime,
- ' Horizontal (meters)': dist2,
- ' Cadence (stokes/min)': spm,
- ' HRCur (bpm)': hr,
- ' longitude': loncoord,
- ' latitude': latcoord,
- ' Stroke500mPace (sec/500m)': pace,
- ' Power (watts)': power,
- ' DragFactor': np.zeros(nr_rows),
- ' DriveLength (meters)': np.zeros(nr_rows),
- ' StrokeDistance (meters)': strokelength,
- ' DriveTime (ms)': np.zeros(nr_rows),
- ' StrokeRecoveryTime (ms)': np.zeros(nr_rows),
- ' AverageDriveForce (lbs)': np.zeros(nr_rows),
- ' PeakDriveForce (lbs)': np.zeros(nr_rows),
- ' lapIdx': lapidx,
- ' WorkoutState': 4,
- ' ElapsedTime (sec)': seconds,
- 'cum_dist': dist2
- })
-
- df.sort_values(by='TimeStamp (sec)', ascending=True)
-
- res = df.to_csv(csvfilename, index_label='index',
- compression='gzip')
-
- userid = r.user.id
-
- uploadoptions = {
- 'secret': UPLOAD_SERVICE_SECRET,
- 'user': userid,
- 'file': csvfilename,
- 'title': title,
- 'workouttype': workouttype,
- 'boattype': '1x',
- 'c2id': c2id,
- }
-
- session = requests.session()
- newHeaders = {'Content-type': 'application/json', 'Accept': 'text/plain'}
- session.headers.update(newHeaders)
-
- response = session.post(UPLOAD_SERVICE_URL, json=uploadoptions)
-
- if response.status_code != 200: # pragma: no cover
- return 0
-
- try:
- workoutid = response.json()['id']
- except KeyError: # pragma: no cover
- workoutid = 1
-
- newc2id = Workout.objects.get(id=workoutid).uploadedtoc2
-
- parkedids = []
- with open('c2blocked.json', 'r') as c2blocked:
- jsondata = json.load(c2blocked)
- parkedids = jsondata['ids']
-
- newparkedids = [id for id in parkedids if id != newc2id]
- with open('c2blocked.json', 'wt') as c2blocked:
- data = {'ids': newparkedids}
- c2blocked.seek(0)
- json.dump(data, c2blocked)
-
- # summary
- if 'workout' in data: # pragma: no cover
- if 'splits' in data['workout']:
- splitdata = data['workout']['splits']
- elif 'intervals' in data['workout']:
- splitdata = data['workout']['intervals']
- else:
- splitdata = False
- else:
- splitdata = False
-
- if splitdata: # pragma: no cover
- summary, sa, results = c2stuff.summaryfromsplitdata(
- splitdata, data, csvfilename, workouttype=workouttype)
- w = Workout.objects.get(id=workoutid)
- w.summary = summary
- w.save()
-
- from rowingdata.trainingparser import getlist
- if sa:
- values = getlist(sa)
- units = getlist(sa, sel='unit')
- types = getlist(sa, sel='type')
-
- rowdata = rdata(w.csvfilename)
- if rowdata:
- rowdata.updateintervaldata(values,
- units, types, results)
-
- rowdata.write_csv(w.csvfilename, gzip=True)
- dataprep.update_strokedata(w.id, rowdata.df)
-
- return workoutid
-
-
-# convert datetime object to seconds
-def makeseconds(t):
- seconds = t.hour*3600.+t.minute*60.+t.second+0.1*int(t.microsecond/1.e5)
- return seconds
-
-# convert our weight class code to Concept2 weight class code
-
-
-def c2wc(weightclass):
- if (weightclass == "lwt"): # pragma: no cover
- res = "L"
- else:
- res = "H"
-
- return res
-
-# Concept2 logbook sends over split data for each interval
-# We use it here to generate a custom summary
-# Some users complained about small differences
-
-
-def summaryfromsplitdata(splitdata, data, filename, sep='|', workouttype='rower'):
- workouttype = workouttype.lower()
-
- totaldist = data['distance']
- totaltime = data['time']/10.
- try:
- spm = data['stroke_rate']
- except KeyError: # pragma: no cover
- spm = 0
- try:
- resttime = data['rest_time']/10.
- except KeyError: # pragma: no cover
- resttime = 0
- try:
- restdistance = data['rest_distance']
- except KeyError: # pragma: no cover
- restdistance = 0
- try:
- avghr = data['heart_rate']['average']
- except KeyError: # pragma: no cover
- avghr = 0
- try:
- maxhr = data['heart_rate']['max']
- except KeyError: # pragma: no cover
- maxhr = 0
-
- try:
- avgpace = 500.*totaltime/totaldist
- except (ZeroDivisionError, OverflowError): # pragma: no cover
- avgpace = 0.
-
- try:
- restpace = 500.*resttime/restdistance
- except (ZeroDivisionError, OverflowError): # pragma: no cover
- restpace = 0.
-
- velo = totaldist/totaltime
- avgpower = 2.8*velo**(3.0)
- if workouttype in ['bike', 'bikeerg']: # pragma: no cover
- velo = velo/2.
- avgpower = 2.8*velo**(3.0)
- velo = velo*2
-
- try:
- restvelo = restdistance/resttime
- except (ZeroDivisionError, OverflowError): # pragma: no cover
- restvelo = 0
-
- restpower = 2.8*restvelo**(3.0)
- if workouttype in ['bike', 'bikeerg']: # pragma: no cover
- restvelo = restvelo/2.
- restpower = 2.8*restvelo**(3.0)
- restvelo = restvelo*2
-
- try:
- avgdps = totaldist/data['stroke_count']
- except (ZeroDivisionError, OverflowError, KeyError): # pragma: no cover
- avgdps = 0
-
- from rowingdata import summarystring, workstring, interval_string
-
- sums = summarystring(totaldist, totaltime, avgpace, spm, avghr, maxhr,
- avgdps, avgpower, readFile=filename,
- separator=sep)
-
- sums += workstring(totaldist, totaltime, avgpace, spm, avghr, maxhr,
- avgdps, avgpower, separator=sep, symbol='W')
-
- sums += workstring(restdistance, resttime, restpace, 0, 0, 0, 0, restpower,
- separator=sep,
- symbol='R')
-
- sums += '\nWorkout Details\n'
- sums += '#-{sep}SDist{sep}-Split-{sep}-SPace-{sep}-Pwr-{sep}SPM-{sep}AvgHR{sep}MaxHR{sep}DPS-\n'.format(
- sep=sep
- )
-
- intervalnr = 0
- sa = []
- results = []
-
- try:
- timebased = data['workout_type'] in [
- 'FixedTimeSplits', 'FixedTimeInterval']
- except KeyError: # pragma: no cover
- timebased = False
-
- for interval in splitdata:
- try:
- idist = interval['distance']
- except KeyError: # pragma: no cover
- idist = 0
-
- try:
- itime = interval['time']/10.
- except KeyError: # pragma: no cover
- itime = 0
- try:
- ipace = 500.*itime/idist
- except (ZeroDivisionError, OverflowError): # pragma: no cover
- ipace = 180.
-
- try:
- ispm = interval['stroke_rate']
- except KeyError: # pragma: no cover
- ispm = 0
- try:
- irest_time = interval['rest_time']/10.
- except KeyError: # pragma: no cover
- irest_time = 0
- try:
- iavghr = interval['heart_rate']['average']
- except KeyError: # pragma: no cover
- iavghr = 0
- try:
- imaxhr = interval['heart_rate']['average']
- except KeyError: # pragma: no cover
- imaxhr = 0
-
- # create interval values
- iarr = [idist, 'meters', 'work']
- resarr = [itime]
- if timebased: # pragma: no cover
- iarr = [itime, 'seconds', 'work']
- resarr = [idist]
-
- if irest_time > 0:
- iarr += [irest_time, 'seconds', 'rest']
- try:
- resarr += [interval['rest_distance']]
- except KeyError: # pragma: no cover
- resarr += [np.nan]
-
- sa += iarr
- results += resarr
-
- if itime != 0:
- ivelo = idist/itime
- ipower = 2.8*ivelo**(3.0)
- if workouttype in ['bike', 'bikeerg']: # pragma: no cover
- ipower = 2.8*(ivelo/2.)**(3.0)
- else: # pragma: no cover
- ivelo = 0
- ipower = 0
-
- sums += interval_string(intervalnr, idist, itime, ipace, ispm,
- iavghr, imaxhr, 0, ipower, separator=sep)
- intervalnr += 1
-
- return sums, sa, results
-
-
-# Create the Data object for the stroke data to be sent to Concept2 logbook
-# API
-def createc2workoutdata(w):
- filename = w.csvfilename
- try:
- row = rowingdata(csvfile=filename)
- except IOError: # pragma: no cover
- return 0
-
- try:
- averagehr = int(row.df[' HRCur (bpm)'].mean())
- maxhr = int(row.df[' HRCur (bpm)'].max())
- except (ValueError, KeyError): # pragma: no cover
- averagehr = 0
- maxhr = 0
-
- # Calculate intervalstats
- itime, idist, itype = row.intervalstats_values()
- try:
- lapnames = row.df[' lapIdx'].unique()
- except KeyError: # pragma: no cover
- lapnames = range(len(itime))
- nrintervals = len(itime)
- if len(lapnames) != nrintervals:
- newlapnames = []
- for name in lapnames:
- newlapnames += [name, name]
- lapnames = newlapnames
- intervaldata = []
- for i in range(nrintervals):
- if itime[i] > 0:
- mask = (row.df[' lapIdx'] == lapnames[i]) & (
- row.df[' WorkoutState'] == itype[i])
- try:
- spmav = int(row.df[' Cadence (stokes/min)']
- [mask].mean().astype(int))
- hrav = int(row.df[' HRCur (bpm)'][mask].mean().astype(int))
- except AttributeError: # pragma: no cover
- try:
- spmav = int(row.df[' Cadence (stokes/min)'][mask].mean())
- hrav = int(row.df[' HRCur (bpm)'][mask].mean())
- except ValueError:
- spmav = 0
- try:
- hrav = int(row.df[' HRCur (bpm)'][mask].mean())
- except ValuError:
- hrav = 0
- intervaldict = {
- 'type': 'distance',
- 'time': int(10*itime[i]),
- 'distance': int(idist[i]),
- 'heart_rate': {
- 'average': hrav,
- },
- 'stroke_rate': spmav,
- }
- intervaldata.append(intervaldict)
-
- # adding diff, trying to see if this is valid
- t = 10*row.df.loc[:, 'TimeStamp (sec)'].values - \
- 10*row.df.loc[:, 'TimeStamp (sec)'].iloc[0]
- try:
- t[0] = t[1]
- except IndexError: # pragma: no cover
- pass
-
- d = 10*row.df.loc[:, ' Horizontal (meters)'].values
- try:
- d[0] = d[1]
- except IndexError: # pragma: no cover
- pass
-
- p = abs(10*row.df.loc[:, ' Stroke500mPace (sec/500m)'].values)
- p = np.clip(p, 0, 3600)
- if w.workouttype == 'bike': # pragma: no cover
- p = 2.0*p
-
- t = t.astype(int)
- d = d.astype(int)
- p = p.astype(int)
- spm = row.df[' Cadence (stokes/min)'].astype(int)
-
- try:
- spm[0] = spm[1]
- except (KeyError, IndexError): # pragma: no cover
- spm = 0*t
- try:
- hr = row.df[' HRCur (bpm)'].astype(int)
- except ValueError: # pragma: no cover
- hr = 0*d
- stroke_data = []
-
- t = t.tolist()
- d = d.tolist()
- p = p.tolist()
- spm = spm.tolist()
- hr = hr.tolist()
-
- for i in range(len(t)):
- thisrecord = {"t": t[i],
- "d": d[i],
- "p": p[i],
- "spm": spm[i],
- "hr": hr[i]}
- stroke_data.append(thisrecord)
-
- try:
- durationstr = datetime.datetime.strptime(
- str(w.duration), "%H:%M:%S.%f")
- except ValueError:
- durationstr = datetime.datetime.strptime(str(w.duration), "%H:%M:%S")
-
- workouttype = w.workouttype
- if workouttype in otwtypes:
- workouttype = 'water'
-
- if w.timezone == 'tzutc()': # pragma: no cover
- w.timezone = 'UTC'
- w.save()
-
- try:
- wendtime = w.startdatetime.astimezone(pytz.timezone(
- w.timezone))+datetime.timedelta(seconds=makeseconds(durationstr))
- except UnknownTimeZoneError:
- wendtime = w.startdatetime.astimezone(pytz.utc)+datetime.timedelta(seconds=makeseconds(durationstr))
-
- data = {
- "type": mytypes.c2mapping[workouttype],
- # w.startdatetime.isoformat(),
- "date": wendtime.strftime('%Y-%m-%d %H:%M:%S'),
- "stroke_count": int(row.stroke_count),
- "timezone": w.timezone,
- "distance": int(w.distance),
- "time": int(10*makeseconds(durationstr)),
- "weight_class": c2wc(w.weightcategory),
- "comments": w.notes,
- 'stroke_rate': int(row.df[' Cadence (stokes/min)'].mean()),
- 'drag_factor': int(row.dragfactor),
- "heart_rate": {
- "average": averagehr,
- "max": maxhr,
- },
- "stroke_data": stroke_data,
- 'workout': {
- 'splits': intervaldata,
- }
- }
-
- return data
-
-# Refresh Concept2 authorization token
-
-
-def do_refresh_token(refreshtoken):
- scope = "results:write,user:read"
- # client_auth = requests.auth.HTTPBasicAuth(C2_CLIENT_ID, C2_CLIENT_SECRET)
- post_data = {"grant_type": "refresh_token",
- "client_secret": C2_CLIENT_SECRET,
- "client_id": C2_CLIENT_ID,
- "refresh_token": refreshtoken,
- }
- headers = {'user-agent': 'sanderroosendaal'}
- url = "https://log.concept2.com/oauth/access_token"
- s = Session()
- req = Request('POST', url, data=post_data, headers=headers)
-
- prepped = req.prepare()
- prepped.body += "&scope="
- prepped.body += scope
-
- response = s.send(prepped)
-
- try:
- token_json = response.json()
- except JSONDecodeError: # pragma: no cover
- return [None, None, None]
-
- try:
- thetoken = token_json['access_token']
- expires_in = token_json['expires_in']
- refresh_token = token_json['refresh_token']
- except: # pragma: no cover
- with open("media/c2errors.log", "a") as errorlog:
- errorstring = str(sys.exc_info()[0])
- timestr = time.strftime("%Y%m%d-%H%M%S")
- errorlog.write(timestr+errorstring+"\r\n")
- errorlog.write(str(token_json)+"\r\n")
- thetoken = None
- expires_in = None
- refresh_token = None
-
- return [thetoken, expires_in, refresh_token]
-
-# Exchange authorization code for authorization token
-
-
-def get_token(code):
- messg = ''
- scope = "user:read,results:write"
- # client_auth = requests.auth.HTTPBasicAuth(C2_CLIENT_ID, C2_CLIENT_SECRET)
- post_data = {"grant_type": "authorization_code",
- "code": code,
- "redirect_uri": C2_REDIRECT_URI,
- "client_secret": C2_CLIENT_SECRET,
- "client_id": C2_CLIENT_ID,
- }
- headers = {'user-agent': 'sanderroosendaal'}
- url = "https://log.concept2.com/oauth/access_token"
- s = Session()
- req = Request('POST', url, data=post_data, headers=headers)
-
- prepped = req.prepare()
- prepped.body += "&scope="
- prepped.body += scope
-
- response = s.send(prepped)
-
- token_json = response.json()
-
- try:
- status_code = response.status_code
- # status_code = token_json['status_code']
- except AttributeError: # pragma: no cover
- # except KeyError:
- return (0, response.text)
- try:
- status_code = token_json.status_code
- except AttributeError: # pragma: no cover
- return (0, 'Attribute Error on c2_get_token')
-
- if status_code == 200:
- thetoken = token_json['access_token']
- expires_in = token_json['expires_in']
- refresh_token = token_json['refresh_token']
- else: # pragma: no cover
- return (0, token_json['message'])
-
- return (thetoken, expires_in, refresh_token, messg)
-
-# Make URL for authorization and load it
-
-
-def make_authorization_url(request): # pragma: no cover
- # Generate a random string for the state parameter
- # Save it for use later to prevent xsrf attacks
- from uuid import uuid4
- # state = str(uuid4())
- scope = "user:read,results:write"
-
- params = {"client_id": C2_CLIENT_ID,
- "response_type": "code",
- "redirect_uri": C2_REDIRECT_URI}
- url = "https://log.concept2.com/oauth/authorize?" + \
- urllib.parse.urlencode(params)
- url += "&scope="+scope
-
- return HttpResponseRedirect(url)
-
-# Get workout from C2 ID
-
-
-def get_workout(user, c2id, do_async=True):
- r = Rower.objects.get(user=user)
- _ = c2_open(user)
-
- _ = myqueue(queuehigh,
- handle_c2_getworkout,
- user.id,
- r.c2token,
- c2id,
- r.defaulttimezone)
-
- return 1
-
-
-# Get list of C2 workouts. We load only the first page,
-# assuming that users don't want to import their old workouts
-def get_c2_workout_list(user, page=1):
- r = Rower.objects.get(user=user)
- if (r.c2token == '') or (r.c2token is None): # pragma: no cover
- s = "Token doesn't exist. Need to authorize"
- return custom_exception_handler(401, s)
- elif (timezone.now() > r.tokenexpirydate): # pragma: no cover
- s = "Token expired. Needs to refresh."
-
- return custom_exception_handler(401, s)
-
- # ready to fetch. Hurray
- authorizationstring = str('Bearer ' + r.c2token)
- headers = {'Authorization': authorizationstring,
- 'user-agent': 'sanderroosendaal',
- 'Content-Type': 'application/json'}
- url = "https://log.concept2.com/api/users/me/results"
- url += "?page={page}".format(page=page)
-
- s = requests.get(url, headers=headers)
-
- return s
-
-
-# Get username, having access token.
-# Handy for checking if the API access is working
-def get_username(access_token): # pragma: no cover
- authorizationstring = str('Bearer ' + access_token)
- headers = {'Authorization': authorizationstring,
- 'user-agent': 'sanderroosendaal',
- 'Content-Type': 'application/json'}
- import urllib
- url = "https://log.concept2.com/api/users/me"
- response = requests.get(url, headers=headers)
-
- me_json = response.json()
-
- try:
- res = me_json['data']['username']
- _ = me_json['data']['id']
- except KeyError:
- res = None
-
- return res
-
-# Get user id, having access token
-# Handy for checking if the API access is working
-
-
-def get_userid(access_token):
- authorizationstring = str('Bearer ' + access_token)
- headers = {'Authorization': authorizationstring,
- 'user-agent': 'sanderroosendaal',
- 'Content-Type': 'application/json'}
- import urllib
- url = "https://log.concept2.com/api/users/me"
- try:
- response = requests.get(url, headers=headers)
- except: # pragma: no cover
- return 0
-
- try:
- me_json = response.json()
- except: # pragma: no cover
- return 0
- try:
- res = me_json['data']['id']
- except KeyError: # pragma: no cover
- return 0
-
- return res
-
-# For debugging purposes
-
-
-def process_callback(request): # pragma: no cover
- # need error handling
-
- code = request.GET['code']
-
- access_token = get_token(code)
-
- username, id = get_username(access_token)
-
- return HttpResponse("got a user name: %s" % username)
-
-
-def default(o): # pragma: no cover
- if isinstance(o, numpy.int64):
- return int(o)
- raise TypeError
-
-# Uploading workout
-
-
-def workout_c2_upload(user, w, asynchron=False):
- message = 'trying C2 upload'
-
- try:
- if mytypes.c2mapping[w.workouttype] is None: # pragma: no cover
- return "This workout type cannot be uploaded to Concept2", 0
- except KeyError: # pragma: no cover
- return "This workout type cannot be uploaded to Concept2", 0
-
- _ = c2_open(user)
-
- r = Rower.objects.get(user=user)
-
- # ready to upload. Hurray
-
- if (is_workout_user(user, w)):
-
- c2userid = get_userid(r.c2token)
- if not c2userid: # pragma: no cover
- raise NoTokenError("User has no token")
-
- dologging('debuglog.log',
- 'Upload to C2 user {userid}'.format(userid=user.id))
- data = createc2workoutdata(w)
- dologging('debuglog.log', json.dumps(data))
-
- if data == 0: # pragma: no cover
- return "Error: No data file. Contact info@rowsandall.com if the problem persists", 0
-
- authorizationstring = str('Bearer ' + r.c2token)
- headers = {'Authorization': authorizationstring,
- 'user-agent': 'sanderroosendaal',
- 'Content-Type': 'application/json'}
- import urllib
- url = "https://log.concept2.com/api/users/%s/results" % (c2userid)
- if not asynchron: # pragma: no cover
- response = requests.post(
- url, headers=headers, data=json.dumps(data, default=default))
-
- if (response.status_code == 409): # pragma: no cover
- message = "Concept2 Duplicate error"
- w.uploadedtoc2 = -1
- c2id = -1
- w.save()
- elif (response.status_code == 201 or response.status_code == 200):
- # s= json.loads(response.text)
- s = response.json()
- c2id = s['data']['id']
- w.uploadedtoc2 = c2id
- w.save()
- message = "Upload to Concept2 was successful"
- else: # pragma: no cover
- message = "Something went wrong in workout_c2_upload_view." \
- " Response code 200/201 but C2 sync failed: "+response.text
- c2id = 0
- else: # pragma: no cover
-
- _ = myqueue(queue,
- handle_c2_sync,
- w.id,
- url,
- headers,
- json.dumps(data, default=default))
- c2id = 0
-
- return message, c2id
-
-# This is token refresh. Looks for tokens in our database, then refreshes
-
-
-def rower_c2_token_refresh(user):
- r = Rower.objects.get(user=user)
- res = do_refresh_token(r.c2refreshtoken)
- if res[0]:
- access_token = res[0]
- expires_in = res[1]
- refresh_token = res[2]
- expirydatetime = timezone.now()+datetime.timedelta(seconds=expires_in)
-
- r = Rower.objects.get(user=user)
- r.c2token = access_token
- r.tokenexpirydate = expirydatetime
- r.c2refreshtoken = refresh_token
-
- r.save()
- return r.c2token
- else: # pragma: no cover
- return None
diff --git a/rowers/imports.py b/rowers/imports.py
index 6e40e0b5..38cac687 100644
--- a/rowers/imports.py
+++ b/rowers/imports.py
@@ -66,7 +66,7 @@ def splitstdata(lijst):
return [np.array(t), np.array(latlong)]
-
+# covered in integrations
def imports_open(user, oauth_data):
r = Rower.objects.get(user=user)
token = getattr(r, oauth_data['tokenname'])
@@ -101,7 +101,7 @@ def imports_open(user, oauth_data):
return token
-
+# covered in integrations
# Refresh token using refresh token
def imports_do_refresh_token(refreshtoken, oauth_data, access_token=''):
# client_auth = requests.auth.HTTPBasicAuth(
@@ -175,7 +175,7 @@ def imports_do_refresh_token(refreshtoken, oauth_data, access_token=''):
# Exchange ST access code for long-lived ST access token
-
+# implemented in integrations
def imports_get_token(
code, oauth_data
):
diff --git a/rowers/integrations/__init__.py b/rowers/integrations/__init__.py
new file mode 100644
index 00000000..56112937
--- /dev/null
+++ b/rowers/integrations/__init__.py
@@ -0,0 +1 @@
+from .c2 import C2Integration
diff --git a/rowers/integrations/c2.py b/rowers/integrations/c2.py
new file mode 100644
index 00000000..316af562
--- /dev/null
+++ b/rowers/integrations/c2.py
@@ -0,0 +1,450 @@
+from .integrations import SyncIntegration, NoTokenError
+from rowers.models import User, Rower, Workout, TombStone
+from rowingdata import rowingdata
+import numpy as np
+import datetime
+import json
+import urllib
+from rowers.utils import dologging, uniqify
+from django.utils import timezone
+import requests
+from pytz.exceptions import UnknownTimeZoneError
+import pytz
+
+from rowsandall_app.settings import (
+ C2_CLIENT_ID, C2_REDIRECT_URI, C2_CLIENT_SECRET,
+ UPLOAD_SERVICE_URL, UPLOAD_SERVICE_SECRET
+)
+
+from rowers.tasks import (
+ handle_c2_import_stroke_data, handle_c2_sync, handle_c2_async_workout,
+ handle_c2_getworkout
+)
+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
+
+from requests import Request, Session
+
+import rowers.mytypes as mytypes
+from rowers.rower_rules import is_workout_user, ispromember
+
+def default(o): # pragma: no cover
+ if isinstance(o, numpy.int64):
+ return int(o)
+ raise TypeError
+
+# convert datetime object to seconds
+def makeseconds(t):
+ seconds = t.hour*3600.+t.minute*60.+t.second+0.1*int(t.microsecond/1.e5)
+ return seconds
+
+# convert our weight class code to Concept2 weight class code
+
+
+def c2wc(weightclass):
+ if (weightclass == "lwt"): # pragma: no cover
+ res = "L"
+ else:
+ res = "H"
+
+ return res
+
+
+class C2Integration(SyncIntegration):
+ def __init__(self, *args, **kwargs):
+ super(C2Integration, self).__init__(self, *args, **kwargs)
+ self.oauth_data = {
+ 'client_id': C2_CLIENT_ID,
+ 'client_secret': C2_CLIENT_SECRET,
+ 'redirect_uri': C2_REDIRECT_URI,
+ 'autorization_uri': "https://log.concept2.com/oauth/authorize",
+ 'content_type': 'application/x-www-form-urlencoded',
+ 'tokenname': 'c2token',
+ 'refreshtokenname': 'c2refreshtoken',
+ 'expirydatename': 'tokenexpirydate',
+ 'bearer_auth': True,
+ 'base_url': "https://log.concept2.com/oauth/access_token",
+ 'scope': 'write',
+ }
+
+ def get_token(self, code, *args, **kwargs):
+ messg = ''
+ scope = "user:read,results:write"
+
+ post_data = {"grant_type": "authorization_code",
+ "code": code,
+ "redirect_uri": C2_REDIRECT_URI,
+ "client_secret": C2_CLIENT_SECRET,
+ "client_id": C2_CLIENT_ID,
+ }
+ headers = {'user-agent': 'sanderroosendaal'}
+ url = "https://log.concept2.com/oauth/access_token"
+ s = Session()
+ req = Request('POST', url, data=post_data, headers=headers)
+
+ prepped = req.prepare()
+ prepped.body += "&scope="
+ prepped.body += scope
+
+ response = s.send(prepped)
+
+ token_json = response.json()
+
+ try:
+ status_code = response.status_code
+ except AttributeError: # pragma: no cover
+ return (0, response.text)
+
+ if status_code == 200:
+ thetoken = token_json['access_token']
+ expires_in = token_json['expires_in']
+ refresh_token = token_json['refresh_token']
+ else: # pragma: no cover
+ raise NoTokenError("No Token")
+
+
+ return (thetoken, expires_in, refresh_token, messg)
+
+
+ def open(self, *args, **kwargs):
+ return super(C2Integration, self).open(*args, **kwargs)
+
+ def token_refresh(self, *args, **kwargs):
+ return super(C2Integration, self).open(*args, **kwargs)
+
+ def make_authorization_url(self, *args, **kwargs):
+ scope = "user:read,results:write"
+
+ params = {"client_id": C2_CLIENT_ID,
+ "response_type": "code",
+ "redirect_uri": C2_REDIRECT_URI}
+ url = "https://log.concept2.com/oauth/authorize?" + \
+ urllib.parse.urlencode(params)
+ url += "&scope="+scope
+ return url
+
+ def get_userid(self, *args, **kwargs):
+ _ = self.open()
+ access_token = self.rower.c2token
+ authorizationstring = str('Bearer ' + access_token)
+ headers = {'Authorization': authorizationstring,
+ 'user-agent': 'sanderroosendaal',
+ 'Content-Type': 'application/json'}
+ url = "https://log.concept2.com/api/users/me"
+ try:
+ response = requests.get(url, headers=headers)
+ except: # pragma: no cover
+ return 0
+
+ try:
+ me_json = response.json()
+ except: # pragma: no cover
+ return 0
+ try:
+ res = me_json['data']['id']
+ except KeyError: # pragma: no cover
+ return 0
+
+ return res
+
+ def createworkoutdata(self, w, *args, **kwargs) -> dict:
+ filename = w.csvfilename
+ try:
+ row = rowingdata(csvfile=filename)
+ except IOError: # pragma: no cover
+ return 0
+
+ try:
+ averagehr = int(row.df[' HRCur (bpm)'].mean())
+ maxhr = int(row.df[' HRCur (bpm)'].max())
+ except (ValueError, KeyError): # pragma: no cover
+ averagehr = 0
+ maxhr = 0
+
+ # Calculate intervalstats
+ itime, idist, itype = row.intervalstats_values()
+ lapnames = range(len(itime))
+ nrintervals = len(itime)
+
+ try:
+ lapnames = row.df[' lapIdx'].unique()
+ except KeyError: # pragma: no cover
+ pass
+
+ if len(lapnames) != nrintervals:
+ newlapnames = []
+ for name in lapnames:
+ newlapnames += [name, name]
+ lapnames = newlapnames
+ intervaldata = []
+ for i in range(nrintervals):
+ if itime[i] > 0:
+ mask = (row.df[' lapIdx'] == lapnames[i]) & (
+ row.df[' WorkoutState'] == itype[i])
+ try:
+ spmav = int(row.df[' Cadence (stokes/min)']
+ [mask].mean().astype(int))
+ hrav = int(row.df[' HRCur (bpm)'][mask].mean().astype(int))
+ except AttributeError: # pragma: no cover
+ try:
+ spmav = int(row.df[' Cadence (stokes/min)'][mask].mean())
+ hrav = int(row.df[' HRCur (bpm)'][mask].mean())
+ except ValueError:
+ spmav = 0
+ try:
+ hrav = int(row.df[' HRCur (bpm)'][mask].mean())
+ except ValuError:
+ hrav = 0
+ intervaldict = {
+ 'type': 'distance',
+ 'time': int(10*itime[i]),
+ 'distance': int(idist[i]),
+ 'heart_rate': {
+ 'average': hrav,
+ },
+ 'stroke_rate': spmav,
+ }
+ intervaldata.append(intervaldict)
+
+ # adding diff, trying to see if this is valid
+ t = 10*row.df.loc[:, 'TimeStamp (sec)'].values - \
+ 10*row.df.loc[:, 'TimeStamp (sec)'].iloc[0]
+ try:
+ t[0] = t[1]
+ except IndexError: # pragma: no cover
+ pass
+
+ d = 10*row.df.loc[:, ' Horizontal (meters)'].values
+ try:
+ d[0] = d[1]
+ except IndexError: # pragma: no cover
+ pass
+
+ p = abs(10*row.df.loc[:, ' Stroke500mPace (sec/500m)'].values)
+ p = np.clip(p, 0, 3600)
+ if w.workouttype == 'bike': # pragma: no cover
+ p = 2.0*p
+
+ t = t.astype(int)
+ d = d.astype(int)
+ p = p.astype(int)
+ spm = row.df[' Cadence (stokes/min)'].astype(int)
+
+ try:
+ spm[0] = spm[1]
+ except (KeyError, IndexError): # pragma: no cover
+ spm = 0*t
+ try:
+ hr = row.df[' HRCur (bpm)'].astype(int)
+ except ValueError: # pragma: no cover
+ hr = 0*d
+ stroke_data = []
+
+ t = t.tolist()
+ d = d.tolist()
+ p = p.tolist()
+ spm = spm.tolist()
+ hr = hr.tolist()
+
+ for i in range(len(t)):
+ thisrecord = {"t": t[i],
+ "d": d[i],
+ "p": p[i],
+ "spm": spm[i],
+ "hr": hr[i]}
+ stroke_data.append(thisrecord)
+
+ try:
+ durationstr = datetime.datetime.strptime(
+ str(w.duration), "%H:%M:%S.%f")
+ except ValueError:
+ durationstr = datetime.datetime.strptime(str(w.duration), "%H:%M:%S")
+
+ workouttype = w.workouttype
+ if workouttype in mytypes.otwtypes:
+ workouttype = 'water'
+
+ if w.timezone == 'tzutc()': # pragma: no cover
+ w.timezone = 'UTC'
+ w.save()
+
+ try:
+ wendtime = w.startdatetime.astimezone(pytz.timezone(
+ w.timezone))+datetime.timedelta(seconds=makeseconds(durationstr))
+ except UnknownTimeZoneError:
+ wendtime = w.startdatetime.astimezone(pytz.utc)+datetime.timedelta(seconds=makeseconds(durationstr))
+
+ data = {
+ "type": mytypes.c2mapping[workouttype],
+ # w.startdatetime.isoformat(),
+ "date": wendtime.strftime('%Y-%m-%d %H:%M:%S'),
+ "stroke_count": int(row.stroke_count),
+ "timezone": w.timezone,
+ "distance": int(w.distance),
+ "time": int(10*makeseconds(durationstr)),
+ "weight_class": c2wc(w.weightcategory),
+ "comments": w.notes,
+ 'stroke_rate': int(row.df[' Cadence (stokes/min)'].mean()),
+ 'drag_factor': int(row.dragfactor),
+ "heart_rate": {
+ "average": averagehr,
+ "max": maxhr,
+ },
+ "stroke_data": stroke_data,
+ 'workout': {
+ 'splits': intervaldata,
+ }
+ }
+
+ return data
+
+
+ def workout_export(self, workout, *args, **kwargs) -> str:
+ try:
+ if mytypes.c2mapping[workout.workouttype] is None: # pragma: no cover
+ return "0"
+ except KeyError: # pragma: no cover
+ return "0"
+
+ _ = self.open()
+ r = self.rower
+ user = self.user
+
+ if (is_workout_user(user, workout)):
+ c2userid = self.get_userid()
+ if not c2userid: # pragma: no cover
+ raise NoTokenError("User has no token")
+
+ dologging('debuglog.log',
+ 'Upload to C2 user {userid}'.format(userid=user.id))
+ data = self.createworkoutdata(workout)
+ dologging('c2_log.log', json.dumps(data))
+
+ if data == 0: # pragma: no cover
+ return 0
+
+ authorizationstring = str('Bearer ' + r.c2token)
+ headers = {'Authorization': authorizationstring,
+ 'user-agent': 'sanderroosendaal',
+ 'Content-Type': 'application/json'}
+
+ url = "https://log.concept2.com/api/users/%s/results" % (c2userid)
+
+ _ = myqueue(queue,
+ handle_c2_sync,
+ workout.id,
+ url,
+ headers,
+ json.dumps(data, default=default))
+ c2id = 0
+
+ return c2id
+
+
+
+ def get_workout(self, id):
+ _ = self.open()
+ _ = myqueue(queuehigh,
+ handle_c2_getworkout,
+ self.user.id,
+ self.rower.c2token,
+ id,
+ self.rower.defaulttimezone)
+
+ return 1
+
+
+ def get_workouts(self, *args, **kwargs):
+ page = kwargs.get('page',1)
+
+ try:
+ _ = self.open()
+ except NoTokenError:
+ return 0
+
+ # this part to get_workout_list
+ workouts = self.get_workout_list(page=page)
+
+ counter = 0
+ for workout in workouts:
+ c2id = workout['id']
+ if workout['new'] == 'NEW':
+ self.get_workout(c2id)
+
+ return 1
+
+ # should be unified to workout list
+ def get_workout_list(self, *args, **kwargs):
+ page = kwargs.get('page',1)
+ r = self.rower
+ if (r.c2token == '') or (r.c2token is None): # pragma: no cover
+ s = "Token doesn't exist. Need to authorize"
+ return custom_exception_handler(401, s)
+ elif (timezone.now() > r.tokenexpirydate): # pragma: no cover
+ s = "Token expired. Needs to refresh."
+
+ return custom_exception_handler(401, s)
+
+ # ready to fetch. Hurray
+ authorizationstring = str('Bearer ' + r.c2token)
+ headers = {'Authorization': authorizationstring,
+ 'user-agent': 'sanderroosendaal',
+ 'Content-Type': 'application/json'}
+ url = "https://log.concept2.com/api/users/me/results"
+ url += "?page={page}".format(page=page)
+
+ res = requests.get(url, headers=headers)
+
+ if (res.status_code != 200): # pragma: no cover
+ return []
+
+ workouts = []
+ c2ids = [item['id'] for item in res.json()['data']]
+ knownc2ids = uniqify([
+ w.uploadedtoc2 for w in Workout.objects.filter(user=self.rower)
+ ])
+ tombstones = [
+ t.uploadedtoc2 for t in TombStone.objects.filter(user=self.rower)
+ ]
+ parkedids = []
+ try:
+ with open('c2blocked.json', 'r') as c2blocked:
+ jsondata = json.load(c2blocked)
+ parkedids = jsondata['ids']
+ except: # pragma: no cover
+ pass
+
+ knownc2ids = uniqify(knownc2ids+tombstones+parkedids)
+ for item in res.json()['data']:
+ d = item['distance']
+ i = item['id']
+ ttot = item['time_formatted']
+ s = item['date']
+ r = item['type']
+ s2 = item['source']
+ c = item['comments']
+ if i in knownc2ids:
+ nnn = ''
+ else: # pragma: no cover
+ nnn = 'NEW'
+ keys = ['id', 'distance', 'duration', 'starttime',
+ 'rowtype', 'source', 'comment', 'new']
+ values = [i, d, ttot, s, r, s2, c, nnn]
+ ress = dict(zip(keys, values))
+ workouts.append(ress)
+
+ return workouts
+
+
+
+
+
+
+# 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
new file mode 100644
index 00000000..6ca777f9
--- /dev/null
+++ b/rowers/integrations/integrations.py
@@ -0,0 +1,250 @@
+from abc import ABCMeta, ABC, abstractmethod
+from importlib import import_module
+from rowers.models import Rower, User
+from rowers.utils import NoTokenError
+
+import requests
+from django.utils import timezone
+from datetime import timedelta
+import arrow
+import urllib
+from uuid import uuid4
+
+
+
+class SyncIntegration(metaclass=ABCMeta):
+ oauth_data = {
+ 'tokenname':'token',
+ 'expirydatename':'exp',
+ 'refreshtokenname':'r',
+ 'redirect_uri': 'r',
+ 'client_secret': 's',
+ 'base_uri': 's'
+ }
+
+ user = User()
+ rower = Rower()
+
+ def __init__(self, *args, **kwargs):
+ user = args[1]
+ self.user = user
+ self.rower = user.rower
+
+ @classmethod
+ def __subclasshook__(cls, subclass):
+ return (hasattr(subclass, 'get_token') and
+ callable(subclass.get_token) or
+ NotImplemented)
+
+
+ @abstractmethod
+ def createworkoutdata(w, *args, **kwargs) -> dict:
+ return {}
+
+ @abstractmethod
+ def workout_export(workout, *args, **kwargs) -> str:
+ pass
+
+ @abstractmethod
+ def get_workouts(*args, **kwargs) -> int:
+ pass
+
+ @abstractmethod
+ def get_workout(id) -> int:
+ pass
+
+ # need to unify workout list
+ @abstractmethod
+ def get_workout_list(*args, **kwargs) -> list:
+ pass
+
+ @abstractmethod
+ def make_authorization_url(self, *args, **kwargs) -> str: # 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": self.oauth_data['client_id'],
+ "response_type": "code",
+ "redirect_uri": self.oauth_data['redirect_uri'],
+ "scope": self.oauth_data['scope'],
+ "state": state}
+
+ url = oauth_data['authorizaton_uri']+urllib.parse.urlencode(params)
+
+ return url
+
+
+ @abstractmethod
+ def get_token(self, code, *args, **kwargs) -> (str, int, str):
+ redirect_uri = self.oauth_data['redirect_uri']
+ client_secret = self.oauth_data['client_secret']
+ client_id = self.oauth_data['client_id']
+ base_uri = self.oauth_data['base_url']
+
+ post_data = {"grant_type": "authorization_code",
+ "code": code,
+ "redirect_uri": redirect_uri,
+ "client_secret": client_secret,
+ "client_id": client_id,
+ }
+
+ try:
+ headers = self.oauth_data['headers']
+ except KeyError:
+ headers = {'Accept': 'application/json',
+ 'Api-Key': client_id,
+ 'Content-Type': 'application/json',
+ 'user-agent': 'sanderroosendaal'}
+
+ if 'grant_type' in self.oauth_data:
+ if self.oauth_data['grant_type']:
+ post_data['grant_type'] = self.oauth_data['grant_type']
+ if 'strava' in self.oauth_data['autorization_uri']:
+ post_data['grant_type'] = "authorization_code"
+
+ if 'json' in self.oauth_data['content_type']:
+ response = requests.post(
+ base_uri,
+ data=json.dumps(post_data),
+ headers=headers)
+ else: # pragma: no cover
+ response = requests.post(
+ base_uri,
+ data=post_data,
+ headers=headers, verify=False)
+
+ if response.status_code == 200 or response.status_code == 201:
+ token_json = response.json()
+ try:
+ thetoken = token_json['access_token']
+ except KeyError: # pragma: no cover
+ raise NoTokenError("Failed to obtain token")
+ try:
+ refresh_token = token_json['refresh_token']
+ except KeyError: # pragma: no cover
+ refresh_token = ''
+ try:
+ expires_in = token_json['expires_in']
+ except KeyError: # pragma: no cover
+ expires_in = 0
+ try:
+ expires_in = int(expires_in)
+ except (ValueError, TypeError): # pragma: no cover
+ expires_in = 0
+ else: # pragma: no cover
+ raise NoTokenError("Failed to obtain token")
+
+ return [thetoken, expires_in, refresh_token]
+
+
+ @abstractmethod
+ def open(self, *args, **kwargs) -> str:
+ token = getattr(self.rower, self.oauth_data['tokenname'])
+
+ try:
+ tokenexpirydate = getattr(self.rower, self.oauth_data['expirydatename'])
+ except (TypeError, AttributeError, KeyError): # pragma: no cover
+ tokenexpirydate = None
+
+ if (token == '') or (token is None):
+ raise NoTokenError("User has no token")
+ else:
+ tokenname = self.oauth_data['tokenname']
+ refreshtokenname = self.oauth_data['refreshtokenname']
+ expirydatename = self.oauth_data['expirydatename']
+ if tokenexpirydate and timezone.now()+timedelta(seconds=60) > tokenexpirydate:
+ token = self.token_refresh()
+ elif tokenexpirydate is None and expirydatename is not None and 'strava' in expirydatename: # pragma: no cover
+ token = self.token_refresh()
+
+ return token
+
+ def do_refresh_token(self, *args, **kwargs) -> (str, int, str):
+ post_data = {"grant_type": "refresh_token",
+ "client_secret": self.oauth_data['client_secret'],
+ "client_id": self.oauth_data['client_id'],
+ "refresh_token": refreshtoken,
+ }
+ headers = {'user-agent': 'sanderroosendaal',
+ 'Accept': 'application/json',
+ 'Content-Type': self.oauth_data['content_type']}
+
+ # for Strava
+ if 'grant_type' in self.oauth_data:
+ if self.oauth_data['grant_type']:
+ post_data['grant_type'] = self.oauth_data['grant_type']
+
+ if self.oauth_data['bearer_auth']:
+ headers['authorization'] = 'Bearer %s' % access_token
+
+ baseurl = self.oauth_data['base_url']
+
+ if 'json' in self.oauth_data['content_type']:
+ try:
+ response = requests.post(baseurl,
+ data=json.dumps(post_data),
+ headers=headers, verify=False)
+ except: # pragma: no cover
+ raise NoTokenError("Failed to get token")
+ else:
+ try:
+ response = requests.post(baseurl,
+ data=post_data,
+ headers=headers, verify=False,
+ )
+ except: # pragma: no cover
+ raise NoTokenError("Failed to get token")
+
+ if response.status_code == 200 or response.status_code == 201:
+ token_json = response.json()
+ else: # pragma: no cover
+ raise NoTokenError("User has no token")
+
+ try:
+ thetoken = token_json['access_token']
+ except KeyError: # pragma: no cover
+ raise NoTokenError("User has no token")
+
+ try:
+ expires_in = token_json['expires_in']
+ except KeyError:
+ try:
+ expires_at = arrow.get(token_json['expires_at']).timestamp()
+ expires_in = expires_at - arrow.now().timestamp()
+ except KeyError: # pragma: no cover
+ expires_in = 0
+ try:
+ refresh_token = token_json['refresh_token']
+ except KeyError: # pragma: no cover
+ refresh_token = refreshtoken
+ try:
+ expires_in = int(expires_in)
+ except (TypeError, ValueError): # pragma: no cover
+ expires_in = 0
+
+ return [thetoken, expires_in, refresh_token]
+
+
+ @abstractmethod
+ def token_refresh(self, *args, **kwargs) -> str:
+ refreshtoken = getattr(self.rower, oauth['refreshtokenname'])
+
+ if not refreshtoken:
+ refreshtoken = getattr(self.rower, oauth['tokenname'])
+
+ access_token, expires_in, refresh_token = self.do_refresh_token()
+ expirydatetime = timezone.now()+timedelta(seconds=expires_in)
+
+ setattr(self.rower, tokenname, access_token)
+ if expirydatename is not None:
+ setattr(self.rower, expirydatename, expirydatetime)
+ if refreshtokenname is not None:
+ setattr(self.rower, refreshtokenname, refresh_token)
+
+ self.rower.save()
+
+ return access_token
+
+
diff --git a/rowers/management/commands/processemail.py b/rowers/management/commands/processemail.py
index 33f3427a..c9ff2cb1 100644
--- a/rowers/management/commands/processemail.py
+++ b/rowers/management/commands/processemail.py
@@ -28,12 +28,12 @@ from rowingdata import rowingdata as rrdata
import rowers.uploads as uploads
import rowers.polarstuff as polarstuff
-import rowers.c2stuff as c2stuff
+
import rowers.rp3stuff as rp3stuff
import rowers.stravastuff as stravastuff
import rowers.nkstuff as nkstuff
from rowers.opaque import encoder
-
+from rowers.integrations import *
from rowers.rower_rules import user_is_not_basic, user_is_coachee
from rowers.utils import dologging
@@ -91,7 +91,9 @@ class Command(BaseCommand):
rowers = Rower.objects.filter(c2_auto_import=True)
for r in rowers: # pragma: no cover
if user_is_not_basic(r.user) or user_is_coachee(r.user):
- c2stuff.get_c2_workouts(r)
+ c2integration = C2Integration(r.user)
+ _ = c2integration.get_workouts()
+
except: # pragma: no cover
exc_type, exc_value, exc_traceback = sys.exc_info()
lines = traceback.format_exception(exc_type, exc_value, exc_traceback)
diff --git a/rowers/ownapistuff.py b/rowers/ownapistuff.py
index d222986d..cf81c148 100644
--- a/rowers/ownapistuff.py
+++ b/rowers/ownapistuff.py
@@ -15,7 +15,6 @@ import math
from math import sin, cos, atan2, sqrt
import urllib
-import rowers.c2stuff as c2stuff
# Django
from django.http import HttpResponseRedirect, HttpResponse, JsonResponse
diff --git a/rowers/templates/list_import.html b/rowers/templates/list_import.html
new file mode 100644
index 00000000..27e1e36c
--- /dev/null
+++ b/rowers/templates/list_import.html
@@ -0,0 +1,84 @@
+{% extends "newbase.html" %}
+{% load static %}
+{% load rowerfilters %}
+
+{% block title %}Workouts{% endblock %}
+
+{% block main %}
+Available on {{ integration }}
+
+
+ {% if workouts %}
+ -
+ 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.
+
+
+ -
+
+
+ {% if page > 1 %}
+
+
+
+ {% endif %}
+
+
+
+
+
+
+ -
+
+
+ {% else %}
+ No workouts found
+ {% endif %}
+
+{% endblock %}
+
+{% block sidebar %}
+{% include 'menu_workouts.html' %}
+{% endblock %}
diff --git a/rowers/templatetags/rowerfilters.py b/rowers/templatetags/rowerfilters.py
index 40924683..21ca77ad 100644
--- a/rowers/templatetags/rowerfilters.py
+++ b/rowers/templatetags/rowerfilters.py
@@ -24,8 +24,7 @@ from rowers.models import PlannedSession
from rowers.teams import coach_getcoachees
from rowers import credits
-from rowers import c2stuff
-from rowers.c2stuff import c2_open
+from rowers.integrations import C2Integration
from rowers.rower_rules import *
from rowers.mytypes import (
otwtypes, adaptivetypes, sexcategories, weightcategories, workouttypes,
@@ -521,12 +520,9 @@ def deltatimeprint(d): # pragma: no cover
@register.filter
def c2userid(user): # pragma: no cover
- try:
- thetoken = c2_open(user)
- except NoTokenError: # pragma: no cover
- return 0
+ c2integration = C2Integration(user)
- c2userid = c2stuff.get_userid(thetoken)
+ c2userid = c2integration.get_userid(thetoken)
return c2userid
diff --git a/rowers/tests/mocks.py b/rowers/tests/mocks.py
index 89ac6292..e65d770a 100644
--- a/rowers/tests/mocks.py
+++ b/rowers/tests/mocks.py
@@ -18,7 +18,6 @@ from django.test import TestCase, Client,override_settings
from django.core.management import call_command
from django.test.client import RequestFactory
-from rowers.views import c2_open
from rowers.models import Workout, User, Rower, WorkoutForm,RowerForm,GraphImage
from rowers.forms import DocumentsForm,CNsummaryForm,RegistrationFormUniqueEmail
import rowers.plots as plots
@@ -39,7 +38,7 @@ from nose.tools import assert_true
from mock import Mock, patch
#from minimocktest import MockTestCase
import pandas as pd
-import rowers.c2stuff as c2stuff
+
import arrow
from django.http import HttpResponseRedirect
diff --git a/rowers/tests/statements.py b/rowers/tests/statements.py
index aa4821ea..5945c690 100644
--- a/rowers/tests/statements.py
+++ b/rowers/tests/statements.py
@@ -37,7 +37,6 @@ from django.core.files.uploadedfile import SimpleUploadedFile
from io import StringIO
from django.test.client import RequestFactory
-from rowers.views import c2_open
from rowers.forms import (
diff --git a/rowers/tests/test_imports.py b/rowers/tests/test_imports.py
index 42a133c7..46bc63af 100644
--- a/rowers/tests/test_imports.py
+++ b/rowers/tests/test_imports.py
@@ -14,7 +14,7 @@ import numpy as np
import rowers
from rowers import dataprep
from rowers import tasks
-from rowers import c2stuff
+
from rowers import stravastuff
from rowers import polarstuff
import urllib
@@ -22,7 +22,7 @@ import json
import rowers.utils as utils
import rowers.rojabo_stuff as rojabo_stuff
-
+from rowers.integrations import *
from django.db import transaction
import rowers.garmin_stuff as gs
@@ -312,7 +312,9 @@ class C2Objects(DjangoTestCase):
)
def test_timezone_c2(self):
- data = c2stuff.createc2workoutdata(self.w)
+ c2integration = C2Integration(self.u)
+ data = c2integration.createworkoutdata(self.w)
+
wenddtime = self.w.startdatetime+datetime.timedelta(seconds=self.totaltime)
t1 = arrow.get(wenddtime).timestamp()
t2 = arrow.get(data['date']).timestamp()
@@ -328,35 +330,34 @@ class C2Objects(DjangoTestCase):
#self.assertEqual(data['date'],wenddtime.strftime('%Y-%m-%d %H:%M:%S'))
- @patch('rowers.c2stuff.Session', side_effect=mocked_requests)
+ @patch('rowers.integrations.c2.Session', side_effect=mocked_requests)
def test_c2_callback(self, mock_Session):
response = self.c.get('/call_back?code=dsdoij232s',follow=True)
self.assertEqual(response.status_code, 200)
- @patch('rowers.c2stuff.Session', side_effect=mocked_requests)
+ @patch('rowers.integrations.c2.Session', side_effect=mocked_requests)
def test_c2_token_refresh(self, mock_Session):
response = self.c.get('/rowers/me/c2refresh/',follow=True)
self.assertEqual(response.status_code, 200)
- @patch('rowers.c2stuff.requests.post', side_effect=mocked_requests)
- @patch('rowers.c2stuff.requests.get', side_effect=mocked_requests)
- @patch('rowers.c2stuff.requests.session', side_effect=mocked_requests)
+ @patch('rowers.integrations.c2.requests.post', side_effect=mocked_requests)
+ @patch('rowers.integrations.c2.requests.get', side_effect=mocked_requests)
+ @patch('rowers.integrations.c2.requests.session', side_effect=mocked_requests)
def test_c2_auto_import(self, mock_get, mock_post,MockSession):
self.r.sporttracks_auto_export = True
self.r.save()
- res = c2stuff.get_c2_workouts(self.r)
- self.assertEqual(res,1)
+ c2integration = C2Integration(self.u)
+ res = c2integration.get_workouts()
- res = c2stuff.get_c2_workouts(self.r,do_async=False)
self.assertEqual(res,1)
- @patch('rowers.c2stuff.requests.post', side_effect=mocked_requests)
- @patch('rowers.c2stuff.requests.get', side_effect=mocked_requests)
+ @patch('rowers.integrations.c2.requests.post', side_effect=mocked_requests)
+ @patch('rowers.integrations.c2.requests.get', side_effect=mocked_requests)
def test_c2_upload(self, mock_get, mock_post):
response = self.c.get('/rowers/workout/'+encoded1+'/c2uploadw/')
@@ -367,14 +368,14 @@ class C2Objects(DjangoTestCase):
self.assertEqual(response.url, '/rowers/workout/'+encoded1+'/edit/')
self.assertEqual(response.status_code, 302)
- @patch('rowers.c2stuff.requests.post', side_effect=mocked_requests)
- @patch('rowers.c2stuff.requests.get', side_effect=mocked_requests)
+ @patch('rowers.integrations.c2.requests.post', side_effect=mocked_requests)
+ @patch('rowers.integrations.c2.requests.get', side_effect=mocked_requests)
def test_c2_list(self, mock_get, mock_post):
response = self.c.get('/rowers/workout/c2list',follow=True)
self.assertEqual(response.status_code,200)
- @patch('rowers.c2stuff.requests.get', side_effect=mocked_requests)
+ @patch('rowers.integrations.c2.requests.get', side_effect=mocked_requests)
@patch('rowers.dataprep.create_engine')
def test_c2_import(self, mock_get, mocked_sqlalchemy):
@@ -506,7 +507,7 @@ class C2Objects(DjangoTestCase):
self.assertEqual(got, want)
- @patch('rowers.c2stuff.requests.get', side_effect=mocked_requests)
+ @patch('rowers.integrations.c2.requests.get', side_effect=mocked_requests)
@patch('rowers.dataprep.create_engine')
@patch('rowers.tasks.requests.session', side_effect=mocked_session)
def test_c2_import_tz(self, mock_get, mocked_sqlalchemy, mock_session):
@@ -526,7 +527,7 @@ class C2Objects(DjangoTestCase):
self.assertEqual(timezone,'Europe/Prague')
- @patch('rowers.c2stuff.requests.get', side_effect=mocked_requests)
+ @patch('rowers.integrations.c2.requests.get', side_effect=mocked_requests)
@patch('rowers.dataprep.create_engine')
def test_c2_import_tz3(self, mock_get, mocked_sqlalchemy):
@@ -547,7 +548,7 @@ class C2Objects(DjangoTestCase):
@patch('rowers.tasks.requests.get', side_effect=mocked_requests)
@patch('rowers.dataprep.create_engine')
- @patch('rowers.c2stuff.requests.session', side_effect=mocked_requests)
+ @patch('rowers.integrations.c2.requests.session', side_effect=mocked_requests)
def test_c2_import_tz2(self, mock_get, mocked_sqlalchemy, MockSession):
response = self.c.get('/rowers/workout/c2import/31/',follow=True)
@@ -643,15 +644,15 @@ class C2ObjectsTokenExpired(DjangoTestCase):
- @patch('rowers.c2stuff.requests.post', side_effect=mocked_requests)
- @patch('rowers.c2stuff.requests.get', side_effect=mocked_requests)
- @patch('rowers.c2stuff.Session',side_effect=mocked_requests)
+ @patch('rowers.integrations.c2.requests.post', side_effect=mocked_requests)
+ @patch('rowers.integrations.c2.requests.get', side_effect=mocked_requests)
+ @patch('rowers.integrations.c2.Session',side_effect=mocked_requests)
def test_c2_list(self, mock_get, mock_post, mock_Session):
response = self.c.get('/rowers/workout/c2list',follow=True)
self.assertEqual(response.status_code,200)
- @patch('rowers.c2stuff.requests.get', side_effect=mocked_requests)
+ @patch('rowers.integrations.c2.requests.get', side_effect=mocked_requests)
@patch('rowers.dataprep.create_engine')
def test_c2_import(self, mock_get, mocked_sqlalchemy):
diff --git a/rowers/tests/test_permissions2.py b/rowers/tests/test_permissions2.py
index 1bd5c3d0..8baa8c28 100644
--- a/rowers/tests/test_permissions2.py
+++ b/rowers/tests/test_permissions2.py
@@ -214,8 +214,8 @@ class PermissionsViewTests(TestCase):
# Test access for anonymous users
@parameterized.expand(viewstotest)
- @patch('rowers.c2stuff.Session', side_effect=mocked_requests)
- @patch('rowers.c2stuff.c2_open')
+ @patch('rowers.integrations.c2.Session', side_effect=mocked_requests)
+ @patch('rowers.integrations.c2.C2Integration.open')
@patch('rowers.dataprep.create_engine')
@patch('rowers.dataprep.read_df_sql')
@patch('rowers.dataprep.getsmallrowdata_db')
@@ -249,8 +249,8 @@ class PermissionsViewTests(TestCase):
# Test access for logged in users - accessing own objects
@parameterized.expand(viewstotest)
- @patch('rowers.c2stuff.Session', side_effect=mocked_requests)
- @patch('rowers.c2stuff.c2_open')
+ @patch('rowers.integrations.c2.Session', side_effect=mocked_requests)
+ @patch('rowers.integrations.c2.C2Integration.open')
@patch('rowers.dataprep.create_engine')
@patch('rowers.dataprep.read_df_sql')
@patch('rowers.dataprep.getsmallrowdata_db')
@@ -331,8 +331,8 @@ class PermissionsViewTests(TestCase):
# Test access for logged in users - accessing team member objects
@parameterized.expand(viewstotest)
- @patch('rowers.c2stuff.Session', side_effect=mocked_requests)
- @patch('rowers.c2stuff.c2_open')
+ @patch('rowers.integrations.c2.Session', side_effect=mocked_requests)
+ @patch('rowers.integrations.c2.C2Integration.open')
@patch('rowers.dataprep.create_engine')
@patch('rowers.dataprep.read_df_sql')
@patch('rowers.dataprep.getsmallrowdata_db')
@@ -414,8 +414,8 @@ class PermissionsViewTests(TestCase):
# Test access for logged in users - accessing coachee
@parameterized.expand(viewstotest)
- @patch('rowers.c2stuff.Session', side_effect=mocked_requests)
- @patch('rowers.c2stuff.c2_open')
+ @patch('rowers.integrations.c2.Session', side_effect=mocked_requests)
+ @patch('rowers.integrations.c2.C2Integration.open')
@patch('rowers.dataprep.create_engine')
@patch('rowers.dataprep.read_df_sql')
@patch('rowers.dataprep.getsmallrowdata_db')
diff --git a/rowers/tests/test_unit_tests.py b/rowers/tests/test_unit_tests.py
index 854185df..aad12551 100644
--- a/rowers/tests/test_unit_tests.py
+++ b/rowers/tests/test_unit_tests.py
@@ -17,12 +17,13 @@ import pytz
# interactive plots
from rowers import interactiveplots
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
class OtherUnitTests(TestCase):
def setUp(self):
@@ -120,7 +121,7 @@ class OtherUnitTests(TestCase):
s = f.read()
data = json.loads(s)
splitdata = data['workout']['intervals']
- summary = c2stuff.summaryfromsplitdata(splitdata,data,'aap.txt')
+ summary = tasks.summaryfromsplitdata(splitdata,data,'aap.txt')
self.assertEqual(len(summary),3)
sums = summary[0]
@@ -592,7 +593,7 @@ class DataPrepTests(TestCase):
def test_getagegrouprecord(self):
records = C2WorldClassAgePerformance.objects.filter(distance=2000,sex=self.r.sex,weightcategory=self.r.weightcategory)
- result = c2stuff.getagegrouprecord(25)
+ result = getagegrouprecord(25)
self.assertEqual(int(result),590)
@patch('rowers.dataprep.getsmallrowdata_db',side_effect=mocked_getsmallrowdata_uh)
diff --git a/rowers/uploads.py b/rowers/uploads.py
index eff59345..bbf63aab 100644
--- a/rowers/uploads.py
+++ b/rowers/uploads.py
@@ -1,9 +1,10 @@
from rowers.mytypes import workouttypes, boattypes, otwtypes, workoutsources, workouttypes_ordered
-import rowers.c2stuff as c2stuff
+
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,
str2bool, range_to_color_hex, absolute, myqueue, NoTokenError
@@ -199,9 +200,9 @@ def do_sync(w, options, quick=False):
if do_c2_export: # pragma: no cover
dologging('c2_log.log','Exporting workout to C2 for user {user}'.format(user=w.user.user.id))
+ c2_integration = C2Integration(w.user.user)
try:
- message, id = c2stuff.workout_c2_upload(
- w.user.user, w, asynchron=True)
+ id = c2_integration.workout_export(w)
dologging('c2_log.log','C2 upload succeeded')
except NoTokenError:
id = 0
diff --git a/rowers/urls.py b/rowers/urls.py
index c9760f9a..ea9448b3 100644
--- a/rowers/urls.py
+++ b/rowers/urls.py
@@ -1169,8 +1169,3 @@ urlpatterns = [
name="braintree_webhook_view"),
]
-if settings.DEBUG: # pragma: no cover
- urlpatterns += [
- re_path(r'^c2listug/(?P\d+)/$', views.c2listdebug_view),
- re_path(r'^c2listug/$', views.c2listdebug_view),
- ]
diff --git a/rowers/views/importviews.py b/rowers/views/importviews.py
index 6d7b2a65..6943ee1d 100644
--- a/rowers/views/importviews.py
+++ b/rowers/views/importviews.py
@@ -7,6 +7,8 @@ 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
+
import numpy
@@ -109,9 +111,10 @@ def workout_c2_upload_view(request, id=0):
)
dologging('c2_log.log', s)
+ c2_integration = C2Integration(request.user)
+
try:
- message, c2id = c2stuff.workout_c2_upload(
- request.user, w, asynchron=True)
+ c2id = c2_integration.workout_export(w)
except NoTokenError: # pragma: no cover
return HttpResponseRedirect("/rowers/me/c2authorize/")
@@ -187,18 +190,9 @@ def rower_nk_authorize(request): # pragma: no cover
# Concept2 authorization
@login_required()
def rower_c2_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())
- scope = "user:read,results:write"
- params = {"client_id": C2_CLIENT_ID,
- "response_type": "code",
- "redirect_uri": C2_REDIRECT_URI}
- url = "http://log.concept2.com/oauth/authorize?" + \
- urllib.parse.urlencode(params)
- url += "&scope="+scope
-
+ c2_integration = C2Integration(request.user)
+ url = c2_integration.make_authorization_url()
+
return HttpResponseRedirect(url)
# Garmin authorization
@@ -312,26 +306,12 @@ def rower_tp_authorize(request): # pragma: no cover
# Concept2 token refresh. URL for manual refresh. Not visible to users
@login_required()
def rower_c2_token_refresh(request):
- r = getrower(request.user)
- res = c2stuff.do_refresh_token(r.c2refreshtoken)
-
- if res[0] is not None:
- 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.c2token = access_token
- r.tokenexpirydate = expirydatetime
- r.c2refreshtoken = refresh_token
-
- r.save()
-
- successmessage = "Tokens refreshed. Good to go"
- messages.info(request, successmessage)
- else: # pragma: no cover
- message = "Something went wrong (refreshing tokens). Please reauthorize:"
- messages.error(request, message)
+ c2_integration = C2Integration(request.user)
+ try:
+ token = c2_integration.token_refresh()
+ messages.info(request, "Tokens refreshed. Good to go")
+ except NoTokenError:
+ messages.error(request, "Something went wrong refreshing C2 tokens. Please reauthorize")
url = reverse('workouts_view')
@@ -395,10 +375,11 @@ def rower_sporttracks_token_refresh(request):
# Concept2 Callback
@login_required()
def rower_process_callback(request):
+ c2_integration = C2Integration(request.user)
try:
code = request.GET['code']
- res = c2stuff.get_token(code)
- except MultiValueDictKeyError: # pragma: no cover
+ res = c2_integration.get_token(code)
+ except (MultiValueDictKeyError, NoTokenError): # pragma: no cover
message = "The resource owner or authorization server denied the request"
messages.error(request, message)
@@ -1677,61 +1658,18 @@ def workout_sporttracksimport_view(request, message="", userid=0):
# List of workouts on Concept2 logbook. This view only used for debugging
-@login_required()
-def c2listdebug_view(request, page=1, message=""): # pragma: no cover
- try:
- _ = c2_open(request.user)
- except NoTokenError: # pragma: no cover
- return HttpResponseRedirect("/rowers/me/c2authorize/")
-
- r = getrower(request.user)
-
- res = c2stuff.get_c2_workout_list(request.user, page=page)
-
- if (res.status_code != 200):
- message = "Something went wrong in workout_c2import_view (C2 token renewal)"
- messages.error(request, message)
- if settings.DEBUG:
- return HttpResponse(res)
- else:
- url = reverse('workouts_view')
- return HttpResponseRedirect(url)
- else:
- workouts = []
-
- for item in res.json()['data']:
- d = item['distance']
- i = item['id']
- ttot = item['time_formatted']
- s = item['date']
- r = item['type']
- s2 = item['source']
- c = item['comments']
- keys = ['id', 'distance', 'duration',
- 'starttime', 'rowtype', 'source', 'comment']
- values = [i, d, ttot, s, r, s2, c]
- res = dict(zip(keys, values))
- workouts.append(res)
-
- return render(request,
- 'c2_list_import2.html',
- {'workouts': workouts,
- 'teams': get_my_teams(request.user),
- })
# Import all unknown workouts available on Concept2 logbook
-
-
@login_required()
def workout_getc2workout_all(request, page=1, message=""): # pragma: no cover
+ r = getrequestrower(request)
+ c2_integration = C2Integration(request.user)
try:
- _ = c2_open(request.user)
+ _ = c2_integration.open()
except NoTokenError: # pragma: no cover
return HttpResponseRedirect("/rowers/me/c2authorize/")
- r = getrequestrower(request)
-
- result = c2stuff.get_c2_workouts(r, page=page, do_async=True)
+ result = c2_integration.get_workouts(page=page)
if result:
messages.info(
@@ -1777,76 +1715,23 @@ def workout_c2import_view(request, page=1, userid=0, message=""):
'userid': request.user.id})
return HttpResponseRedirect(url)
+ c2_integration = C2Integration(request.user)
try:
- _ = c2_open(request.user)
+ _ = c2_integration.open()
except NoTokenError: # pragma: no cover
return HttpResponseRedirect("/rowers/me/c2authorize/")
- res = c2stuff.get_c2_workout_list(request.user, page=page)
+ workouts = c2_integration.get_workout_list(page=1)
- if (res.status_code != 200): # pragma: no cover
- message = "Something went wrong in workout_c2import_view (C2 token refresh)"
- messages.error(request, message)
- url = reverse('workouts_view')
- return HttpResponseRedirect(url)
-
- workouts = []
- c2ids = [item['id'] for item in res.json()['data']]
- knownc2ids = uniqify([
- w.uploadedtoc2 for w in Workout.objects.filter(user=rower)
- ])
- tombstones = [
- t.uploadedtoc2 for t in TombStone.objects.filter(user=rower)
- ]
- parkedids = []
- try:
- with open('c2blocked.json', 'r') as c2blocked:
- jsondata = json.load(c2blocked)
- parkedids = jsondata['ids']
- except: # pragma: no cover
- pass
-
- knownc2ids = uniqify(knownc2ids+tombstones+parkedids)
- for item in res.json()['data']:
- d = item['distance']
- i = item['id']
- ttot = item['time_formatted']
- s = item['date']
- r = item['type']
- s2 = item['source']
- c = item['comments']
- if i in knownc2ids:
- nnn = ''
- else: # pragma: no cover
- nnn = 'NEW'
- keys = ['id', 'distance', 'duration', 'starttime',
- 'rowtype', 'source', 'comment', 'new']
- values = [i, d, ttot, s, r, s2, c, nnn]
- ress = dict(zip(keys, values))
- workouts.append(ress)
if request.method == "POST":
try: # pragma: no cover
tdict = dict(request.POST.lists())
ids = tdict['workoutid']
c2ids = [int(id) for id in ids]
- alldata = {}
- for item in res.json()['data']:
- alldata[item['id']] = item
- counter = 0
for c2id in c2ids:
- _ = myqueue(
- queue,
- handle_c2_async_workout,
- alldata,
- rower.user.id,
- rower.c2token,
- c2id,
- counter,
- rower.defaulttimezone
- )
- counter = counter+1
+ c2_integration.get_workout(c2id)
# done, redirect to workouts list
messages.info(
request,
@@ -1877,7 +1762,7 @@ def workout_c2import_view(request, page=1, userid=0, message=""):
checknew = request.GET.get('selectallnew', False)
return render(request,
- 'c2_list_import2.html',
+ 'list_import.html',
{'workouts': workouts,
'rower': rower,
'active': 'nav-workouts',
@@ -1885,6 +1770,7 @@ def workout_c2import_view(request, page=1, userid=0, message=""):
'teams': get_my_teams(request.user),
'page': page,
'checknew': checknew,
+ 'integration': 'C2 Logbook'
})
@@ -1909,7 +1795,7 @@ importauthorizeviews = {
}
importsources = {
- 'c2': c2stuff,
+ 'c2': C2Integration,
'strava': stravastuff,
'polar': polarstuff,
'ownapi': ownapistuff,
@@ -1953,19 +1839,18 @@ def workout_getimportview(request, externalid, source='c2', do_async=True):
startdate = request.session.get('startdate')
enddate = request.session.get('enddate')
+ integration = importsources[source](request.user)
+
try:
- result = importsources[source].get_workout(request.user, externalid, do_async=do_async,
- startdate=startdate, enddate=enddate)
+ result = integration.get_workout(externalid, startdate=startdate, enddate=enddate)
except NoTokenError:
return HttpResponseRedirect(reverse(importauthorizeviews[source]))
url = reverse(importlistviews[source])
return HttpResponseRedirect(url)
try:
- result = importsources[source].get_workout(request.user, externalid,
- do_async=do_async)
+ result = integration.get_workout(externalid)
except NoTokenError:
-
return HttpResponseRedirect(reverse(importauthorizeviews[source]))
if result: # pragma: no cover
diff --git a/rowers/views/statements.py b/rowers/views/statements.py
index 8770ed20..118fc2c5 100644
--- a/rowers/views/statements.py
+++ b/rowers/views/statements.py
@@ -1,5 +1,6 @@
import rowers.teams as teams
from rowers.serializers import RowerSerializer, WorkoutSerializer
+from rowers.integrations import integrations
from rq import Queue, cancel_job
from redis import StrictRedis, Redis
from rowers.models import C2WorldClassAgePerformance
@@ -191,10 +192,8 @@ import os
import sys
import datetime
import iso8601
-import rowers.c2stuff as c2stuff
import rowers.nkstuff as nkstuff
import rowers.rojabo_stuff as rojabo_stuff
-from rowers.c2stuff import c2_open
from rowers.nkstuff import nk_open
from rowers.rp3stuff import rp3_open
from rowers.sporttracksstuff import sporttracks_open
@@ -208,6 +207,7 @@ from rowers.rojabo_stuff import rojabo_open
import rowers.polarstuff as polarstuff
import rowers.sporttracksstuff as sporttracksstuff
+from rowers.integrations import *
import rowers.tpstuff as tpstuff
import rowers.rp3stuff as rp3stuff
diff --git a/rowers/views/workoutviews.py b/rowers/views/workoutviews.py
index d5df4565..1e7cf862 100644
--- a/rowers/views/workoutviews.py
+++ b/rowers/views/workoutviews.py
@@ -5630,7 +5630,8 @@ def workout_upload_view(request,
# upload to C2
if (upload_to_c2): # pragma: no cover
try:
- message, id = c2stuff.workout_c2_upload(request.user, w)
+ c2integration = C2Integration(request.user)
+ id = c2integration.workout_export(w)
except NoTokenError:
id = 0
message = "Something went wrong with the Concept2 sync"
From 792faf414e3b286b3a0744f81b5689592e325e83 Mon Sep 17 00:00:00 2001
From: Sander Roosendaal
Date: Fri, 10 Feb 2023 17:44:23 +0100
Subject: [PATCH 02/27] fixing failing tests
---
rowers/integrations/c2.py | 2 +-
rowers/integrations/integrations.py | 16 ++++---
rowers/tests/statements.py | 2 +-
rowers/tests/test_async_tasks.py | 11 ++---
rowers/views/importviews.py | 67 ++++++++++++++++++++++++++++-
5 files changed, 82 insertions(+), 16 deletions(-)
diff --git a/rowers/integrations/c2.py b/rowers/integrations/c2.py
index 316af562..09af9f4a 100644
--- a/rowers/integrations/c2.py
+++ b/rowers/integrations/c2.py
@@ -114,7 +114,7 @@ class C2Integration(SyncIntegration):
return super(C2Integration, self).open(*args, **kwargs)
def token_refresh(self, *args, **kwargs):
- return super(C2Integration, self).open(*args, **kwargs)
+ return super(C2Integration, self).token_refresh(*args, **kwargs)
def make_authorization_url(self, *args, **kwargs):
scope = "user:read,results:write"
diff --git a/rowers/integrations/integrations.py b/rowers/integrations/integrations.py
index 6ca777f9..d1fd8646 100644
--- a/rowers/integrations/integrations.py
+++ b/rowers/integrations/integrations.py
@@ -162,6 +162,8 @@ class SyncIntegration(metaclass=ABCMeta):
return token
def do_refresh_token(self, *args, **kwargs) -> (str, int, str):
+ refreshtoken = getattr(self.rower, self.oauth_data['refreshtokenname'])
+ access_token = kwargs.get('access_token','')
post_data = {"grant_type": "refresh_token",
"client_secret": self.oauth_data['client_secret'],
"client_id": self.oauth_data['client_id'],
@@ -229,19 +231,19 @@ class SyncIntegration(metaclass=ABCMeta):
@abstractmethod
def token_refresh(self, *args, **kwargs) -> str:
- refreshtoken = getattr(self.rower, oauth['refreshtokenname'])
+ refreshtoken = getattr(self.rower, self.oauth_data['refreshtokenname'])
if not refreshtoken:
- refreshtoken = getattr(self.rower, oauth['tokenname'])
+ refreshtoken = getattr(self.rower, self.oauth_data['tokenname'])
access_token, expires_in, refresh_token = self.do_refresh_token()
expirydatetime = timezone.now()+timedelta(seconds=expires_in)
- setattr(self.rower, tokenname, access_token)
- if expirydatename is not None:
- setattr(self.rower, expirydatename, expirydatetime)
- if refreshtokenname is not None:
- setattr(self.rower, refreshtokenname, refresh_token)
+ setattr(self.rower, self.oauth_data['tokenname'], access_token)
+ if self.oauth_data['expirydatename'] is not None:
+ setattr(self.rower, self.oauth_data['expirydatename'], expirydatetime)
+ if self.oauth_data['refreshtokenname'] is not None:
+ setattr(self.rower, self.oauth_data['refreshtokenname'], refresh_token)
self.rower.save()
diff --git a/rowers/tests/statements.py b/rowers/tests/statements.py
index 5945c690..f78857cf 100644
--- a/rowers/tests/statements.py
+++ b/rowers/tests/statements.py
@@ -37,7 +37,7 @@ from django.core.files.uploadedfile import SimpleUploadedFile
from io import StringIO
from django.test.client import RequestFactory
-
+from rowers.integrations import *
from rowers.forms import (
DocumentsForm,CNsummaryForm,RegistrationFormUniqueEmail,
diff --git a/rowers/tests/test_async_tasks.py b/rowers/tests/test_async_tasks.py
index 66aa36dd..cadddd32 100644
--- a/rowers/tests/test_async_tasks.py
+++ b/rowers/tests/test_async_tasks.py
@@ -186,15 +186,16 @@ class AsyncTaskTests(TestCase):
self.u.id)
self.assertEqual(res,1)
- @patch('rowers.c2stuff.requests.post', side_effect=mocked_requests)
- @patch('rowers.c2stuff.requests.get', side_effect=mocked_requests)
+ @patch('rowers.integrations.c2.requests.post', side_effect=mocked_requests)
+ @patch('rowers.integrations.c2.requests.get', side_effect=mocked_requests)
def test_c2_sync(self, mock_get, mock_post):
authorizationstring = str('Bearer aap')
headers = {'Authorization': authorizationstring,
'user-agent': 'sanderroosendaal',
'Content-Type': 'application/json'}
- data = c2stuff.createc2workoutdata(self.wwater)
+ integration = C2Integration(self.u)
+ data = integration.createworkoutdata(self.wwater)
url = "https://log.concept2.com/api/users/%s/results" % (1)
res = tasks.handle_c2_sync(1,url,headers,data)
@@ -469,8 +470,8 @@ class AsyncTaskTests(TestCase):
self.assertEqual(res,1)
- @patch('rowers.c2stuff.requests.post',side_effect=mocked_requests)
- @patch('rowers.c2stuff.requests.get',side_effect=mocked_requests)
+ @patch('rowers.integrations.c2.requests.post',side_effect=mocked_requests)
+ @patch('rowers.integrations.c2.requests.get',side_effect=mocked_requests)
def test_import_c2_strokedata(self, mock_get, mock_post):
c2token = 'aap'
c2id = 1212
diff --git a/rowers/views/importviews.py b/rowers/views/importviews.py
index 6943ee1d..3371674b 100644
--- a/rowers/views/importviews.py
+++ b/rowers/views/importviews.py
@@ -1832,14 +1832,77 @@ def workout_getrp3importview(request, externalid):
url = reverse('workout_rp3import_view')
return HttpResponseRedirect(url)
-
@login_required()
-def workout_getimportview(request, externalid, source='c2', do_async=True):
+def workout_getimportview_old(request, externalid, source='c2', do_async=True):
if 'startdate' in request.session and source == 'nk': # pragma: no cover
startdate = request.session.get('startdate')
enddate = request.session.get('enddate')
+ try:
+ result = importsources[source].get_workout(request.user, externalid, do_async=do_async,
+ startdate=startdate, enddate=enddate)
+ except NoTokenError:
+ return HttpResponseRedirect(reverse(importauthorizeviews[source]))
+
+ url = reverse(importlistviews[source])
+ return HttpResponseRedirect(url)
+ try:
+ result = importsources[source].get_workout(request.user, externalid,
+ do_async=do_async)
+ except NoTokenError:
+
+ return HttpResponseRedirect(reverse(importauthorizeviews[source]))
+
+ if result: # pragma: no cover
+ messages.info(
+ request, "Your workout will be imported in the background")
+ # this should return to the respective import list page
+ else: # pragma: no cover
+ messages.error(request, 'Error getting the workout')
+
+ url = reverse(importlistviews[source])
+ return HttpResponseRedirect(url)
+
+
+# Imports all new workouts from SportTracks
+@login_required()
+def workout_getsporttracksworkout_all(request):
+ res = sporttracksstuff.get_sporttracks_workout_list(request.user)
+ if (res.status_code == 200):
+ r = getrower(request.user)
+ stids = [int(getidfromuri(item['uri']))
+ for item in res.json()['items']]
+ knownstids = uniqify([
+ w.uploadedtosporttracks for w in Workout.objects.filter(user=r)
+ ])
+ newids = [stid for stid in stids if stid not in knownstids]
+ for sporttracksid in newids:
+ id = sporttracksstuff.get_workout(
+ request.user, sporttracksid)
+
+ if id == 0: # pragma: no cover
+ messages.error(
+ request, "Something went wrong with workout {id}".format(id=sporttracksid))
+
+ else:
+ w = Workout.objects.get(id=id)
+ w.uploadedtosporttracks = sporttracksid
+ w.save()
+
+ url = reverse('workouts_view')
+ return HttpResponseRedirect(url)
+
+
+@login_required()
+def workout_getimportview(request, externalid, source='c2', do_async=True):
+ try:
integration = importsources[source](request.user)
+ except TypeError:
+ return workout_getimportview_old(request, externalid, source=source, do_async=True)
+ if 'startdate' in request.session and source == 'nk': # pragma: no cover
+ startdate = request.session.get('startdate')
+ enddate = request.session.get('enddate')
+
try:
result = integration.get_workout(externalid, startdate=startdate, enddate=enddate)
From eaedf30369b3bca8c91363c943e22a8b4bb10585 Mon Sep 17 00:00:00 2001
From: Sander Roosendaal
Date: Sat, 11 Feb 2023 17:20:02 +0100
Subject: [PATCH 03/27] adding strava
---
rowers/integrations/__init__.py | 1 +
rowers/integrations/c2.py | 8 +-
rowers/integrations/integrations.py | 8 +-
rowers/integrations/strava.py | 344 ++++++++++++++++++
rowers/interactiveplots.py | 7 +-
rowers/management/commands/processemail.py | 2 +-
rowers/stravastuff.py | 404 ---------------------
rowers/templates/list_import.html | 10 +-
rowers/templatetags/rowerfilters.py | 4 +-
rowers/tests/test_api.py | 2 +-
rowers/tests/test_braintree.py | 2 +-
rowers/tests/test_imports.py | 45 +--
rowers/tests/test_permissions2.py | 2 +-
rowers/tests/test_unit_tests.py | 2 +-
rowers/tests/viewnames.csv | 2 +-
rowers/uploads.py | 7 +-
rowers/urls.py | 2 -
rowers/views/importviews.py | 237 ++++--------
rowers/views/statements.py | 4 +-
rowers/views/workoutviews.py | 7 +-
20 files changed, 474 insertions(+), 626 deletions(-)
create mode 100644 rowers/integrations/strava.py
delete mode 100644 rowers/stravastuff.py
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 %}