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

+ + +{% 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 %}
  • {% csrf_token %} - Select All New + Select All New @@ -43,7 +45,7 @@ - + @@ -62,7 +64,7 @@ - + diff --git a/rowers/templatetags/rowerfilters.py b/rowers/templatetags/rowerfilters.py index 21ca77ad..6fc2fee0 100644 --- a/rowers/templatetags/rowerfilters.py +++ b/rowers/templatetags/rowerfilters.py @@ -522,7 +522,7 @@ def deltatimeprint(d): # pragma: no cover def c2userid(user): # pragma: no cover c2integration = C2Integration(user) - c2userid = c2integration.get_userid(thetoken) + c2userid = c2integration.get_userid(user) return c2userid @@ -566,6 +566,8 @@ def lookup(dict, key): s = dict.get(key) except KeyError: # pragma: no cover return None + except AttributeError: + return None if isinstance(s, string_types) and len(s) > 22: s = s[:22] diff --git a/rowers/tests/test_api.py b/rowers/tests/test_api.py index bf7d74e4..f0f8eab6 100644 --- a/rowers/tests/test_api.py +++ b/rowers/tests/test_api.py @@ -14,7 +14,7 @@ import rowers from rowers import dataprep from rowers import tasks from rowers import c2stuff -from rowers import stravastuff + import urllib import json import pandas as pd diff --git a/rowers/tests/test_braintree.py b/rowers/tests/test_braintree.py index 9933bd15..ca540e2b 100644 --- a/rowers/tests/test_braintree.py +++ b/rowers/tests/test_braintree.py @@ -14,7 +14,7 @@ import rowers from rowers import dataprep from rowers import tasks from rowers import c2stuff -from rowers import stravastuff + import urllib import json diff --git a/rowers/tests/test_imports.py b/rowers/tests/test_imports.py index 46bc63af..5d1596a9 100644 --- a/rowers/tests/test_imports.py +++ b/rowers/tests/test_imports.py @@ -15,7 +15,6 @@ import rowers from rowers import dataprep from rowers import tasks -from rowers import stravastuff from rowers import polarstuff import urllib import json @@ -26,6 +25,7 @@ from rowers.integrations import * from django.db import transaction import rowers.garmin_stuff as gs +import rowers.integrations.strava as strava @pytest.mark.django_db @override_settings(TESTING=True) @@ -1059,14 +1059,14 @@ class StravaObjects(DjangoTestCase): csvfilename=filename,uploadedtostrava=123, ) - @patch('rowers.stravastuff.requests.post', side_effect=mocked_requests) - @patch('rowers.stravastuff.requests.get', side_effect=mocked_requests) + @patch('rowers.integrations.strava.requests.post', side_effect=mocked_requests) + @patch('rowers.integrations.strava.requests.get', side_effect=mocked_requests) def test_strava_webhook(self, mock_get, mock_post): url = reverse('strava_webhook_view') params = { 'hub.challenge':'aap', - 'hub.verify_token':stravastuff.webhookverification, + 'hub.verify_token':strava.webhookverification, } url2 = url+'?'+urllib.parse.urlencode(params) @@ -1117,21 +1117,18 @@ class StravaObjects(DjangoTestCase): response = self.c.generic('POST', url, raw_data) self.assertEqual(response.status_code,200) - @patch('rowers.stravastuff.requests.post', side_effect=mocked_requests) - @patch('rowers.stravastuff.requests.get', side_effect=mocked_requests) - @patch('rowers.stravastuff.stravalib.Client',side_effect=MockStravalibClient) - def test_workout_strava_upload(self, mock_get, mock_post,MockStravalibClient): + @patch('rowers.integrations.strava.requests.post', side_effect=mocked_requests) + @patch('rowers.integrations.strava.requests.get', side_effect=mocked_requests) + def test_workout_strava_upload(self, mock_get, mock_post): w = Workout.objects.get(id=1) - res = stravastuff.workout_strava_upload(self.r.user,w,asynchron=True) - self.assertEqual(res[1],-1) - res = stravastuff.workout_strava_upload(self.r.user,w,asynchron=False) + integration = StravaIntegration(self.r.user) + res = integration.workout_export(w) - self.assertEqual(len(res[0]),43) + self.assertEqual(res,1) - @patch('rowers.stravastuff.requests.post', side_effect=mocked_requests) - @patch('rowers.stravastuff.requests.get', side_effect=mocked_requests) - @patch('rowers.stravastuff.stravalib.Client',side_effect=MockStravalibClient) - def test_strava_upload(self, mock_get, mock_post,MockStravalibClient): + @patch('rowers.integrations.strava.requests.post', side_effect=mocked_requests) + @patch('rowers.integrations.strava.requests.get', side_effect=mocked_requests) + def test_strava_upload(self, mock_get, mock_post): response = self.c.get('/rowers/workout/'+encoded1+'/stravauploadw/') self.assertRedirects(response, @@ -1142,17 +1139,18 @@ class StravaObjects(DjangoTestCase): self.assertEqual(response.status_code, 302) - @patch('rowers.stravastuff.requests.get', side_effect=mocked_requests) - @patch('rowers.stravastuff.requests.post', side_effect=mocked_requests) + @patch('rowers.integrations.strava.requests.get', side_effect=mocked_requests) + @patch('rowers.integrations.strava.requests.post', side_effect=mocked_requests) def test_strava_list(self, mock_get, mockpost): - result = rowers.stravastuff.rower_strava_token_refresh(self.u) + integration = StravaIntegration(self.u) + result = integration.token_refresh() self.assertEqual(result,"987654321234567898765432123456789") response = self.c.get('/rowers/workout/stravaimport/') self.assertEqual(response.status_code,200) @patch('rowers.utils.requests.get', side_effect=mocked_requests) - @patch('rowers.stravastuff.requests.post', side_effect=mocked_requests) + @patch('rowers.integrations.strava.requests.post', side_effect=mocked_requests) @patch('rowers.dataprep.getsmallrowdata_db') def test_strava_import(self, mock_get, mock_post, mocked_getsmallrowdata_db): @@ -1166,16 +1164,11 @@ class StravaObjects(DjangoTestCase): self.assertEqual(response.status_code, 200) - @patch('rowers.stravastuff.requests.post', side_effect=mocked_requests) + @patch('rowers.integrations.strava.requests.post', side_effect=mocked_requests) def test_strava_callback(self, mock_post): response = self.c.get('/stravacall_back?code=absdef23&scope=read',follow=True) self.assertEqual(response.status_code, 200) - @patch('rowers.stravastuff.requests.post', side_effect=mocked_requests) - def test_strava_token_refresh(self, mock_post): - result = rowers.stravastuff.rower_strava_token_refresh(self.u) - self.assertEqual(result,"987654321234567898765432123456789") - #@pytest.mark.django_db diff --git a/rowers/tests/test_permissions2.py b/rowers/tests/test_permissions2.py index 8baa8c28..52d495aa 100644 --- a/rowers/tests/test_permissions2.py +++ b/rowers/tests/test_permissions2.py @@ -424,7 +424,7 @@ class PermissionsViewTests(TestCase): @patch('rowers.dataprep.get_video_data',side_effect=mocked_get_video_data) def test_permissions_coachee( self,view,permissions, - mock_Session, + mocked_session, mock_c2open, mocked_sqlalchemy, mocked_read_df_sql, diff --git a/rowers/tests/test_unit_tests.py b/rowers/tests/test_unit_tests.py index aad12551..a4984a1a 100644 --- a/rowers/tests/test_unit_tests.py +++ b/rowers/tests/test_unit_tests.py @@ -20,7 +20,7 @@ from rowers import dataprep from rowers import tasks from rowers import plannedsessions from rowers.views.workoutviews import get_video_id -from rowers import stravastuff + import rowingdata from rowers.c2stuff import getagegrouprecord diff --git a/rowers/tests/viewnames.csv b/rowers/tests/viewnames.csv index 34c2b7cd..02e35056 100644 --- a/rowers/tests/viewnames.csv +++ b/rowers/tests/viewnames.csv @@ -84,7 +84,7 @@ 95,112,WorkoutDelete,delete workout,TRUE,403,basic,200,302,basic,403,403,coach,403,403,FALSE,FALSE,TRUE,FALSE,FALSE, 96,113,workout_smoothenpace_view,smoothen pace,TRUE,403,pro,302,302,pro,403,403,coach,302,403,FALSE,FALSE,TRUE,TRUE,TRUE, 97,114,workout_undo_smoothenpace_view,unsmoothen pace,TRUE,403,pro,302,302,pro,403,403,coach,302,403,FALSE,FALSE,TRUE,TRUE,TRUE, -98,115,workout_c2import_view,list workouts to be imported (test stops at notokenerror),TRUE,302,basic,302,302,basic,403,403,coach,302,403,FALSE,TRUE,FALSE,TRUE,TRUE, +98,115,workout_c2import_view,list workouts to be imported (test stops at notokenerror),TRUE,302,basic,200,302,basic,403,403,coach,302,403,FALSE,TRUE,FALSE,TRUE,TRUE, 99,120,workout_stravaimport_view,list workouts to be imported (test stops at notokenerror),TRUE,302,basic,302,302,basic,403,403,coach,302,403,FALSE,TRUE,FALSE,TRUE,TRUE, 101,124,workout_getimportview,imports a workout from third party,TRUE,200,basic,200,302,FALSE,200,302,FALSE,200,302,FALSE,FALSE,FALSE,FALSE,FALSE, 103,126,workout_getstravaworkout_next,gets all strava workouts,TRUE,302,basic,302,302,FALSE,200,302,FALSE,200,302,FALSE,FALSE,FALSE,FALSE,FALSE, diff --git a/rowers/uploads.py b/rowers/uploads.py index bbf63aab..7d699303 100644 --- a/rowers/uploads.py +++ b/rowers/uploads.py @@ -3,7 +3,7 @@ from rowers.mytypes import workouttypes, boattypes, otwtypes, workoutsources, wo from rowers.rower_rules import is_promember import rowers.tpstuff as tpstuff import rowers.sporttracksstuff as sporttracksstuff -import rowers.stravastuff as stravastuff + from rowers.integrations import * from rowers.utils import ( geo_distance, serialize_list, deserialize_list, uniqify, @@ -213,10 +213,9 @@ def do_sync(w, options, quick=False): pass if do_strava_export: # pragma: no cover + strava_integration = StravaIntegration(w.user.user) try: - message, id = stravastuff.workout_strava_upload( - w.user.user, w, quick=quick, asynchron=True, - ) + id = strava_integration.workout_export(w) dologging( 'strava_export_log.log', 'exporting workout {id} as {type}'.format( diff --git a/rowers/urls.py b/rowers/urls.py index ea9448b3..29b13906 100644 --- a/rowers/urls.py +++ b/rowers/urls.py @@ -664,8 +664,6 @@ urlpatterns = [ re_path(r'^workout/(?P\w+.*)import/(?P\d+)/async/$', views.workout_getimportview, {'do_async': True}, name='workout_getimportview'), # re_path(r'^workout/stravaimport/all/$',views.workout_getstravaworkout_all,name='workout_getstravaworkout_all'), - re_path(r'^workout/stravaimport/next/$', views.workout_getstravaworkout_next, - name='workout_getstravaworkout_next'), re_path(r'^workout/sporttracksimport/$', views.workout_sporttracksimport_view, name='workout_sporttracksimport_view'), re_path(r'^workout/sporttracksimport/user/(?P\d+)/$', diff --git a/rowers/views/importviews.py b/rowers/views/importviews.py index 90a160e6..b454e031 100644 --- a/rowers/views/importviews.py +++ b/rowers/views/importviews.py @@ -7,7 +7,9 @@ from rowers.views.statements import * from rowers.plannedsessions import get_dates_timeperiod from rowers.tasks import fetch_strava_workout -from rowers.integrations.c2 import C2Integration +from rowers.integrations import C2Integration, StravaIntegration + +import rowers.integrations.strava as strava import numpy @@ -82,8 +84,12 @@ def workout_strava_upload_view(request, id=0): r = getrower(request.user) w = get_workout_by_opaqueid(request, id) result = -1 - comment, result = stravastuff.workout_strava_upload( - r.user, w, asynchron=True) + strava_integration = StravaIntegration(request.user) + try: + stravaid = strava_integration.workout_export(w) + except NoTokenError: + return HttpResponseRedirect("/rowers/me/stravaauthorize") + messages.info( request, 'Your workout will be synchronized to Strava in the background') @@ -210,21 +216,12 @@ def rower_garmin_authorize(request): # pragma: no cover @login_required() def rower_strava_authorize(request): # pragma: no cover - # Generate a random string for the state parameter - # Save it for use later to prevent xsrf attacks - - # state = str(uuid4()) - - params = {"client_id": STRAVA_CLIENT_ID, - "response_type": "code", - "redirect_uri": STRAVA_REDIRECT_URI, - "scope": "activity:write,activity:read_all"} - - url = "https://www.strava.com/oauth/authorize?" + \ - urllib.parse.urlencode(params) + strava_integration = StravaIntegration(request.user) + url = strava_integration.make_authorization_url() return HttpResponseRedirect(url) + # Polar Authorization @@ -673,7 +670,7 @@ def workout_nkimport_view(request, userid=0, after=0, before=0): if (res.status_code != 200): # pragma: no cover if (res.status_code == 401): r = getrower(request.user) - if (r.stravatoken == '') or (r.stravatoken is None): + if (r.nktoken == '') or (r.nktoken is None): s = "Token doesn't exist. Need to authorize" return HttpResponseRedirect("/rowers/me/nkauthorize/") message = "Something went wrong in workout_nkimport_view" @@ -786,6 +783,7 @@ def workout_nkimport_view(request, userid=0, after=0, before=0): @login_required() def rower_process_stravacallback(request): + strava_integration = StravaIntegration(request.user) try: code = request.GET['code'] _ = request.GET['scope'] @@ -800,7 +798,7 @@ def rower_process_stravacallback(request): return HttpResponseRedirect(url) - res = stravastuff.get_token(code) + res = strava_integration.get_token(code) if res[0]: access_token = res[0] @@ -815,7 +813,7 @@ def rower_process_stravacallback(request): r.stravarefreshtoken = refresh_token r.save() - _ = stravastuff.set_strava_athlete_id(r.user) + _ = strava_integration.set_strava_athlete_id() successmessage = "Tokens stored. Good to go. Please check your import/export settings" messages.info(request, successmessage) @@ -1169,7 +1167,7 @@ def workout_rojaboimport_view(request, message="", userid=0): }, { 'url': reverse('workout_rojaboimport_view'), - 'name': 'Strava' + 'name': 'Rojabo' }, ] @@ -1197,130 +1195,69 @@ def workout_stravaimport_view(request, message="", userid=0): url = reverse('workout_stravaimport_view', kwargs={'userid': request.user.id}) return HttpResponseRedirect(url) - # if r.user != request.user: - # messages.info(request,"You cannot import other people's workouts from Strava") + + strava_integration = StravaIntegration(request.user) try: - _ = strava_open(request.user) + _ = strava_integration.open() except NoTokenError: # pragma: no cover return HttpResponseRedirect("/rowers/me/stravaauthorize/") - res = stravastuff.get_strava_workout_list(request.user) + workouts = strava_integration.get_workout_list() - if (res.status_code != 200): # pragma: no cover - if (res.status_code == 401): - r = getrower(request.user) - if (r.stravatoken == '') or (r.stravatoken is None): - s = "Token doesn't exist. Need to authorize" - return HttpResponseRedirect("/rowers/me/stravaauthorize/") - message = "Something went wrong in workout_stravaimport_view" - messages.error(request, message) - url = reverse('workouts_view') - return HttpResponseRedirect(url) - else: - workouts = [] - r = getrower(request.user) - rower = r - stravaids = [int(item['id']) for item in res.json()] - stravadata = [{ - 'id': int(item['id']), - 'elapsed_time':item['elapsed_time'], - 'start_date':item['start_date'], - } for item in res.json()] + if request.method == "POST": + try: # pragma: no cover + tdict = dict(request.POST.lists()) + ids = tdict['workoutid'] + stravaids = [int(id) for id in ids] + alldata = {} - wfailed = Workout.objects.filter(user=r, uploadedtostrava=-1) + for stravaid in stravaids: + csvfilename = 'media/{code}_{stravaid}.csv'.format( + code=uuid4().hex[:16], stravaid=stravaid) + _ = myqueue( + queue, + fetch_strava_workout, + r.stravatoken, + strava_integration.oauth_data, + stravaid, + csvfilename, + r.user.id + ) + # done, redirect to workouts list + messages.info(request, + 'Your Strava workouts will be imported in the background.' + ' It may take a few minutes before they appear.') + url = reverse('workouts_view') + return HttpResponseRedirect(url) + except KeyError: # pragma: no cover + pass - for w in wfailed: # pragma: no cover - for item in stravadata: - elapsed_time = item['elapsed_time'] - start_date = item['start_date'] - stravaid = item['id'] - if arrow.get(start_date) == arrow.get(w.startdatetime): - elapsed_td = datetime.timedelta(seconds=int(elapsed_time)) - elapsed_time = datetime.datetime.strptime( - str(elapsed_td), - "%H:%M:%S" - ) - if str(elapsed_time)[-7:] == str(w.duration)[-7:]: - w.uploadedtostrava = int(stravaid) - w.save() + breadcrumbs = [ + { + 'url': '/rowers/list-workouts/', + 'name': 'Workouts' + }, + { + 'url': reverse('workout_stravaimport_view'), + 'name': 'Strava' + }, + ] - knownstravaids = uniqify([ - w.uploadedtostrava for w in Workout.objects.filter(user=r) - ]) + checknew = request.GET.get('selectallnew', False) + + # 2022-10-24 sorting the results + workouts = sorted(workouts, key = lambda d:d['starttime'], reverse=True) + + return render(request, 'list_import.html', + {'workouts': workouts, + 'rower': r, + 'active': 'nav-workouts', + 'breadcrumbs': breadcrumbs, + 'teams': get_my_teams(request.user), + 'checknew': checknew, + 'integration': 'Strava' + }) - for item in res.json(): - d = int(float(item['distance'])) - i = item['id'] - if i in knownstravaids: # pragma: no cover - nnn = '' - else: - nnn = 'NEW' - n = item['name'] - ttot = str(datetime.timedelta( - seconds=int(float(item['elapsed_time'])))) - s = item['start_date'] - r = item['type'] - keys = ['id', 'distance', 'duration', - 'starttime', 'type', 'name', 'new'] - values = [i, d, ttot, s, r, n, nnn] - res2 = dict(zip(keys, values)) - workouts.append(res2) - - if request.method == "POST": - try: # pragma: no cover - tdict = dict(request.POST.lists()) - ids = tdict['workoutid'] - stravaids = [int(id) for id in ids] - alldata = {} - for item in res.json(): - alldata[item['id']] = item - for stravaid in stravaids: - csvfilename = 'media/{code}_{stravaid}.csv'.format( - code=uuid4().hex[:16], stravaid=stravaid) - _ = myqueue( - queue, - fetch_strava_workout, - rower.stravatoken, - stravastuff.oauth_data, - stravaid, - csvfilename, - rower.user.id - ) - # done, redirect to workouts list - messages.info(request, - 'Your Strava workouts will be imported in the background.' - ' It may take a few minutes before they appear.') - url = reverse('workouts_view') - return HttpResponseRedirect(url) - except KeyError: # pragma: no cover - pass - - breadcrumbs = [ - { - 'url': '/rowers/list-workouts/', - 'name': 'Workouts' - }, - { - 'url': reverse('workout_stravaimport_view'), - 'name': 'Strava' - }, - ] - - checknew = request.GET.get('selectallnew', False) - - # 2022-10-24 sorting the results - workouts = sorted(workouts, key = lambda d:d['starttime'], reverse=True) - - return render(request, 'strava_list_import.html', - {'workouts': workouts, - 'rower': rower, - 'active': 'nav-workouts', - 'breadcrumbs': breadcrumbs, - 'teams': get_my_teams(request.user), - 'checknew': checknew, - }) - - return HttpResponse(res) # pragma: no cover # for Strava webhook request validation @@ -1330,7 +1267,7 @@ def strava_webhook_view(request): if request.method == 'GET': challenge = request.GET.get('hub.challenge') verificationtoken = request.GET.get('hub.verify_token') - if verificationtoken != stravastuff.webhookverification: # pragma: no cover + if verificationtoken != strava.webhookverification: # pragma: no cover return HttpResponse(status=403) data = {"hub.challenge": challenge} return JSONResponse(data) @@ -1374,8 +1311,9 @@ def strava_webhook_view(request): ws = Workout.objects.filter(uploadedtostrava=stravaid) if ws.count() == 0 and r.strava_auto_import: - job = stravastuff.async_get_workout(r.user, stravaid) - if job == 0: # pragma: no cover + strava_integration = StravaIntegration(r.user) + jobid = strava_integration.get_workout(stravaid) + if jobid == 0: # pragma: no cover dologging('strava_webhooks.log', 'Strava strava_open yielded NoTokenError') else: # pragma: no cover @@ -1728,7 +1666,7 @@ def workout_c2import_view(request, page=1, userid=0, message=""): workouts = c2_integration.get_workout_list(page=1) - + if request.method == "POST": try: # pragma: no cover tdict = dict(request.POST.lists()) @@ -1801,7 +1739,7 @@ importauthorizeviews = { importsources = { 'c2': C2Integration, - 'strava': stravastuff, + 'strava': StravaIntegration, 'polar': polarstuff, 'ownapi': ownapistuff, 'sporttracks': sporttracksstuff, @@ -1960,24 +1898,3 @@ def workout_getsporttracksworkout_all(request): url = reverse('workouts_view') return HttpResponseRedirect(url) - -# Imports all new workouts from SportTracks -@login_required() -def workout_getstravaworkout_next(request): # pragma: no cover - - r = Rower.objects.get(user=request.user) - - res = stravastuff.get_strava_workout_list(r.user) - - if (res.status_code != 200): - return 0 - else: - alldata = {} - for item in res.json(): - alldata[item['id']] = item - - _ = stravastuff.create_async_workout( - alldata, r.user, stravaid, debug=True) - - url = reverse('workouts_view') - return HttpResponseRedirect(url) diff --git a/rowers/views/statements.py b/rowers/views/statements.py index 118fc2c5..b56e8dfa 100644 --- a/rowers/views/statements.py +++ b/rowers/views/statements.py @@ -199,10 +199,10 @@ from rowers.rp3stuff import rp3_open from rowers.sporttracksstuff import sporttracks_open from rowers.tpstuff import tp_open from iso8601 import ParseError -import rowers.stravastuff as stravastuff + import rowers.rojabo_stuff as rojabo_stuff import rowers.garmin_stuff as garmin_stuff -from rowers.stravastuff import strava_open + from rowers.rojabo_stuff import rojabo_open import rowers.polarstuff as polarstuff import rowers.sporttracksstuff as sporttracksstuff diff --git a/rowers/views/workoutviews.py b/rowers/views/workoutviews.py index 1e7cf862..9bafbd6e 100644 --- a/rowers/views/workoutviews.py +++ b/rowers/views/workoutviews.py @@ -2566,7 +2566,7 @@ def workout_smoothenpace_view(request, id=0, message="", successmessage=""): if 'originalvelo' not in row.df: row.df['originalvelo'] = velo - velo2 = stravastuff.ewmovingaverage(velo, 5) + velo2 = utils.ewmovingaverage(velo, 5) pace2 = 500./abs(velo2) @@ -5641,10 +5641,9 @@ def workout_upload_view(request, messages.error(request, message) if (upload_to_strava): # pragma: no cover + strava_integration = StravaIntegration(request.user) try: - message, id = stravastuff.workout_strava_upload( - request.user, w, - ) + id = strava_integration.workout_export(w) except NoTokenError: id = 0 message = "Please connect to Strava first" From def98b9a85ade630002cdbaf8f4614f3efe6aa63 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Sat, 11 Feb 2023 17:33:31 +0100 Subject: [PATCH 04/27] fix tets --- rowers/integrations/integrations.py | 2 +- rowers/integrations/strava.py | 8 ++++---- rowers/tests/test_imports.py | 2 +- rowers/tests/testdata/testdata.tcx.gz | Bin 0 -> 4000 bytes 4 files changed, 6 insertions(+), 6 deletions(-) create mode 100644 rowers/tests/testdata/testdata.tcx.gz diff --git a/rowers/integrations/integrations.py b/rowers/integrations/integrations.py index 95bae300..686277cf 100644 --- a/rowers/integrations/integrations.py +++ b/rowers/integrations/integrations.py @@ -10,7 +10,7 @@ import arrow import urllib from uuid import uuid4 - +import json class SyncIntegration(metaclass=ABCMeta): oauth_data = { diff --git a/rowers/integrations/strava.py b/rowers/integrations/strava.py index cedebb8b..f4e08439 100644 --- a/rowers/integrations/strava.py +++ b/rowers/integrations/strava.py @@ -203,13 +203,13 @@ class StravaIntegration(SyncIntegration): _ = self.open() except NoTokenError: return 0 - csvfilename = 'media/{code}_{stravaid}.csv'.format( - code=uuid4().hex[:16], stravaid=stravaid) + csvfilename = 'media/{code}_{id}.csv'.format( + code=uuid4().hex[:16], id=id) job = myqueue(queue, fetch_strava_workout, self.rower.stravatoken, - oauth_data, + self.oauth_data, id, csvfilename, self.user.id, @@ -311,7 +311,7 @@ class StravaIntegration(SyncIntegration): # token_refresh def token_refresh(self, *args, **kwargs): - return super(StravaIntegration).token_refresh(*args, **kwargs) + return super(StravaIntegration, self).token_refresh(*args, **kwargs) def set_strava_athlete_id(): r = self.rower() diff --git a/rowers/tests/test_imports.py b/rowers/tests/test_imports.py index 5d1596a9..52f000e3 100644 --- a/rowers/tests/test_imports.py +++ b/rowers/tests/test_imports.py @@ -1164,7 +1164,7 @@ class StravaObjects(DjangoTestCase): self.assertEqual(response.status_code, 200) - @patch('rowers.integrations.strava.requests.post', side_effect=mocked_requests) + @patch('rowers.integrations.integrations.requests.post', side_effect=mocked_requests) def test_strava_callback(self, mock_post): response = self.c.get('/stravacall_back?code=absdef23&scope=read',follow=True) self.assertEqual(response.status_code, 200) diff --git a/rowers/tests/testdata/testdata.tcx.gz b/rowers/tests/testdata/testdata.tcx.gz new file mode 100644 index 0000000000000000000000000000000000000000..c12836b36d7d50853e8f0f22f122601ca760e22a GIT binary patch literal 4000 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 GfB^u0h%RUV literal 0 HcmV?d00001 From 3a82d91d2a3577288b65f193461e2c5d809324f9 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Sat, 11 Feb 2023 17:48:39 +0100 Subject: [PATCH 05/27] passing test_imports --- rowers/integrations/integrations.py | 10 +++++++--- rowers/integrations/strava.py | 4 ++-- rowers/tests/mocks.py | 2 +- rowers/tests/test_imports.py | 3 ++- 4 files changed, 12 insertions(+), 7 deletions(-) diff --git a/rowers/integrations/integrations.py b/rowers/integrations/integrations.py index 686277cf..4857f80f 100644 --- a/rowers/integrations/integrations.py +++ b/rowers/integrations/integrations.py @@ -128,13 +128,17 @@ class SyncIntegration(metaclass=ABCMeta): try: expires_in = token_json['expires_in'] except KeyError: # pragma: no cover - expires_in = 0 + 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: expires_in = int(expires_in) except (ValueError, TypeError): # pragma: no cover expires_in = 0 - else: # pragma: no cover - raise NoTokenError("Failed to obtain token") + else: # pragma: no cover + raise NoTokenError("Failed to obtain token") return [thetoken, expires_in, refresh_token] diff --git a/rowers/integrations/strava.py b/rowers/integrations/strava.py index f4e08439..82cb0561 100644 --- a/rowers/integrations/strava.py +++ b/rowers/integrations/strava.py @@ -313,8 +313,8 @@ class StravaIntegration(SyncIntegration): def token_refresh(self, *args, **kwargs): return super(StravaIntegration, self).token_refresh(*args, **kwargs) - def set_strava_athlete_id(): - r = self.rower() + def set_strava_athlete_id(self, *args, **kwargs): + 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) diff --git a/rowers/tests/mocks.py b/rowers/tests/mocks.py index e65d770a..64f9d64e 100644 --- a/rowers/tests/mocks.py +++ b/rowers/tests/mocks.py @@ -1366,7 +1366,7 @@ def mocked_requests(*args, **kwargs): - + if stravatester.match(args[0]): if stravaworkoutlisttester.match(args[0]): return MockResponse(stravaworkoutlist,200) diff --git a/rowers/tests/test_imports.py b/rowers/tests/test_imports.py index 52f000e3..1d5a383a 100644 --- a/rowers/tests/test_imports.py +++ b/rowers/tests/test_imports.py @@ -1165,7 +1165,8 @@ class StravaObjects(DjangoTestCase): self.assertEqual(response.status_code, 200) @patch('rowers.integrations.integrations.requests.post', side_effect=mocked_requests) - def test_strava_callback(self, mock_post): + @patch('rowers.integrations.strava.requests.get', side_effect=mocked_requests) + def test_strava_callback(self, mock_post, mock_get): response = self.c.get('/stravacall_back?code=absdef23&scope=read',follow=True) self.assertEqual(response.status_code, 200) From 461611a1f4e96e35fe445947949dcdd0591655c4 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Sat, 11 Feb 2023 19:35:14 +0100 Subject: [PATCH 06/27] passing tests --- rowers/tests/testdata/testdata.tcx.gz | Bin 4000 -> 3999 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/rowers/tests/testdata/testdata.tcx.gz b/rowers/tests/testdata/testdata.tcx.gz index c12836b36d7d50853e8f0f22f122601ca760e22a..f5a12aadc2cf639d2e461ba66f7807b43fcf1291 100644 GIT binary patch literal 3999 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 F0RRHrKuIcmVC4TW=Ic7J%RR6&4T4!zwlBQl~DC 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 GfB^u0h%RUV From fa9f33d692630c6a0ee6627239708263746bc410 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Sun, 12 Feb 2023 16:42:26 +0100 Subject: [PATCH 07/27] nk, not fully tested --- rowers/integrations/__init__.py | 1 + rowers/integrations/c2.py | 1 - rowers/integrations/integrations.py | 12 +- rowers/integrations/nk.py | 316 +++++++++++++++++++++ rowers/integrations/strava.py | 4 - rowers/management/commands/processemail.py | 4 +- rowers/tasks.py | 1 - rowers/tests/test_imports.py | 33 ++- rowers/tests/testdata/testdata.tcx.gz | Bin 3999 -> 4001 bytes rowers/views/importviews.py | 148 ++-------- rowers/views/statements.py | 2 - 11 files changed, 363 insertions(+), 159 deletions(-) create mode 100644 rowers/integrations/nk.py diff --git a/rowers/integrations/__init__.py b/rowers/integrations/__init__.py index 89ea5f85..6677bbaa 100644 --- a/rowers/integrations/__init__.py +++ b/rowers/integrations/__init__.py @@ -1,2 +1,3 @@ from .c2 import C2Integration from .strava import StravaIntegration +from .nk import NKIntegration diff --git a/rowers/integrations/c2.py b/rowers/integrations/c2.py index 1d5d3ea4..69774d33 100644 --- a/rowers/integrations/c2.py +++ b/rowers/integrations/c2.py @@ -369,7 +369,6 @@ class C2Integration(SyncIntegration): # 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': diff --git a/rowers/integrations/integrations.py b/rowers/integrations/integrations.py index 4857f80f..03fb2a3a 100644 --- a/rowers/integrations/integrations.py +++ b/rowers/integrations/integrations.py @@ -38,24 +38,24 @@ class SyncIntegration(metaclass=ABCMeta): @abstractmethod - def createworkoutdata(w, *args, **kwargs): + def createworkoutdata(self, w, *args, **kwargs): return None @abstractmethod - def workout_export(workout, *args, **kwargs) -> str: + def workout_export(self, workout, *args, **kwargs) -> str: pass @abstractmethod - def get_workouts(*args, **kwargs) -> int: + def get_workouts(self, *args, **kwargs) -> int: pass @abstractmethod - def get_workout(id) -> int: + def get_workout(self, id) -> int: return 0 # need to unify workout list @abstractmethod - def get_workout_list(*args, **kwargs) -> list: + def get_workout_list(self, *args, **kwargs) -> list: return [] @abstractmethod @@ -71,7 +71,7 @@ class SyncIntegration(metaclass=ABCMeta): "scope": self.oauth_data['scope'], "state": state} - url = oauth_data['authorizaton_uri']+urllib.parse.urlencode(params) + url = self.oauth_data['authorizaton_uri']+urllib.parse.urlencode(params) return url diff --git a/rowers/integrations/nk.py b/rowers/integrations/nk.py new file mode 100644 index 00000000..80f73df6 --- /dev/null +++ b/rowers/integrations/nk.py @@ -0,0 +1,316 @@ +from .integrations import SyncIntegration, NoTokenError +from rowers.models import User, Rower, Workout, TombStone + +from rowingdata import rowingdata + +from rowers import mytypes +from rowers.nkimportutils import * +from rowers.tasks import handle_nk_async_workout +from rowsandall_app.settings import ( + NK_CLIENT_ID, NK_REDIRECT_URI, NK_CLIENT_SECRET, + SITE_URL, NK_API_LOCATION, NK_OAUTH_LOCATION, + UPLOAD_SERVICE_URL, UPLOAD_SERVICE_SECRET, +) + +import time +from time import strftime +import urllib +import requests +import arrow +from json.decoder import JSONDecodeError +from django.utils import timezone +from rowers.utils import dologging, uniqify, custom_exception_handler, myqueue + +from requests_oauthlib import OAuth2Session +from requests.auth import HTTPBasicAuth + +import django_rq +queue = django_rq.get_queue('default') +queuelow = django_rq.get_queue('low') +queuehigh = django_rq.get_queue('low') + +class NKIntegration(SyncIntegration): + def __init__(self, *args, **kwargs): + super(NKIntegration, self).__init__(self, *args, **kwargs) + self.oauth_data = { + 'client_id': NK_CLIENT_ID, + 'client_secret': NK_CLIENT_SECRET, + 'redirect_uri': NK_REDIRECT_URI, + 'autorization_uri': NK_OAUTH_LOCATION+"/oauth/authorize", + 'content_type': 'application/json', + 'tokenname': 'nktoken', + 'refreshtokenname': 'nkrefreshtoken', + 'expirydatename': 'nktokenexpirydate', + 'bearer_auth': True, + 'base_url': NK_OAUTH_LOCATION+"/oauth/token", + 'scope': 'read', + } + + def createworkoutdata(self, w, *args, **kwargs): + return None + + def workout_export(self, workout, *args, **kwargs) -> str: + return "" # there is no export + + def get_workouts(*args, **kwargs) -> int: + before = kwargs.get('before',0) + after = kwargs.get('after',0) + try: + _ = self.open() + except NoTokenError: # pragma: no cover + dologging("nklog.log","NK Token error for user {id}".format(id=rower.user.id)) + return 0 + + workouts = self.get_workout_list(before=before, after=after) + + for workout in workuots: + nkid = workout['id'] + if workout['new'] == 'NEW': + dologging('nklog.log','Queueing {id}'.format(id=nkid)) + self.get_workout(nkid) + + return 1 + + + def get_workout(self, id, *args, **kwargs) -> int: + startdate = kwargs.get('startdate','') + enddate = kwargs.get('enddate','') + _ = self.open() + r = self.rower + + before = 0 + after = 0 + if startdate: # pragma: no cover + startdate = arrow.get(startdate) + after = str(int(startdate.timestamp())*1000) + if enddate: # pragma: no cover + enddate = arrow.get(enddate) + before = str(int(enddate.timestamp())*1000) + + jsondata = self.get_workout_list_json(before=before, after=after) + + alldata = {} + + for item in jsondata: + alldata[item['id']] = item + + res = myqueue( + queuehigh, + handle_nk_async_workout, + alldata, + r.user.id, + r.nktoken, + id, + 0, + r.defaulttimezone, + ) + + return 1 + + + def get_workout_list_json(self, *args, **kwargs) -> dict: + before = kwargs.get('before',0) + after = kwargs.get('after',0) + + # For debugging + startdate = '2021-01-01' + enddate = '2021-06-01' + before = arrow.get(enddate) + before = str(int(before.timestamp()*1000)) + + after = arrow.get(startdate) + after = str(int(after.timestamp()*1000)) + + r = self.rower + authorizationstring = str('Bearer ' + r.nktoken) + headers = {'Authorization': authorizationstring, + 'user-agent': 'sanderroosendaal', + 'Content-Type': 'application/json', + } + + url = NK_API_LOCATION+"api/v1/sessions" + + params = { + 'after': after, + 'before': before, + } # start / end time + + res = requests.get(url, headers=headers, params=params) + if (res.status_code != 200): # pragma: no cover + raise NoTokenError("No NK Token") + + + return res.json() + + # need to unify workout list + def get_workout_list(self, *args, **kwargs) -> list: + _ = self.open() + r = self.rower + + before = kwargs.get('before',0) + after = kwargs.get('after',0) + + # ready to fetch. Hurray + if not before: # pragma: no cover + before = arrow.now()+timedelta(days=1) + before = str(int(before.timestamp())*1000) + if not after: # pragma: no cover + after = arrow.now()-timedelta(days=7) + after = str(int(after.timestamp())*1000) + + jsondata = self.get_workout_list_json(before=before,after=after) + + + # get NK IDs + nkids = [item['id'] for item in jsondata] + knownnkids = uniqify([ + w.uploadedtonk for w in Workout.objects.filter(user=r) + ]) + tombstones = [ + t.uploadedtonk for t in TombStone.objects.filter(user=r) + ] + parkedids = [] + try: + with open('nkblocked.json', 'r') as nkblocked: + try: + jsondatal = json.load(nkblocked) + except: + jsondatal = { + 'ids':[] + } + parkedids = jsondatal['ids'] + except FileNotFoundError: # pragma: no cover + pass + + knownnkids = uniqify(knownnkids+tombstones+parkedids) + workouts = [] + + + for item in jsondata: + d = int(float(item['totalDistanceGps'])) # could also be Impeller + i = item['id'] + n = item['name'] + if i in knownnkids: + nnn = '' + else: # pragma: no cover + nnn = 'NEW' + ttot = str(datetime.timedelta( + seconds=int(float(item['elapsedTime'])/1000.))) + s = arrow.get(item['startTime'], tzinfo=r.defaulttimezone).format( + arrow.FORMAT_RFC850) + keys = ['id', 'distance', 'duration', 'starttime', + 'rowtype', 'source', 'name','new'] + values = [i, d, ttot, s, None, None, n, nnn] + rs = dict(zip(keys, values)) + workouts.append(rs) + + workouts = workouts[::-1] + + return workouts + + + def make_authorization_url(self, *args, **kwargs) -> str: # pragma: no cover + state = str(uuid4()) + scope = "read" + params = { + "grant_type": "authorization_code", + "response_type": "code", + "client_id": NK_CLIENT_ID, + "scope": scope, + "state": state, + "redirect_uri": NK_REDIRECT_URI, + } + + url = NK_OAUTH_LOCATION+"/oauth/authorize?"+urllib.parse.urlencode(params) + return url + + + def get_token(self, code, *args, **kwargs) -> (str, int, str): + url = self.oauth_data['base_url'] + + post_data = {"client_id": self.oauth_data['client_id'], + "grant_type": "authorization_code", + "redirect_uri": self.oauth_data['redirect_uri'], + "code": code, + } + + response = requests.post( + url, + auth=HTTPBasicAuth(self.oauth_data['client_id'], + self.oauth_data['client_secret']), + data=post_data + ) + + if response.status_code != 200: + raise NoTokenError("Failed to obtain token") + + token_json = response.json() + + access_token = token_json['access_token'] + refresh_token = token_json['refresh_token'] + expires_in = token_json['expires_in'] + nk_owner_id = token_json['user_id'] + + return [access_token, expires_in, refresh_token] #, nk_owner_id] + + def open(self, *args, **kwargs) -> str: + r = self.rower + if (r.nktoken == '') or (r.nktoken is None): # pragma: no cover + raise NoTokenError("User has no token") + else: + if (timezone.now() > r.nktokenexpirydate): + thetoken = self.token_refresh() + if thetoken is None: # pragma: no cover + raise NoTokenError("User has no token") + return thetoken + else: + thetoken = r.nktoken + + return thetoken + + def do_refresh_token(self, *args, **kwargs): + post_data = {"grant_type": "refresh_token", + # "client_id":NK_CLIENT_ID, + "refresh_token": self.rower.nkrefreshtoken, + } + + url = self.oauth_data['base_url'] + + response = requests.post( + url, + data=post_data, + auth=HTTPBasicAuth( + self.oauth_data['client_id'], + self.oauth_data['client_secret'] + ) + ) + + if response.status_code != 200: # pragma: no cover + return [0, 0, 0] + + token_json = response.json() + + access_token = token_json['access_token'] + refresh_token = token_json['refresh_token'] + expires_in = token_json['expires_in'] + + return access_token, expires_in, refresh_token + + + def token_refresh(self, *args, **kwargs) -> str: + r = self.rower + access_token, expires_in, refresh_token = self.do_refresh_token() + + expirydatetime = timezone.now()+timedelta(seconds=expires_in) + + r.nktoken = access_token + r.nktokenexpirydate = expirydatetime + r.nkrefreshtoken = refresh_token + r.save() + + return r.nktoken + +# just as a quick test during development +u = User.objects.get(id=1) + +nk_integration_1 = NKIntegration(u) diff --git a/rowers/integrations/strava.py b/rowers/integrations/strava.py index 82cb0561..079bab33 100644 --- a/rowers/integrations/strava.py +++ b/rowers/integrations/strava.py @@ -338,7 +338,3 @@ class StravaIntegration(SyncIntegration): -# just as a quick test during development -u = User.objects.get(id=1) - -strava_integration_1 = StravaIntegration(u) diff --git a/rowers/management/commands/processemail.py b/rowers/management/commands/processemail.py index a89c1eb9..6769ae74 100644 --- a/rowers/management/commands/processemail.py +++ b/rowers/management/commands/processemail.py @@ -31,7 +31,6 @@ import rowers.polarstuff as polarstuff import rowers.rp3stuff as rp3stuff -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 @@ -114,7 +113,8 @@ class Command(BaseCommand): for r in rowers: # pragma: no cover dologging("nklog.log","NK Auto import set for rower {id}".format(id=r.user.id)) if user_is_not_basic(r.user) or user_is_coachee(r.user): - _ = nkstuff.get_nk_workouts(r) + nk_integration = NKIntegration(r.user) + _ = nk_integration.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/tasks.py b/rowers/tasks.py index 5c318741..73e8e773 100644 --- a/rowers/tasks.py +++ b/rowers/tasks.py @@ -3086,7 +3086,6 @@ def handle_nk_async_workout(alldata, userid, nktoken, nkid, delaysec, defaulttim data = alldata[int(nkid)] except KeyError: return 0 - params = { 'sessionIds': nkid, } diff --git a/rowers/tests/test_imports.py b/rowers/tests/test_imports.py index 1d5a383a..8426aee5 100644 --- a/rowers/tests/test_imports.py +++ b/rowers/tests/test_imports.py @@ -716,10 +716,12 @@ class NKObjects(DjangoTestCase): csvfilename=filename ) - @patch('rowers.nkstuff.requests.get', side_effect=mocked_requests) - @patch('rowers.nkstuff.requests.post', side_effect=mocked_requests) + @patch('rowers.integrations.nk.requests.get', side_effect=mocked_requests) + @patch('rowers.integrations.nk.requests.post', side_effect=mocked_requests) def test_nk_list(self, mock_get, mockpost): - result = rowers.nkstuff.rower_nk_token_refresh(self.u) + integration = NKIntegration(self.u) + result = integration.token_refresh() + self.assertEqual(result,"TA3n1vrNjuQJWw0TdCDHnjSmrjIPULhTlejMIWqq") response = self.c.get('/rowers/workout/nkimport/') @@ -742,16 +744,18 @@ class NKObjects(DjangoTestCase): ) self.assertTrue(res>0) - @patch('rowers.nkstuff.requests.post', side_effect=mocked_requests) + @patch('rowers.integrations.nk.requests.post', side_effect=mocked_requests) def notest_nk_callback(self, mock_post): response = self.c.get('/nk_callback?code=absdef23&scope=read',follow=True) self.assertEqual(response.status_code, 200) - @patch('rowers.nkstuff.requests.get', side_effect=mocked_requests) - @patch('rowers.nkstuff.requests.post', side_effect=mocked_requests) + @patch('rowers.integrations.nk.requests.get', side_effect=mocked_requests) + @patch('rowers.integrations.nk.requests.post', side_effect=mocked_requests) def test_nk_get_workouts(self, mock_get, mockpost): - result = rowers.nkstuff.nk_open(self.u) + integration = NKIntegration(self.u) + result = integration.open() + self.assertEqual(result,"TA3n1vrNjuQJWw0TdCDHnjSmrjIPULhTlejMIWqq") response = self.c.get('/rowers/workout/nkimport/all/',follow=True) @@ -763,15 +767,17 @@ class NKObjects(DjangoTestCase): self.assertEqual(response.status_code, 200) - @patch('rowers.nkstuff.requests.get', side_effect=mocked_requests) - @patch('rowers.nkstuff.requests.post', side_effect=mocked_requests) + @patch('rowers.integrations.nk.requests.get', side_effect=mocked_requests) + @patch('rowers.integrations.nk.requests.post', side_effect=mocked_requests) @patch('rowers.nkimportutils.requests.session', side_effect=mocked_session) @patch('rowers.dataprep.getsmallrowdata_db', side_effect=mocked_getsmallrowdata_db) def test_nk_import(self, mock_get, mock_post, mocked_session, mocked_getsmallrowdata_db): - result = rowers.nkstuff.rower_nk_token_refresh(self.u) + integration = NKIntegration(self.u) + result = integration.token_refresh() + response = self.c.get('/rowers/workout/nkimport/469',follow=True) expected_url = reverse('workout_nkimport_view') @@ -786,15 +792,16 @@ class NKObjects(DjangoTestCase): #self.assertEqual(w.inboard,0.89) #self.assertEqual(w.oarlength,2.87) - @patch('rowers.nkstuff.requests.get', side_effect=mocked_requests) - @patch('rowers.nkstuff.requests.post', side_effect=mocked_requests) + @patch('rowers.integrations.nk.requests.get', side_effect=mocked_requests) + @patch('rowers.integrations.nk.requests.post', side_effect=mocked_requests) @patch('rowers.nkimportutils.requests.session', side_effect=mocked_session) @patch('rowers.dataprep.getsmallrowdata_db', side_effect=mocked_getsmallrowdata_db) def test_nk_import_impeller(self, mock_get, mock_post, mocked_session, mocked_getsmallrowdata_db): - result = rowers.nkstuff.rower_nk_token_refresh(self.u) + integration = NKIntegration(self.u) + result = integration.token_refresh() response = self.c.get('/rowers/workout/nkimport/404',follow=True) expected_url = reverse('workout_nkimport_view') diff --git a/rowers/tests/testdata/testdata.tcx.gz b/rowers/tests/testdata/testdata.tcx.gz index f5a12aadc2cf639d2e461ba66f7807b43fcf1291..8a8e14fa8f1a5834aa6d5bfe3190c0853a1014cd 100644 GIT binary patch literal 4001 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 H#DD<+b+SXt literal 3999 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 F0RRHrK Date: Sun, 12 Feb 2023 17:09:09 +0100 Subject: [PATCH 08/27] passing tests --- rowers/integrations/nk.py | 2 +- rowers/nkstuff.py | 284 -------------------------- rowers/tests/testdata/testdata.tcx.gz | Bin 4001 -> 3999 bytes 3 files changed, 1 insertion(+), 285 deletions(-) delete mode 100644 rowers/nkstuff.py diff --git a/rowers/integrations/nk.py b/rowers/integrations/nk.py index 80f73df6..276d2a28 100644 --- a/rowers/integrations/nk.py +++ b/rowers/integrations/nk.py @@ -52,7 +52,7 @@ class NKIntegration(SyncIntegration): def workout_export(self, workout, *args, **kwargs) -> str: return "" # there is no export - def get_workouts(*args, **kwargs) -> int: + def get_workouts(self, *args, **kwargs) -> int: before = kwargs.get('before',0) after = kwargs.get('after',0) try: diff --git a/rowers/nkstuff.py b/rowers/nkstuff.py deleted file mode 100644 index 1671c563..00000000 --- a/rowers/nkstuff.py +++ /dev/null @@ -1,284 +0,0 @@ -from requests.auth import HTTPBasicAuth -from rowers.nkimportutils import * -from rowers.imports import * -from rowers.tasks import handle_nk_async_workout -from rowsandall_app.settings import ( - NK_CLIENT_ID, NK_REDIRECT_URI, NK_CLIENT_SECRET, - SITE_URL, NK_API_LOCATION, NK_OAUTH_LOCATION, - UPLOAD_SERVICE_URL, UPLOAD_SERVICE_SECRET, -) -import gzip -import rowers.mytypes as mytypes -from rowers.utils import myqueue -from iso8601 import ParseError -from rowers.rower_rules import is_workout_user, ispromember - -import time -from time import strftime - -import requests - -from rowers.utils import dologging -from json.decoder import JSONDecodeError - -# https:#oauth-stage.nkrowlink.com/oauth/authorizegrant_type=authorization_code&response_type=code&client_id=rowsandall-staging&scope=read&state=fc8fc3d8-ce0a-443e-838a-1c06fb5317c6&redirect_uri=https%3A%2F%2Fdunav.ngrok.io%2Fnk_callback%2F -# https:#oauth-stage.nkrowlink.com/oauth/authorize?grant_type=authorization_code&response_type=code&client_id=rowsandall-staging&scope=read&state=1234&redirect_uri=https%3A%2F%2Fdev.rowsandall.com%2Fnk_callback - -from requests_oauthlib import OAuth2Session - -import django_rq -queue = django_rq.get_queue('default') -queuelow = django_rq.get_queue('low') -queuehigh = django_rq.get_queue('low') - -oauth_data = { - 'client_id': NK_CLIENT_ID, - 'client_secret': NK_CLIENT_SECRET, - 'redirect_uri': NK_REDIRECT_URI, - 'autorization_uri': NK_OAUTH_LOCATION+"/oauth/authorize", - 'content_type': 'application/json', - 'tokenname': 'nktoken', - 'refreshtokenname': 'nkrefreshtoken', - 'expirydatename': 'nktokenexpirydate', - 'bearer_auth': True, - 'base_url': NK_OAUTH_LOCATION+"/oauth/token", - 'scope': 'read', -} - - -def get_token(code): # pragma: no cover - url = oauth_data['base_url'] - - post_data = {"client_id": oauth_data['client_id'], - "grant_type": "authorization_code", - "redirect_uri": oauth_data['redirect_uri'], - "code": code, - } - - response = requests.post(url, auth=HTTPBasicAuth(oauth_data['client_id'], oauth_data['client_secret']), - data=post_data) - - - if response.status_code != 200: - return [0, response.text, 0, 0] - - token_json = response.json() - - access_token = token_json['access_token'] - refresh_token = token_json['refresh_token'] - expires_in = token_json['expires_in'] - nk_owner_id = token_json['user_id'] - - return [access_token, expires_in, refresh_token, nk_owner_id] - - -def nk_open(user): - r = Rower.objects.get(user=user) - - if (r.nktoken == '') or (r.nktoken is None): # pragma: no cover - raise NoTokenError("User has no token") - else: - if (timezone.now() > r.nktokenexpirydate): - - thetoken = rower_nk_token_refresh(user) - if thetoken is None: # pragma: no cover - raise NoTokenError("User has no token") - return thetoken - else: - thetoken = r.nktoken - - return thetoken - - -def get_nk_workouts(rower, do_async=True, before=0, after=0): - try: - _ = nk_open(rower.user) - except NoTokenError: # pragma: no cover - dologging("nklog.log","NK Token error for user {id}".format(id=rower.user.id)) - return 0 - - res = get_nk_workout_list(rower.user, before=before, after=after) - - if res.status_code != 200: # pragma: no cover - dologging('nklog.log','Status code {code}'.format(code=res.status_code)) - return 0 - - #dologging('nklog.log',json.dumps(res.json())) - nkids = [item['id'] for item in res.json()] - dologging('nklog.log',json.dumps(nkids)) - alldata = {} - for item in res.json(): - alldata[item['id']] = item - - knownnkids = [ - w.uploadedtonk for w in Workout.objects.filter(user=rower) - ] - - tombstones = [ - t.uploadedtonk for t in TombStone.objects.filter(user=rower) - ] - - parkedids = [] - try: - with open('nkblocked.json', 'r') as nkblocked: - jsondata = json.load(nkblocked) - parkedids = jsondata['ids'] - except (FileNotFoundError, JSONDecodeError): # pragma: no cover - pass - - knownnkids = uniqify(knownnkids+tombstones+parkedids) - newids = [nkid for nkid in nkids if nkid not in knownnkids] - newids2 = newids.copy() - dologging('nklog.log',json.dumps(newids)) - dologging('nklog.log','Nr of new IDs is {i}'.format(i=len(newids))) - #if len(newids)>0: - # s = 'Starting NK Auto Import for user {id}'.format(id=r.user.id) - # dologging('nklog.log', s) - # s = 'New NK IDs {newids} (user {id})'.format(newids=newids,id=rower.user.id) - # dologging('nklog.log', s) - #else: - # dologging('nklog.log','Newids is false') - - newparkedids = uniqify(newids+parkedids) - - with open('nkblocked.json', 'wt') as nkblocked: - data = {'ids': newparkedids} - json.dump(data, nkblocked) - - counter = 0 - #dologging('nklog.log','Ik ben hier') - for nkid in newids2: - dologging('nklog.log','Queueing {id}'.format(id=nkid)) - res = myqueue(queuehigh, - handle_nk_async_workout, - alldata, - rower.user.id, - rower.nktoken, - nkid, - counter, - rower.defaulttimezone - ) - counter += 1 - - return 1 - - -def do_refresh_token(refreshtoken): - post_data = {"grant_type": "refresh_token", - # "client_id":NK_CLIENT_ID, - "refresh_token": refreshtoken, - } - - url = oauth_data['base_url'] - - response = requests.post(url, data=post_data, auth=HTTPBasicAuth( - oauth_data['client_id'], oauth_data['client_secret'])) - - if response.status_code != 200: # pragma: no cover - return [0, 0, 0] - - token_json = response.json() - - access_token = token_json['access_token'] - refresh_token = token_json['refresh_token'] - expires_in = token_json['expires_in'] - - return access_token, expires_in, refresh_token - - -def rower_nk_token_refresh(user): - r = Rower.objects.get(user=user) - res = do_refresh_token(r.nkrefreshtoken) - access_token = res[0] - expires_in = res[1] - refresh_token = res[2] - expirydatetime = timezone.now()+timedelta(seconds=expires_in) - - r.nktoken = access_token - r.nktokenexpirydate = expirydatetime - r.nkrefreshtoken = refresh_token - r.save() - - return r.nktoken - - -def make_authorization_url(request): # pragma: no cover - return imports_make_authorization_url(oauth_data) - - -def get_nk_workout_list(user, fake=False, after=0, before=0): - r = Rower.objects.get(user=user) - - if (r.nktoken == '') or (r.nktoken is None): # pragma: no cover - s = "Token doesn't exist. Need to authorize" - return custom_exception_handler(401, s) - elif (r.nktokenexpirydate is None or - timezone.now()+timedelta(seconds=10) > r.nktokenexpirydate): # pragma: no cover - s = "Token expired. Needs to refresh." - return custom_exception_handler(401, s) - else: - # ready to fetch. Hurray - if not before: # pragma: no cover - before = arrow.now()+timedelta(days=1) - before = str(int(before.timestamp())*1000) - if not after: # pragma: no cover - after = arrow.now()-timedelta(days=7) - after = str(int(after.timestamp())*1000) - authorizationstring = str('Bearer ' + r.nktoken) - headers = {'Authorization': authorizationstring, - 'user-agent': 'sanderroosendaal', - 'Content-Type': 'application/json', - } - - url = NK_API_LOCATION+"api/v1/sessions" - - params = { - 'after': after, - 'before': before, - } # start / end time - - s = requests.get(url, headers=headers, params=params) - - return s - -# - - -def get_workout(user, nkid, do_async=True, startdate='', enddate=''): - r = Rower.objects.get(user=user) - if (r.nktoken == '') or (r.nktoken is None): # pragma: no cover - s = "Token doesn't exist. Need to authorize" - return custom_exception_handler(401, s), 0 - elif (timezone.now() > r.nktokenexpirydate): # pragma: no cover - s = "Token expired. Needs to refresh." - return custom_exception_handler(401, s), 0 - - before = 0 - after = 0 - if startdate: # pragma: no cover - startdate = arrow.get(startdate) - after = str(int(startdate.timestamp())*1000) - if enddate: # pragma: no cover - enddate = arrow.get(enddate) - before = str(int(enddate.timestamp())*1000) - - res = get_nk_workout_list(r.user, before=before, after=after) - if res.status_code != 200: # pragma: no cover - # dologging('nklog.log','Status code {code}'.format(code=res.status_code)) - return 0 - alldata = {} - for item in res.json(): - alldata[item['id']] = item - - res = myqueue( - queuehigh, - handle_nk_async_workout, - alldata, - r.user.id, - r.nktoken, - nkid, - 0, - r.defaulttimezone, - ) - - return res diff --git a/rowers/tests/testdata/testdata.tcx.gz b/rowers/tests/testdata/testdata.tcx.gz index 8a8e14fa8f1a5834aa6d5bfe3190c0853a1014cd..101aac3a947b169a697c808ac0e1a519cc5ebe6f 100644 GIT binary patch literal 3999 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 H#DD<+b+SXt From 35f50ab7d3a465b2750373c4f34190affc053222 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Sun, 12 Feb 2023 19:05:48 +0100 Subject: [PATCH 09/27] typo --- rowers/integrations/nk.py | 14 +++++++------- rowers/tests/testdata/testdata.tcx.gz | Bin 3999 -> 4000 bytes 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/rowers/integrations/nk.py b/rowers/integrations/nk.py index 276d2a28..41bea80f 100644 --- a/rowers/integrations/nk.py +++ b/rowers/integrations/nk.py @@ -63,7 +63,7 @@ class NKIntegration(SyncIntegration): workouts = self.get_workout_list(before=before, after=after) - for workout in workuots: + for workout in workouts: nkid = workout['id'] if workout['new'] == 'NEW': dologging('nklog.log','Queueing {id}'.format(id=nkid)) @@ -113,13 +113,13 @@ class NKIntegration(SyncIntegration): after = kwargs.get('after',0) # For debugging - startdate = '2021-01-01' - enddate = '2021-06-01' - before = arrow.get(enddate) - before = str(int(before.timestamp()*1000)) + #startdate = '2021-01-01' + #enddate = '2021-06-01' + #before = arrow.get(enddate) + #before = str(int(before.timestamp()*1000)) - after = arrow.get(startdate) - after = str(int(after.timestamp()*1000)) + #after = arrow.get(startdate) + #after = str(int(after.timestamp()*1000)) r = self.rower authorizationstring = str('Bearer ' + r.nktoken) diff --git a/rowers/tests/testdata/testdata.tcx.gz b/rowers/tests/testdata/testdata.tcx.gz index 101aac3a947b169a697c808ac0e1a519cc5ebe6f..f7f55a427903f4ab12f33ce1c95dc7d54fb2700d 100644 GIT binary patch literal 4000 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 GfB^tE-7};B literal 3999 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 Date: Sun, 12 Feb 2023 19:57:13 +0100 Subject: [PATCH 10/27] temp file --- rowers/tests/testdata/testdata.tcx.gz | Bin 4000 -> 4000 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/rowers/tests/testdata/testdata.tcx.gz b/rowers/tests/testdata/testdata.tcx.gz index f7f55a427903f4ab12f33ce1c95dc7d54fb2700d..e3322f74f34401df6bc7b02ad7ae7912eb951e78 100644 GIT binary patch literal 4000 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 GfB^tmDJS^= literal 4000 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 GfB^tE-7};B From 410722a990c78f80e3443ac2997da6e5d7d0b571 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Mon, 13 Feb 2023 20:12:42 +0100 Subject: [PATCH 11/27] st half implemented --- rowers/integrations/__init__.py | 1 + rowers/integrations/c2.py | 2 +- rowers/integrations/integrations.py | 2 +- rowers/integrations/nk.py | 8 +- rowers/integrations/sporttracks.py | 143 +++++++++++++++++++++++ rowers/integrations/strava.py | 2 +- rowers/sporttracksstuff.py | 172 +--------------------------- rowers/tasks.py | 168 +++++++++++++++++++++++++++ rowers/uploads.py | 4 + rowers/views/workoutviews.py | 1 + 10 files changed, 327 insertions(+), 176 deletions(-) create mode 100644 rowers/integrations/sporttracks.py diff --git a/rowers/integrations/__init__.py b/rowers/integrations/__init__.py index 6677bbaa..343df09b 100644 --- a/rowers/integrations/__init__.py +++ b/rowers/integrations/__init__.py @@ -1,3 +1,4 @@ from .c2 import C2Integration from .strava import StravaIntegration from .nk import NKIntegration +from .sporttracks import SportTracksIntegration diff --git a/rowers/integrations/c2.py b/rowers/integrations/c2.py index 69774d33..45397cfa 100644 --- a/rowers/integrations/c2.py +++ b/rowers/integrations/c2.py @@ -56,7 +56,7 @@ def c2wc(weightclass): class C2Integration(SyncIntegration): def __init__(self, *args, **kwargs): - super(C2Integration, self).__init__(self, *args, **kwargs) + super(C2Integration, self).__init__(*args, **kwargs) self.oauth_data = { 'client_id': C2_CLIENT_ID, 'client_secret': C2_CLIENT_SECRET, diff --git a/rowers/integrations/integrations.py b/rowers/integrations/integrations.py index 03fb2a3a..8434ce1e 100644 --- a/rowers/integrations/integrations.py +++ b/rowers/integrations/integrations.py @@ -26,7 +26,7 @@ class SyncIntegration(metaclass=ABCMeta): rower = Rower() def __init__(self, *args, **kwargs): - user = args[1] + user = args[0] self.user = user self.rower = user.rower diff --git a/rowers/integrations/nk.py b/rowers/integrations/nk.py index 41bea80f..42b1d17a 100644 --- a/rowers/integrations/nk.py +++ b/rowers/integrations/nk.py @@ -1,8 +1,6 @@ from .integrations import SyncIntegration, NoTokenError from rowers.models import User, Rower, Workout, TombStone -from rowingdata import rowingdata - from rowers import mytypes from rowers.nkimportutils import * from rowers.tasks import handle_nk_async_workout @@ -31,7 +29,7 @@ queuehigh = django_rq.get_queue('low') class NKIntegration(SyncIntegration): def __init__(self, *args, **kwargs): - super(NKIntegration, self).__init__(self, *args, **kwargs) + super(NKIntegration, self).__init__(*args, **kwargs) self.oauth_data = { 'client_id': NK_CLIENT_ID, 'client_secret': NK_CLIENT_SECRET, @@ -310,7 +308,3 @@ class NKIntegration(SyncIntegration): return r.nktoken -# just as a quick test during development -u = User.objects.get(id=1) - -nk_integration_1 = NKIntegration(u) diff --git a/rowers/integrations/sporttracks.py b/rowers/integrations/sporttracks.py new file mode 100644 index 00000000..e6dcbccb --- /dev/null +++ b/rowers/integrations/sporttracks.py @@ -0,0 +1,143 @@ +from .integrations import SyncIntegration, NoTokenError +from rowers.models import User, Rower, Workout, TombStone + +from rowers.tasks import handle_sporttracks_sync +from rowers.rower_rules import is_workout_user +import rowers.mytypes as mytypes +from rowsandall_app.settings import ( + SPORTTRACKS_CLIENT_SECRET, SPORTTRACKS_CLIENT_ID, + SPORTTRACKS_REDIRECT_URI +) + +import re +import numpy + +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 + +class SportTracksIntegration(SyncIntegration): + def __init__(self, *args, **kwargs): + super(SportTracksIntegration, self).__init__(*args, **kwargs) + + self.oauth_data = { + 'client_id': SPORTTRACKS_CLIENT_ID, + 'client_secret': SPORTTRACKS_CLIENT_SECRET, + 'redirect_uri': SPORTTRACKS_REDIRECT_URI, + 'autorization_uri': "https://api.sporttracks.mobi/oauth2/authorize", + 'content_type': 'application/json', + 'tokenname': 'sporttrackstoken', + 'refreshtokenname': 'sporttracksrefreshtoken', + 'expirydatename': 'sporttrackstokenexpirydate', + 'bearer_auth': False, + 'base_url': "https://api.sporttracks.mobi/oauth2/token", + 'scope': 'write', + } + + + def open(self, *args, **kwargs) -> str: + return super(SportTracksIntegration, self).open(*args, **kwargs) + + def createworkoutdata(self, w, *args, **kwargs): + return None + + def workout_export(self, workout, *args, **kwargs) -> str: + pass + + def get_workouts(self, *args, **kwargs) -> int: + pass + + + + def get_workout(self, id) -> int: + _ = self.open() + + r = self.rower + + authorizationstring = str('Bearer ' + r.sporttrackstoken) + headers = {'Authorization': authorizationstring, + 'user-agent': 'sanderroosendaal', + 'Content-Type': 'application/json'} + url = "https://api.sporttracks.mobi/api/v2/fitnessActivities/" + \ + str(sporttracksid) + s = requests.get(url, headers=headers) + + data = s.json() + + strokedata = pd.DataFrame.from_dict({ + key: pd.Series(value, dtype='object') for key, value in data.items() + }) + + id = myqueue( + queue, + handle_sporttracks_workout_from_data, + self.user, + sporttracksid, data, + strokedata, + 'sporttracks', + 'sporttracks' + ) + return id + + + def get_workout_list(self, *args, **kwargs) -> list: + _ = self.open() + r = self.rower + + authorizationstring = str('Bearer ' + r.sporttrackstoken) + headers = {'Authorization': authorizationstring, + 'user-agent': 'sanderroosendaal', + 'Content-Type': 'application/json'} + url = "https://api.sporttracks.mobi/api/v2/fitnessActivities" + res = requests.get(url, headers=headers) + + if (res.status_code != 200): + s = "Token doesn't exist. Need to authorize" + raise NoTokenError(s) + + + workouts = [] + + knownstids = uniqify([ + w.uploadedtosporttracks for w in Workout.objects.filter(user=r) + ]) + for item in res.json()['items']: + d = int(float(item['total_distance'])) + i = int(getidfromuri(item['uri'])) + if i in knownstids: # pragma: no cover + nnn = '' + else: + nnn = 'NEW' + n = item['name'] + ttot = str(datetime.timedelta(seconds=int(float(item['duration'])))) + s = item['start_time'] + r = item['type'] + keys = ['id', 'distance', 'duration', + 'starttime', 'rowtype', 'source', 'name', 'new'] + values = [i, d, ttot, s, r, None, n, nnn] + res = dict(zip(keys, values)) + workouts.append(res) + + return workouts + + def make_authorization_url(self, *args, **kwargs) -> str: # pragma: no cover + return super(SportTracksIntegration, self).make_authorization_url(*args, **kwargs) + + def get_token(self, code, *args, **kwargs) -> (str, int, str): + return "" + + + + def token_refresh(self, *args, **kwargs) -> str: + return super(SportTracksIntegration, self).token_refresh(*args, **kwargs) + + + +# just as a quick test during development +u = User.objects.get(id=1) + +nk_integration_1 = SportTracksIntegration(u) + diff --git a/rowers/integrations/strava.py b/rowers/integrations/strava.py index 079bab33..efd83ccc 100644 --- a/rowers/integrations/strava.py +++ b/rowers/integrations/strava.py @@ -79,7 +79,7 @@ def strava_push_delete(id): # pragma: no cover class StravaIntegration(SyncIntegration): def __init__(self, *args, **kwargs): - super(StravaIntegration, self).__init__(self, *args, **kwargs) + super(StravaIntegration, self).__init__(*args, **kwargs) self.oauth_data = { 'client_id': STRAVA_CLIENT_ID, 'client_secret': STRAVA_CLIENT_SECRET, diff --git a/rowers/sporttracksstuff.py b/rowers/sporttracksstuff.py index 4b4f2dd8..e21cae76 100644 --- a/rowers/sporttracksstuff.py +++ b/rowers/sporttracksstuff.py @@ -1,4 +1,4 @@ -from rowers.tasks import handle_sporttracks_sync +from rowers.tasks import handle_sporttracks_sync, handle_sporttracks_workout_from_data from rowers.rower_rules import is_workout_user import rowers.mytypes as mytypes from rowsandall_app.settings import ( @@ -127,12 +127,14 @@ def get_workout(user, sporttracksid, do_async=False): key: pd.Series(value, dtype='object') for key, value in data.items() }) - id, message = add_workout_from_data( + id= myqueue( + queue, + handle_sporttracks_workout_from_data, user, sporttracksid, data, strokedata, - source='sporttracks', - workoutsource='sporttracks') + 'sporttracks', + 'sporttracks') return id @@ -346,165 +348,3 @@ def workout_sporttracks_upload(user, w, asynchron=False): # pragma: no cover # Create workout from SportTracks Data, which are slightly different # than Strava or Concept2 data - -def add_workout_from_data(user, importid, data, strokedata, source='sporttracks', - workoutsource='sporttracks'): - try: - workouttype = data['type'] - except KeyError: # pragma: no cover - workouttype = 'other' - - if workouttype not in [x[0] for x in Workout.workouttypes]: - workouttype = 'other' - try: - comments = data['comments'] - except: - comments = '' - - r = Rower.objects.get(user=user) - try: - rowdatetime = iso8601.parse_date(data['start_time']) - except iso8601.ParseError: # pragma: no cover - try: - rowdatetime = datetime.datetime.strptime( - data['start_time'], "%Y-%m-%d %H:%M:%S") - rowdatetime = thetimezone.localize(rowdatetime).astimezone(utc) - except: - try: - rowdatetime = dateutil.parser.parse(data['start_time']) - rowdatetime = thetimezone.localize(rowdatetime).astimezone(utc) - except: - rowdatetime = datetime.datetime.strptime( - data['date'], "%Y-%m-%d %H:%M:%S") - rowdatetime = thetimezone.localize(rowdatetime).astimezone(utc) - starttimeunix = arrow.get(rowdatetime).timestamp() - - try: - title = data['name'] - except: # pragma: no cover - title = "Imported data" - - try: - res = splitstdata(data['distance']) - distance = res[1] - times_distance = res[0] - except KeyError: # pragma: no cover - try: - res = splitstdata(data['heartrate']) - times_distance = res[0] - distance = 0*times_distance - except KeyError: - return (0, "No distance or heart rate data in the workout") - - try: - locs = data['location'] - - res = splitstdata(locs) - times_location = res[0] - latlong = res[1] - latcoord = [] - loncoord = [] - - for coord in latlong: - lat = coord[0] - lon = coord[1] - latcoord.append(lat) - loncoord.append(lon) - except: - times_location = times_distance - latcoord = np.zeros(len(times_distance)) - loncoord = np.zeros(len(times_distance)) - if workouttype in mytypes.otwtypes: # pragma: no cover - workouttype = 'rower' - - try: - res = splitstdata(data['cadence']) - times_spm = res[0] - spm = res[1] - except KeyError: # pragma: no cover - times_spm = times_distance - spm = 0*times_distance - - try: - res = splitstdata(data['heartrate']) - hr = res[1] - times_hr = res[0] - except KeyError: - times_hr = times_distance - hr = 0*times_distance - - # create data series and remove duplicates - distseries = pd.Series(distance, index=times_distance) - distseries = distseries.groupby(distseries.index).first() - latseries = pd.Series(latcoord, index=times_location) - latseries = latseries.groupby(latseries.index).first() - lonseries = pd.Series(loncoord, index=times_location) - lonseries = lonseries.groupby(lonseries.index).first() - spmseries = pd.Series(spm, index=times_spm) - spmseries = spmseries.groupby(spmseries.index).first() - hrseries = pd.Series(hr, index=times_hr) - hrseries = hrseries.groupby(hrseries.index).first() - - # Create dicts and big dataframe - d = { - ' Horizontal (meters)': distseries, - ' latitude': latseries, - ' longitude': lonseries, - ' Cadence (stokes/min)': spmseries, - ' HRCur (bpm)': hrseries, - } - - df = pd.DataFrame(d) - - df = df.groupby(level=0).last() - - cum_time = df.index.values - df[' ElapsedTime (sec)'] = cum_time - - velo = df[' Horizontal (meters)'].diff()/df[' ElapsedTime (sec)'].diff() - - df[' Power (watts)'] = 0.0*velo - - nr_rows = len(velo.values) - - df[' DriveLength (meters)'] = np.zeros(nr_rows) - df[' StrokeDistance (meters)'] = np.zeros(nr_rows) - df[' DriveTime (ms)'] = np.zeros(nr_rows) - df[' StrokeRecoveryTime (ms)'] = np.zeros(nr_rows) - df[' AverageDriveForce (lbs)'] = np.zeros(nr_rows) - df[' PeakDriveForce (lbs)'] = np.zeros(nr_rows) - df[' lapIdx'] = np.zeros(nr_rows) - - unixtime = cum_time+starttimeunix - unixtime[0] = starttimeunix - - df['TimeStamp (sec)'] = unixtime - - dt = np.diff(cum_time).mean() - wsize = round(5./dt) - - velo2 = ewmovingaverage(velo, wsize) - - df[' Stroke500mPace (sec/500m)'] = 500./velo2 - - df = df.fillna(0) - - df.sort_values(by='TimeStamp (sec)', ascending=True) - -# csvfilename ='media/Import_'+str(importid)+'.csv' - csvfilename = 'media/{code}_{importid}.csv'.format( - importid=importid, - code=uuid4().hex[:16] - ) - - res = df.to_csv(csvfilename+'.gz', index_label='index', - compression='gzip') - - id, message = dataprep.save_workout_database(csvfilename, r, - workouttype=workouttype, - title=title, - notes=comments, - dosmooth=r.dosmooth, - workoutsource='sporttracks') - - return (id, message) diff --git a/rowers/tasks.py b/rowers/tasks.py index 73e8e773..7e52311a 100644 --- a/rowers/tasks.py +++ b/rowers/tasks.py @@ -447,6 +447,174 @@ def handle_c2_sync(workoutid, url, headers, data, debug=False, **kwargs): return 1 +def splitstdata(lijst): + t = [] + latlong = [] + while len(lijst) >= 2: + t.append(lijst[0]) + latlong.append(lijst[1]) + lijst = lijst[2:] + + return [np.array(t), np.array(latlong)] + +@app.task +def handle_sporttracks_workout_from_data(user, importid, data, strokedata, source, + workoutsource, debug=False, **kwargs): + + try: + workouttype = data['type'] + except KeyError: # pragma: no cover + workouttype = 'other' + + if workouttype not in [x[0] for x in Workout.workouttypes]: + workouttype = 'other' + try: + comments = data['comments'] + except: + comments = '' + + r = Rower.objects.get(user=user) + rowdatetime = iso8601.parse_date(data['start_time']) + starttimeunix = arrow.get(rowdatetime).timestamp() + + try: + title = data['name'] + except: # pragma: no cover + title = "Imported data" + + try: + res = splitstdata(data['distance']) + distance = res[1] + times_distance = res[0] + except KeyError: # pragma: no cover + try: + res = splitstdata(data['heartrate']) + times_distance = res[0] + distance = 0*times_distance + except KeyError: + return (0, "No distance or heart rate data in the workout") + + try: + locs = data['location'] + + res = splitstdata(locs) + times_location = res[0] + latlong = res[1] + latcoord = [] + loncoord = [] + + for coord in latlong: + lat = coord[0] + lon = coord[1] + latcoord.append(lat) + loncoord.append(lon) + except: + times_location = times_distance + latcoord = np.zeros(len(times_distance)) + loncoord = np.zeros(len(times_distance)) + if workouttype in mytypes.otwtypes: # pragma: no cover + workouttype = 'rower' + + try: + res = splitstdata(data['cadence']) + times_spm = res[0] + spm = res[1] + except KeyError: # pragma: no cover + times_spm = times_distance + spm = 0*times_distance + + try: + res = splitstdata(data['heartrate']) + hr = res[1] + times_hr = res[0] + except KeyError: + times_hr = times_distance + hr = 0*times_distance + + # create data series and remove duplicates + distseries = pd.Series(distance, index=times_distance) + distseries = distseries.groupby(distseries.index).first() + latseries = pd.Series(latcoord, index=times_location) + latseries = latseries.groupby(latseries.index).first() + lonseries = pd.Series(loncoord, index=times_location) + lonseries = lonseries.groupby(lonseries.index).first() + spmseries = pd.Series(spm, index=times_spm) + spmseries = spmseries.groupby(spmseries.index).first() + hrseries = pd.Series(hr, index=times_hr) + hrseries = hrseries.groupby(hrseries.index).first() + + # Create dicts and big dataframe + d = { + ' Horizontal (meters)': distseries, + ' latitude': latseries, + ' longitude': lonseries, + ' Cadence (stokes/min)': spmseries, + ' HRCur (bpm)': hrseries, + } + + df = pd.DataFrame(d) + + df = df.groupby(level=0).last() + + cum_time = df.index.values + df[' ElapsedTime (sec)'] = cum_time + + velo = df[' Horizontal (meters)'].diff()/df[' ElapsedTime (sec)'].diff() + + df[' Power (watts)'] = 0.0*velo + + nr_rows = len(velo.values) + + df[' DriveLength (meters)'] = np.zeros(nr_rows) + df[' StrokeDistance (meters)'] = np.zeros(nr_rows) + df[' DriveTime (ms)'] = np.zeros(nr_rows) + df[' StrokeRecoveryTime (ms)'] = np.zeros(nr_rows) + df[' AverageDriveForce (lbs)'] = np.zeros(nr_rows) + df[' PeakDriveForce (lbs)'] = np.zeros(nr_rows) + df[' lapIdx'] = np.zeros(nr_rows) + + unixtime = cum_time+starttimeunix + unixtime[0] = starttimeunix + + df['TimeStamp (sec)'] = unixtime + + dt = np.diff(cum_time).mean() + wsize = round(5./dt) + + velo2 = ewmovingaverage(velo, wsize) + + df[' Stroke500mPace (sec/500m)'] = 500./velo2 + + df = df.fillna(0) + + df.sort_values(by='TimeStamp (sec)', ascending=True) + + + csvfilename = 'media/{code}_{importid}.csv'.format( + importid=importid, + code=uuid4().hex[:16] + ) + + res = df.to_csv(csvfilename+'.gz', index_label='index', + compression='gzip') + + uploadoptions = { + 'secret': UPLOAD_SERVICE_SECRET, + 'user': user.id, + 'file': csvfilename+'.gz', + 'title': '', + 'workouttype': workouttype, + 'boattype': '1x', + 'sporttracksid': importid, + 'title':title, + } + session = requests.session() + newHeaders = {'Content-type': 'application/json', 'Accept': 'text/plain'} + session.headers.update(newHeaders) + _ = session.post(UPLOAD_SERVICE_URL, json=uploadoptions) + + return 1 + @app.task def handle_sporttracks_sync(workoutid, url, headers, data, debug=False, **kwargs): diff --git a/rowers/uploads.py b/rowers/uploads.py index 7d699303..d0eb510f 100644 --- a/rowers/uploads.py +++ b/rowers/uploads.py @@ -236,6 +236,10 @@ def do_sync(w, options, quick=False): f.write(str(e)) do_st_export = w.user.sporttracks_auto_export + if options['sporttracksid'] != 0 and options['sporttracksid'] != '': + w.uploadedtost = options['sporttracksid'] + w.save() + do_st_export = False try: # pragma: no cover upload_to_st = options['upload_to_SportTracks'] or do_st_export do_st_export = upload_to_st diff --git a/rowers/views/workoutviews.py b/rowers/views/workoutviews.py index 9bafbd6e..b110322e 100644 --- a/rowers/views/workoutviews.py +++ b/rowers/views/workoutviews.py @@ -5209,6 +5209,7 @@ def workout_upload_api(request): # sync related IDs + sporttracksid = post_data.get('sporttracksid','') c2id = post_data.get('c2id', '') workoutid = post_data.get('id','') startdatetime = post_data.get('startdatetime', '') From 91583134d228ac1c042d2cea021ce595881bafb1 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Mon, 13 Feb 2023 22:50:01 +0100 Subject: [PATCH 12/27] sporttracks, not fully tested --- rowers/integrations/integrations.py | 2 +- rowers/integrations/sporttracks.py | 237 +++++++++++++++-- rowers/sporttracksstuff.py | 350 -------------------------- rowers/tasks.py | 17 +- rowers/tests/statements.py | 2 +- rowers/tests/test_imports.py | 30 +-- rowers/tests/testdata/testdata.tcx.gz | Bin 4000 -> 4001 bytes rowers/uploads.py | 12 +- rowers/views/importviews.py | 194 ++++---------- rowers/views/statements.py | 2 - rowers/views/workoutviews.py | 5 +- 11 files changed, 290 insertions(+), 561 deletions(-) delete mode 100644 rowers/sporttracksstuff.py diff --git a/rowers/integrations/integrations.py b/rowers/integrations/integrations.py index 8434ce1e..d014a221 100644 --- a/rowers/integrations/integrations.py +++ b/rowers/integrations/integrations.py @@ -71,7 +71,7 @@ class SyncIntegration(metaclass=ABCMeta): "scope": self.oauth_data['scope'], "state": state} - url = self.oauth_data['authorizaton_uri']+urllib.parse.urlencode(params) + url = self.oauth_data['authorization_uri']+urllib.parse.urlencode(params) return url diff --git a/rowers/integrations/sporttracks.py b/rowers/integrations/sporttracks.py index e6dcbccb..e790f9e2 100644 --- a/rowers/integrations/sporttracks.py +++ b/rowers/integrations/sporttracks.py @@ -1,7 +1,9 @@ from .integrations import SyncIntegration, NoTokenError from rowers.models import User, Rower, Workout, TombStone -from rowers.tasks import handle_sporttracks_sync +from rowingdata import rowingdata + +from rowers.tasks import handle_sporttracks_sync, handle_sporttracks_workout_from_data from rowers.rower_rules import is_workout_user import rowers.mytypes as mytypes from rowsandall_app.settings import ( @@ -11,13 +13,38 @@ from rowsandall_app.settings import ( import re import numpy +import pytz +import json +import requests +import datetime +import pandas as pd 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 rowers.utils import myqueue, dologging, uniqify + +def getidfromuri(uri): # pragma: no cover + m = re.search('/(\w.*)\/(\d+)', uri) + return m.group(2) + +def getidfromresponse(response): # pragma: no cover + t = response.json() + uri = t['uris'][0] + regex = '.*?sporttracks\.mobi\/api\/v2\/fitnessActivities/(\d+)\.json$' + m = re.compile(regex).match(uri).group(1) + + id = int(m) + + return int(id) + + +def default(o): # pragma: no cover + if isinstance(o, numpy.int64): + return int(o) + raise TypeError class SportTracksIntegration(SyncIntegration): def __init__(self, *args, **kwargs): @@ -27,7 +54,7 @@ class SportTracksIntegration(SyncIntegration): 'client_id': SPORTTRACKS_CLIENT_ID, 'client_secret': SPORTTRACKS_CLIENT_SECRET, 'redirect_uri': SPORTTRACKS_REDIRECT_URI, - 'autorization_uri': "https://api.sporttracks.mobi/oauth2/authorize", + 'authorization_uri': "https://api.sporttracks.mobi/oauth2/authorize", 'content_type': 'application/json', 'tokenname': 'sporttrackstoken', 'refreshtokenname': 'sporttracksrefreshtoken', @@ -42,13 +69,178 @@ class SportTracksIntegration(SyncIntegration): return super(SportTracksIntegration, self).open(*args, **kwargs) def createworkoutdata(self, w, *args, **kwargs): - return None + timezone = pytz.timezone(w.timezone) + + filename = w.csvfilename + try: + row = rowingdata(csvfile=filename) + except: # pragma: no cover + return {} + + try: + averagehr = int(row.df[' HRCur (bpm)'].mean()) + maxhr = int(row.df[' HRCur (bpm)'].max()) + except KeyError: # pragma: no cover + averagehr = 0 + maxhr = 0 + + try: + duration = w.duration.hour*3600 + duration += w.duration.minute*60 + duration += w.duration.second + duration += +1.0e-6*w.duration.microsecond + except AttributeError: # pragma: no cover + return {} + + t = row.df.loc[:, 'TimeStamp (sec)'].values - \ + row.df.loc[:, 'TimeStamp (sec)'].iloc[0] + try: + t[0] = t[1] + except IndexError: # pragma: no cover + return {} + + d = row.df.loc[:, 'cum_dist'].values + d[0] = d[1] + t = t.astype(int) + d = d.astype(int) + spm = row.df[' Cadence (stokes/min)'].astype(int).values + spm[0] = spm[1] + hr = row.df[' HRCur (bpm)'].astype(int).values + + haslatlon = 1 + + try: + lat = row.df[' latitude'].values + lon = row.df[' longitude'].values + if not lat.std() and not lon.std(): # pragma: no cover + haslatlon = 0 + except KeyError: + haslatlon = 0 + + haspower = 1 + try: + power = row.df[' Power (watts)'].astype(int).values + except KeyError: # pragma: no cover + haspower = 0 + + locdata = [] + hrdata = [] + spmdata = [] + distancedata = [] + powerdata = [] + + t = t.tolist() + hr = hr.tolist() + d = d.tolist() + spm = spm.tolist() + if haslatlon: + lat = lat.tolist() + lon = lon.tolist() + power = power.tolist() + + for i in range(len(t)): + hrdata.append(t[i]) + hrdata.append(hr[i]) + distancedata.append(t[i]) + distancedata.append(d[i]) + spmdata.append(t[i]) + spmdata.append(spm[i]) + if haslatlon: + locdata.append(t[i]) + locdata.append([lat[i], lon[i]]) + if haspower: + powerdata.append(t[i]) + powerdata.append(power[i]) + + try: + w.notes = w.notes+'\n from '+w.workoutsource+' via rowsandall.com' + except TypeError: + w.notes = 'from '+w.workoutsource+' via rowsandall.com' + + st = w.startdatetime.astimezone(timezone) + st = st.replace(microsecond=0) + + data = { + "type": "Rowing", + "name": w.name, + "start_time": st.isoformat(), + "total_distance": int(w.distance), + "duration": duration, + "notes": w.notes, + "avg_heartrate": averagehr, + "max_heartrate": maxhr, + "distance": distancedata, + "cadence": spmdata, + "heartrate": hrdata, + } + + if haslatlon: + data = { + "type": "Rowing", + "name": w.name, + "start_time": st.isoformat(), + "total_distance": int(w.distance), + "duration": duration, + "notes": w.notes, + "avg_heartrate": averagehr, + "max_heartrate": maxhr, + "location": locdata, + "distance": distancedata, + "cadence": spmdata, + "heartrate": hrdata, + } + + + if haspower: + data['power'] = powerdata + + return data + def workout_export(self, workout, *args, **kwargs) -> str: - pass + thetoken = self.open() + stid = "0" + # ready to upload. Hurray + + if not(is_workout_user(self.user, workout)): + return "0" + + authorizationstring = str('Bearer ' + thetoken) + headers = {'Authorization': authorizationstring, + 'user-agent': 'sanderroosendaal', + 'Content-Type': 'application/json'} + + data = self.createworkoutdata(workout) + + if not data: + return "0" + + url = "https://api.sporttracks.mobi/api/v2/fitnessActivities.json" + _ = myqueue( + queue, + handle_sporttracks_sync, + workout.id, + url, + headers, + json.dumps(data, default=default)) + + return 1 def get_workouts(self, *args, **kwargs) -> int: - pass + r = self.rower + workouts_json = self.get_workout_list_json(*args, **kwargs) + + stids = [int(getidfromuri(item['uri'])) + for item in workouts_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 = self.get_workout(sporttracksid) + + return 1 @@ -57,33 +249,19 @@ class SportTracksIntegration(SyncIntegration): r = self.rower - authorizationstring = str('Bearer ' + r.sporttrackstoken) - headers = {'Authorization': authorizationstring, - 'user-agent': 'sanderroosendaal', - 'Content-Type': 'application/json'} - url = "https://api.sporttracks.mobi/api/v2/fitnessActivities/" + \ - str(sporttracksid) - s = requests.get(url, headers=headers) - - data = s.json() - strokedata = pd.DataFrame.from_dict({ - key: pd.Series(value, dtype='object') for key, value in data.items() - }) - - id = myqueue( + job = myqueue( queue, handle_sporttracks_workout_from_data, self.user, - sporttracksid, data, - strokedata, + id, 'sporttracks', 'sporttracks' ) - return id + + return job.id - - def get_workout_list(self, *args, **kwargs) -> list: + def get_workout_list_json(self, *args, **kwargs) -> dict: _ = self.open() r = self.rower @@ -98,13 +276,18 @@ class SportTracksIntegration(SyncIntegration): s = "Token doesn't exist. Need to authorize" raise NoTokenError(s) + return res.json() + + def get_workout_list(self, *args, **kwargs) -> list: + r = self.rower + workouts_json = self.get_workout_list_json(*args, **kwargs) workouts = [] knownstids = uniqify([ w.uploadedtosporttracks for w in Workout.objects.filter(user=r) ]) - for item in res.json()['items']: + for item in workouts_json['items']: d = int(float(item['total_distance'])) i = int(getidfromuri(item['uri'])) if i in knownstids: # pragma: no cover @@ -127,7 +310,7 @@ class SportTracksIntegration(SyncIntegration): return super(SportTracksIntegration, self).make_authorization_url(*args, **kwargs) def get_token(self, code, *args, **kwargs) -> (str, int, str): - return "" + return super(SportTracksIntegration, self).get_token(code, *args, **kwargs) diff --git a/rowers/sporttracksstuff.py b/rowers/sporttracksstuff.py deleted file mode 100644 index e21cae76..00000000 --- a/rowers/sporttracksstuff.py +++ /dev/null @@ -1,350 +0,0 @@ -from rowers.tasks import handle_sporttracks_sync, handle_sporttracks_workout_from_data -from rowers.rower_rules import is_workout_user -import rowers.mytypes as mytypes -from rowsandall_app.settings import ( - C2_CLIENT_ID, C2_REDIRECT_URI, C2_CLIENT_SECRET, - STRAVA_CLIENT_ID, STRAVA_REDIRECT_URI, STRAVA_CLIENT_SECRET, - SPORTTRACKS_CLIENT_SECRET, SPORTTRACKS_CLIENT_ID, - SPORTTRACKS_REDIRECT_URI -) -import re -from rowers.imports import * -from rowers.utils import myqueue - -# All the functionality to connect to SportTracks - -import numpy - -import django_rq -queue = django_rq.get_queue('default') -queuelow = django_rq.get_queue('low') -queuehigh = django_rq.get_queue('low') - - -oauth_data = { - 'client_id': SPORTTRACKS_CLIENT_ID, - 'client_secret': SPORTTRACKS_CLIENT_SECRET, - 'redirect_uri': SPORTTRACKS_REDIRECT_URI, - 'autorization_uri': "https://api.sporttracks.mobi/oauth2/authorize", - 'content_type': 'application/json', - 'tokenname': 'sporttrackstoken', - 'refreshtokenname': 'sporttracksrefreshtoken', - 'expirydatename': 'sporttrackstokenexpirydate', - 'bearer_auth': False, - 'base_url': "https://api.sporttracks.mobi/oauth2/token", - 'scope': 'write', -} - -# Checks if user has SportTracks token, renews them if they are expired - - -def sporttracks_open(user): - return imports_open(user, oauth_data) - - -# Refresh ST token using refresh token -def do_refresh_token(refreshtoken): - return imports_do_refresh_token(refreshtoken, oauth_data) - -# Exchange ST access code for long-lived ST access token - - -def get_token(code): - return imports_get_token(code, oauth_data) - -# Make authorization URL including random string - - -def make_authorization_url(request): # pragma: no cover - return imports_make_authorization_url(oauth_data) - -# This is token refresh. Looks for tokens in our database, then refreshes - - -def rower_sporttracks_token_refresh(user): # pragma: no cover - r = Rower.objects.get(user=user) - res = do_refresh_token(r.sporttracksrefreshtoken) - access_token = res[0] - expires_in = res[1] - refresh_token = res[2] - expirydatetime = timezone.now()+timedelta(seconds=expires_in) - - r = Rower.objects.get(user=user) - r.sporttrackstoken = access_token - r.tokenexpirydate = expirydatetime - r.sporttracksrefreshtoken = refresh_token - - r.save() - - return r.sporttrackstoken - -# Get list of workouts available on SportTracks - - -def get_sporttracks_workout_list(user): - r = Rower.objects.get(user=user) - if (r.sporttrackstoken == '') or (r.sporttrackstoken is None): - s = "Token doesn't exist. Need to authorize" - return custom_exception_handler(401, s) - elif (timezone.now() > r.sporttrackstokenexpirydate): # pragma: no cover - s = "Token expired. Needs to refresh." - return custom_exception_handler(401, s) - else: - # ready to fetch. Hurray - authorizationstring = str('Bearer ' + r.sporttrackstoken) - headers = {'Authorization': authorizationstring, - 'user-agent': 'sanderroosendaal', - 'Content-Type': 'application/json'} - url = "https://api.sporttracks.mobi/api/v2/fitnessActivities" - s = requests.get(url, headers=headers) - - return s - -# Get workout summary data by SportTracks ID - - -def get_workout(user, sporttracksid, do_async=False): - r = Rower.objects.get(user=user) - if (r.sporttrackstoken == '') or (r.sporttrackstoken is None): # pragma: no cover - return custom_exception_handler(401, s) - s = "Token doesn't exist. Need to authorize" - elif (timezone.now() > r.sporttrackstokenexpirydate): # pragma: no cover - s = "Token expired. Needs to refresh." - return custom_exception_handler(401, s) - else: - # ready to fetch. Hurray - authorizationstring = str('Bearer ' + r.sporttrackstoken) - headers = {'Authorization': authorizationstring, - 'user-agent': 'sanderroosendaal', - 'Content-Type': 'application/json'} - url = "https://api.sporttracks.mobi/api/v2/fitnessActivities/" + \ - str(sporttracksid) - s = requests.get(url, headers=headers) - - data = s.json() - - strokedata = pd.DataFrame.from_dict({ - key: pd.Series(value, dtype='object') for key, value in data.items() - }) - - id= myqueue( - queue, - handle_sporttracks_workout_from_data, - user, - sporttracksid, data, - strokedata, - 'sporttracks', - 'sporttracks') - - return id - -# Create Workout Data for upload to SportTracks - - -def createsporttracksworkoutdata(w): - timezone = pytz.timezone(w.timezone) - - filename = w.csvfilename - try: - row = rowingdata(csvfile=filename) - except: # pragma: no cover - return 0 - - try: - averagehr = int(row.df[' HRCur (bpm)'].mean()) - maxhr = int(row.df[' HRCur (bpm)'].max()) - except KeyError: # pragma: no cover - averagehr = 0 - maxhr = 0 - - try: - duration = w.duration.hour*3600 - duration += w.duration.minute*60 - duration += w.duration.second - duration += +1.0e-6*w.duration.microsecond - except AttributeError: # pragma: no cover - return 0 - - # adding diff, trying to see if this is valid - # t = row.df.loc[:,'TimeStamp (sec)'].values-10*row.df.ix[0,'TimeStamp (sec)'] - t = row.df.loc[:, 'TimeStamp (sec)'].values - \ - row.df.loc[:, 'TimeStamp (sec)'].iloc[0] - try: - t[0] = t[1] - except IndexError: # pragma: no cover - return 0 - - d = row.df.loc[:, 'cum_dist'].values - d[0] = d[1] - t = t.astype(int) - d = d.astype(int) - spm = row.df[' Cadence (stokes/min)'].astype(int).values - spm[0] = spm[1] - hr = row.df[' HRCur (bpm)'].astype(int).values - - haslatlon = 1 - - try: - lat = row.df[' latitude'].values - lon = row.df[' longitude'].values - if not lat.std() and not lon.std(): # pragma: no cover - haslatlon = 0 - except KeyError: - haslatlon = 0 - - haspower = 1 - try: - power = row.df[' Power (watts)'].astype(int).values - except KeyError: # pragma: no cover - haspower = 0 - - locdata = [] - hrdata = [] - spmdata = [] - distancedata = [] - powerdata = [] - - t = t.tolist() - hr = hr.tolist() - d = d.tolist() - spm = spm.tolist() - if haslatlon: - lat = lat.tolist() - lon = lon.tolist() - power = power.tolist() - - for i in range(len(t)): - hrdata.append(t[i]) - hrdata.append(hr[i]) - distancedata.append(t[i]) - distancedata.append(d[i]) - spmdata.append(t[i]) - spmdata.append(spm[i]) - if haslatlon: - locdata.append(t[i]) - locdata.append([lat[i], lon[i]]) - if haspower: - powerdata.append(t[i]) - powerdata.append(power[i]) - - try: - w.notes = w.notes+'\n from '+w.workoutsource+' via rowsandall.com' - except TypeError: - w.notes = 'from '+w.workoutsource+' via rowsandall.com' - - st = w.startdatetime.astimezone(timezone) - st = st.replace(microsecond=0) - - if haslatlon: - data = { - "type": "Rowing", - "name": w.name, - "start_time": st.isoformat(), - "total_distance": int(w.distance), - "duration": duration, - "notes": w.notes, - "avg_heartrate": averagehr, - "max_heartrate": maxhr, - "location": locdata, - "distance": distancedata, - "cadence": spmdata, - "heartrate": hrdata, - } - else: - data = { - "type": "Rowing", - "name": w.name, - "start_time": st.isoformat(), - "total_distance": int(w.distance), - "duration": duration, - "notes": w.notes, - "avg_heartrate": averagehr, - "max_heartrate": maxhr, - "distance": distancedata, - "cadence": spmdata, - "heartrate": hrdata, - } - - if haspower: - data['power'] = powerdata - - return data - -# Obtain SportTracks Workout ID from the response returned on successful -# upload - - -def getidfromresponse(response): # pragma: no cover - t = response.json() - uri = t['uris'][0] - regex = '.*?sporttracks\.mobi\/api\/v2\/fitnessActivities/(\d+)\.json$' - m = re.compile(regex).match(uri).group(1) - - id = int(m) - - return int(id) - - -def default(o): # pragma: no cover - if isinstance(o, numpy.int64): - return int(o) - raise TypeError - - -def workout_sporttracks_upload(user, w, asynchron=False): # pragma: no cover - message = "Uploading to SportTracks" - stid = 0 - # ready to upload. Hurray - - thetoken = sporttracks_open(user) - - if (is_workout_user(user, w)): - data = createsporttracksworkoutdata(w) - if not data: - message = "Data error" - stid = 0 - return message, stid - - authorizationstring = str('Bearer ' + thetoken) - headers = {'Authorization': authorizationstring, - 'user-agent': 'sanderroosendaal', - 'Content-Type': 'application/json'} - - url = "https://api.sporttracks.mobi/api/v2/fitnessActivities.json" - if asynchron: - _ = myqueue(queue, handle_sporttracks_sync, - w.id, url, headers, json.dumps(data, default=default)) - return "Asynchronous sync", 0 - - response = requests.post(url, headers=headers, - data=json.dumps(data, default=default)) - - # check for duplicate error first - if (response.status_code == 409): - message = "Duplicate error" - w.uploadedtosporttracks = -1 - stid = -1 - w.save() - return message, stid - elif (response.status_code == 201 or response.status_code == 200): - s = response.json() - stid = getidfromresponse(response) - w.uploadedtosporttracks = stid - w.save() - return 'Successfully synced to SportTracks', stid - else: - s = response - message = "Something went wrong in workout_sporttracks_upload_view: %s" % s.reason - stid = 0 - return message, stid - - else: - message = "You are not authorized to upload this workout" - stid = 0 - return message, stid - - return message, stid - -# Create workout from SportTracks Data, which are slightly different -# than Strava or Concept2 data - diff --git a/rowers/tasks.py b/rowers/tasks.py index 7e52311a..a327a511 100644 --- a/rowers/tasks.py +++ b/rowers/tasks.py @@ -458,9 +458,24 @@ def splitstdata(lijst): return [np.array(t), np.array(latlong)] @app.task -def handle_sporttracks_workout_from_data(user, importid, data, strokedata, source, +def handle_sporttracks_workout_from_data(user, importid, source, workoutsource, debug=False, **kwargs): + r = user.rower + authorizationstring = str('Bearer ' + r.sporttrackstoken) + headers = {'Authorization': authorizationstring, + 'user-agent': 'sanderroosendaal', + 'Content-Type': 'application/json'} + url = "https://api.sporttracks.mobi/api/v2/fitnessActivities/" + \ + str(importid) + s = requests.get(url, headers=headers) + + data = s.json() + + strokedata = pd.DataFrame.from_dict({ + key: pd.Series(value, dtype='object') for key, value in data.items() + }) + try: workouttype = data['type'] except KeyError: # pragma: no cover diff --git a/rowers/tests/statements.py b/rowers/tests/statements.py index f78857cf..7a18725d 100644 --- a/rowers/tests/statements.py +++ b/rowers/tests/statements.py @@ -64,7 +64,7 @@ from mock import Mock, patch #from minimocktest import MockTestCase import pandas as pd import rowers.c2stuff as c2stuff -import rowers.sporttracksstuff as sporttracksstuff + import rowers.rojabo_stuff as rojabo_stuff from django.urls import reverse, reverse_lazy diff --git a/rowers/tests/test_imports.py b/rowers/tests/test_imports.py index 8426aee5..6a2f5ee2 100644 --- a/rowers/tests/test_imports.py +++ b/rowers/tests/test_imports.py @@ -1236,7 +1236,7 @@ class STObjects(DjangoTestCase): csvfilename=filename ) - @patch('rowers.sporttracksstuff.requests.post', side_effect=mocked_requests) + @patch('rowers.integrations.sporttracks.requests.post', side_effect=mocked_requests) def test_sporttracks_callback(self, mock_post): response = self.c.get('/sporttracks_callback?code=dsdoij232s',follow=True) @@ -1244,15 +1244,15 @@ class STObjects(DjangoTestCase): self.assertEqual(response.status_code, 200) - @patch('rowers.sporttracksstuff.requests.post', side_effect=mocked_requests) + @patch('rowers.integrations.sporttracks.requests.post', side_effect=mocked_requests) def test_sporttracks_token_refresh(self, mock_post): response = self.c.get('/rowers/me/sporttracksrefresh/',follow=True) self.assertEqual(response.status_code, 200) - @patch('rowers.sporttracksstuff.requests.post', side_effect=mocked_requests) - @patch('rowers.sporttracksstuff.requests.get', side_effect=mocked_requests) + @patch('rowers.integrations.sporttracks.requests.post', side_effect=mocked_requests) + @patch('rowers.integrations.sporttracks.requests.get', side_effect=mocked_requests) def test_sporttracks_upload(self, mock_get, mock_post): response = self.c.get('/rowers/workout/'+encoded1+'/sporttracksuploadw/') @@ -1263,7 +1263,7 @@ class STObjects(DjangoTestCase): self.assertEqual(response.url, '/rowers/workout/'+encoded1+'/edit/') self.assertEqual(response.status_code, 302) - @patch('rowers.sporttracksstuff.requests.get', side_effect=mocked_requests) + @patch('rowers.integrations.sporttracks.requests.get', side_effect=mocked_requests) def test_sporttracks_list(self, mock_get): response = self.c.get('/rowers/workout/sporttracksimport',follow=True) @@ -1306,26 +1306,6 @@ class STObjects(DjangoTestCase): self.assertEqual(response.status_code, 200) - @patch('rowers.dataprep.create_engine') - def test_strokedata(self, mocked_sqlalchemy): - with open('rowers/tests/testdata/sporttrackstestdata.txt','r') as infile: - data = json.load(infile) - - from rowers.sporttracksstuff import add_workout_from_data - - res = add_workout_from_data(self.u,1,data,data) - - @patch('rowers.dataprep.create_engine') - def test_strokedatanohr(self, mocked_sqlalchemy): - with open('rowers/tests/testdata/sporttrackstestnohr.txt','r') as infile: - data = json.load(infile) - - from rowers.sporttracksstuff import add_workout_from_data - - - - res = add_workout_from_data(self.u,1,data,data) - #@pytest.mark.django_db diff --git a/rowers/tests/testdata/testdata.tcx.gz b/rowers/tests/testdata/testdata.tcx.gz index e3322f74f34401df6bc7b02ad7ae7912eb951e78..f87905524517a7050d53b8859644520469a3beb5 100644 GIT binary patch delta 3906 zcmV-I554f9AE6%yABzYGfUoM22OkxRnscdB7sogvkkujq8wHfj+p1$GkvkzUAK#sx z?7dlDY*uIM`*+d6-Mt?V?;O0i=vM31`uO2;-7ha5b$$0ow^{C`o!6WDk&G={2S@$t z&8mO@^Sgez&Nq1F1n)Mh&(83c&Dq8Af&g&w`_m`Sx2L$bT5tMpeY8wRZC3lc^FBE{ z>iX@kz0nQq>5qCt=H2GyUmy7Qu7B8_oL#Jzo5dUa>;3(Z@8A7r|7v-0 zclTxb`ti#{0(AF4?h(8|-nY0<&O}EX;co!@00)b-)5ravbmx1|`|hHDu{vGezx!9m zUS232`r@qbPV!;Tmq%yomzzUjf3Wy!>!$-At#)tF(`EXxn{=P~UcT7)Y44Bt4iUq_ z;_}m{9Udf6>yDRy|5&EG|F!FvKb@a$U(NpgdHN-nn;)-#T6X>Bm*vIN)%tb6{C9ul zN2JsI)}6dw9->Vb+5P$H3X5wlapmi$-MiK4>(l?!4T!tu2Clipm9HPKx36md{&4$N zEECzwEmt4p%Vm1NhlKet?*6_)@nUs+oNjH|zdCz)__SMpzwS;F#m}~0-Mc&b?`LDI=ouy3?@mt zSOs-tgVVr&o*#5Y+{snMJwZfpLBXAp-c*o#xH)&KOWv(i#N8QURE_d!h~R?U<7~J) zTM_q|I+_5U|0V?7yiF*<%jW+L%n+W&1 z z@1Y{@&Nv{@=yg$w>htrFcZ7-*JBfx_=cYv{OP+5*p6Vh`)lBiU56P4hohgSXC4Dz+ z=Ognh6_K|lCZ%2_^CTt|Brh`}A1flC%sD|0s_={}YR9|zkhiWL@(9LRB$do3tRY|6cNb?wo+={m3_6_?2c0iD&s%7t{V3#vi6mANo`tNQ< zzK!;S1lqEl(2pvjARvlR*fT+^&Vk~LT&xyPZ6_HPRIuojruOOC;(pj3( zXpa?A^Bfou3qdkjZ2VOJJlPyc$&WLe z;$(FD7Ns2~?~E4=nkX$37PaT+M7|<>K3iyPRr5SEnm7^i*yj1`$#c%VWkAw+a?*=Z zGV9v9=C>b(ycl2zDw!858nJ2Q$06@cND_}4@{XI#-;g}lLmt6MfS_uU;kl^H`=;BE zLY@s<6xBQ*xTuEbo6L_w-WiN2Dw&Uewy5{E$^1Cvx4E~f7HGC?Mdhu?WAot+kAnX`GH!El-Q~x|JDKQH!mJJR9$HQCb^g$()=yk*}x5hI8(%^N50 z3DFT2)$Qj*zNXPW8bA!1_`G8(T1D+!e0~h_IrlbT(Dc1^mWt-y&WU_~MfN;dLk?Lr z&kIc^_m*4z{2+O6&?2cudq+j>`5BRSR1x{yIe}Qy-`;sHI?vCEd_}r_($+YlYSl6q zxJ7AaKwb=hsJfiH$Sq1c{n4Ji5z%!*^w7xPlzc_Hy#>QwRGu7;SH1L z-2GJLz4gfwj4cCm^5n&TcW~~oRK|WQGDJ8W06slljGCdVkbhrHXmN7 zhrD2}rB$tI;iYK({G4RICJj&6cp=pI@Vs*+{r0n(=PR=3ll*cdRgI1Ju2odtkbFf^ zT8RdjKsDM2HyPc&d2a{E%RZp-N*|t&MOPu8mCQ>;@|H}b>eqj71l*NpaoLQ#8uPNRLOhdh$87NaKXI&f?=AqUx3sT+!7@=j6ThR1bNA+#8EEo}5s2jY^?LeiZU-kaRsTLnyjx z*__DNG|!WmAQ&}&8JHnr(aa2*(LC=fA|H$-nb%ZWM4%?~Q;*fjKBKWrs?3at+{jNo zVw1>QL+PK2JQZEXCfA>~%#0Dp`<#cT>W3G(=sGrHXVm?)Ci2k)hfyW-g|o42>w7x} zc}vELuDU7~&c>S4y0jHVX_Nd4D|LJ4iduDZB43dw#|A@xkwBAgAH6R++Pk(E^%&%L zxwpD`K7^uH-JHl*EqR4fvKOgC1MI>p}Fv2ojPC zeNqj1B1V0GllnpQxt%E?>(Dz})V7m0*N=kU8Y`&s;UaixQh!7AR1tcgTV#1up$~2{ zQePQ*cJ-i7cA5YZUgQqA>Rk0x5BONYP}HSVZ+^Tt4*l=X-lt{(+Gp@~GGO3-4O&YEV;hd+$on!w8H z8)i3|rfDrpXBfRV-U_QUe70rRxuI5bHwOBgVJWDquV*RSPKBD9Hwt<-0vL5t@4{pT zd~69?gXo>Hs9Vbr*~g-d)2P|?qo9w5B3hk>AIgSrxOwx&KyNAc&(eAGIL4;tO@F|D zw_-q)g;y3~GV%J_lCdia+S;VCA;qXdPt@l6>7y4gQMqt2TBf3ryXM0m2R)nUkaXxR zPiA?u=ExmJzipZ5Rcf9U+gv)+cjRtcCZgBP^-d;}pw-ec2hfLY%S6yEd4twVGw!Bt z*N5CP(TbqTk!wS%zPTy-P!al&J80>Dx@y>1HhjZvz#juWqG1-Nl6rx%%hmIY)I6vN zz1_A}P^o!RxQ)`B>g#Pq=)EDU4B84qQE#4Y1O6E32@Db=tI%7R%-V*{n>UPpTVzeD zczv=6T9Ne(q~7JO8c~PdQBmWrb=4RLeaKSpbpyWV$w+-0@CVT|njox;*GsT}MK$~u zv>gY%H4YI~T{R@y=FOYFBbUALOjZ@Nb>JqGs?pTELG;N^vkrAxIu2bM@TZ=em(a5j z%1#;$dYVoNMr)%qjNTawInh#so@STG; z*UtdG7$U8j=b>ffPQCCd7?FD(a0S%HE$I3!LVS^NxitDsTxhq8wEWj(>vX zXA^{Uk-PMwl+^HZCyIuDegyQ+c(zgH(g_Yrx^!lRUMfN#^P0AxlX{Ou2Yla3FvdVH zWSqsI%F+p;Xr=9~(6^eqVf2!#Z-Oc#H^icqwznGaTQ8kq^dYZltBc%4D7x6$tTg;u zvUCKtD_H}2l_NKzOva@Ht#95S`t3^Ax^?a%l?~cLo9jnG?{oEkjS@Zk$*gYK=K4YO zc_nK`QhD>D^+l_1sP)Yo1wC&UF;^LPQDR%&a^l{+jNawKDGkU5<>k+R{VlzZ>Ch_==fjq#-G9<0 zv$Npp=dX8LtNpZ42sxO}uDI@OA07Vu zEVJ4Ex!e9rdW)aEKK)~v?hN1_!0!Kp#g#91Pk#HD4)1JHuh!`~JWME;7mw1@_#<8V Q@Xr4Lk6Fb9SHyq;0Q{}NE{<_TAge_JHVP=4w^hdskvkzUpWdCG z?7dlDY*uIMdw0>m-MyaPs+N}0>=Y4#3 z)b-n6d!rlJ(;xMQ%)8BtzdrEqUH_mvIlEXbH;Xs;*ZaSd6agiFJ3qVV@7?`p|7v-0 zclTxb`q7I+0(3us{6O#=dEeqbITIamguemo0~{>UP9OJw+@0?|>${8o`Ra6e@9tk6 zdwHRB=<~C_JIRMVTOOUQUu+JA{lVg^t)C8fxZ1rzPnPM&Zqj|`d--DHr@cSoJ46fz zi_1@+cDSEJtvgdD-=wUzZn8R_oXO^56ZH zACXS;dw24Bd5AV$WcTN%D=e+ou+Gngdp zVinYp4Ne1pdw$RraVJ+1_XH8a1qF9XdQ(B};pW__E_t_75qD>ZQ8mh^A%Y8XkF(+K zY(?B-l6Na!!#$RicZS(;_Z4xsv>oT2g1Zfd3UbF=aL1~+2dId?9gyotqY+EP1{Kd8&&%RWrrYJ|t64bfz4ll=R)O zosZ13R7BpIn3Q^z%#)ZA$-b z`8L`Qk|#8dz^g|4P?r3BB6KJfS#@NM0HR96i?O6VKPU1wR75`M=}f3fzJgdXN@rNMcDJ-mJ)1v@+SerWVoiFsKpOIY zh?CLnTaZ3-tx8#b-d}vnPDCE7Nh)(0Zwd{)K+lMyVk3pVc+Y(Eo z+gt03>h__<=SLw=#xX=y6FezPeohsYt|IbctT3zQc~`WSc2?x2BJ#l`?y3xbOk1|1 zQlQEF800OQq;Wb@wLC4_>sDq|MJ=`>@@%}*MQLq}C3AA-M82LH8_v16&Le6@dqJ3t z%r|>}n0z!8AO;or*mV2pM{I<0w-i;BRv4OYKYf`OYeLfPRWfhOW?jctOFIgAZ-jLb z8xCiAb#R-a5dRdpmuZM>Z0OG*McI(;?5zlQT%(<*aK} zCqzeBRJWfK`I<)iXaF&2;`5HFXce_@@%b^x=iJ+XLDTovSt^=)J16pg71{G-4LM}h zJTEkv+*@w-^MmBQL5rjs?Hv`h=VwITQAOl)=LBL+e|zV-=sZ6s@)ha!Nn7KHs#VKe z;1;Ew0eLY1qUv()BDW~*^hbO4Mnu;M(L*DDQ}Pw*_7)6#QF(Ga+L9XHt@7}ihBr)} zbN5q~_tqy%Ft!ZL$&(j<kH)3JUNhPU3PtVtIgR$A9`Z=WT8x^k>%g(e{0+&+ipYzBpd9UO z(Ul5k<;kgNv`25eh^kvka79-qos;+0Q$6Gfa&IiwcydD7H7bP~`BBKTLDKcW458?% zWpg55(>zaNf?(8tWMGDfMKd#KM)SO{hFykn_Pd|GBZXX?{glWsvlnDqU+d*ol*DGn#e~J97dJQ7tY4At?%s^ z(W*frA_iHtkmtDD{9rviF`$#92*ROMFLH}ee}NQXz$uu)MJp} z<=*P%`4Ea)b#o$Lktauzg~kLb@-Y<6$#HF-ABDU#!4hktv$+t_RTvBS=Up z^hq`3i5T^NP3i~H=XR!qtV8c?QQJ=1Tt5nWYpkHkhl}8)N&OAcQ$^@~Zjt3tg+92+ zNPT7K+0}zS*=aTqSQYx{i>i5Z8t}0q^ql5;0@YlP5GI4ZwgJB;sdt6}Nj29amMyoL z6MC))easqu)Jc8WRSO&XanR>ZsE$>cS(s+5rs2Z@Y$AK=Z0F%-5BU|hNYmczMiFQI~8hb-YDqV2w>Dny$h2W z@UbOm4Wf6(qHZliWFLz*PNQbmkAglLifDBjekdEh;pWX71HGl(KTGG$;~1NoH~j&B z---cI7G7C|$;9hxOUAA!Xls+kh7_X;JyDzMr;lF1MCHQ8Xqk#e?wSvO9Q16WL(-wQ zJelRqnj?1@{kCPISE+ecY;);M-;uj*nTTFD*E^X^f>uk*96%qoEfYbv#AX6+3*dw0e=kih=y65O6mp5E?3VpQuCl9 z^mf}?L8azN;WkQhs;{>dq4$QYGH5FdMZI~p4ftcACoo8itU_;LGHV+)Z{9HaZILyp z;`PZQXhqgDkb0N9YD67+M@5ah)>UI1^dU>V*A4idCnNQ3z#l}TOzj$HP}Gg(#8)`6Q$szy`u2GJ)w%{tU&={R(4z@K_qa zTt5T!Vu-YAo`;r^JN3e=kXt5yV%nxdA8;}aRhpVNfIjAyiGUb&c73FlmN^6TYyv2~ zc^=9xXAG^E&M4?zMjusny+KYLZ4T%1`Im%94CXmtzSQk zo=p(aMefpzQc}auohTZA`Vr7OC%}MdZ`F~%xl_$PU<}t9q@fC!59O* zkZ~4+DoZDXqLsF{Lf>lchS5u|z6q*~+z^Xa+TLowZ@qMe(TBXItuAsGq3B{~v(oTu z$(;r8R5oY}ZLS{$z0cKuH%j#EC$qX`o9hSB z=asA(N#)Ip))%e5q1HEV6!g4Z#9U?EMTu>7%ZYpQGJ2N_uaxLrEGqS_QR+D8V=lbX z+4V6%Yo0c7Z(c&bUCCM*xr-57XY3iEwss&I8p!LYTDSAH^`VU(_Za7#x?#?ei zJU#1|o5Sx;`X7IPIa!`A*Zto2+fCo^b^RW^^DjGi`I5bVY2mx${>OvG?tnYHGkyGx z`Sazc)$aR)2N#Ls*~N!_pLXf(xj1{f>DDj0lauY!N*`D2;}>1urSo>Xw<}Tkblk!H z*Zr%riz~0m{qysa)lt`{pMLpbT%P-@lcn!|e!V(*artw9e^2jYI`qoJ`LN|l_n&mh z>@2wY`Rm$KU+r~S4YE+6d|=RbI!Zu;=I<;k+!EE9+P>BFPDa$g6JyY=y-hljsB z&1|-R?zaD$-r}dPPybw|I|KLuVE6yQ;>s7hC%=76hj+H9SL^f~9wd~@i-+lH{FyF& Pc;|oY=e?H%#DD<+#Bu1q diff --git a/rowers/uploads.py b/rowers/uploads.py index d0eb510f..3a784473 100644 --- a/rowers/uploads.py +++ b/rowers/uploads.py @@ -2,7 +2,6 @@ from rowers.mytypes import workouttypes, boattypes, otwtypes, workoutsources, wo from rowers.rower_rules import is_promember import rowers.tpstuff as tpstuff -import rowers.sporttracksstuff as sporttracksstuff from rowers.integrations import * from rowers.utils import ( @@ -131,7 +130,6 @@ def make_plot(r, w, f1, f2, plottype, title, imagename='', plotnr=0): def do_sync(w, options, quick=False): - do_strava_export = w.user.strava_auto_export try: do_strava_export = options['upload_to_Strava'] or do_strava_export @@ -236,8 +234,10 @@ def do_sync(w, options, quick=False): f.write(str(e)) do_st_export = w.user.sporttracks_auto_export + if options['sporttracksid'] != 0 and options['sporttracksid'] != '': - w.uploadedtost = options['sporttracksid'] + w.uploadedtosporttracks = options['sporttracksid'] + w.save() do_st_export = False try: # pragma: no cover @@ -248,9 +248,9 @@ def do_sync(w, options, quick=False): if do_st_export: # pragma: no cover try: - message, id = sporttracksstuff.workout_sporttracks_upload( - w.user.user, w, asynchron=True, - ) + st_integration = SportTracksIntegration(w.user.user) + id = st_integration.workout_export(w) + dologging('st_export.log', 'exported workout {wid} for user {uid}'.format( wid = w.id, diff --git a/rowers/views/importviews.py b/rowers/views/importviews.py index 953169bb..56dfc70b 100644 --- a/rowers/views/importviews.py +++ b/rowers/views/importviews.py @@ -138,13 +138,13 @@ def workout_c2_upload_view(request, id=0): # Upload workout to SportTracks @permission_required('workout.change_workout', fn=get_workout_by_opaqueid) def workout_sporttracks_upload_view(request, id=0): - message = "" + st_integration = SportTracksIntegration(request.user) + # ready to upload. Hurray w = get_workout(id) r = w.user - message, res = sporttracksstuff.workout_sporttracks_upload( - r.user, w, asynchron=True) + id = st_integration.workout_export(w) messages.info( request, 'Your workout will be synchronized with SportTracks in the background') @@ -240,16 +240,8 @@ def rower_polar_authorize(request): # pragma: no cover def rower_sporttracks_authorize(request): # pragma: no cover # Generate a random string for the state parameter # Save it for use later to prevent xsrf attacks - - state = str(uuid4()) - - params = {"client_id": SPORTTRACKS_CLIENT_ID, - "response_type": "code", - "state": state, - "redirect_uri": SPORTTRACKS_REDIRECT_URI} - - url = "https://api.sporttracks.mobi/oauth2/authorize?" + \ - urllib.parse.urlencode(params) + st_integration = SportTracksIntegration(request.user) + url = st_integration.make_authorization_url() return HttpResponseRedirect(url) @@ -335,24 +327,8 @@ def rower_tp_token_refresh(request): # SportTracks token refresh. URL for manual refresh. Not visible to users @login_required() def rower_sporttracks_token_refresh(request): - r = getrower(request.user) - res = sporttracksstuff.do_refresh_token( - r.sporttracksrefreshtoken, - ) - 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.sporttrackstoken = access_token - r.sporttrackstokenexpirydate = expirydatetime - r.sporttracksrefreshtoken = refresh_token - - r.save() - - successmessage = "Tokens refreshed. Good to go" - messages.info(request, successmessage) + st_integration = SportTracksIntegration(request.user) + result = st_integration.token_refresh() url = reverse('workouts_view') @@ -725,20 +701,8 @@ def rower_process_sporttrackscallback(request): url = reverse('rower_exportsettings_view') return HttpResponseRedirect(url) - res = sporttracksstuff.get_token(code) - - 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.sporttrackstoken = access_token - r.sporttrackstokenexpirydate = expirydatetime - r.sporttracksrefreshtoken = refresh_token - - r.save() + st_integration = SportTracksIntegration(request.user) + token = st_integration.get_token(code) successmessage = "Tokens stored. Good to go. Please check your import/export settings" messages.info(request, successmessage) @@ -1100,17 +1064,8 @@ def workout_stravaimport_view(request, message="", userid=0): alldata = {} for stravaid in stravaids: - csvfilename = 'media/{code}_{stravaid}.csv'.format( - code=uuid4().hex[:16], stravaid=stravaid) - _ = myqueue( - queue, - fetch_strava_workout, - r.stravatoken, - strava_integration.oauth_data, - stravaid, - csvfilename, - r.user.id - ) + res = strava_integration.get_workout(id) + # done, redirect to workouts list messages.info(request, 'Your Strava workouts will be imported in the background.' @@ -1416,54 +1371,34 @@ def workout_polarimport_view(request, userid=0): # pragma: no cover @permission_required('rower.is_coach', fn=get_user_by_userid, raise_exception=True) @permission_required('rower.is_not_freecoach', fn=get_user_by_userid, raise_exception=True) def workout_sporttracksimport_view(request, message="", userid=0): - r = getrequestrower(request, userid=userid) - if r.user != request.user: - messages.error( - request, 'You can only access your own workouts on the NK Logbook, not those of your athletes') - url = reverse('workout_sporttracksimport_view', - kwargs={'userid': request.user.id}) - return HttpResponseRedirect(url) + st_integration = SportTracksIntegration(request.user) + try: + _ = st_integration.open() + except NoTokenError: + return HttpResponseRedirect("/rowers/me/sporttracksauthorize/") - res = sporttracksstuff.get_sporttracks_workout_list(request.user) - if (res.status_code != 200): - if (res.status_code == 401): - r = getrower(request.user) - if (r.sporttrackstoken == '') or (r.sporttrackstoken is None): - s = "Token doesn't exist. Need to authorize" - return HttpResponseRedirect("/rowers/me/sporttracksauthorize/") - else: # pragma: no cover - return HttpResponseRedirect("/rowers/me/sporttracksrefresh/") - message = "Something went wrong in workout_sporttracksimport_view" # pragma: no cover - messages.error(request, message) # pragma: no cover - if settings.DEBUG: # pragma: no cover - return HttpResponse(res) - else: # pragma: no cover - url = reverse('workouts_view') - return HttpResponseRedirect(url) - - workouts = [] - - knownstids = uniqify([ - w.uploadedtosporttracks for w in Workout.objects.filter(user=r) - ]) - for item in res.json()['items']: - d = int(float(item['total_distance'])) - i = int(getidfromuri(item['uri'])) - if i in knownstids: # pragma: no cover - nnn = '' - else: - nnn = 'NEW' - n = item['name'] - ttot = str(datetime.timedelta(seconds=int(float(item['duration'])))) - s = item['start_time'] - r = item['type'] - keys = ['id', 'distance', 'duration', - 'starttime', 'type', 'name', 'new'] - values = [i, d, ttot, s, r, n, nnn] - res = dict(zip(keys, values)) - workouts.append(res) + workouts = st_integration.get_workout_list() r = getrower(request.user) + + if request.method == "POST": + try: # pragma: no cover + tdict = dict(request.POST.lists()) + ids = tdict['workoutid'] + stids = [int(id) for id in ids] + alldata = {} + + for id in stids: + res = st_integration.get_workout(id) + + # done, redirect to workouts list + messages.info(request, + 'Your SportTracks workouts will be imported in the background.' + ' It may take a few minutes before they appear.') + url = reverse('workouts_view') + return HttpResponseRedirect(url) + except KeyError: # pragma: no cover + pass breadcrumbs = [ { @@ -1476,12 +1411,17 @@ def workout_sporttracksimport_view(request, message="", userid=0): }, ] - return render(request, 'sporttracks_list_import.html', + checknew = request.GET.get('selectallnew', False) + + + return render(request, 'list_import.html', {'workouts': workouts, 'breadcrumbs': breadcrumbs, 'active': 'nav-workouts', 'rower': r, 'teams': get_my_teams(request.user), + 'integration':'SportTracks', + 'checknew': checknew, }) return HttpResponse(res) # pragma: no cover @@ -1630,7 +1570,7 @@ importsources = { 'strava': StravaIntegration, 'polar': polarstuff, 'ownapi': ownapistuff, - 'sporttracks': sporttracksstuff, + 'sporttracks': SportTracksIntegration, 'trainingpeaks': tpstuff, 'nk': NKIntegration, } @@ -1698,27 +1638,9 @@ def workout_getimportview_old(request, externalid, source='c2', do_async=True): # 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() + st_integration = SportTracksIntegration(request.user) + st_integration.get_workouts() + messages.info(request,"Your SportTracks workouts will be imported in the background") url = reverse('workouts_view') return HttpResponseRedirect(url) @@ -1761,27 +1683,9 @@ def workout_getimportview(request, externalid, source='c2', do_async=True): # 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() + st_integration = SportTracksIntegration(request.user) + _ = st_integration.get_workouts() + messages.info(request,"Your SportTracks workouts will be imported in the background") url = reverse('workouts_view') return HttpResponseRedirect(url) diff --git a/rowers/views/statements.py b/rowers/views/statements.py index c5004b2d..7d65e69b 100644 --- a/rowers/views/statements.py +++ b/rowers/views/statements.py @@ -194,7 +194,6 @@ import datetime import iso8601 import rowers.rojabo_stuff as rojabo_stuff from rowers.rp3stuff import rp3_open -from rowers.sporttracksstuff import sporttracks_open from rowers.tpstuff import tp_open from iso8601 import ParseError @@ -203,7 +202,6 @@ import rowers.garmin_stuff as garmin_stuff from rowers.rojabo_stuff import rojabo_open import rowers.polarstuff as polarstuff -import rowers.sporttracksstuff as sporttracksstuff from rowers.integrations import * diff --git a/rowers/views/workoutviews.py b/rowers/views/workoutviews.py index b110322e..6e8d0a62 100644 --- a/rowers/views/workoutviews.py +++ b/rowers/views/workoutviews.py @@ -5654,10 +5654,9 @@ def workout_upload_view(request, messages.error(request, message) if (upload_to_st): # pragma: no cover + st_integration = SportTracksIntegration(request.user) try: - message, id = sporttracksstuff.workout_sporttracks_upload( - request.user, w - ) + id = st_integration.workout_export(w) except NoTokenError: message = "Please connect to SportTracks first" id = 0 From e8f3b1057eadad623076defbaf896334f7f68a82 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Tue, 14 Feb 2023 08:40:35 +0100 Subject: [PATCH 13/27] fixing tests --- rowers/tests/test_async_tasks.py | 8 +++++--- rowers/tests/testdata/testdata.tcx.gz | Bin 4001 -> 4000 bytes 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/rowers/tests/test_async_tasks.py b/rowers/tests/test_async_tasks.py index cadddd32..6cd15a06 100644 --- a/rowers/tests/test_async_tasks.py +++ b/rowers/tests/test_async_tasks.py @@ -453,8 +453,8 @@ class AsyncTaskTests(TestCase): self.assertEqual(y,'3.142') - @patch('rowers.sporttracksstuff.requests.post', side_effect=mocked_requests) - @patch('rowers.sporttracksstuff.requests.get', side_effect=mocked_requests) + @patch('rowers.integrations.sporttracks.requests.post', side_effect=mocked_requests) + @patch('rowers.integrations.sporttracks.requests.get', side_effect=mocked_requests) def test_sporttracks_sync(self, mock_get, mock_post): authorizationstring = str('Bearer aap') headers = {'Authorization': authorizationstring, @@ -463,7 +463,9 @@ class AsyncTaskTests(TestCase): url = "https://api.sporttracks.mobi/api/v2/fitnessActivities.json" - data = sporttracksstuff.createsporttracksworkoutdata(self.wwater) + integration = SportTracksIntegration(self.u) + + data = integration.createworkoutdata(self.wwater) res = tasks.handle_sporttracks_sync(1,url,headers,json.dumps(data,default=sporttracksstuff.default)) diff --git a/rowers/tests/testdata/testdata.tcx.gz b/rowers/tests/testdata/testdata.tcx.gz index f87905524517a7050d53b8859644520469a3beb5..7fe9f5fa8cb806f06325faf11ecc596079fee673 100644 GIT binary patch delta 3913 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 Xc$l8Xi*)J3JO2X^txXsN#DD<+vw+>= delta 3914 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 Y7mw1@_#<8V@Xr4Lk6Fb9SHyq;0NeWc9smFU From 4a70d7a3d068dcb5c21a52cbbd22f11f479a6119 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Tue, 14 Feb 2023 08:43:11 +0100 Subject: [PATCH 14/27] fixing st test --- rowers/tests/test_async_tasks.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rowers/tests/test_async_tasks.py b/rowers/tests/test_async_tasks.py index 6cd15a06..f644e53d 100644 --- a/rowers/tests/test_async_tasks.py +++ b/rowers/tests/test_async_tasks.py @@ -11,7 +11,7 @@ nu = datetime.datetime.now() from rowers import tasks import rowers.courses as courses - +from rowers.integrations.sporttracks import default as stdefault class fakejob: def __init__(self): @@ -468,7 +468,7 @@ class AsyncTaskTests(TestCase): data = integration.createworkoutdata(self.wwater) - res = tasks.handle_sporttracks_sync(1,url,headers,json.dumps(data,default=sporttracksstuff.default)) + res = tasks.handle_sporttracks_sync(1,url,headers,json.dumps(data,default=stdefault)) self.assertEqual(res,1) From 3c0fbc431d5042284a563f17af8a93a377d31af3 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Tue, 14 Feb 2023 20:57:42 +0100 Subject: [PATCH 15/27] rp3, untested --- rowers/integrations/__init__.py | 1 + rowers/integrations/rp3.py | 252 ++++++++++++++++++++++++ rowers/rp3stuff.py | 267 -------------------------- rowers/tasks.py | 6 + rowers/tests/test_imports.py | 6 +- rowers/tests/testdata/testdata.tcx.gz | Bin 4000 -> 3999 bytes rowers/uploads.py | 6 +- rowers/views/importviews.py | 101 ++++------ rowers/views/statements.py | 4 +- 9 files changed, 308 insertions(+), 335 deletions(-) create mode 100644 rowers/integrations/rp3.py delete mode 100644 rowers/rp3stuff.py diff --git a/rowers/integrations/__init__.py b/rowers/integrations/__init__.py index 343df09b..fff8ac50 100644 --- a/rowers/integrations/__init__.py +++ b/rowers/integrations/__init__.py @@ -2,3 +2,4 @@ from .c2 import C2Integration from .strava import StravaIntegration from .nk import NKIntegration from .sporttracks import SportTracksIntegration +from .rp3 import RP3Integration diff --git a/rowers/integrations/rp3.py b/rowers/integrations/rp3.py new file mode 100644 index 00000000..2900e6d3 --- /dev/null +++ b/rowers/integrations/rp3.py @@ -0,0 +1,252 @@ +from .integrations import SyncIntegration, NoTokenError +from rowers.models import User, Rower, Workout, TombStone + +from rowers.tasks import handle_rp3_async_workout +from rowsandall_app.settings import ( + RP3_CLIENT_ID, RP3_CLIENT_KEY, RP3_REDIRECT_URI, RP3_CLIENT_SECRET, + UPLOAD_SERVICE_URL, UPLOAD_SERVICE_SECRET +) + +from rowers.utils import myqueue, NoTokenError, dologging, uniqify +from django.utils import timezone +import requests +import pandas as pd +import arrow +import django_rq +queue = django_rq.get_queue('default') +queuelow = django_rq.get_queue('low') +queuehigh = django_rq.get_queue('high') + +from datetime import timedelta + +graphql_url = "https://rp3rowing-app.com/graphql" + + +class RP3Integration(SyncIntegration): + def __init__(self, *args, **kwargs): + super(RP3Integration, self).__init__(*args, **kwargs) + self.oauth_data = { + 'client_id': RP3_CLIENT_ID, + 'client_secret': RP3_CLIENT_SECRET, + 'redirect_uri': RP3_REDIRECT_URI, + 'autorization_uri': "https://rp3rowing-app.com/oauth/authorize?", + 'content_type': 'application/x-www-form-urlencoded', + # 'content_type': 'application/json', + 'tokenname': 'rp3token', + 'refreshtokenname': 'rp3refreshtoken', + 'expirydatename': 'rp3tokenexpirydate', + 'bearer_auth': False, + 'base_url': "https://rp3rowing-app.com/oauth/token", + 'scope': 'read,write', + } + + + def createworkoutdata(self, w, *args, **kwargs): + return None + + + def workout_export(self, workout, *args, **kwargs) -> str: + pass + + + def get_workouts(self, *args, **kwargs) -> int: + auth_token = self.open() + + r = self.rower + workouts_json = self.get_workout_list_json() + + workouts_list = pd.json_normalize(workouts_json['data']['workouts']) + + try: + rp3ids = workouts_list['id'].values + workouts_list.set_index('id',inpace=True) + except (KeyError, IndexError): + return 0 + + knownrp3ids = uniqify([ + w.uploadedtorp3 for w in Workout.objects.filter(user=rower) + ]) + + dologging('rp3_import.log',rp3ids) + + newids = [rp3id for rp3id in rp3ids if rp3id not in knownrp3ids] + + dologging('rp3_import.log',newids) + + for id in newids: + startdatetime = workouts_list.loc[id, 'executed_at_ios8601'] + dologging('rp3_import.log', startdatetime) + + _ = myqueue( + queuehigh, + handle_rp3_async_workout, + self.user.id, + auth_token, + id, + startdatetime, + 20, + {'timezone':self.rower.defaulttimezone} + ) + + return 1 + + def get_workout(self, id, *args, **kwargs) -> int: + startdatetime = kwargs.get('startdatetime', None) + if not startdatetime: + startdatetime = str(timezone.now()) + + auth_token = self.open() + _ = myqueue( + queuehigh, + handle_rp3_async_workout, + self.user.id, + auth_token, + id, + startdatetime, + 20, + timezone = self.rower.defaulttimezone + ) + + def get_workout_schema(self, *args, **kwargs) -> dict: + auth_token = self.open() + headers = {'Authorization': 'Bearer ' + auth_token} + get_schema = """{ + __type(name:"Workout") { + name + fields { + name + description + type { + name + kind + ofType { + name + kind + } + } + } + } + }""" + + response = requests.post( + url = graphql_url, + headers=headers, + json={'query':get_schema} + ) + return response.json() + + def get_workout_list_json(self, *args, **kwargs) -> dict: + auth_token = self.open() + r = self.rower + + headers = {'Authorization': 'Bearer ' + auth_token} + + get_workouts_list = """{ + workouts{ + id + executed_at_iso8601 + } + }""" + + response = requests.post( + url=graphql_url, + headers=headers, + json={'query': get_workouts_list} + ) + + if (response.status_code != 200): # pragma: no cover + raise NoTokenError("Need to authorize") + + return response.json() + + def get_workout_list(self, *args, **kwargs) -> list: + r = self.rower + + workouts_json = self.get_workout_list_json(*args, **kwargs) + + workouts_list = pd.json_normalize(workouts_json['data']['workouts']) + + knownrp3ids = uniqify([ + w.uploadedtorp3 for w in Workout.objects.filter(user=r) + ]) + + workouts = [] + + for key, data in workouts_list.iterrows(): + print(data) + try: + i = data['id'] + except KeyError: # pragma: no cover + i = 0 + if i in knownrp3ids: # pragma: no cover + nnn = '' + else: + nnn = 'NEW' + + try: + s = arrow.get(data['executed_at_iso8601']).isoformat() + except KeyError: # pragma: no cover + s = '' + + keys = ['id', 'distance', 'duration', 'starttime', + 'rowtype', 'source', 'name', 'new'] + values = [i, '', '', s, '', 'rp3', '', nnn] + + res = dict(zip(keys, values)) + + workouts.append(res) + + + return workouts + + + def make_authorization_url(self, *args, **kwargs) -> str: # pragma: no cover + return super(RP3Integration, self).make_authorization_url(*args, **krags) + + def get_token(self, code, *args, **kwargs) -> (str, int, str): + post_data = { + "client_id": RP3_CLIENT_KEY, + "grant_type": "authorization_code", + "code": code, + "redirect_uri": RP3_REDIRECT_URI, + "client_secret": RP3_CLIENT_SECRET, + } + + response = requests.post( + "https://rp3rowing-app.com/oauth/token", + data=post_data, verify=False, + ) + + try: + token_json = response.json() + thetoken = token_json['access_token'] + expires_in = token_json['expires_in'] + refresh_token = token_json['refresh_token'] + except KeyError: + thetoken = "" + expires_in = 0 + refresh_token = "" + + return thetoken, expires_in, refresh_token + + + def open(self, *args, **kwargs) -> str: + tokenexpirydate = self.user.rower.rp3tokenexpirydate + if tokenexpirydate is None: + raise NoTokenError("No Token") + if tokenexpirydate is not None and timezone.now()-timedelta(days=120)>tokenexpirydate: + self.rower.rp3tokenexpirydate = timezone.now()-timedelta(days=1) + self.rower.save() + raise NoTokenError("No Token") + return super(RP3Integration, self).open() + + + def token_refresh(self, *args, **kwargs) -> str: + return super(RP3Integration, self).token_refresh(*args, **kwargs) + + +# just as a quick test during development +u = User.objects.get(id=1) + +integration_1 = RP3Integration(u) + diff --git a/rowers/rp3stuff.py b/rowers/rp3stuff.py deleted file mode 100644 index b4eed2f6..00000000 --- a/rowers/rp3stuff.py +++ /dev/null @@ -1,267 +0,0 @@ -from celery import Celery, app -from rowers.rower_rules import is_workout_user -import time -from django_rq import job -from rowers.tasks import handle_rp3_async_workout -from rowsandall_app.settings import ( - C2_CLIENT_ID, C2_REDIRECT_URI, C2_CLIENT_SECRET, - STRAVA_CLIENT_ID, STRAVA_REDIRECT_URI, STRAVA_CLIENT_SECRET, - RP3_CLIENT_ID, RP3_CLIENT_KEY, RP3_REDIRECT_URI, RP3_CLIENT_SECRET, - UPLOAD_SERVICE_URL, UPLOAD_SERVICE_SECRET -) -from rowers.utils import myqueue, NoTokenError -# All the functionality needed to connect to Runkeeper -from rowers.imports import * - -# Python -import gzip - -from datetime import timedelta - -import base64 -from io import BytesIO - -from rowers.utils import dologging - -import django_rq -queue = django_rq.get_queue('default') -queuelow = django_rq.get_queue('low') -queuehigh = django_rq.get_queue('high') - - -oauth_data = { - 'client_id': RP3_CLIENT_ID, - 'client_secret': RP3_CLIENT_SECRET, - 'redirect_uri': RP3_REDIRECT_URI, - 'autorization_uri': "https://rp3rowing-app.com/oauth/authorize?", - 'content_type': 'application/x-www-form-urlencoded', - # 'content_type': 'application/json', - 'tokenname': 'rp3token', - 'refreshtokenname': 'rp3refreshtoken', - 'expirydatename': 'rp3tokenexpirydate', - 'bearer_auth': False, - 'base_url': "https://rp3rowing-app.com/oauth/token", - 'scope': 'read,write', -} - - -graphql_url = "https://rp3rowing-app.com/graphql" - - -# Checks if user has UnderArmour token, renews them if they are expired -def rp3_open(user): - tokenexpirydate = user.rower.rp3tokenexpirydate - if tokenexpirydate is None: - raise NoTokenError("No Token") - if tokenexpirydate is not None and timezone.now()-timedelta(days=120)>tokenexpirydate: - user.rower.rp3tokenexpirydate = timezone.now()-timedelta(days=1) - user.rower.save() - raise NoTokenError("No Token") - return imports_open(user, oauth_data) - -# Refresh ST token using refresh token - - -def do_refresh_token(refreshtoken): # pragma: no cover - return imports_do_refresh_token(refreshtoken, oauth_data) - -# Exchange access code for long-lived access token - - -def get_token(code): # pragma: no cover - post_data = { - "client_id": RP3_CLIENT_KEY, - "grant_type": "authorization_code", - "code": code, - "redirect_uri": RP3_REDIRECT_URI, - "client_secret": RP3_CLIENT_SECRET, - } - - response = requests.post( - "https://rp3rowing-app.com/oauth/token", - data=post_data, verify=False, - ) - - try: - token_json = response.json() - thetoken = token_json['access_token'] - expires_in = token_json['expires_in'] - refresh_token = token_json['refresh_token'] - except KeyError: - thetoken = 0 - expires_in = 0 - refresh_token = 0 - - return thetoken, expires_in, refresh_token - -# Make authorization URL including random string - - -def make_authorization_url(request): # pragma: no cover - return imports_make_authorization_url(oauth_data) - - -def get_rp3_workout_list(user): - auth_token = rp3_open(user) - - headers = {'Authorization': 'Bearer ' + auth_token} - - get_workouts_list = """{ - workouts{ - id - executed_at - } -}""" - - response = requests.post( - url=graphql_url, - headers=headers, - json={'query': get_workouts_list} - ) - - return response - - -def get_rp3_workouts(rower, do_async=True): # pragma: no cover - try: - auth_token = rp3_open(rower.user) - except NoTokenError: - return 0 - - res = get_rp3_workout_list(rower.user) - - if (res.status_code != 200): - return 0 - - s = '{d}'.format(d=res.json()) - dologging('rp3_import.log', s) - workouts_list = pd.json_normalize(res.json()['data']['workouts']) - try: - rp3ids = workouts_list['id'].values - workouts_list.set_index('id', inplace=True) - except (KeyError, IndexError): - return 0 - - knownrp3ids = uniqify([ - w.uploadedtorp3 for w in Workout.objects.filter(user=rower) - ]) - - dologging('rp3_import.log',rp3ids) - - newids = [rp3id for rp3id in rp3ids if rp3id not in knownrp3ids] - - dologging('rp3_import.log',newids) - - for id in newids: - startdatetime = workouts_list.loc[id, 'executed_at'] - dologging('rp3_import.log', startdatetime) - - _ = myqueue( - queuehigh, - handle_rp3_async_workout, - rower.user.id, - auth_token, - id, - startdatetime, - 20, - ) - - return 1 - - -def download_rp3_file(url, auth_token, filename): # pragma: no cover - headers = {'Authorization': 'Bearer ' + auth_token} - - res = requests.get(url, headers=headers) - - if res.status_code == 200: - with open(filename, 'wb') as f: - f.write(res.content) - - return res.status_code - - -def get_rp3_workout_token(workout_id, auth_token, waittime=3, max_attempts=20): # pragma: no cover - headers = {'Authorization': 'Bearer ' + auth_token} - - get_download_link = """{ - download(workout_id: """ + str(workout_id) + """, type:csv){ - id - status - link - } -}""" - - have_link = False - download_url = '' - counter = 0 - while not have_link: - response = requests.post( - url=graphql_url, - headers=headers, - json={'query': get_download_link} - ) - - if response.status_code != 200: - have_link = True - - workout_download_details = pd.json_normalize( - response.json()['data']['download']) - - if workout_download_details.iat[0, 1] == 'ready': - download_url = workout_download_details.iat[0, 2] - have_link = True - - counter += 1 - - if counter > max_attempts: - have_link = True - - time.sleep(waittime) - - return download_url - - -def get_rp3_workout_link(user, workout_id, waittime=3, max_attempts=20): # pragma: no cover - auth_token = rp3_open(user) - - return get_rp3_workout_token(workout_id, auth_token, waittime=waittime, max_attempts=max_attempts) - - -def get_rp3_workout(user, workout_id, startdatetime=None): # pragma: no cover - url = get_rp3_workout_link(user, workout_id) - filename = 'media/RP3Import_'+str(workout_id)+'.csv' - - auth_token = rp3_open(user) - - if not startdatetime: - startdatetime = str(timezone.now()) - - status_code = download_rp3_file(url, auth_token, filename) - - if status_code != 200: - return 0 - - userid = user.id - - uploadoptions = { - 'secret': UPLOAD_SERVICE_SECRET, - 'user': userid, - 'file': filename, - 'workouttype': 'dynamic', - 'boattype': '1x', - 'rp3id': workout_id, - 'startdatetime': startdatetime, - 'timezone': str(user.rower.defaulttimezone) - } - - 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: - return 0 - - return response.json()['id'] diff --git a/rowers/tasks.py b/rowers/tasks.py index a327a511..5a311f1f 100644 --- a/rowers/tasks.py +++ b/rowers/tasks.py @@ -3158,6 +3158,9 @@ def handle_update_wps(rid, types, ids, mode, debug=False, **kwargs): @app.task def handle_rp3_async_workout(userid, rp3token, rp3id, startdatetime, max_attempts, debug=False, **kwargs): + + timezone = kwargs.get('timezone', 'UTC') + headers = {'Authorization': 'Bearer ' + rp3token} get_download_link = """{ @@ -3239,8 +3242,11 @@ def handle_rp3_async_workout(userid, rp3token, rp3id, startdatetime, max_attempt 'boattype': '1x', 'rp3id': int(rp3id), 'startdatetime': startdatetime, + 'timezone': timezone, } + print(uploadoptions) + session = requests.session() newHeaders = {'Content-type': 'application/json', 'Accept': 'text/plain'} session.headers.update(newHeaders) diff --git a/rowers/tests/test_imports.py b/rowers/tests/test_imports.py index 6a2f5ee2..0bac3505 100644 --- a/rowers/tests/test_imports.py +++ b/rowers/tests/test_imports.py @@ -971,8 +971,8 @@ class RP3Objects(DjangoTestCase): csvfilename=filename ) - @patch('rowers.rp3stuff.requests.get', side_effect=mocked_requests) - @patch('rowers.rp3stuff.requests.post', side_effect=mocked_requests) + @patch('rowers.integrations.rp3.requests.get', side_effect=mocked_requests) + @patch('rowers.integrations.rp3.requests.post', side_effect=mocked_requests) @patch('rowers.dataprep.getsmallrowdata_db', side_effect=mocked_getsmallrowdata_db) def test_rp3_import(self, mock_get, mockpost, mocked_getsmallrowdata_db): @@ -1002,7 +1002,7 @@ class RP3Objects(DjangoTestCase): res = tasks.handle_rp3_async_workout(userid,rp3token,rp3id,startdatetime,max_attempts) self.assertEqual(res,1) - @patch('rowers.rp3stuff.requests.post', side_effect=mocked_requests) + @patch('rowers.integrations.rp3.requests.post', side_effect=mocked_requests) def notest_rp3_callback(self, mock_post): response = self.c.get('/rp3_callback?code=absdef23&scope=read',follow=True) self.assertEqual(response.status_code, 200) diff --git a/rowers/tests/testdata/testdata.tcx.gz b/rowers/tests/testdata/testdata.tcx.gz index 7fe9f5fa8cb806f06325faf11ecc596079fee673..ef36d3798ef2f5e2a84326924060dec69b447979 100644 GIT binary patch literal 3999 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 F0RVK4I*I@Q literal 4000 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 GfB^uPLKmt4 diff --git a/rowers/uploads.py b/rowers/uploads.py index 3a784473..f99501a4 100644 --- a/rowers/uploads.py +++ b/rowers/uploads.py @@ -235,9 +235,9 @@ def do_sync(w, options, quick=False): do_st_export = w.user.sporttracks_auto_export - if options['sporttracksid'] != 0 and options['sporttracksid'] != '': - w.uploadedtosporttracks = options['sporttracksid'] - + sporttracksid = options.get('sporttracksid','') + if sporttracksid != 0 and sporttracksid != '': + w.uploadedtosporttracks = sporttracksid w.save() do_st_export = False try: # pragma: no cover diff --git a/rowers/views/importviews.py b/rowers/views/importviews.py index 56dfc70b..8fa1bad4 100644 --- a/rowers/views/importviews.py +++ b/rowers/views/importviews.py @@ -725,11 +725,9 @@ def rower_process_rp3callback(request): # pragma: no cover url = reverse('rower_exportsettings_view') return HttpResponseRedirect(url) - res = rp3stuff.get_token(code) + rp3_integration = RP3Integration(request.user) + access_token, expires_in, refresh_token = rp3_integration.get_token(code) - access_token = res[0] - expires_in = res[1] - refresh_token = res[2] expirydatetime = timezone.now()+datetime.timedelta(seconds=expires_in) r = getrower(request.user) @@ -803,56 +801,39 @@ def rower_process_testcallback(request): # pragma: no cover @permission_required('rower.is_coach', fn=get_user_by_userid, raise_exception=True) def workout_rp3import_view(request, userid=0): r = getrequestrower(request, userid=userid) + rp3_integration = RP3Integration(request.user) try: - _ = rp3stuff.rp3_open(request.user) + _ = rp3_integration.open() except NoTokenError: # pragma: no cover url = reverse('rower_rp3_authorize') return HttpResponseRedirect(url) - res = rp3stuff.get_rp3_workout_list(request.user) + workouts = rp3_integration.get_workout_list() + datedict = {} + for workout in workouts: + datedict[workout['id']] = workout['starttime'] - if (res.status_code != 200): # pragma: no cover - if (res.status_code == 401): - r = getrower(request.user) - if (r.stravatoken == '') or (r.stravatoken is None): - s = "Token doesn't exist. Need to authorize" - return HttpResponseRedirect("/rowers/me/stravaauthorize/") - message = "Something went wrong in workout_rp3import_view" - messages.error(request, message) - url = reverse('workouts_view') - return HttpResponseRedirect(url) - workouts_list = pd.json_normalize(res.json()['data']['workouts']) + if request.method == "POST": + try: # pragma: no cover + tdict = dict(request.POST.lists()) + ids = tdict['workoutid'] + rp3ids = [int(id) for id in ids] - knownrp3ids = uniqify([ - w.uploadedtorp3 for w in Workout.objects.filter(user=r) - ]) - - workouts = [] - - for key, data in workouts_list.iterrows(): - try: - i = data['id'] + for rp3id in rp3ids: + rp3_integration.get_workout(rp3id,startdatetime=datedict[rp3id]) + # done, redirect to workouts list + messages.info( + request, + 'Your RP3 workouts will be imported in the background.' + ' It may take a few minutes before they appear.') + url = reverse('workouts_view') + return HttpResponseRedirect(url) except KeyError: # pragma: no cover - i = 0 - if i in knownrp3ids: # pragma: no cover - nnn = '' - else: - nnn = 'NEW' - - try: - s = data['executed_at'] - except KeyError: # pragma: no cover - s = '' - - keys = ['id', 'starttime', 'new'] - values = [i, s, nnn] - - res = dict(zip(keys, values)) - - workouts.append(res) + pass + breadcrumbs = [ { 'url': '/rowers/list-workouts/', @@ -864,13 +845,18 @@ def workout_rp3import_view(request, userid=0): }, ] - return render(request, 'rp3_list_import.html', + checknew = request.GET.get('selectallnew', False) + + + return render(request, 'list_import.html', { 'workouts': workouts, 'rower': r, 'active': 'nav-workouts', 'breadcrumbs': breadcrumbs, - 'teams': get_my_teams(request.user) + 'teams': get_my_teams(request.user), + 'integration': 'RP3', + 'checknew': checknew, }) # The page where you select which Strava workout to import @@ -1461,7 +1447,8 @@ def workout_getrp3workout_all(request): # pragma: no cover r = getrequestrower(request) - result = rp3stuff.get_rp3_workouts(r, do_async=True) + rp3_integration = RP3Integration(request.user) + result = rp3_integration.get_workouts() if result: messages.info( @@ -1582,21 +1569,12 @@ def workout_getrp3importview(request, externalid): r = getrequestrower(request) if r.user != request.user: # pragma: no cover messages.error( - request, 'You can only access your own workouts on the NK Logbook, not those of your athletes') + request, 'You can only access your own workouts on the RP3 Logbook, not those of your athletes') url = reverse('workout_rp3import_view', kwargs={ 'userid': request.user.id}) return HttpResponseRedirect(url) - token = rp3stuff.rp3_open(r.user) - startdatetime = request.GET.get('startdatetime') - - _ = myqueue(queuehigh, - handle_rp3_async_workout, - r.user.id, - token, - externalid, - startdatetime, - 20, - ) + rp3_integration = RP3Integration(request.user) + result = rp3_integration.get_workout(externalid) messages.info(request, 'The workout will be imported in the background') @@ -1684,8 +1662,11 @@ def workout_getimportview(request, externalid, source='c2', do_async=True): @login_required() def workout_getsporttracksworkout_all(request): st_integration = SportTracksIntegration(request.user) - _ = st_integration.get_workouts() - messages.info(request,"Your SportTracks workouts will be imported in the background") + try: + _ = st_integration.get_workouts() + messages.info(request,"Your SportTracks workouts will be imported in the background") + except NoTokenError: + messages.error(request,"You have to connect to SportTracks first") url = reverse('workouts_view') return HttpResponseRedirect(url) diff --git a/rowers/views/statements.py b/rowers/views/statements.py index 7d65e69b..f3f2def6 100644 --- a/rowers/views/statements.py +++ b/rowers/views/statements.py @@ -193,7 +193,7 @@ import sys import datetime import iso8601 import rowers.rojabo_stuff as rojabo_stuff -from rowers.rp3stuff import rp3_open + from rowers.tpstuff import tp_open from iso8601 import ParseError @@ -206,7 +206,7 @@ import rowers.polarstuff as polarstuff from rowers.integrations import * import rowers.tpstuff as tpstuff -import rowers.rp3stuff as rp3stuff + import rowers.ownapistuff as ownapistuff from rowers.ownapistuff import TEST_CLIENT_ID, TEST_CLIENT_SECRET, TEST_REDIRECT_URI from rowsandall_app.settings import ( From 66562ead59eed1b15e76fe6cf6efb88f61040dae Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Tue, 14 Feb 2023 20:58:04 +0100 Subject: [PATCH 16/27] t --- rowers/tests/testdata/testdata.tcx.gz | Bin 3999 -> 3999 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/rowers/tests/testdata/testdata.tcx.gz b/rowers/tests/testdata/testdata.tcx.gz index ef36d3798ef2f5e2a84326924060dec69b447979..8d57aba4559f7daef8b0dc0037d526ea7702b389 100644 GIT binary patch delta 3642 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 M2RoZm0K|X+09yrqPXGV_ From 8912ee3c8e4e132bc7ec23e4c02799d03866017d Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Tue, 14 Feb 2023 21:37:57 +0100 Subject: [PATCH 17/27] passing most tests --- rowers/integrations/rp3.py | 9 ++++----- rowers/management/commands/processemail.py | 4 ++-- rowers/tasks.py | 2 -- rowers/tests/testdata/testdata.tcx.gz | Bin 3999 -> 4000 bytes rowers/urls.py | 2 -- rowers/views/importviews.py | 20 +++----------------- 6 files changed, 9 insertions(+), 28 deletions(-) diff --git a/rowers/integrations/rp3.py b/rowers/integrations/rp3.py index 2900e6d3..657176ef 100644 --- a/rowers/integrations/rp3.py +++ b/rowers/integrations/rp3.py @@ -59,12 +59,12 @@ class RP3Integration(SyncIntegration): try: rp3ids = workouts_list['id'].values - workouts_list.set_index('id',inpace=True) + workouts_list.set_index('id',inplace=True) except (KeyError, IndexError): return 0 knownrp3ids = uniqify([ - w.uploadedtorp3 for w in Workout.objects.filter(user=rower) + w.uploadedtorp3 for w in Workout.objects.filter(user=r) ]) dologging('rp3_import.log',rp3ids) @@ -74,7 +74,7 @@ class RP3Integration(SyncIntegration): dologging('rp3_import.log',newids) for id in newids: - startdatetime = workouts_list.loc[id, 'executed_at_ios8601'] + startdatetime = workouts_list.loc[id, 'executed_at_iso8601'] dologging('rp3_import.log', startdatetime) _ = myqueue( @@ -85,7 +85,7 @@ class RP3Integration(SyncIntegration): id, startdatetime, 20, - {'timezone':self.rower.defaulttimezone} + timezone = self.rower.defaulttimezone ) return 1 @@ -173,7 +173,6 @@ class RP3Integration(SyncIntegration): workouts = [] for key, data in workouts_list.iterrows(): - print(data) try: i = data['id'] except KeyError: # pragma: no cover diff --git a/rowers/management/commands/processemail.py b/rowers/management/commands/processemail.py index 6769ae74..13e9dba9 100644 --- a/rowers/management/commands/processemail.py +++ b/rowers/management/commands/processemail.py @@ -29,7 +29,6 @@ import rowers.uploads as uploads import rowers.polarstuff as polarstuff -import rowers.rp3stuff as rp3stuff from rowers.opaque import encoder from rowers.integrations import * @@ -102,7 +101,8 @@ class Command(BaseCommand): rowers = Rower.objects.filter(rp3_auto_import=True) for r in rowers: # pragma: no cover if user_is_not_basic(r.user) or user_is_coachee(r.user): - _ = rp3stuff.get_rp3_workouts(r) + rp3_integration = RP3Integration(r.user) + _ = rp3_integration.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/tasks.py b/rowers/tasks.py index 5a311f1f..5430cf20 100644 --- a/rowers/tasks.py +++ b/rowers/tasks.py @@ -3245,8 +3245,6 @@ def handle_rp3_async_workout(userid, rp3token, rp3id, startdatetime, max_attempt 'timezone': timezone, } - print(uploadoptions) - session = requests.session() newHeaders = {'Content-type': 'application/json', 'Accept': 'text/plain'} session.headers.update(newHeaders) diff --git a/rowers/tests/testdata/testdata.tcx.gz b/rowers/tests/testdata/testdata.tcx.gz index 8d57aba4559f7daef8b0dc0037d526ea7702b389..3133a666a6014bd9b73a9408007b84f22df346ba 100644 GIT binary patch literal 4000 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 GfB^v0#}*L) literal 3999 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 F0RTP?IXVCU diff --git a/rowers/urls.py b/rowers/urls.py index 29b13906..e81ffb7f 100644 --- a/rowers/urls.py +++ b/rowers/urls.py @@ -653,8 +653,6 @@ urlpatterns = [ re_path(r'^workout/nkimport/all/(?P\d+-\d+-\d+)/(?P\d+-\d+-\d+)/$', views.workout_getnkworkout_all, name='workout_getnkworkout_all'), - re_path(r'^workout/rp3import/(?P\d+)/$', views.workout_getrp3importview, - name='workout_getrp3importview'), re_path(r'^workout/rp3import/user/(?P\d+)/$', views.workout_rp3import_view, name='workout_rp3import_view'), re_path(r'^workout/rp3import/all/$', views.workout_getrp3workout_all, diff --git a/rowers/views/importviews.py b/rowers/views/importviews.py index 8fa1bad4..06f51140 100644 --- a/rowers/views/importviews.py +++ b/rowers/views/importviews.py @@ -1540,6 +1540,7 @@ importlistviews = { 'sporttracks': 'workout_sporttracksimport_view', 'trainingpeaks': 'workout_view', 'nk': 'workout_nkimport_view', + 'rp3': 'workout_rp3import_view', } importauthorizeviews = { @@ -1550,6 +1551,7 @@ importauthorizeviews = { 'sporttracks': 'rower_sporttracks_authorize', 'trainingpeaks': 'rower_tp_authorize', 'nk': 'rower_nk_authorize', + 'rp3': 'rower_rp3_authorize', } importsources = { @@ -1560,26 +1562,10 @@ importsources = { 'sporttracks': SportTracksIntegration, 'trainingpeaks': tpstuff, 'nk': NKIntegration, + 'rp3':RP3Integration, } -@login_required() -@permission_required('rower.is_not_freecoach', fn=get_user_by_userid, raise_exception=True) -def workout_getrp3importview(request, externalid): - r = getrequestrower(request) - if r.user != request.user: # pragma: no cover - messages.error( - request, 'You can only access your own workouts on the RP3 Logbook, not those of your athletes') - url = reverse('workout_rp3import_view', kwargs={ - 'userid': request.user.id}) - return HttpResponseRedirect(url) - rp3_integration = RP3Integration(request.user) - result = rp3_integration.get_workout(externalid) - - messages.info(request, 'The workout will be imported in the background') - - url = reverse('workout_rp3import_view') - return HttpResponseRedirect(url) @login_required() def workout_getimportview_old(request, externalid, source='c2', do_async=True): From 3eaef9ae7072c1932fb4c8e67b0f44a783f6a1ac Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Wed, 15 Feb 2023 19:25:45 +0100 Subject: [PATCH 18/27] doing trainingpeaks, untested --- rowers/integrations/__init__.py | 2 + rowers/integrations/rp3.py | 4 - rowers/integrations/trainingpeaks.py | 134 +++++++++++++++ rowers/tasks.py | 76 ++++++++- rowers/templates/menu_workout.html | 4 - rowers/tests/test_imports.py | 6 +- rowers/tests/testdata/testdata.tcx.gz | Bin 4000 -> 4001 bytes rowers/tpstuff.py | 232 -------------------------- rowers/uploads.py | 6 +- rowers/views/importviews.py | 65 ++------ rowers/views/statements.py | 2 - rowers/views/workoutviews.py | 19 +-- 12 files changed, 230 insertions(+), 320 deletions(-) create mode 100644 rowers/integrations/trainingpeaks.py delete mode 100644 rowers/tpstuff.py diff --git a/rowers/integrations/__init__.py b/rowers/integrations/__init__.py index fff8ac50..06b659fd 100644 --- a/rowers/integrations/__init__.py +++ b/rowers/integrations/__init__.py @@ -3,3 +3,5 @@ from .strava import StravaIntegration from .nk import NKIntegration from .sporttracks import SportTracksIntegration from .rp3 import RP3Integration +from .trainingpeaks import TPIntegration + diff --git a/rowers/integrations/rp3.py b/rowers/integrations/rp3.py index 657176ef..fb1230c4 100644 --- a/rowers/integrations/rp3.py +++ b/rowers/integrations/rp3.py @@ -244,8 +244,4 @@ class RP3Integration(SyncIntegration): return super(RP3Integration, self).token_refresh(*args, **kwargs) -# just as a quick test during development -u = User.objects.get(id=1) - -integration_1 = RP3Integration(u) diff --git a/rowers/integrations/trainingpeaks.py b/rowers/integrations/trainingpeaks.py new file mode 100644 index 00000000..d9cea185 --- /dev/null +++ b/rowers/integrations/trainingpeaks.py @@ -0,0 +1,134 @@ +from .integrations import SyncIntegration, NoTokenError +from rowers.models import User, Rower, Workout, TombStone + +import django_rq +queue = django_rq.get_queue('default') +queuelow = django_rq.get_queue('low') +queuehigh = django_rq.get_queue('high') + +from rowers.utils import myqueue, dologging, myqueue + +import requests + +from rowingdata import rowingdata + +from rowers.rower_rules import is_workout_user +import time +from django_rq import job + +from rowers.tasks import check_tp_workout_id, handle_workout_tp_upload + +from rowsandall_app.settings import ( + TP_CLIENT_ID, TP_CLIENT_SECRET, + TP_REDIRECT_URI, TP_CLIENT_KEY,TP_API_LOCATION, + TP_OAUTH_LOCATION, +) + +import gzip + +import base64 +from io import BytesIO + + +tpapilocation = TP_API_LOCATION + + + +class TPIntegration(SyncIntegration): + def __init__(self, *args, **kwargs): + super(TPIntegration, self).__init__(*args, **kwargs) + self.oauth_data = { + 'client_id': TP_CLIENT_ID, + 'client_secret': TP_CLIENT_SECRET, + 'redirect_uri': TP_REDIRECT_URI, + 'autorization_uri': "https://oauth.trainingpeaks.com/oauth/authorize?", + 'content_type': 'application/x-www-form-urlencoded', + 'tokenname': 'tptoken', + 'refreshtokenname': 'tprefreshtoken', + 'expirydatename': 'tptokenexpirydate', + 'bearer_auth': False, + 'base_url': "https://oauth.trainingpeaks.com/oauth/token", + 'scope': 'write', + } + + def createworkoutdata(self, w, *args, **kwargs): + filename = w.csvfilename + row = rowingdata(csvfile=filename) + tcxfilename = filename[:-4]+'.tcx' + try: + newnotes = w.notes+'\n from '+w.workoutsource+' via rowsandall.com' + except TypeError: + newnotes = 'from '+w.workoutsource+' via rowsandall.com' + + row.exporttotcx(tcxfilename, notes=newnotes) + + return tcxfilename + + + def workout_export(self, workout, *args, **kwargs) -> str: + thetoken = self.open() + tcxfilename = self.createworkoutdata(workout) + job = myqueue( + queue, + handle_workout_tp_upload, + workout, + thetoken, + tcxfilename + ) + return job.id + + + def get_workouts(self, *args, **kwargs) -> int: + raise NotImplementedError("not implemented") + + def get_workout(self, id) -> int: + raise NotImplementedError("not implemented") + + def get_workout_list(self, *args, **kwargs) -> list: + raise NotImplementedError("not implemented") + + def make_authorization_url(self, *args, **kwargs) -> str: # pragma: no cover + return super(TPIntegration, self).make_authorization_url(self, *args, **kwargs) + + def get_token(self, code, *args, **kwargs) -> (str, int, str): + # client_auth = requests.auth.HTTPBasicAuth(TP_CLIENT_KEY, TP_CLIENT_SECRET) + post_data = { + "client_id": TP_CLIENT_KEY, + "grant_type": "authorization_code", + "code": code, + "redirect_uri": TP_REDIRECT_URI, + "client_secret": TP_CLIENT_SECRET, + } + + response = requests.post( + TP_OAUTH_LOCATION+"/oauth/token/", + data=post_data, verify=False, + ) + + if response.status_code != 200: + raise NoTokenError + + try: + token_json = response.json() + thetoken = token_json['access_token'] + expires_in = token_json['expires_in'] + refresh_token = token_json['refresh_token'] + except KeyError: # pragma: no cover + thetoken = "" + expires_in = 0 + refresh_token = "" + + return thetoken, expires_in, refresh_token + + + def open(self, *args, **kwargs) -> str: + return super(TPIntegration, self).open(*args, **kwargs) + + def token_refresh(self, *args, **kwargs) -> str: + return super(TPIntegration, self).token_refresh(*args, **kwargs) + +# just as a quick test during development +u = User.objects.get(id=1) + +integration_1 = TPIntegration(u) + diff --git a/rowers/tasks.py b/rowers/tasks.py index 5430cf20..6418787a 100644 --- a/rowers/tasks.py +++ b/rowers/tasks.py @@ -47,6 +47,8 @@ import sys import json import traceback from time import strftime +import base64 +from io import BytesIO from scipy import optimize from scipy.signal import savgol_filter @@ -105,7 +107,8 @@ except KeyError: # pragma: no cover NK_API_LOCATION = CFG["nk_api_location"] TP_CLIENT_ID = CFG["tp_client_id"] TP_CLIENT_SECRET = CFG["tp_client_secret"] - +TP_API_LOCATION = CFG["tp_api_location"] +tpapilocation = TP_API_LOCATION from requests_oauthlib import OAuth1, OAuth1Session @@ -344,6 +347,46 @@ def handle_add_workouts_team(ws, t, debug=False, **kwargs): return 1 +def uploadactivity(access_token, filename, description='', + name='Rowsandall.com workout'): + + data_gz = BytesIO() + with open(filename, 'rb') as inF: + s = inF.read() + with gzip.GzipFile(fileobj=data_gz, mode="w") as gzf: + gzf.write(s) + + headers = { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Authorization': 'Bearer %s' % access_token + } + + data = { + "UploadClient": "rowsandall", + "Filename": filename, + "SetWorkoutPublic": True, + "Title": name, + "Type": "rowing", + "Comment": description, + "Data": base64.b64encode(data_gz.getvalue()).decode("ascii") + } + + resp = requests.post(tpapilocation+"/v3/file", + data=json.dumps(data), + headers=headers, verify=False) + + if resp.status_code not in (200, 202): # pragma: no cover + dologging('tp_export.log',resp.status_code) + dologging('tp_export.log',resp.reason) + dologging('tp_export.log',json.dumps(data)) + return 0, resp.reason, resp.status_code, headers + else: + return 1, "ok", 200, resp.headers + + return 0, 0, 0, 0 # pragma: no cover + + @app.task def check_tp_workout_id(workout, location, attempts=5, debug=False, **kwargs): authorizationstring = str('Bearer ' + workout.user.tptoken) @@ -361,6 +404,37 @@ def check_tp_workout_id(workout, location, attempts=5, debug=False, **kwargs): return 1 +@app.task +def handle_workout_tp_upload(w, thetoken, tcxfilename, debug=False, **kwargs): + tpid = 0 + r = w.user + if not tcxfilename: + return 0 + + res, reason, status_code, headers = uploadactivity( + thetoken, tcxfilename, + name=w.name + ) + + if res == 0: + w.tpid = -1 + try: + os.remove(tcxfilename) + except WindowsError: + pass + + w.save() + return 0 + + w.uploadedtotp = res + tpid = res + w.save() + os.remove(tcxfilename) + + check_tp_workout_id(w,headers['Location']) + + return tpid + @app.task def instroke_static(w, metric, debug=False, **kwargs): f1 = w.csvfilename[6:-4] diff --git a/rowers/templates/menu_workout.html b/rowers/templates/menu_workout.html index bba56d91..6d8ff847 100644 --- a/rowers/templates/menu_workout.html +++ b/rowers/templates/menu_workout.html @@ -234,10 +234,6 @@ Connect to RP3 - {% else %} - - RP3 - {% endif %}
  • diff --git a/rowers/tests/test_imports.py b/rowers/tests/test_imports.py index 0bac3505..8ff6ab46 100644 --- a/rowers/tests/test_imports.py +++ b/rowers/tests/test_imports.py @@ -1373,15 +1373,15 @@ class TPObjects(DjangoTestCase): self.assertEqual(response.status_code, 200) - @patch('rowers.tpstuff.requests.post', side_effect=mocked_requests) + @patch('rowers.integrations.trainingpeaks.requests.post', side_effect=mocked_requests) def test_tp_token_refresh(self, mock_post): response = self.c.get('/rowers/me/tprefresh/',follow=True) self.assertEqual(response.status_code, 200) - @patch('rowers.tpstuff.requests.post', side_effect=mocked_requests) - @patch('rowers.tpstuff.requests.get', side_effect=mocked_requests) + @patch('rowers.integrations.trainingpeaks.requests.post', side_effect=mocked_requests) + @patch('rowers.integrations.trainingpeaks.requests.get', side_effect=mocked_requests) def test_tp_upload(self, mock_get, mock_post): url = '/rowers/workout/'+encoded1+'/tpuploadw/' diff --git a/rowers/tests/testdata/testdata.tcx.gz b/rowers/tests/testdata/testdata.tcx.gz index 3133a666a6014bd9b73a9408007b84f22df346ba..dcef8d75f9aaf94262982865db76e4a6f2dbaf4f 100644 GIT binary patch literal 4001 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 GfB^v0#}*L) diff --git a/rowers/tpstuff.py b/rowers/tpstuff.py deleted file mode 100644 index 25ef8616..00000000 --- a/rowers/tpstuff.py +++ /dev/null @@ -1,232 +0,0 @@ -from celery import Celery, app -from rowers.rower_rules import is_workout_user -import time -from django_rq import job -# All the functionality needed to connect to Runkeeper -from rowers.imports import * -from rowers.utils import dologging -from rowers.tasks import check_tp_workout_id - -import django_rq -queue = django_rq.get_queue('default') -queuelow = django_rq.get_queue('low') -queuehigh = django_rq.get_queue('low') - -from rowers.utils import myqueue - -# Python -import gzip - -import base64 -from io import BytesIO - - -from rowsandall_app.settings import ( - C2_CLIENT_ID, C2_REDIRECT_URI, C2_CLIENT_SECRET, - STRAVA_CLIENT_ID, STRAVA_REDIRECT_URI, STRAVA_CLIENT_SECRET, - TP_CLIENT_ID, TP_CLIENT_SECRET, - TP_REDIRECT_URI, TP_CLIENT_KEY,TP_API_LOCATION, - TP_OAUTH_LOCATION, -) - -tpapilocation = TP_API_LOCATION - -oauth_data = { - 'client_id': TP_CLIENT_ID, - 'client_secret': TP_CLIENT_SECRET, - 'redirect_uri': TP_REDIRECT_URI, - 'autorization_uri': "https://oauth.trainingpeaks.com/oauth/authorize?", - 'content_type': 'application/x-www-form-urlencoded', - # 'content_type': 'application/json', - 'tokenname': 'tptoken', - 'refreshtokenname': 'tprefreshtoken', - 'expirydatename': 'tptokenexpirydate', - 'bearer_auth': False, - 'base_url': "https://oauth.trainingpeaks.com/oauth/token", - 'scope': 'write', -} - - -# Checks if user has UnderArmour token, renews them if they are expired -def tp_open(user): - return imports_open(user, oauth_data) - -# Refresh ST token using refresh token - - -def do_refresh_token(refreshtoken): - return imports_do_refresh_token(refreshtoken, oauth_data) - -# Exchange access code for long-lived access token - -def get_token(code): - # client_auth = requests.auth.HTTPBasicAuth(TP_CLIENT_KEY, TP_CLIENT_SECRET) - post_data = { - "client_id": TP_CLIENT_KEY, - "grant_type": "authorization_code", - "code": code, - "redirect_uri": TP_REDIRECT_URI, - "client_secret": TP_CLIENT_SECRET, - } - - response = requests.post( - TP_OAUTH_LOCATION+"/oauth/token/", - data=post_data, verify=False, - ) - - if response.status_code != 200: - return 0,0,0 - - try: - token_json = response.json() - thetoken = token_json['access_token'] - expires_in = token_json['expires_in'] - refresh_token = token_json['refresh_token'] - except KeyError: # pragma: no cover - thetoken = 0 - expires_in = 0 - refresh_token = 0 - - return thetoken, expires_in, refresh_token - -# Make authorization URL including random string - - -def make_authorization_url(request): # pragma: no cover - return imports_make_authorization_url(oauth_data) - - -def getidfromresponse(response): # pragma: no cover - t = json.loads(response.text) - - links = t["_links"] - - id = links["self"][0]["id"] - - return int(id) - - -def createtpworkoutdata(w): - filename = w.csvfilename - row = rowingdata(csvfile=filename) - tcxfilename = filename[:-4]+'.tcx' - try: - newnotes = w.notes+'\n from '+w.workoutsource+' via rowsandall.com' - except TypeError: - newnotes = 'from '+w.workoutsource+' via rowsandall.com' - - row.exporttotcx(tcxfilename, notes=newnotes) - - return tcxfilename - - -def tp_check(access_token): # pragma: no cover - headers = { - "Content-Type": "application/json", - 'Accept': 'application/json', - 'authorization': 'Bearer %s' % access_token - } - - resp = requests.post(tpapilocation+"/v2/info/version", - headers=headers, verify=False) - - return resp - - -def uploadactivity(access_token, filename, description='', - name='Rowsandall.com workout'): - - data_gz = BytesIO() - with open(filename, 'rb') as inF: - s = inF.read() - with gzip.GzipFile(fileobj=data_gz, mode="w") as gzf: - gzf.write(s) - - headers = { - 'Content-Type': 'application/json', - 'Accept': 'application/json', - 'Authorization': 'Bearer %s' % access_token - } - - data = { - "UploadClient": "rowsandall", - "Filename": filename, - "SetWorkoutPublic": True, - "Title": name, - "Type": "rowing", - "Comment": description, - "Data": base64.b64encode(data_gz.getvalue()).decode("ascii") - } - - #resp = requests.post(tpapilocation+"/v2/file/synchronous", - # data=json.dumps(data), - # headers=headers, verify=False) - - resp = requests.post(tpapilocation+"/v3/file", - data=json.dumps(data), - headers=headers, verify=False) - - if resp.status_code not in (200, 202): # pragma: no cover - dologging('tp_export.log',resp.status_code) - dologging('tp_export.log',resp.reason) - dologging('tp_export.log',json.dumps(data)) - return 0, resp.reason, resp.status_code, headers - else: - return 1, "ok", 200, resp.headers - - return 0, 0, 0, 0 # pragma: no cover - - -def workout_tp_upload(user, w): # pragma: no cover - message = "Uploading to TrainingPeaks" - tpid = 0 - r = w.user - - thetoken = tp_open(r.user) - - # need some code if token doesn't refresh - - if (is_workout_user(user, w)): - tcxfile = createtpworkoutdata(w) - if tcxfile: - res, reason, status_code, headers = uploadactivity( - thetoken, tcxfile, - name=w.name - ) - if res == 0: - message = "Upload to TrainingPeaks failed with status code " + \ - str(status_code)+": "+reason - w.tpid = -1 - try: - os.remove(tcxfile) - except WindowsError: - pass - - return message, tpid - - else: # res != 0 - w.uploadedtotp = res - tpid = res - w.save() - os.remove(tcxfile) - - job = myqueue(queuelow, - check_tp_workout_id, - w, - headers['Location']) - - return 'Successfully synchronized to TrainingPeaks', tpid - - else: # no tcxfile - dologging('tp_export.log','Failed to create tcx file') - message = "Upload to TrainingPeaks failed" - w.uploadedtotp = -1 - tpid = -1 - w.save() - return message, tpid - else: # not allowed to upload - message = "You are not allowed to export this workout to TP" - tpid = 0 - return message, tpid - - return message, tpid diff --git a/rowers/uploads.py b/rowers/uploads.py index f99501a4..606d5a9e 100644 --- a/rowers/uploads.py +++ b/rowers/uploads.py @@ -1,7 +1,6 @@ from rowers.mytypes import workouttypes, boattypes, otwtypes, workoutsources, workouttypes_ordered from rowers.rower_rules import is_promember -import rowers.tpstuff as tpstuff from rowers.integrations import * from rowers.utils import ( @@ -270,9 +269,8 @@ def do_sync(w, options, quick=False): upload_to_st = False if do_tp_export: try: - _, id = tpstuff.workout_tp_upload( - w.user.user, w - ) + tp_integration = TPIntegration(w.user.user) + id = tp_integration.workout_export(w) dologging('tp_export.log', 'exported workout {wid} for user {uid}'.format( wid = w.id, diff --git a/rowers/views/importviews.py b/rowers/views/importviews.py index 06f51140..4193d681 100644 --- a/rowers/views/importviews.py +++ b/rowers/views/importviews.py @@ -23,12 +23,15 @@ def default(o): # pragma: no cover # Send workout to TP @permission_required('workout.change_workout', fn=get_workout_by_opaqueid, raise_exception=True) def workout_tp_upload_view(request, id=0): - + message = "" r = getrower(request.user) res = -1 + + tp_integration = TPIntegration(request.user) + try: - _ = tp_open(r.user) + _ = tp_integration.open() except NoTokenError: # pragma: no cover return HttpResponseRedirect("/rowers/me/tpauthorize/") @@ -36,38 +39,9 @@ def workout_tp_upload_view(request, id=0): w = get_workout_by_opaqueid(request, id) r = w.user - tcxfile = tpstuff.createtpworkoutdata(w) - if tcxfile: - res, reason, status_code, headers = tpstuff.uploadactivity( - r.tptoken, tcxfile, - name=w.name - ) - if res == 0: # pragma: no cover - message = "Upload to TrainingPeaks failed with status code " + \ - str(status_code)+": "+reason - try: - os.remove(tcxfile) - except WindowsError: - pass + jobid = tp_integration.workout_export(w) + messages.info(request,'Your workout will be exported to TrainingPeaks in the background') - messages.error(request, message) - - else: # res != 0 - w.uploadedtotp = res - w.save() - os.remove(tcxfile) - job = myqueue(queuelow, - check_tp_workout_id, - w, - headers['Location']) - - messages.info(request, 'Uploaded to TrainingPeaks') - - else: # pragma: no cover # no tcxfile - message = "Upload to TrainingPeaks failed" - w.uploadedtotp = -1 - w.save() - messages.error(request, message) url = reverse(r.defaultlandingpage, kwargs={ @@ -301,20 +275,8 @@ def rower_c2_token_refresh(request): @login_required() def rower_tp_token_refresh(request): r = getrower(request.user) - res = tpstuff.do_refresh_token( - r.tprefreshtoken, - ) - access_token = res[0] - expires_in = res[1] - refresh_token = res[2] - expirydatetime = timezone.now()+datetime.timedelta(seconds=expires_in) - - r = getrower(request.user) - r.tptoken = access_token - r.tptokenexpirydate = expirydatetime - r.tprefreshtoken = refresh_token - - r.save() + tp_integration = TPIntegration(request.user) + token = tp_integration.token_refresh() successmessage = "Tokens refreshed. Good to go" messages.info(request, successmessage) @@ -759,11 +721,8 @@ def rower_process_tpcallback(request): url = reverse('rower_exportsettings_view') return HttpResponseRedirect(url) - res = tpstuff.get_token(code) - - access_token = res[0] - expires_in = res[1] - refresh_token = res[2] + tp_integration = TPIntegration(request.user) + access_token, expires_in, refresh_token = tp_integration.get_token(code) expirydatetime = timezone.now()+datetime.timedelta(seconds=expires_in) r = getrower(request.user) @@ -1560,7 +1519,7 @@ importsources = { 'polar': polarstuff, 'ownapi': ownapistuff, 'sporttracks': SportTracksIntegration, - 'trainingpeaks': tpstuff, + 'trainingpeaks': TPIntegration, 'nk': NKIntegration, 'rp3':RP3Integration, } diff --git a/rowers/views/statements.py b/rowers/views/statements.py index f3f2def6..d0526ca8 100644 --- a/rowers/views/statements.py +++ b/rowers/views/statements.py @@ -194,7 +194,6 @@ import datetime import iso8601 import rowers.rojabo_stuff as rojabo_stuff -from rowers.tpstuff import tp_open from iso8601 import ParseError import rowers.rojabo_stuff as rojabo_stuff @@ -205,7 +204,6 @@ import rowers.polarstuff as polarstuff from rowers.integrations import * -import rowers.tpstuff as tpstuff import rowers.ownapistuff as ownapistuff from rowers.ownapistuff import TEST_CLIENT_ID, TEST_CLIENT_SECRET, TEST_REDIRECT_URI diff --git a/rowers/views/workoutviews.py b/rowers/views/workoutviews.py index 6e8d0a62..4a72c3e0 100644 --- a/rowers/views/workoutviews.py +++ b/rowers/views/workoutviews.py @@ -5636,9 +5636,6 @@ def workout_upload_view(request, except NoTokenError: id = 0 message = "Something went wrong with the Concept2 sync" - if id > 1: - messages.info(request, message) - else: messages.error(request, message) if (upload_to_strava): # pragma: no cover @@ -5648,9 +5645,6 @@ def workout_upload_view(request, except NoTokenError: id = 0 message = "Please connect to Strava first" - if id > 1: - messages.info(request, message) - else: messages.error(request, message) if (upload_to_st): # pragma: no cover @@ -5660,23 +5654,14 @@ def workout_upload_view(request, except NoTokenError: message = "Please connect to SportTracks first" id = 0 - if id > 1: - messages.info(request, message) - else: messages.error(request, message) if (upload_to_tp): # pragma: no cover + tp_integration = TPIntegration(request.user) try: - message, id = tpstuff.workout_tp_upload( - request.user, w - ) + id = tp_integration.workout_export(w) except NoTokenError: message = "Please connect to TrainingPeaks first" - id = 0 - - if id > 1: - messages.info(request, message) - else: messages.error(request, message) if int(registrationid) < 0: # pragma: no cover From b179f3f9cfde17e81e2b1b2ec16acdd98b5fa58a Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Wed, 15 Feb 2023 19:46:08 +0100 Subject: [PATCH 19/27] passing tests --- rowers/tests/testdata/testdata.tcx.gz | Bin 4001 -> 4001 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/rowers/tests/testdata/testdata.tcx.gz b/rowers/tests/testdata/testdata.tcx.gz index dcef8d75f9aaf94262982865db76e4a6f2dbaf4f..2dd6e24406acadd4db84d1855eeae830fc2ab225 100644 GIT binary patch literal 4001 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 H#DD<+O*R%J literal 4001 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 Date: Thu, 16 Feb 2023 08:39:27 +0100 Subject: [PATCH 20/27] refactoring importviews, part 1 --- rowers/integrations/__init__.py | 9 + rowers/integrations/c2.py | 8 + rowers/integrations/integrations.py | 7 + rowers/integrations/nk.py | 6 + rowers/integrations/rp3.py | 14 +- rowers/integrations/sporttracks.py | 5 + rowers/integrations/strava.py | 6 + rowers/integrations/trainingpeaks.py | 17 +- rowers/urls.py | 32 +--- rowers/views/importviews.py | 272 ++++----------------------- 10 files changed, 113 insertions(+), 263 deletions(-) diff --git a/rowers/integrations/__init__.py b/rowers/integrations/__init__.py index 06b659fd..96bb9221 100644 --- a/rowers/integrations/__init__.py +++ b/rowers/integrations/__init__.py @@ -5,3 +5,12 @@ from .sporttracks import SportTracksIntegration from .rp3 import RP3Integration from .trainingpeaks import TPIntegration +importsources = { + 'c2': C2Integration, + 'strava': StravaIntegration, + 'sporttracks': SportTracksIntegration, + 'trainingpeaks': TPIntegration, + 'nk': NKIntegration, + 'rp3':RP3Integration, +} + diff --git a/rowers/integrations/c2.py b/rowers/integrations/c2.py index 45397cfa..60a066e3 100644 --- a/rowers/integrations/c2.py +++ b/rowers/integrations/c2.py @@ -71,6 +71,14 @@ class C2Integration(SyncIntegration): 'scope': 'write', } + def get_name(self): + return "Concept2 Logbook" + + def get_shortname(self): + return "c2" + + + def get_token(self, code, *args, **kwargs): messg = '' scope = "user:read,results:write" diff --git a/rowers/integrations/integrations.py b/rowers/integrations/integrations.py index d014a221..df11b674 100644 --- a/rowers/integrations/integrations.py +++ b/rowers/integrations/integrations.py @@ -36,6 +36,13 @@ class SyncIntegration(metaclass=ABCMeta): callable(subclass.get_token) or NotImplemented) + @abstractmethod + def get_name(self): + raise NotImplementedError + + @abstractmethod + def get_shortname(self): + raise NotImplementedError @abstractmethod def createworkoutdata(self, w, *args, **kwargs): diff --git a/rowers/integrations/nk.py b/rowers/integrations/nk.py index 42b1d17a..987b8972 100644 --- a/rowers/integrations/nk.py +++ b/rowers/integrations/nk.py @@ -44,6 +44,12 @@ class NKIntegration(SyncIntegration): 'scope': 'read', } + def get_name(self): + return "NK Logbook" + + def get_shortname(self): + return "nk" + def createworkoutdata(self, w, *args, **kwargs): return None diff --git a/rowers/integrations/rp3.py b/rowers/integrations/rp3.py index fb1230c4..02c8f8d0 100644 --- a/rowers/integrations/rp3.py +++ b/rowers/integrations/rp3.py @@ -40,6 +40,11 @@ class RP3Integration(SyncIntegration): 'scope': 'read,write', } + def get_name(self): + return "RP3 Logbook" + + def get_shortname(self): + return "rp3" def createworkoutdata(self, w, *args, **kwargs): return None @@ -200,7 +205,14 @@ class RP3Integration(SyncIntegration): def make_authorization_url(self, *args, **kwargs) -> str: # pragma: no cover - return super(RP3Integration, self).make_authorization_url(*args, **krags) + params = {"client_id": RP3_CLIENT_KEY, + "response_type": "code", + "redirect_uri": RP3_REDIRECT_URI, + } + url = "https://rp3rowing-app.com/oauth/authorize/?" + \ + urllib.parse.urlencode(params) + + return url def get_token(self, code, *args, **kwargs) -> (str, int, str): post_data = { diff --git a/rowers/integrations/sporttracks.py b/rowers/integrations/sporttracks.py index e790f9e2..1c65b4da 100644 --- a/rowers/integrations/sporttracks.py +++ b/rowers/integrations/sporttracks.py @@ -64,6 +64,11 @@ class SportTracksIntegration(SyncIntegration): 'scope': 'write', } + def get_name(self): + return "SportTracks" + + def get_shortname(self): + return "sporttracks" def open(self, *args, **kwargs) -> str: return super(SportTracksIntegration, self).open(*args, **kwargs) diff --git a/rowers/integrations/strava.py b/rowers/integrations/strava.py index efd83ccc..6344fd1c 100644 --- a/rowers/integrations/strava.py +++ b/rowers/integrations/strava.py @@ -99,6 +99,12 @@ class StravaIntegration(SyncIntegration): def get_token(self, code, *args, **kwargs): return super(StravaIntegration, self).get_token(code, *args, **kwargs) + def get_name(self): + return "Strava" + + def get_shortname(self): + return "strava" + 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) diff --git a/rowers/integrations/trainingpeaks.py b/rowers/integrations/trainingpeaks.py index d9cea185..c816ee6a 100644 --- a/rowers/integrations/trainingpeaks.py +++ b/rowers/integrations/trainingpeaks.py @@ -51,6 +51,12 @@ class TPIntegration(SyncIntegration): 'scope': 'write', } + def get_name(self): + return "TrainingPeaks" + + def get_shortname(self): + return "trainingpeaks" + def createworkoutdata(self, w, *args, **kwargs): filename = w.csvfilename row = rowingdata(csvfile=filename) @@ -88,7 +94,16 @@ class TPIntegration(SyncIntegration): raise NotImplementedError("not implemented") def make_authorization_url(self, *args, **kwargs) -> str: # pragma: no cover - return super(TPIntegration, self).make_authorization_url(self, *args, **kwargs) + params = {"client_id": TP_CLIENT_KEY, + "response_type": "code", + "redirect_uri": TP_REDIRECT_URI, + "scope": "file:write", + } + url = TP_OAUTH_LOCATION+"oauth/authorize/?" + \ + urllib.parse.urlencode(params) + + return url + def get_token(self, code, *args, **kwargs) -> (str, int, str): # client_auth = requests.auth.HTTPBasicAuth(TP_CLIENT_KEY, TP_CLIENT_SECRET) diff --git a/rowers/urls.py b/rowers/urls.py index e81ffb7f..40810641 100644 --- a/rowers/urls.py +++ b/rowers/urls.py @@ -672,16 +672,10 @@ urlpatterns = [ name='workout_polarimport_view'), re_path(r'^workout/polarimport/user/(?P\d+)/', views.workout_polarimport_view, name='workout_polarimport_view'), - re_path(r'^workout/(?P\b[0-9A-Fa-f]+\b)/c2uploadw/$', - views.workout_c2_upload_view, name='workout_c2_upload_view'), - re_path(r'^workout/(?P\b[0-9A-Fa-f]+\b)/stravauploadw/$', - views.workout_strava_upload_view, name='workout_strava_upload_view'), + re_path('r^workout/(?P\b[0-9A-Fa-f]+\b)/(?P\w+.*)uploadw/$', + views.workout_export_view, name='workout_export_view'), re_path(r'^workout/(?P\b[0-9A-Fa-f]+\b)/recalcsummary/$', views.workout_recalcsummary_view, name='workout_recalcsummary_view'), - re_path(r'^workout/(?P\b[0-9A-Fa-f]+\b)/sporttracksuploadw/$', - views.workout_sporttracks_upload_view, name='workout_sporttracks_upload_view'), - re_path(r'^workout/(?P\b[0-9A-Fa-f]+\b)/tpuploadw/$', - views.workout_tp_upload_view, name='workout_tp_upload_view'), re_path(r'^alerts/user/(?P\d+)/$', views.alerts_view, name='alerts_view'), re_path(r'^alerts/$', views.alerts_view, name='alerts_view'), @@ -791,32 +785,18 @@ urlpatterns = [ re_path(r'^me/prefs/user/(?P\d+)/$', views.rower_simpleprefs_view, name='rower_simpleprefs_view'), re_path(r'^me/edit/(.+.*)/$', views.rower_edit_view, name='rower_edit_view'), - re_path(r'^me/c2authorize/$', views.rower_c2_authorize, - name='rower_c2_authorize'), - re_path(r'^me/nkauthorize/$', views.rower_nk_authorize, - name='rower_nk_authorize'), + re_path(r'^me/(?P\w+.*)authorize', views.rower_integration_authorize, + name='rower_integration_authorize'), re_path(r'^me/rojaboauthorize/$', views.rower_rojabo_authorize, name='rower_rojabo_authorize'), re_path(r'^me/polarauthorize/$', views.rower_polar_authorize, name='rower_polar_authorize'), re_path(r'^me/revokeapp/(?P\d+)/$', views.rower_revokeapp_view, name='rower_revokeapp_view'), - re_path(r'^me/stravaauthorize/$', views.rower_strava_authorize, - name='rower_strava_authorize'), re_path(r'^me/garminauthorize/$', views.rower_garmin_authorize, name='rower_garmin_authorize'), - re_path(r'^me/sporttracksauthorize/$', views.rower_sporttracks_authorize, - name='rower_sporttracks_authorize'), - re_path(r'^me/tpauthorize/$', views.rower_tp_authorize, - name='rower_tp_authorize'), - re_path(r'^me/rp3authorize/$', views.rower_rp3_authorize, - name='rower_rp3_authorize'), - re_path(r'^me/sporttracksrefresh/$', views.rower_sporttracks_token_refresh, - name='rower_sporttracks_token_refresh'), - re_path(r'^me/tprefresh/$', views.rower_tp_token_refresh, - name='rower_tp_token_refresh'), - re_path(r'^me/c2refresh/$', views.rower_c2_token_refresh, - name='rower_c2_token_refresh'), + re_path(r'^me/(?P\w+.*)refresh/$', views.rower_integration_token_refresh, + name='rower_integration_token_refresh'), re_path(r'^me/favoritecharts/$', views.rower_favoritecharts_view, name='rower_favoritecharts_view'), re_path(r'^me/favoritecharts/user/(?P\d+)/$', diff --git a/rowers/views/importviews.py b/rowers/views/importviews.py index 4193d681..9e102150 100644 --- a/rowers/views/importviews.py +++ b/rowers/views/importviews.py @@ -7,9 +7,9 @@ from rowers.views.statements import * from rowers.plannedsessions import get_dates_timeperiod from rowers.tasks import fetch_strava_workout -from rowers.integrations import C2Integration, StravaIntegration, NKIntegration import rowers.integrations.strava as strava +from rowers.integrations import importsources import numpy @@ -19,87 +19,21 @@ def default(o): # pragma: no cover return int(o) raise TypeError - -# Send workout to TP -@permission_required('workout.change_workout', fn=get_workout_by_opaqueid, raise_exception=True) -def workout_tp_upload_view(request, id=0): - - message = "" - r = getrower(request.user) - res = -1 - - tp_integration = TPIntegration(request.user) - - try: - _ = tp_integration.open() - except NoTokenError: # pragma: no cover - return HttpResponseRedirect("/rowers/me/tpauthorize/") - - # ready to upload. Hurray - w = get_workout_by_opaqueid(request, id) - r = w.user - - jobid = tp_integration.workout_export(w) - messages.info(request,'Your workout will be exported to TrainingPeaks in the background') - - - url = reverse(r.defaultlandingpage, - kwargs={ - 'id': encoder.encode_hex(w.id), - }) - - return HttpResponseRedirect(url) - - -# Send workout to Strava -# abundance of error logging here because there were/are some bugs -@permission_required('workout.change_workout', fn=get_workout_by_opaqueid, raise_exception=True) -def workout_strava_upload_view(request, id=0): +def workout_export_view(request, id=0, source='c2'): r = getrower(request.user) w = get_workout_by_opaqueid(request, id) - result = -1 - strava_integration = StravaIntegration(request.user) + integration = importsources[source](request.user) try: - stravaid = strava_integration.workout_export(w) + id = integration.workout_export(w) except NoTokenError: - return HttpResponseRedirect("/rowers/me/stravaauthorize") + return HttpResponseRedirect(integration.make_authorization_url()) messages.info( - request, 'Your workout will be synchronized to Strava in the background') - - url = reverse(r.defaultlandingpage, - kwargs={ - 'id': encoder.encode_hex(w.id), - } - ) - return HttpResponseRedirect(url) - -# Upload workout to Concept2 logbook - - -@login_required() -def workout_c2_upload_view(request, id=0): - message = "" - # ready to upload. Hurray - w = get_workout(id) - r = w.user - - s = 'C2 Upload Workout starttime {startdatetime} timezone {timezone} user id {userid}'.format( - startdatetime=w.startdatetime, - timezone=w.timezone, - userid=w.user.user.id + request, + "Your workout will be synchronized to {name} in the background".format( + name=integration.get_name() + ) ) - dologging('c2_log.log', s) - - c2_integration = C2Integration(request.user) - - try: - c2id = c2_integration.workout_export(w) - except NoTokenError: # pragma: no cover - return HttpResponseRedirect("/rowers/me/c2authorize/") - - messages.info( - request, 'Your workout will be synchronized to the Concept2 Logbook in the background') url = reverse(r.defaultlandingpage, kwargs={ @@ -109,38 +43,12 @@ def workout_c2_upload_view(request, id=0): return HttpResponseRedirect(url) -# Upload workout to SportTracks -@permission_required('workout.change_workout', fn=get_workout_by_opaqueid) -def workout_sporttracks_upload_view(request, id=0): - st_integration = SportTracksIntegration(request.user) - - # ready to upload. Hurray - w = get_workout(id) - r = w.user - - id = st_integration.workout_export(w) - - messages.info( - request, 'Your workout will be synchronized with SportTracks in the background') - - url = reverse(r.defaultlandingpage, - kwargs={ - 'id': encoder.encode_hex(w.id), - }) # pragma: no cover - - return HttpResponseRedirect(url) # pragma: no cover - # ROJABO authorization def rower_rojabo_authorize(request): # pragma: no cover state = str(uuid4()) scope = "read" params = { - # "grant_type": "authorization_code", - # "response_type": "code", "client_id": ROJABO_CLIENT_ID, - #"client_secret": ROJABO_CLIENT_SECRET, - # "scope": scope, - #"state": state, "redirect_uri": ROJABO_REDIRECT_URI, } @@ -148,25 +56,13 @@ def rower_rojabo_authorize(request): # pragma: no cover return HttpResponseRedirect(url) -# NK Logbook authorization @login_required() -def rower_nk_authorize(request): # pragma: no cover - nk_integration = NKIntegration(request.user) - url = nk_integration.make_authorization_url() - +def rower_integration_authorize(request, source='c2'): + integration = importsources[source](request.user) + url = integration.make_authorization_url() return HttpResponseRedirect(url) -# Concept2 authorization -@login_required() -def rower_c2_authorize(request): # pragma: no cover - c2_integration = C2Integration(request.user) - url = c2_integration.make_authorization_url() - - return HttpResponseRedirect(url) - -# Garmin authorization - @login_required() def rower_garmin_authorize(request): # pragma: no cover @@ -175,20 +71,8 @@ def rower_garmin_authorize(request): # pragma: no cover request.session['garmin_owner_secret'] = secret return HttpResponseRedirect(authorization_url) -# Strava Authorization - - -@login_required() -def rower_strava_authorize(request): # pragma: no cover - strava_integration = StravaIntegration(request.user) - url = strava_integration.make_authorization_url() - - return HttpResponseRedirect(url) - # Polar Authorization - - @login_required() def rower_polar_authorize(request): # pragma: no cover @@ -208,89 +92,17 @@ def rower_polar_authorize(request): # pragma: no cover return HttpResponseRedirect(url) - -# SportTracks Authorization @login_required() -def rower_sporttracks_authorize(request): # pragma: no cover - # Generate a random string for the state parameter - # Save it for use later to prevent xsrf attacks - st_integration = SportTracksIntegration(request.user) - url = st_integration.make_authorization_url() - - return HttpResponseRedirect(url) - - -# RP3 Authorization -@login_required() -def rower_rp3_authorize(request): # pragma: no cover - # Generate a random string for the state parameter - # Save it for use later to prevent xsrf attacks - - # state = str(uuid4()) - params = {"client_id": RP3_CLIENT_KEY, - "response_type": "code", - "redirect_uri": RP3_REDIRECT_URI, - } - url = "https://rp3rowing-app.com/oauth/authorize/?" + \ - urllib.parse.urlencode(params) - - return HttpResponseRedirect(url) - -# TrainingPeaks Authorization - - -@login_required() -def rower_tp_authorize(request): # pragma: no cover - # Generate a random string for the state parameter - # Save it for use later to prevent xsrf attacks - - # state = str(uuid4()) - params = {"client_id": TP_CLIENT_KEY, - "response_type": "code", - "redirect_uri": TP_REDIRECT_URI, - "scope": "file:write", - } - url = TP_OAUTH_LOCATION+"oauth/authorize/?" + \ - urllib.parse.urlencode(params) - - return HttpResponseRedirect(url) - - -# Concept2 token refresh. URL for manual refresh. Not visible to users -@login_required() -def rower_c2_token_refresh(request): - c2_integration = C2Integration(request.user) +def rower_integration_token_refresh(request, source='c2'): + integration = importsource[source](request.user) try: - token = c2_integration.token_refresh() + token = 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') - - return HttpResponseRedirect(url) - - -# TrainingPeaks token refresh. URL for manual refresh. Not visible to users -@login_required() -def rower_tp_token_refresh(request): - r = getrower(request.user) - tp_integration = TPIntegration(request.user) - token = tp_integration.token_refresh() - - successmessage = "Tokens refreshed. Good to go" - messages.info(request, successmessage) - - url = reverse('workouts_view') - - return HttpResponseRedirect(url) - - -# SportTracks token refresh. URL for manual refresh. Not visible to users -@login_required() -def rower_sporttracks_token_refresh(request): - st_integration = SportTracksIntegration(request.user) - result = st_integration.token_refresh() + messages.error(request, + "Something went wrong refreshing your access tokens to {name}. Please reauthorize".format( + name=integration.get_name() + )) url = reverse('workouts_view') @@ -300,7 +112,7 @@ def rower_sporttracks_token_refresh(request): # Concept2 Callback @login_required() def rower_process_callback(request): - c2_integration = C2Integration(request.user) + c2_integration = importsources['c2'](request.user) try: code = request.GET['code'] res = c2_integration.get_token(code) @@ -473,7 +285,7 @@ def rower_process_nkcallback(request): # pragma: no cover # do stuff try: code = request.GET.get('code', None) - nk_integration = NKIntegration(request.user) + nk_integration = importsources['nk'](request.user) access_token, expires_in, refresh_token = nk_integration.get_token(code) except MultiValueDictKeyError: message = "The resource owner or authorization server denied the request" @@ -520,7 +332,7 @@ def workout_getnkworkout_all(request, startdatestring='', enddatestring=''): after = arrow.get(startdate) after = str(int(after.timestamp()*1000)) - nk_integration = NKIntegration(request.user) + nk_integration = importsources['nk'](request.user) try: _ = nk_integration.open() @@ -550,7 +362,7 @@ def workout_nkimport_view(request, userid=0, after=0, before=0): startdate = startdate.date() enddate = enddate.date() r = getrequestrower(request, userid=userid) - nk_integration = NKIntegration(request.user) + nk_integration = importsources['nk'](request.user) try: _ = nk_integration.open() @@ -609,7 +421,7 @@ def workout_nkimport_view(request, userid=0, after=0, before=0): @login_required() def rower_process_stravacallback(request): - strava_integration = StravaIntegration(request.user) + strava_integration = importsources['strava'](request.user) try: code = request.GET['code'] _ = request.GET['scope'] @@ -663,7 +475,7 @@ def rower_process_sporttrackscallback(request): url = reverse('rower_exportsettings_view') return HttpResponseRedirect(url) - st_integration = SportTracksIntegration(request.user) + st_integration = importsources['sporttracks'](request.user) token = st_integration.get_token(code) successmessage = "Tokens stored. Good to go. Please check your import/export settings" @@ -687,7 +499,7 @@ def rower_process_rp3callback(request): # pragma: no cover url = reverse('rower_exportsettings_view') return HttpResponseRedirect(url) - rp3_integration = RP3Integration(request.user) + rp3_integration = importsources['rp3'](request.user) access_token, expires_in, refresh_token = rp3_integration.get_token(code) expirydatetime = timezone.now()+datetime.timedelta(seconds=expires_in) @@ -721,7 +533,7 @@ def rower_process_tpcallback(request): url = reverse('rower_exportsettings_view') return HttpResponseRedirect(url) - tp_integration = TPIntegration(request.user) + tp_integration = importsources['trainingpeaks'](request.user) access_token, expires_in, refresh_token = tp_integration.get_token(code) expirydatetime = timezone.now()+datetime.timedelta(seconds=expires_in) @@ -760,7 +572,7 @@ def rower_process_testcallback(request): # pragma: no cover @permission_required('rower.is_coach', fn=get_user_by_userid, raise_exception=True) def workout_rp3import_view(request, userid=0): r = getrequestrower(request, userid=userid) - rp3_integration = RP3Integration(request.user) + rp3_integration = importsources['rp3'](request.user) try: _ = rp3_integration.open() @@ -993,7 +805,7 @@ def workout_stravaimport_view(request, message="", userid=0): kwargs={'userid': request.user.id}) return HttpResponseRedirect(url) - strava_integration = StravaIntegration(request.user) + strava_integration = importsources['strava'](request.user) try: _ = strava_integration.open() except NoTokenError: # pragma: no cover @@ -1099,7 +911,7 @@ def strava_webhook_view(request): ws = Workout.objects.filter(uploadedtostrava=stravaid) if ws.count() == 0 and r.strava_auto_import: - strava_integration = StravaIntegration(r.user) + strava_integration = importsources['strava'](r.user) jobid = strava_integration.get_workout(stravaid) if jobid == 0: # pragma: no cover dologging('strava_webhooks.log', @@ -1316,7 +1128,7 @@ def workout_polarimport_view(request, userid=0): # pragma: no cover @permission_required('rower.is_coach', fn=get_user_by_userid, raise_exception=True) @permission_required('rower.is_not_freecoach', fn=get_user_by_userid, raise_exception=True) def workout_sporttracksimport_view(request, message="", userid=0): - st_integration = SportTracksIntegration(request.user) + st_integration = importsources['sporttracks'](request.user) try: _ = st_integration.open() except NoTokenError: @@ -1379,7 +1191,7 @@ def workout_sporttracksimport_view(request, message="", userid=0): @login_required() def workout_getc2workout_all(request, page=1, message=""): # pragma: no cover r = getrequestrower(request) - c2_integration = C2Integration(request.user) + c2_integration = importsources['c2'](request.user) try: _ = c2_integration.open() except NoTokenError: # pragma: no cover @@ -1406,7 +1218,7 @@ def workout_getrp3workout_all(request): # pragma: no cover r = getrequestrower(request) - rp3_integration = RP3Integration(request.user) + rp3_integration = importsources['rp3'](request.user) result = rp3_integration.get_workouts() if result: @@ -1432,7 +1244,7 @@ def workout_c2import_view(request, page=1, userid=0, message=""): 'userid': request.user.id}) return HttpResponseRedirect(url) - c2_integration = C2Integration(request.user) + c2_integration = importsources['c2'](request.user) try: _ = c2_integration.open() except NoTokenError: # pragma: no cover @@ -1513,16 +1325,6 @@ importauthorizeviews = { 'rp3': 'rower_rp3_authorize', } -importsources = { - 'c2': C2Integration, - 'strava': StravaIntegration, - 'polar': polarstuff, - 'ownapi': ownapistuff, - 'sporttracks': SportTracksIntegration, - 'trainingpeaks': TPIntegration, - 'nk': NKIntegration, - 'rp3':RP3Integration, -} @@ -1561,7 +1363,7 @@ def workout_getimportview_old(request, externalid, source='c2', do_async=True): # Imports all new workouts from SportTracks @login_required() def workout_getsporttracksworkout_all(request): - st_integration = SportTracksIntegration(request.user) + st_integration = importsources['sporttracks'](request.user) st_integration.get_workouts() messages.info(request,"Your SportTracks workouts will be imported in the background") @@ -1573,7 +1375,7 @@ def workout_getsporttracksworkout_all(request): def workout_getimportview(request, externalid, source='c2', do_async=True): try: integration = importsources[source](request.user) - except TypeError: + except (TypeError, NotImplementedError, KeyError): 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') @@ -1606,7 +1408,7 @@ def workout_getimportview(request, externalid, source='c2', do_async=True): # Imports all new workouts from SportTracks @login_required() def workout_getsporttracksworkout_all(request): - st_integration = SportTracksIntegration(request.user) + st_integration = importsources['sporttracks'](request.user) try: _ = st_integration.get_workouts() messages.info(request,"Your SportTracks workouts will be imported in the background") From 20a7ebea46c5b8ab46ae1645c0f3dcc0c8c0e1ff Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Fri, 17 Feb 2023 17:14:56 +0100 Subject: [PATCH 21/27] testing full refactor, excluding garmin, polar, rojabo --- rowers/integrations/__init__.py | 2 + rowers/templates/list_import.html | 11 +- rowers/templates/menu_workouts.html | 2 +- rowers/tests/test_imports.py | 38 +- rowers/tests/testdata/testdata.tcx.gz | Bin 4001 -> 4001 bytes rowers/traverselinktest.py | 1 - rowers/urls.py | 50 +-- rowers/views/importviews.py | 492 ++++---------------------- 8 files changed, 97 insertions(+), 499 deletions(-) diff --git a/rowers/integrations/__init__.py b/rowers/integrations/__init__.py index 96bb9221..4e34497e 100644 --- a/rowers/integrations/__init__.py +++ b/rowers/integrations/__init__.py @@ -5,12 +5,14 @@ from .sporttracks import SportTracksIntegration from .rp3 import RP3Integration from .trainingpeaks import TPIntegration + importsources = { 'c2': C2Integration, 'strava': StravaIntegration, 'sporttracks': SportTracksIntegration, 'trainingpeaks': TPIntegration, 'nk': NKIntegration, + 'tp':TPIntegration, 'rp3':RP3Integration, } diff --git a/rowers/templates/list_import.html b/rowers/templates/list_import.html index 1a0f21c2..643bd6db 100644 --- a/rowers/templates/list_import.html +++ b/rowers/templates/list_import.html @@ -8,6 +8,15 @@

    Available on {{ integration }}

      +
    • + +
  • Total Distance Type Source Comment Name New
    {{ workout|lookup:'distance' }} {{ workout|lookup:'rowtype' }} {{ workout|lookup:'source' }}{{ workout|lookup:'comment' }}{{ workout|lookup:'name' }} {{ workout|lookup:'new' }}
    + {{ dateform.as_table }} +
    + {% csrf_token %} + +
    +
  • {% if workouts %} {% if integration == 'C2 Logbook' %}
  • @@ -30,7 +39,7 @@

  • - {% endif %} + {% endif %}
  • {% csrf_token %} diff --git a/rowers/templates/menu_workouts.html b/rowers/templates/menu_workouts.html index e7bc91c7..b85f728f 100644 --- a/rowers/templates/menu_workouts.html +++ b/rowers/templates/menu_workouts.html @@ -51,7 +51,7 @@
      -
    • Concept2
    • +
    • Concept2
    • Strava
    • SportTracks
    • diff --git a/rowers/tests/test_imports.py b/rowers/tests/test_imports.py index 8ff6ab46..e84c7404 100644 --- a/rowers/tests/test_imports.py +++ b/rowers/tests/test_imports.py @@ -359,8 +359,8 @@ class C2Objects(DjangoTestCase): @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/') - + url = '/rowers/workout/'+encoded1+'/c2uploadw/' + response = self.c.get(url) self.assertRedirects(response, expected_url = '/rowers/workout/'+encoded1+'/edit/', status_code=302,target_status_code=200) @@ -371,7 +371,7 @@ class C2Objects(DjangoTestCase): @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) + response = self.c.get('/rowers/workout/c2import',follow=True) self.assertEqual(response.status_code,200) @@ -381,7 +381,7 @@ class C2Objects(DjangoTestCase): response = self.c.get('/rowers/workout/c2import/12/',follow=True) - expected_url = reverse('workout_c2import_view') + expected_url = reverse('workout_import_view',kwargs={'source':'c2'}) self.assertRedirects(response, expected_url=expected_url, @@ -513,7 +513,7 @@ class C2Objects(DjangoTestCase): def test_c2_import_tz(self, mock_get, mocked_sqlalchemy, mock_session): response = self.c.get('/rowers/workout/c2import/22/',follow=True) - expected_url = '/rowers/workout/c2list/' + expected_url = '/rowers/workout/c2import/' self.assertRedirects(response, expected_url=expected_url, @@ -532,7 +532,7 @@ class C2Objects(DjangoTestCase): def test_c2_import_tz3(self, mock_get, mocked_sqlalchemy): response = self.c.get('/rowers/workout/c2import/32/',follow=True) - expected_url = '/rowers/workout/c2list/' + expected_url = '/rowers/workout/c2import/' self.assertRedirects(response, expected_url=expected_url, @@ -552,7 +552,7 @@ class C2Objects(DjangoTestCase): def test_c2_import_tz2(self, mock_get, mocked_sqlalchemy, MockSession): response = self.c.get('/rowers/workout/c2import/31/',follow=True) - expected_url = '/rowers/workout/c2list/' + expected_url = '/rowers/workout/c2import/' result = tasks.handle_c2_getworkout(self.r.user.id,self.r.c2token,31,self.r.defaulttimezone) @@ -648,7 +648,7 @@ class C2ObjectsTokenExpired(DjangoTestCase): @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) + response = self.c.get('/rowers/workout/c2import/',follow=True) self.assertEqual(response.status_code,200) @@ -757,14 +757,10 @@ class NKObjects(DjangoTestCase): result = integration.open() self.assertEqual(result,"TA3n1vrNjuQJWw0TdCDHnjSmrjIPULhTlejMIWqq") - response = self.c.get('/rowers/workout/nkimport/all/',follow=True) + response = self.c.get('/rowers/workout/nkimport/?selectallnew=true',follow=True) expected = reverse('workouts_view') - self.assertRedirects(response, - expected_url=expected, - status_code=302,target_status_code=200) - self.assertEqual(response.status_code, 200) @patch('rowers.integrations.nk.requests.get', side_effect=mocked_requests) @@ -780,7 +776,7 @@ class NKObjects(DjangoTestCase): response = self.c.get('/rowers/workout/nkimport/469',follow=True) - expected_url = reverse('workout_nkimport_view') + expected_url = reverse('workout_import_view',kwargs={'source':'nk'}) self.assertRedirects(response, expected_url=expected_url, @@ -804,7 +800,7 @@ class NKObjects(DjangoTestCase): result = integration.token_refresh() response = self.c.get('/rowers/workout/nkimport/404',follow=True) - expected_url = reverse('workout_nkimport_view') + expected_url = reverse('workout_import_view',kwargs={'source':'nk'}) self.assertRedirects(response, expected_url=expected_url, @@ -979,7 +975,7 @@ class RP3Objects(DjangoTestCase): response = self.c.get('/rowers/workout/rp3import/591621',follow=True) - expected_url = reverse('workout_rp3import_view') + expected_url = reverse('workout_import_view',kwargs={'source':'rp3'}) self.assertRedirects(response, expected_url=expected_url, @@ -1163,7 +1159,7 @@ class StravaObjects(DjangoTestCase): mocked_getsmallrowdata_db): response = self.c.get('/rowers/workout/stravaimport/12',follow=True) - expected_url = reverse('workout_stravaimport_view') + expected_url = reverse('workout_import_view',kwargs={'source':'strava'}) self.assertRedirects(response, expected_url=expected_url, @@ -1296,13 +1292,7 @@ class STObjects(DjangoTestCase): @patch('rowers.imports.requests.get', side_effect=mocked_requests) def test_sporttracks_import_all(self, mock_get): - response = self.c.get('/rowers/workout/sporttracksimport/all/',follow=True) - - expected_url = reverse('workouts_view') - - self.assertRedirects(response, - expected_url=expected_url, - status_code=302,target_status_code=200) + response = self.c.get('/rowers/workout/sporttracksimport/?selectallnew=true') self.assertEqual(response.status_code, 200) diff --git a/rowers/tests/testdata/testdata.tcx.gz b/rowers/tests/testdata/testdata.tcx.gz index 2dd6e24406acadd4db84d1855eeae830fc2ab225..7322a59a7d7e228cffee8b1b4a65350d9f9b57f1 100644 GIT binary patch literal 4001 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 H#DD<+O*R%J diff --git a/rowers/traverselinktest.py b/rowers/traverselinktest.py index 44fbd53a..503b4fbd 100644 --- a/rowers/traverselinktest.py +++ b/rowers/traverselinktest.py @@ -95,7 +95,6 @@ class TraverseLinksTest(TestCase): '.*authorize.*', '.*youtu.*', '.*earth.*', - '.*c2list.*', '.*stravaimport.*', '.*performancephones.*', '.*sporttracks.*', diff --git a/rowers/urls.py b/rowers/urls.py index 40810641..d448a193 100644 --- a/rowers/urls.py +++ b/rowers/urls.py @@ -618,61 +618,17 @@ urlpatterns = [ views.workout_smoothenpace_view, name='workout_smoothenpace_view'), re_path(r'^workout/(?P\b[0-9A-Fa-f]+\b)/undosmoothenpace/$', views.workout_undo_smoothenpace_view, name='workout_undo_smoothenpace_view'), - re_path(r'^workout/c2import/$', views.workout_c2import_view, - name='workout_c2import_view'), - re_path(r'^workout/c2list/$', views.workout_c2import_view, - name='workout_c2import_view'), - re_path(r'^workout/c2list/(?P\d+)/$', - views.workout_c2import_view, name='workout_c2import_view'), - re_path(r'^workout/c2list/user/(?P\d+)/$', - views.workout_c2import_view, name='workout_c2import_view'), - re_path(r'^workout/c2list/(?P\d+)/user/(?P\d+)/$', - views.workout_c2import_view, name='workout_c2import_view'), - re_path(r'^workout/rp3import/$', views.workout_rp3import_view, - name='workout_rp3import_view'), - re_path(r'^workout/rp3import/user/(?P\d+)/$', - views.workout_rp3import_view, name='workout_rp3import_view'), - re_path(r'^workout/stravaimport/$', views.workout_stravaimport_view, - name='workout_stravaimport_view'), re_path(r'^session/rojaboimport/$', views.workout_rojaboimport_view, name='workout_rojaboimport_view'), - re_path(r'^workout/stravaimport/user/(?P\d+)/$', - views.workout_stravaimport_view, name='workout_stravaimport_view'), - re_path(r'^workout/c2import/all/$', views.workout_getc2workout_all, - name='workout_getc2workout_all'), - re_path(r'^workout/c2import/all/(?P\d+)/$', - views.workout_getc2workout_all, name='workout_getc2workout_all'), - re_path(r'^workout/nkimport/$', views.workout_nkimport_view, - name='workout_nkimport_view'), - re_path(r'^workout/nkimport/(?P\d+)/(?P\d+)/$', - views.workout_nkimport_view, name='workout_nkimport_view'), - re_path(r'^workout/nkimport/user/(?P\d+)/$', - views.workout_nkimport_view, name='workout_nkimport_view'), - re_path(r'^workout/nkimport/all/$', views.workout_getnkworkout_all, - name='workout_getnkworkout_all'), - re_path(r'^workout/nkimport/all/(?P\d+-\d+-\d+)/(?P\d+-\d+-\d+)/$', - views.workout_getnkworkout_all, - name='workout_getnkworkout_all'), - re_path(r'^workout/rp3import/user/(?P\d+)/$', - views.workout_rp3import_view, name='workout_rp3import_view'), - re_path(r'^workout/rp3import/all/$', views.workout_getrp3workout_all, - name='workout_getrp3workout_all'), + re_path(r'^workout/(?P\w+.*)import/$', + views.workout_import_view, name='workout_import_view'), re_path(r'^workout/(?P\w+.*)import/(?P\d+)/$', views.workout_getimportview, name='workout_getimportview'), - re_path(r'^workout/(?P\w+.*)import/(?P\d+)/async/$', - views.workout_getimportview, {'do_async': True}, name='workout_getimportview'), - # re_path(r'^workout/stravaimport/all/$',views.workout_getstravaworkout_all,name='workout_getstravaworkout_all'), - re_path(r'^workout/sporttracksimport/$', views.workout_sporttracksimport_view, - name='workout_sporttracksimport_view'), - re_path(r'^workout/sporttracksimport/user/(?P\d+)/$', - views.workout_sporttracksimport_view, name='workout_sporttracksimport_view'), - re_path(r'^workout/sporttracksimport/all/$', views.workout_getsporttracksworkout_all, - name='workout_getsporttracksworkout_all'), re_path(r'^workout/polarimport/$', views.workout_polarimport_view, name='workout_polarimport_view'), re_path(r'^workout/polarimport/user/(?P\d+)/', views.workout_polarimport_view, name='workout_polarimport_view'), - re_path('r^workout/(?P\b[0-9A-Fa-f]+\b)/(?P\w+.*)uploadw/$', + re_path(r'^workout/(?P[0-9A-Fa-f]+)/(?P[0-9A-za-z]+)uploadw/$', views.workout_export_view, name='workout_export_view'), re_path(r'^workout/(?P\b[0-9A-Fa-f]+\b)/recalcsummary/$', views.workout_recalcsummary_view, name='workout_recalcsummary_view'), diff --git a/rowers/views/importviews.py b/rowers/views/importviews.py index 9e102150..99dae081 100644 --- a/rowers/views/importviews.py +++ b/rowers/views/importviews.py @@ -10,6 +10,7 @@ from rowers.tasks import fetch_strava_workout import rowers.integrations.strava as strava from rowers.integrations import importsources +from rowers.utils import NoTokenError import numpy @@ -94,7 +95,11 @@ def rower_polar_authorize(request): # pragma: no cover @login_required() def rower_integration_token_refresh(request, source='c2'): - integration = importsource[source](request.user) + try: + integration = importsources[source](request.user) + except KeyError: + url = reverse('workouts_view') + return HttpResponseRedirect(url) try: token = integration.token_refresh() messages.info(request, "Tokens refreshed. Good to go") @@ -320,58 +325,57 @@ def rower_process_nkcallback(request): # pragma: no cover return HttpResponseRedirect(url) + @login_required() -def workout_getnkworkout_all(request, startdatestring='', enddatestring=''): +@permission_required('rower.is_coach', fn=get_user_by_userid, raise_exception=True) +@permission_required('rower.is_not_freecoach', fn=get_user_by_userid, raise_exception=True) +def workout_import_view(request, source='c2'): startdate, enddate = get_dates_timeperiod( - request, startdatestring=startdatestring, enddatestring=enddatestring) + request, defaulttimeperiod='last30') startdate = startdate.date() enddate = enddate.date() + r = getrequestrower(request) + integration = importsources[source](request.user) + + try: + _ = integration.open() + except NoTokenError: # pragma: no cover + return HttpResponseRedirect("/rowers/me/nkauthorize/") + + + + + if request.method == 'POST': # pragma: no cover + dateform = DateRangeForm(request.POST) + if dateform.is_valid(): + startdate = dateform.cleaned_data['startdate'] + enddate = dateform.cleaned_data['enddate'] + \ + datetime.timedelta(days=1) + else: + dateform = DateRangeForm(initial={ + 'startdate': startdate, + 'enddate': enddate, + }) + + if enddate < startdate: # pragma: no cover + s = enddate + enddate = startdate + startdate = s + + startdatestring = startdate.strftime('%Y-%m-%d') + enddatestring = enddate.strftime('%Y-%m-%d') + + request.session['startdate'] = startdatestring + request.session['enddate'] = enddatestring + before = arrow.get(enddate) before = str(int(before.timestamp()*1000)) after = arrow.get(startdate) after = str(int(after.timestamp()*1000)) - nk_integration = importsources['nk'](request.user) - try: - _ = nk_integration.open() - except NoTokenError: # pragma: no cover - return HttpResponseRedirect("rower_nk_authorize") - - r = getrequestrower(request) - - result = nk_integration.get_workouts(before=before, after=after) - - if result: - messages.info( - request, "Your NK workouts will be imported in the coming few minutes") - else: # pragma: no cover - messages.error(request, "Your NK workouts import failed") - - url = reverse('workouts_view') - return HttpResponseRedirect(url) - - -@login_required() -@permission_required('rower.is_coach', fn=get_user_by_userid, raise_exception=True) -@permission_required('rower.is_not_freecoach', fn=get_user_by_userid, raise_exception=True) -def workout_nkimport_view(request, userid=0, after=0, before=0): - startdate, enddate = get_dates_timeperiod( - request, defaulttimeperiod='last30') - startdate = startdate.date() - enddate = enddate.date() - r = getrequestrower(request, userid=userid) - nk_integration = importsources['nk'](request.user) - - try: - _ = nk_integration.open() - except NoTokenError: # pragma: no cover - return HttpResponseRedirect("/rowers/me/nkauthorize/") - - - - workouts = nk_integration.get_workout_list(before=0, after=0) + workouts = integration.get_workout_list(before=before, after=after, startdate=startdate, enddate=enddate) if request.method == 'POST': # pragma: no cover @@ -380,11 +384,11 @@ def workout_nkimport_view(request, userid=0, after=0, before=0): ids = tdict['workoutid'] nkids = [int(id) for id in ids] for nkid in nkids: - _ = nk_integration.get_workout(nkid) + _ = integration.get_workout(nkid) messages.info( request, - 'Your NK logbook workouts will be imported in the background.' - ' It may take a few minutes before they appear.') + 'Your {source} workouts will be imported in the background.' + ' It may take a few minutes before they appear.'.format(source=integration.get_name())) url = reverse('workouts_view') return HttpResponseRedirect(url) except KeyError: @@ -396,27 +400,29 @@ def workout_nkimport_view(request, userid=0, after=0, before=0): 'name': 'Workouts' }, { - 'url': reverse('workout_nkimport_view'), - 'name': 'NK Logbook' + 'url': reverse('workout_import_view',kwargs={'source':source}), + 'name': integration.get_name() }, ] checknew = request.GET.get('selectallnew', False) + return render(request, 'list_import.html', { 'workouts': workouts, 'rower': r, - 'startdate': startdate, - 'enddate': enddate, + 'dateform':dateform, 'active': 'nav-workouts', 'breadcrumbs': breadcrumbs, 'teams': get_my_teams(request.user), 'checknew': checknew, - 'integration': 'NK Logbook' + 'startdate':startdate, + 'enddate': enddate, + 'integration': integration.get_name() }) + -# Process Strava Callback @login_required() @@ -568,67 +574,6 @@ def rower_process_testcallback(request): # pragma: no cover return HttpResponse(text) -@login_required() -@permission_required('rower.is_coach', fn=get_user_by_userid, raise_exception=True) -def workout_rp3import_view(request, userid=0): - r = getrequestrower(request, userid=userid) - rp3_integration = importsources['rp3'](request.user) - - try: - _ = rp3_integration.open() - except NoTokenError: # pragma: no cover - url = reverse('rower_rp3_authorize') - return HttpResponseRedirect(url) - - workouts = rp3_integration.get_workout_list() - datedict = {} - for workout in workouts: - datedict[workout['id']] = workout['starttime'] - - - if request.method == "POST": - try: # pragma: no cover - tdict = dict(request.POST.lists()) - ids = tdict['workoutid'] - rp3ids = [int(id) for id in ids] - - for rp3id in rp3ids: - rp3_integration.get_workout(rp3id,startdatetime=datedict[rp3id]) - # done, redirect to workouts list - messages.info( - request, - 'Your RP3 workouts will be imported in the background.' - ' It may take a few minutes before they appear.') - url = reverse('workouts_view') - return HttpResponseRedirect(url) - except KeyError: # pragma: no cover - pass - - - breadcrumbs = [ - { - 'url': '/rowers/list-workouts/', - 'name': 'Workouts' - }, - { - 'url': reverse('workout_rp3import_view'), - 'name': 'RP3' - }, - ] - - checknew = request.GET.get('selectallnew', False) - - - return render(request, 'list_import.html', - { - 'workouts': workouts, - 'rower': r, - 'active': 'nav-workouts', - 'breadcrumbs': breadcrumbs, - 'teams': get_my_teams(request.user), - 'integration': 'RP3', - 'checknew': checknew, - }) # The page where you select which Strava workout to import @login_required() @@ -791,72 +736,6 @@ def workout_rojaboimport_view(request, message="", userid=0): 'checknew': checknew, }) -# The page where you select which Strava workout to import -@login_required() -@permission_required('rower.is_coach', fn=get_user_by_userid, raise_exception=True) -@permission_required('rower.is_not_freecoach', fn=get_user_by_userid, raise_exception=True) -def workout_stravaimport_view(request, message="", userid=0): - - r = getrequestrower(request, userid=userid) - if r.user != request.user: - messages.error( - request, 'You can only access your own workouts on the NK Logbook, not those of your athletes') - url = reverse('workout_stravaimport_view', - kwargs={'userid': request.user.id}) - return HttpResponseRedirect(url) - - strava_integration = importsources['strava'](request.user) - try: - _ = strava_integration.open() - except NoTokenError: # pragma: no cover - return HttpResponseRedirect("/rowers/me/stravaauthorize/") - - workouts = strava_integration.get_workout_list() - - if request.method == "POST": - try: # pragma: no cover - tdict = dict(request.POST.lists()) - ids = tdict['workoutid'] - stravaids = [int(id) for id in ids] - alldata = {} - - for stravaid in stravaids: - res = strava_integration.get_workout(id) - - # done, redirect to workouts list - messages.info(request, - 'Your Strava workouts will be imported in the background.' - ' It may take a few minutes before they appear.') - url = reverse('workouts_view') - return HttpResponseRedirect(url) - except KeyError: # pragma: no cover - pass - - breadcrumbs = [ - { - 'url': '/rowers/list-workouts/', - 'name': 'Workouts' - }, - { - 'url': reverse('workout_stravaimport_view'), - 'name': 'Strava' - }, - ] - - checknew = request.GET.get('selectallnew', False) - - # 2022-10-24 sorting the results - workouts = sorted(workouts, key = lambda d:d['starttime'], reverse=True) - - return render(request, 'list_import.html', - {'workouts': workouts, - 'rower': r, - 'active': 'nav-workouts', - 'breadcrumbs': breadcrumbs, - 'teams': get_my_teams(request.user), - 'checknew': checknew, - 'integration': 'Strava' - }) # for Strava webhook request validation @@ -1122,261 +1001,24 @@ def workout_polarimport_view(request, userid=0): # pragma: no cover 'teams': get_my_teams(request.user), }) - -# The page where you select which SportTracks workout to import -@login_required() -@permission_required('rower.is_coach', fn=get_user_by_userid, raise_exception=True) -@permission_required('rower.is_not_freecoach', fn=get_user_by_userid, raise_exception=True) -def workout_sporttracksimport_view(request, message="", userid=0): - st_integration = importsources['sporttracks'](request.user) - try: - _ = st_integration.open() - except NoTokenError: - return HttpResponseRedirect("/rowers/me/sporttracksauthorize/") - - workouts = st_integration.get_workout_list() - - r = getrower(request.user) - - if request.method == "POST": - try: # pragma: no cover - tdict = dict(request.POST.lists()) - ids = tdict['workoutid'] - stids = [int(id) for id in ids] - alldata = {} - - for id in stids: - res = st_integration.get_workout(id) - - # done, redirect to workouts list - messages.info(request, - 'Your SportTracks workouts will be imported in the background.' - ' It may take a few minutes before they appear.') - url = reverse('workouts_view') - return HttpResponseRedirect(url) - except KeyError: # pragma: no cover - pass - - breadcrumbs = [ - { - 'url': '/rowers/list-workouts/', - 'name': 'Workouts' - }, - { - 'url': reverse('workout_sporttracksimport_view'), - 'name': 'SportTracks' - }, - ] - - checknew = request.GET.get('selectallnew', False) - - - return render(request, 'list_import.html', - {'workouts': workouts, - 'breadcrumbs': breadcrumbs, - 'active': 'nav-workouts', - 'rower': r, - 'teams': get_my_teams(request.user), - 'integration':'SportTracks', - 'checknew': checknew, - }) - - return HttpResponse(res) # pragma: no cover - -# List of workouts on Concept2 logbook. This view only used for debugging - - - -# 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 = importsources['c2'](request.user) - try: - _ = c2_integration.open() - except NoTokenError: # pragma: no cover - return HttpResponseRedirect("/rowers/me/c2authorize/") - - result = c2_integration.get_workouts(page=page) - - if result: - messages.info( - request, 'Your C2 workouts will be imported in the coming few minutes') - else: - messages.error(request, 'Your C2 workouts import failed') - - url = reverse('workouts_view') - return HttpResponseRedirect(url) - - -@login_required() -def workout_getrp3workout_all(request): # pragma: no cover - try: - _ = rp3_open(request.user) - except NoTokenError: # pragma: no cover - return HttpResponseRedirect("/rowers/me/rp3authorize/") - - r = getrequestrower(request) - - rp3_integration = importsources['rp3'](request.user) - result = rp3_integration.get_workouts() - - if result: - messages.info( - request, 'Your RP3 workouts will be imported in the coming few minutes') - else: - messages.error(request, 'Your RP3 workouts import failed') - - url = reverse('workouts_view') - return HttpResponseRedirect(url) - -# List of workouts available on Concept2 logbook - for import -@login_required() -@permission_required('rower.is_coach', fn=get_user_by_userid, raise_exception=True) -@permission_required('rower.is_not_freecoach', fn=get_user_by_userid, raise_exception=True) -def workout_c2import_view(request, page=1, userid=0, message=""): - - rower = getrequestrower(request, userid=userid) - if rower.user != request.user: - messages.error( - request, 'You can only access your own workouts on the Concept2 Logbook, not those of your athletes') - url = reverse('workout_c2import_view', kwargs={ - 'userid': request.user.id}) - return HttpResponseRedirect(url) - - c2_integration = importsources['c2'](request.user) - try: - _ = c2_integration.open() - except NoTokenError: # pragma: no cover - return HttpResponseRedirect("/rowers/me/c2authorize/") - - workouts = c2_integration.get_workout_list(page=1) - - - if request.method == "POST": - try: # pragma: no cover - tdict = dict(request.POST.lists()) - ids = tdict['workoutid'] - c2ids = [int(id) for id in ids] - - for c2id in c2ids: - c2_integration.get_workout(c2id) - # done, redirect to workouts list - messages.info( - request, - 'Your Concept2 workouts will be imported in the background.' - ' It may take a few minutes before they appear.') - url = reverse('workouts_view') - return HttpResponseRedirect(url) - except KeyError: # pragma: no cover - pass - - breadcrumbs = [ - { - 'url': '/rowers/list-workouts/', - 'name': 'Workouts' - }, - { - 'url': reverse('workout_c2import_view'), - 'name': 'Concept2' - }, - { - 'url': reverse('workout_c2import_view', kwargs={'page': page}), - 'name': 'Page '+str(page) - } - ] - - rower = getrower(request.user) - - checknew = request.GET.get('selectallnew', False) - - return render(request, - 'list_import.html', - {'workouts': workouts, - 'rower': rower, - 'active': 'nav-workouts', - 'breadcrumbs': breadcrumbs, - 'teams': get_my_teams(request.user), - 'page': page, - 'checknew': checknew, - 'integration': 'C2 Logbook' - }) - - -importlistviews = { - 'c2': 'workout_c2import_view', - 'strava': 'workout_stravaimport_view', - 'polar': 'workout_polarimport_view', - 'ownapi': 'workout_view', - 'sporttracks': 'workout_sporttracksimport_view', - 'trainingpeaks': 'workout_view', - 'nk': 'workout_nkimport_view', - 'rp3': 'workout_rp3import_view', -} - importauthorizeviews = { - 'c2': 'rower_c2_authorize', - 'strava': 'rower_strava_authorize', + 'c2': 'rower_integration_authorize', + 'strava': 'rower_integration_authorize', 'polar': 'rower_polar_authorize', 'ownapi': 'workout_view', - 'sporttracks': 'rower_sporttracks_authorize', - 'trainingpeaks': 'rower_tp_authorize', - 'nk': 'rower_nk_authorize', - 'rp3': 'rower_rp3_authorize', + 'sporttracks': 'rower_integration_authorize', + 'trainingpeaks': 'rower_integration_authorize', + 'nk': 'rower_integration_authorize', + 'rp3': 'rower_integration_authorize', } - - -@login_required() -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): - st_integration = importsources['sporttracks'](request.user) - st_integration.get_workouts() - messages.info(request,"Your SportTracks workouts will be imported in the background") - - 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, NotImplementedError, KeyError): - return workout_getimportview_old(request, externalid, source=source, do_async=True) + return reverse("workouts_view") if 'startdate' in request.session and source == 'nk': # pragma: no cover startdate = request.session.get('startdate') enddate = request.session.get('enddate') @@ -1385,14 +1027,14 @@ def workout_getimportview(request, externalid, source='c2', do_async=True): try: result = integration.get_workout(externalid, startdate=startdate, enddate=enddate) except NoTokenError: - return HttpResponseRedirect(reverse(importauthorizeviews[source])) + return HttpResponseRedirect(reverse(importauthorizeviews[source],kwargs={'source':source})) url = reverse(importlistviews[source]) return HttpResponseRedirect(url) try: result = integration.get_workout(externalid) except NoTokenError: - return HttpResponseRedirect(reverse(importauthorizeviews[source])) + return HttpResponseRedirect(reverse(importauthorizeviews[source],kwargs={'source':source})) if result: # pragma: no cover messages.info( @@ -1401,7 +1043,7 @@ def workout_getimportview(request, externalid, source='c2', do_async=True): else: # pragma: no cover messages.error(request, 'Error getting the workout') - url = reverse(importlistviews[source]) + url = reverse("workout_import_view", kwargs={'source':source}) return HttpResponseRedirect(url) From 04d850aa977883d0ce093897ffe5516883835962 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Fri, 17 Feb 2023 17:48:43 +0100 Subject: [PATCH 22/27] fixing tests --- rowers/tests/testdata/testdata.tcx.gz | Bin 4001 -> 4002 bytes rowers/tests/viewnames.csv | 10 ++-------- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/rowers/tests/testdata/testdata.tcx.gz b/rowers/tests/testdata/testdata.tcx.gz index 7322a59a7d7e228cffee8b1b4a65350d9f9b57f1..f224282b1374038e8d0ba1dc29087eef74b0f48b 100644 GIT binary patch literal 4002 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 I6vTi50OK%2i~s-t literal 4001 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 Date: Sat, 18 Feb 2023 14:52:26 +0100 Subject: [PATCH 23/27] passing tests --- rowers/templates/list_import.html | 4 ++-- rowers/tests/testdata/testdata.tcx.gz | Bin 4002 -> 4000 bytes 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/rowers/templates/list_import.html b/rowers/templates/list_import.html index 643bd6db..fc4373a7 100644 --- a/rowers/templates/list_import.html +++ b/rowers/templates/list_import.html @@ -29,11 +29,11 @@

      {% if page > 1 %} - + {% endif %} - + diff --git a/rowers/tests/testdata/testdata.tcx.gz b/rowers/tests/testdata/testdata.tcx.gz index f224282b1374038e8d0ba1dc29087eef74b0f48b..d33bfac882a7ba205c60b1f4120ffb56d654651d 100644 GIT binary patch literal 4000 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 GfB^t{d>*0z literal 4002 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 I6vTi50OK%2i~s-t From 3420da47d7bad579b670a32f39a5a6f74a1c4716 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Sat, 18 Feb 2023 17:16:54 +0100 Subject: [PATCH 24/27] polar passing tests --- rowers/integrations/__init__.py | 3 +- rowers/integrations/polar.py | 521 +++++++++++++++++++++ rowers/integrations/sporttracks.py | 8 +- rowers/management/commands/processemail.py | 8 +- rowers/tests/test_imports.py | 32 +- rowers/tests/testdata/testdata.tcx.gz | Bin 4000 -> 4000 bytes rowers/urls.py | 4 - rowers/views/importviews.py | 98 +--- rowers/views/statements.py | 1 - 9 files changed, 565 insertions(+), 110 deletions(-) create mode 100644 rowers/integrations/polar.py diff --git a/rowers/integrations/__init__.py b/rowers/integrations/__init__.py index 4e34497e..f68aed3e 100644 --- a/rowers/integrations/__init__.py +++ b/rowers/integrations/__init__.py @@ -4,7 +4,7 @@ from .nk import NKIntegration from .sporttracks import SportTracksIntegration from .rp3 import RP3Integration from .trainingpeaks import TPIntegration - +from .polar import PolarIntegration importsources = { 'c2': C2Integration, @@ -14,5 +14,6 @@ importsources = { 'nk': NKIntegration, 'tp':TPIntegration, 'rp3':RP3Integration, + 'polar': PolarIntegration } diff --git a/rowers/integrations/polar.py b/rowers/integrations/polar.py new file mode 100644 index 00000000..8b0b9768 --- /dev/null +++ b/rowers/integrations/polar.py @@ -0,0 +1,521 @@ +from rowers.rower_rules import ispromember +from .integrations import SyncIntegration +from rowers.models import User, Rower, Workout +from rowsandall_app.settings import ( + POLAR_CLIENT_ID, POLAR_REDIRECT_URI, POLAR_CLIENT_SECRET, UPLOAD_SERVICE_URL +) + +import urllib +import requests + +from rowers.utils import dologging, myqueue, NoTokenError +from django.utils import timezone + +from uuid import uuid4 +import base64 +from rowers.tasks import handle_request_post +from json.decoder import JSONDecodeError + +from rowers.opaque import encoder +import rowers.mytypes as mytypes + +from django.conf import settings + +import django_rq +queue = django_rq.get_queue('default') +queuelow = django_rq.get_queue('low') +queuehigh = django_rq.get_queue('high') + +baseurl = 'https://polaraccesslink.com/v3' + + +class PolarIntegration(SyncIntegration): + def __init__(self, *args, **kwargs): + if args[0] is not None: + super(PolarIntegration, self).__init__(*args, **kwargs) + + def get_notifications(self): + url = baseurl+'/notifications' + # state = str(uuid4()) + auth_string = '{id}:{secret}'.format( + id=POLAR_CLIENT_ID, + secret=POLAR_CLIENT_SECRET + ) + + try: + headers = {'Authorization': 'Basic %s' % base64.b64encode(auth_string)} + except TypeError: + headers = {'Authorization': 'Basic %s' % base64.b64encode( + bytes(auth_string, 'utf-8')).decode('utf-8')} + + try: + response = requests.get(url, headers=headers) + except ConnectionError: # pragma: no cover + response = { + 'status_code': 400, + } + + available_data = [] + + try: + if response.status_code == 200: + available_data = response.json()['available-user-data'] + dologging('polar.log', available_data) + else: # pragma: no cover + dologging('polar.log', response.status_code) + dologging('polar.log', response.text) + except AttributeError: # pragma: no cover + try: + dologging('polar.log', response.text) + except AttributeError: + pass + pass + + return available_data + + + def get_name(self): + return "Polar Flow" + + def get_shortname(self): + raise "polar" + + def createworkoutdata(self, w, *args, **kwargs): + raise NotImplementedError + + + def workout_export(self, workout, *args, **kwargs) -> str: + raise NotImplementedError + + def revoke_access(self): # pragma: no cover + user = self.user + headers = { + 'Authorization': 'Bearer {token}'.format(token=user.rower.polartoken) + } + + response = requests.delete('https://www.polaraccesslink.com/v3/users/{userid}'.format( + userid=user.rower.polaruserid + ), headers=headers) + + dologging('polar.log', response.text) + dologging('polar.log', response.reason) + + return 1 + + def get_polar_workouts(self, user): + r = Rower.objects.get(user=user) + + exercise_list = [] + + if (r.polartoken == '') or (r.polartoken is None): + s = "Token doesn't exist. Need to authorize" + return [] + elif (timezone.now() > r.polartokenexpirydate): # pragma: no cover + s = "Token expired. Needs to refresh" + dologging('polar.log', s) + return [] + + authorizationstring = str('Bearer ' + r.polartoken) + headers = {'Authorization': authorizationstring, + 'Accept': 'application/json'} + + headers2 = { + 'Authorization': authorizationstring, + } + + url = baseurl+'/users/{userid}/exercise-transactions'.format( + userid=r.polaruserid + ) + + response = requests.post(url, headers=headers) + dologging('polar.log', url) + dologging('polar.log', authorizationstring) + dologging('polar.log', str(response.status_code)) + + if response.status_code == 201: + transactionid = response.json()['transaction-id'] + url = baseurl+'/users/{userid}/exercise-transactions/{transactionid}'.format( + transactionid=transactionid, + userid=r.polaruserid + ) + + dologging('polar.log', url) + + response = requests.get(url, headers=headers) + if response.status_code == 200: + exerciseurls = response.json()['exercises'] + dologging('polar.log', exerciseurls) + for exerciseurl in exerciseurls: + response = requests.get(exerciseurl, headers=headers) + if response.status_code == 200: + exercise_dict = response.json() + tcxuri = exerciseurl+'/tcx' + response = requests.get(tcxuri, headers=headers2) + + if response.status_code == 200: + filename = 'media/mailbox_attachments/{code}_{id}.tcx'.format( + id=exercise_dict['id'], + code=uuid4().hex[:16] + ) + dologging('polar.log', filename) + + with open(filename, 'wb') as fop: + fop.write(response.content) + + workouttype = 'other' + try: + workouttype = mytypes.polaraccesslink_sports[ + exercise_dict['detailed-sport-info']] + except KeyError: # pragma: no cover + dologging( + 'polar.log', exercise_dict['detailed-sport-info']) + dologging('polar.log', workouttype) + try: + workouttype = mytypes.polarmappinginv[exercise_dict['sport'].lower( + )] + except KeyError: + dologging('polar.log', workouttype) + pass + + dologging('polar.log', workouttype) + + # post file to upload api + # TODO: add workouttype + uploadoptions = { + 'title': '', + 'workouttype': workouttype, + 'boattype': '1x', + 'user': user.id, + 'secret': settings.UPLOAD_SERVICE_SECRET, + 'file': filename, + 'title': '', + } + + url = settings.UPLOAD_SERVICE_URL + + dologging('polar.log', uploadoptions) + dologging('polar.log', url) + + _ = myqueue( + queuehigh, + handle_request_post, + url, + uploadoptions + ) + + dologging('polar.log', response.status_code) + if response.status_code != 200: # pragma: no cover + try: + dologging('polar.log', response.text) + except: + pass + try: + dologging('polar.log', response.json()) + except: + pass + + exercise_dict['filename'] = filename + else: # pragma: no cover + exercise_dict['filename'] = '' + + exercise_list.append(exercise_dict) + dologging('polar.log', str(exercise_dict)) + + # commit transaction + url = baseurl+'/users/{userid}/exercise-transactions/{transactionid}'.format( + transactionid=transactionid, + userid=r.polaruserid + ) + requests.put(url, headers=headers) + dologging( + 'polar.log', 'Committed transation at {url}'.format(url=url)) + + return exercise_list + + + def get_workouts(self, *args, **kwargs) -> int: + available_data = self.get_notifications() + polaruserid = self.rower.polaruserid + for record in available_data: + dologging('polar.log', str(record)) + + if record['data-type'] == 'EXERCISE': + try: + r = Rower.objects.get(polaruserid=record['user-id']) + u = r.user + if r.polar_auto_import and ispromember(u): + exercise_list = self.get_polar_workouts(u) + dologging('polar.log', exercise_list) + elif record['user-id'] == polaruserid: + exercise_list = self.get_polar_workouts(u) + except Rower.DoesNotExist: # pragma: no cover + pass + + return 1 + + def register_user(self, token): + _ = self.open() + + authorizationstring = 'Bearer {token}'.format(token=token) + headers = { + 'Content-Type': 'application/xml', + 'Authorization': authorizationstring, + 'Accept': 'application/json' + } + + payload = { + "member-id": encoder.encode_hex(self.user.id) + } + + headers = { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Authorization': 'Bearer {token}'.format(token=token) + } + + dologging('polar.log', 'Registering user') + + response = requests.post( + 'https://www.polaraccesslink.com/v3/users', + json=payload, + headers=headers + ) + + + + if response.status_code not in [200, 201]: # pragma: no cover + # dologging('polar.log',url) + dologging('polar.log', headers) + dologging('polar.log', payload) + dologging('polar.log', response.status_code) + dologging('polar.log', response.content) + try: + dologging('polar.log', response.reason) + dologging('polar.log', response.text) + except KeyError: + pass + + try: + jsondata = response.json() + if jsondata['error']['error_type'] == 'user_already_registered': + return jsondata + except: + pass + + return {} + + polar_user_data = response.json() + + return polar_user_data + + def get_polar_user_info(self, physical=False): # pragma: no cover + r = self.rower + _ = self.open() + + authorizationstring = str('Bearer ' + r.polartoken) + headers = { + 'Authorization': authorizationstring, + 'Accept': 'application/json' + } + + if not physical: + url = baseurl+'/users/{userid}'.format( + userid=r.polaruserid + ) + else: + url = 'https://www.polaraccesslink.com/v3/users/{userid}/physical-information-transactions/'.format( + userid=r.polaruserid + ) + + if physical: + response = requests.post(url, headers=headers) + else: + response = requests.get(url, headers=headers) + + return response + + + def get_workout(self, id, transaction_id) -> int: + r = self.rower + _ = self.open() + authorizationstring = str('Bearer ' + r.polartoken) + headers = { + 'Authorization': authorizationstring, + 'Accept': 'application/json' + } + + url = baseurl+'/users/{userid}/exercise-transactions'.format( + userid=r.polaruserid + ) + + response = requests.post(url, headers=headers) + + if response.status_code == 201: + transactionid = response.json()['transaction-id'] + url = baseurl+'/users/{userid}/exercise-transactions/{transactionid}'.format( + transactionid=transactionid, + userid=r.polaruserid + ) + + response = requests.get(url, headers=headers) + if response.status_code == 200: + exerciseurls = response.json()['exercises'] + for exerciseurl in exerciseurls: + response = requests.get(exerciseurl, headers=headers) + if response.status_code == 200: + exercise_dict = response.json() + thisid = exercise_dict['id'] + if thisid == id: + url = baseurl+'/users/{userid}/exercise-transactions/{transactionid}' \ + '/exercises/{exerciseid}/tcx'.format( + userid=r.polaruserid, + transactionid=transactionid, + exerciseid=id) + authorizationstring = str('Bearer ' + r.polartoken) + headers2 = { + 'Authorization': authorizationstring, + } + + response = requests.get(url, headers=headers2) + + if response.status_code == 200: + result = response.content + # commit transaction + url = baseurl+'/users/{userid}/exercise-transactions/{transactionid}'.format( + transactionid=transactionid, + userid=r.polaruserid + ) + response = requests.put(url, headers=headers) + dologging( + 'polar.log', 'Committing transaction on {url}'.format(url=url)) + else: # pragma: no cover + result = None + + return result + + + def get_workout_list(self, *args, **kwargs) -> list: + exercises = self.get_polar_workouts(self.user) + workouts = [] + try: + a = exercises.status_code + return [] + except: + return [] + + for exercise in exercises: + try: + d = exercise['distance'] + except KeyError: + d = 0 + + i = exercise['id'] + transactionid = exercise['transaction-id'] + starttime = exercise['start-time'] + rowtype = exercise['sport'] + durationstring = exercise['duration'] + duration = isodate.parse_duration(durationstring) + keys = ['id', 'distance', 'duration', + 'starttime', 'rowtype', 'source', 'name', 'new'] + values = [i, d, duration, starttime, rowtype, transactionid, '', ''] + res = dict(zip(keys, values)) + workouts.append(res) + + return workouts + + + def make_authorization_url(self, *args, **kwargs) -> str: # pragma: no cover + + state = str(uuid4()) + + params = {"client_id": POLAR_CLIENT_ID, + "response_type": "code", + # "redirect_uri": POLAR_REDIRECT_URI, + "state": state, + # "scope":"accesslink.read_all" + } + url = "https://flow.polar.com/oauth2/authorization?" + \ + urllib.parse.urlencode(params) + dologging('polar.log', 'Authorizing') + dologging('polar.log', url) + dologging('polar.log', params) + + return url + + def get_token(self, code, *args, **kwargs) -> (str, int, str): + post_data = {"grant_type": "authorization_code", + "code": code, + # "redirect_uri": POLAR_REDIRECT_URI, + } + + auth_string = '{id}:{secret}'.format( + id=POLAR_CLIENT_ID, + secret=POLAR_CLIENT_SECRET + ) + + try: + headers = {'Authorization': 'Basic %s' % base64.b64encode(auth_string)} + except TypeError: + headers = {'Authorization': 'Basic %s' % base64.b64encode( + bytes(auth_string, 'utf-8')).decode('utf-8')} + + dologging('polar.log', 'Getting token') + dologging('polar.log', post_data) + dologging('polar.log', auth_string) + + response = requests.post("https://polarremote.com/v2/oauth2/token", + data=post_data, + headers=headers) + + if response.status_code != 200: # pragma: no cover + dologging('polar.log', 'Getting token, got:') + dologging('polar.log', response.status_code) + dologging('polar.log', response.reason) + dologging('polar.log', response.text) + + try: + token_json = response.json() + thetoken = token_json['access_token'] + expires_in = token_json['expires_in'] + user_id = token_json['x_user_id'] + dologging('polar.log', response.status_code) + try: + dologging('polar.log', response.text) + except AttributeError: + pass + dologging('polar.log', token_json) + except (KeyError, JSONDecodeError) as e: # pragma: no cover + dologging('polar.log', e) + try: + dologging('polar.log', response.text) + except AttributeError: + pass + thetoken = 0 + expires_in = 0 + user_id = 0 + + return [thetoken, expires_in, user_id] + + + def open(self, *args, **kwargs) -> str: + r = self.rower + if (r.polartoken == '') or (r.polartoken is None): + s = "Token doesn't exist. Need to authorize" + raise NoTokenError(s) + elif (timezone.now() > r.polartokenexpirydate): + s = "Token expired. Needs to refresh" + raise NoTokenError(s) + + token = self.rower.polartoken + + return token + + + def token_refresh(self, *args, **kwargs) -> str: + raise NotImplementedError + +# just as a quick test during development +u = User.objects.get(id=1) + +nk_integration_1 = PolarIntegration(u) + diff --git a/rowers/integrations/sporttracks.py b/rowers/integrations/sporttracks.py index 1c65b4da..199f95a9 100644 --- a/rowers/integrations/sporttracks.py +++ b/rowers/integrations/sporttracks.py @@ -322,10 +322,4 @@ class SportTracksIntegration(SyncIntegration): def token_refresh(self, *args, **kwargs) -> str: return super(SportTracksIntegration, self).token_refresh(*args, **kwargs) - - -# just as a quick test during development -u = User.objects.get(id=1) - -nk_integration_1 = SportTracksIntegration(u) - + diff --git a/rowers/management/commands/processemail.py b/rowers/management/commands/processemail.py index 13e9dba9..593a4e9c 100644 --- a/rowers/management/commands/processemail.py +++ b/rowers/management/commands/processemail.py @@ -27,9 +27,6 @@ from rowingdata import rowingdata as rrdata import rowers.uploads as uploads -import rowers.polarstuff as polarstuff - - from rowers.opaque import encoder from rowers.integrations import * from rowers.rower_rules import user_is_not_basic, user_is_coachee @@ -77,8 +74,9 @@ class Command(BaseCommand): def handle(self, *args, **options): # Polar try: - polar_available = polarstuff.get_polar_notifications() - _ = polarstuff.get_all_new_workouts(polar_available) + polarintegration = PolarIntegration(None) + + _ = polarintegration.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/tests/test_imports.py b/rowers/tests/test_imports.py index e84c7404..e4baa269 100644 --- a/rowers/tests/test_imports.py +++ b/rowers/tests/test_imports.py @@ -15,7 +15,6 @@ import rowers from rowers import dataprep from rowers import tasks -from rowers import polarstuff import urllib import json @@ -875,39 +874,40 @@ class PolarObjects(DjangoTestCase): csvfilename=filename ) - @patch('rowers.polarstuff.requests.post', side_effect=mocked_requests) - @patch('rowers.polarstuff.requests.get', side_effect=mocked_requests) + @patch('rowers.integrations.polar.requests.post', side_effect=mocked_requests) + @patch('rowers.integrations.polar.requests.get', side_effect=mocked_requests) def test_polar_auto_import(self, mock_get, mock_post): self.r.polar_auto_import = True self.r.save() + integration = PolarIntegration(self.r.user) + res = integration.get_workouts(self.r.user) + self.assertEqual(res,1) - res = polarstuff.get_polar_workouts(self.r.user) - self.assertEqual(len(res),2) - - @patch('rowers.polarstuff.requests.post', side_effect=mocked_requests) - @patch('rowers.polarstuff.requests.get', side_effect=mocked_requests) + @patch('rowers.integrations.polar.requests.post', side_effect=mocked_requests) + @patch('rowers.integrations.polar.requests.get', side_effect=mocked_requests) def test_polar_callback(self, mock_get, mock_post): response = self.c.get('/polarflowcallback?code=abcdef&state=12sdss',follow=True) self.assertEqual(response.status_code,200) - @patch('rowers.polarstuff.requests.post', side_effect=mocked_requests) - @patch('rowers.polarstuff.requests.get', side_effect=mocked_requests) + @patch('rowers.integrations.polar.requests.post', side_effect=mocked_requests) + @patch('rowers.integrations.polar.requests.get', side_effect=mocked_requests) def test_polar_notifications(self, mock_get, mock_post): - data = polarstuff.get_polar_notifications() + integration = PolarIntegration(self.r.user) + data = integration.get_notifications() self.assertEqual(data[0]['user-id'],475) - response = polarstuff.get_all_new_workouts(data) + response = integration.get_workouts() self.assertEqual(response,1) - @patch('rowers.polarstuff.requests.post', side_effect=mocked_requests) - @patch('rowers.polarstuff.requests.get', side_effect=mocked_requests) + @patch('rowers.integrations.polar.requests.post', side_effect=mocked_requests) + @patch('rowers.integrations.polar.requests.get', side_effect=mocked_requests) def test_polar_get_workout(self, mock_get, mock_post): transaction_id = 240522162 id = 1937529874 - - response = polarstuff.get_polar_workout(self.u, id, transaction_id) + integration = PolarIntegration(self.u) + response = integration.get_workout(id, transaction_id) self.assertEqual(len(response),14836) diff --git a/rowers/tests/testdata/testdata.tcx.gz b/rowers/tests/testdata/testdata.tcx.gz index d33bfac882a7ba205c60b1f4120ffb56d654651d..8f8a0dc99dca2b5c97c3e1000a87574c6737bf2f 100644 GIT binary patch delta 3905 zcmV-H55DlAAD|xxABzYG<@xZD2OkxRnscdB7sogvkkujq8wHfj+p1$GkvkzUAK#sx z?7dlDY*uIM`*+d6-Mt?V?;O0i=vM31`uO2;-7ha5b$$0ow^{C`o!6WDk&G={2S@$t z&8mO@^Sgez&Nq1F1n)Mh&(83c&Dq8Af&g&w`_m`Sx2L$bT5tMpeY8wRZC3lc^FBE{ z>iX@kz0nQq>5qCt=H2GyUmy7Qu7B8_oL#Jzo5dUa>;3(Z@8A7r|7v-0 zclTxb`ti#{0(AF4?h(8|-nY0<&O}EX;co!@00)b-)5ravbmx1|`|hHDu{vGezx!9m zUS232`r@qbPV!;Tmq%yomzzUjf3Wy!>!$-At#)tF(`EXxn{=P~UcT7)Y44Bt4iUq_ z;_}m{9Udf6>yDRy|5&EG|F!FvKb@a$U(NpgdHN-nn;)-#T6X>Bm*vIN)%tb6{C9ul zN2JsI)}6dw9->Vb+5P$H3X5wlapmi$-MiK4>(l?!4T!tu2Clipm9HPKx36md{&4$N zEECzwEmt4p%Vm1NhlKet?*6_)@nUs+oNjH|zdCz)__SMpzwS;F#m}~0-Mc&b?`LDI=ouy3?@mt zSOs-tgVVr&o*#5Y+{snMJwZfpLBXAp-c*o#xH)&KOWv(i#N8QURE_d!h~R?U<7~J) zTM_q|I+_5U|0V?7yiF*<%jW+L%n+W&1 z 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 P&i?@Im=Xa5#DD<+Gyn3; delta 3905 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 diff --git a/rowers/urls.py b/rowers/urls.py index d448a193..6baf4b8d 100644 --- a/rowers/urls.py +++ b/rowers/urls.py @@ -624,10 +624,6 @@ urlpatterns = [ views.workout_import_view, name='workout_import_view'), re_path(r'^workout/(?P\w+.*)import/(?P\d+)/$', views.workout_getimportview, name='workout_getimportview'), - re_path(r'^workout/polarimport/$', views.workout_polarimport_view, - name='workout_polarimport_view'), - re_path(r'^workout/polarimport/user/(?P\d+)/', - views.workout_polarimport_view, name='workout_polarimport_view'), re_path(r'^workout/(?P[0-9A-Fa-f]+)/(?P[0-9A-za-z]+)uploadw/$', views.workout_export_view, name='workout_export_view'), re_path(r'^workout/(?P\b[0-9A-Fa-f]+\b)/recalcsummary/$', diff --git a/rowers/views/importviews.py b/rowers/views/importviews.py index 99dae081..06275ff1 100644 --- a/rowers/views/importviews.py +++ b/rowers/views/importviews.py @@ -76,21 +76,8 @@ def rower_garmin_authorize(request): # pragma: no cover # Polar Authorization @login_required() def rower_polar_authorize(request): # pragma: no cover - - state = str(uuid4()) - - params = {"client_id": POLAR_CLIENT_ID, - "response_type": "code", - # "redirect_uri": POLAR_REDIRECT_URI, - "state": state, - # "scope":"accesslink.read_all" - } - url = "https://flow.polar.com/oauth2/authorization?" + \ - urllib.parse.urlencode(params) - dologging('polar.log', 'Authorizing') - dologging('polar.log', url) - dologging('polar.log', params) - + integration = importsources['polar'](request.user) + url = integration.make_authorization_url() return HttpResponseRedirect(url) @login_required() @@ -166,8 +153,9 @@ def rower_process_twittercallback(request): # pragma: no cover @login_required() def rower_process_polarcallback(request): - + integration = importsources['polar'](request.user) error = request.GET.get('error', 'no error') + dologging('polar.log', 'Callback: {error}'.format(error=error)) if error != 'no error': # pragma: no cover messages.error(request, error) @@ -188,7 +176,7 @@ def rower_process_polarcallback(request): return HttpResponseRedirect(url) - access_token, expires_in, user_id = polarstuff.get_token(code) + access_token, expires_in, user_id = integration.get_token(code) expirydatetime = timezone.now()+datetime.timedelta(seconds=expires_in) r = getrower(request.user) r.polartoken = access_token @@ -198,12 +186,28 @@ def rower_process_polarcallback(request): r.save() if user_id: - polar_user_data = polarstuff.register_user(request.user, access_token) + polar_user_data = integration.register_user(access_token) else: # pragma: no cover messages.error(request, 'Polar Flow Authorization Failed') url = reverse('rower_exportsettings_view') return HttpResponseRedirect(url) + try: + error = polar_user_data['error'] + if error['error_type'] == 'user_already_registered': + s = error['message'] + tester = re.compile(r'.*userid:(?P\d+)') + testresult = tester.match(s) + if testresult: + user_id2 = testresult.group('id') + if user_id2 == str(user_id): + messages.info(request, + "Tokens stored. Good to go. Please check your import/export settings") + url = reverse('rower_exportsettings_view') + return HttpResponseRedirect(url) + + except KeyError: + pass try: user_id2 = polar_user_data['polar-user-id'] except KeyError: # pragma: no cover @@ -942,64 +946,6 @@ def garmin_details_view(request): return HttpResponse(status=200) -# the page where you select which Polar workout to Import -@login_required() -@permission_required('rower.is_coach', fn=get_user_by_userid, raise_exception=True) -@permission_required('rower.is_not_freecoach', fn=get_user_by_userid, raise_exception=True) -def workout_polarimport_view(request, userid=0): # pragma: no cover - exercises = polarstuff.get_polar_workouts(request.user) - workouts = [] - - try: - a = exercises.status_code - if a == 401: - messages.error( - request, 'Not authorized. You need to connect to Polar first') - url = reverse('workouts_view') - return HttpResponseRedirect(url) - except: # pragma: no cover - exercises = [] - pass - - for exercise in exercises: - try: - d = exercise['distance'] - except KeyError: - d = 0 - - i = exercise['id'] - transactionid = exercise['transaction-id'] - starttime = exercise['start-time'] - rowtype = exercise['sport'] - durationstring = exercise['duration'] - duration = isodate.parse_duration(durationstring) - keys = ['id', 'distance', 'duration', - 'starttime', 'type', 'transactionid'] - values = [i, d, duration, starttime, rowtype, transactionid] - res = dict(zip(keys, values)) - workouts.append(res) - - breadcrumbs = [ - { - 'url': '/rowers/list-workouts/', - 'name': 'Workouts' - }, - { - 'url': reverse('workout_polarimport_view'), - 'name': 'Polar' - }, - ] - - r = getrower(request.user) - - return render(request, 'polar_list_import.html', - { - 'workouts': workouts, - 'active': 'nav-workouts', - 'rower': r, - 'breadcrumbs': breadcrumbs, - 'teams': get_my_teams(request.user), - }) importauthorizeviews = { 'c2': 'rower_integration_authorize', diff --git a/rowers/views/statements.py b/rowers/views/statements.py index d0526ca8..a0ef5835 100644 --- a/rowers/views/statements.py +++ b/rowers/views/statements.py @@ -200,7 +200,6 @@ import rowers.rojabo_stuff as rojabo_stuff import rowers.garmin_stuff as garmin_stuff from rowers.rojabo_stuff import rojabo_open -import rowers.polarstuff as polarstuff from rowers.integrations import * From 9f871c2c8e954536a8867a3595c66c0b39167cf3 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Sat, 18 Feb 2023 17:17:34 +0100 Subject: [PATCH 25/27] removed polarstuff.py --- rowers/polarstuff.py | 495 ------------------------------------------- 1 file changed, 495 deletions(-) delete mode 100644 rowers/polarstuff.py diff --git a/rowers/polarstuff.py b/rowers/polarstuff.py deleted file mode 100644 index 79073f93..00000000 --- a/rowers/polarstuff.py +++ /dev/null @@ -1,495 +0,0 @@ -from rowers.rower_rules import ispromember -from stravalib.exc import ActivityUploadFailed, TimeoutExceeded -from rowers.models import Rower, Workout -import rowers.mytypes as mytypes -from rowers.utils import NoTokenError, custom_exception_handler -from rowers.utils import dologging -from rowsandall_app.settings import ( - POLAR_CLIENT_ID, POLAR_REDIRECT_URI, POLAR_CLIENT_SECRET, UPLOAD_SERVICE_URL -) -import stravalib -from io import StringIO -from rowers.dataprep import columndict -import rowers.dataprep as dataprep -from rowers.tasks import handle_request_post -import pandas as pd -from rowingdata import rowingdata - -# All the functionality needed to connect to Strava - -# Python -import oauth2 as oauth -import cgi -import requests -import requests.auth -import json -from django.utils import timezone -from datetime import datetime -import numpy as np -from dateutil import parser -import time -import math -from math import sin, cos, atan2, sqrt -import os -import sys -import gzip -import base64 -import yaml -from uuid import uuid4 -from requests import ConnectionError -from json.decoder import JSONDecodeError - -# Django -from django.http import HttpResponseRedirect, HttpResponse, JsonResponse -from django.conf import settings -from django.contrib.auth import authenticate, login, logout -from django.contrib.auth.models import User -from django.contrib.auth.decorators import login_required -from django.urls import reverse, reverse_lazy - -from rowers.utils import myqueue -from rowers.opaque import encoder -import django_rq -queue = django_rq.get_queue('default') -queuelow = django_rq.get_queue('low') -queuehigh = django_rq.get_queue('high') - -# Project -# from .models import Profile - -baseurl = 'https://polaraccesslink.com/v3' - - -# Exchange access code for long-lived access token - -def get_token(code): - - post_data = {"grant_type": "authorization_code", - "code": code, - # "redirect_uri": POLAR_REDIRECT_URI, - } - - auth_string = '{id}:{secret}'.format( - id=POLAR_CLIENT_ID, - secret=POLAR_CLIENT_SECRET - ) - - try: - headers = {'Authorization': 'Basic %s' % base64.b64encode(auth_string)} - except TypeError: - headers = {'Authorization': 'Basic %s' % base64.b64encode( - bytes(auth_string, 'utf-8')).decode('utf-8')} - - dologging('polar.log', 'Getting token') - dologging('polar.log', post_data) - dologging('polar.log', auth_string) - - response = requests.post("https://polarremote.com/v2/oauth2/token", - data=post_data, - headers=headers) - - if response.status_code != 200: # pragma: no cover - dologging('polar.log', 'Getting token, got:') - dologging('polar.log', response.status_code) - dologging('polar.log', response.reason) - dologging('polar.log', response.text) - - try: - token_json = response.json() - thetoken = token_json['access_token'] - expires_in = token_json['expires_in'] - user_id = token_json['x_user_id'] - dologging('polar.log', response.status_code) - try: - dologging('polar.log', response.text) - except AttributeError: - pass - dologging('polar.log', token_json) - except (KeyError, JSONDecodeError) as e: # pragma: no cover - dologging('polar.log', e) - try: - dologging('polar.log', response.text) - except AttributeError: - pass - thetoken = 0 - expires_in = 0 - user_id = 0 - - return [thetoken, expires_in, user_id] - -# Make authorization URL including random string - - -def make_authorization_url(): # 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": POLAR_CLIENT_ID, - "response_type": "code", - "redirect_uri": POLAR_REDIRECT_URI, - "scope": "write"} - import urllib - url = "https://flow.polar.com/oauth2/authorization" + \ - urllib.parse.urlencode(params) - - return HttpResponseRedirect(url) - - -def revoke_access(user): # pragma: no cover - headers = { - 'Authorization': 'Bearer {token}'.format(token=user.rower.polartoken) - } - - response = requests.delete('https://www.polaraccesslink.com/v3/users/{userid}'.format( - userid=user.rower.polaruserid - ), headers=headers) - - dologging('polar.log', response.text) - dologging('polar.log', response.reason) - - return 1 - - -def get_polar_notifications(): - url = baseurl+'/notifications' - # state = str(uuid4()) - auth_string = '{id}:{secret}'.format( - id=POLAR_CLIENT_ID, - secret=POLAR_CLIENT_SECRET - ) - - try: - headers = {'Authorization': 'Basic %s' % base64.b64encode(auth_string)} - except TypeError: - headers = {'Authorization': 'Basic %s' % base64.b64encode( - bytes(auth_string, 'utf-8')).decode('utf-8')} - - try: - response = requests.get(url, headers=headers) - except ConnectionError: # pragma: no cover - response = { - 'status_code': 400, - } - - available_data = [] - - try: - if response.status_code == 200: - available_data = response.json()['available-user-data'] - dologging('polar.log', available_data) - else: # pragma: no cover - dologging('polar.log', response.status_code) - dologging('polar.log', response.text) - except AttributeError: # pragma: no cover - try: - dologging('polar.log', response.text) - except AttributeError: - pass - pass - - return available_data - - -def get_all_new_workouts(available_data, testing=False): - for record in available_data: - dologging('polar.log', str(record)) - if testing: # pragma: no cover - print(record) - if record['data-type'] == 'EXERCISE': - try: - r = Rower.objects.get(polaruserid=record['user-id']) - u = r.user - if r.polar_auto_import and ispromember(u): - exercise_list = get_polar_workouts(u) - dologging('polar.log', exercise_list) - if testing: # pragma: no cover - print(exercise_list) - except Rower.DoesNotExist: # pragma: no cover - pass - - return 1 - - -def get_polar_workouts(user): - r = Rower.objects.get(user=user) - - exercise_list = [] - - if (r.polartoken == '') or (r.polartoken is None): - s = "Token doesn't exist. Need to authorize" - return custom_exception_handler(401, s) - elif (timezone.now() > r.polartokenexpirydate): # pragma: no cover - s = "Token expired. Needs to refresh" - dologging('polar.log', s) - return custom_exception_handler(401, s) - else: - authorizationstring = str('Bearer ' + r.polartoken) - headers = {'Authorization': authorizationstring, - 'Accept': 'application/json'} - - headers2 = { - 'Authorization': authorizationstring, - } - - url = baseurl+'/users/{userid}/exercise-transactions'.format( - userid=r.polaruserid - ) - - response = requests.post(url, headers=headers) - dologging('polar.log', url) - dologging('polar.log', authorizationstring) - dologging('polar.log', str(response.status_code)) - - if response.status_code == 201: - transactionid = response.json()['transaction-id'] - url = baseurl+'/users/{userid}/exercise-transactions/{transactionid}'.format( - transactionid=transactionid, - userid=r.polaruserid - ) - - dologging('polar.log', url) - - response = requests.get(url, headers=headers) - if response.status_code == 200: - exerciseurls = response.json()['exercises'] - dologging('polar.log', exerciseurls) - for exerciseurl in exerciseurls: - response = requests.get(exerciseurl, headers=headers) - if response.status_code == 200: - exercise_dict = response.json() - tcxuri = exerciseurl+'/tcx' - response = requests.get(tcxuri, headers=headers2) - - if response.status_code == 200: - filename = 'media/mailbox_attachments/{code}_{id}.tcx'.format( - id=exercise_dict['id'], - code=uuid4().hex[:16] - ) - dologging('polar.log', filename) - - with open(filename, 'wb') as fop: - fop.write(response.content) - - workouttype = 'other' - try: - workouttype = mytypes.polaraccesslink_sports[ - exercise_dict['detailed-sport-info']] - except KeyError: # pragma: no cover - dologging( - 'polar.log', exercise_dict['detailed-sport-info']) - dologging('polar.log', workouttype) - try: - workouttype = mytypes.polarmappinginv[exercise_dict['sport'].lower( - )] - except KeyError: - dologging('polar.log', workouttype) - pass - - dologging('polar.log', workouttype) - - # post file to upload api - # TODO: add workouttype - uploadoptions = { - 'title': '', - 'workouttype': workouttype, - 'boattype': '1x', - 'user': user.id, - 'secret': settings.UPLOAD_SERVICE_SECRET, - 'file': filename, - 'title': '', - } - - url = settings.UPLOAD_SERVICE_URL - - dologging('polar.log', uploadoptions) - dologging('polar.log', url) - - _ = myqueue( - queuehigh, - handle_request_post, - url, - uploadoptions - ) - - dologging('polar.log', response.status_code) - if response.status_code != 200: # pragma: no cover - try: - dologging('polar.log', response.text) - except: - pass - try: - dologging('polar.log', response.json()) - except: - pass - - exercise_dict['filename'] = filename - else: # pragma: no cover - exercise_dict['filename'] = '' - - exercise_list.append(exercise_dict) - dologging('polar.log', str(exercise_dict)) - - # commit transaction - url = baseurl+'/users/{userid}/exercise-transactions/{transactionid}'.format( - transactionid=transactionid, - userid=r.polaruserid - ) - requests.put(url, headers=headers) - dologging( - 'polar.log', 'Committed transation at {url}'.format(url=url)) - - return exercise_list - - -def register_user(user, token): - r = Rower.objects.get(user=user) - if (r.polartoken == '') or (r.polartoken is None): # pragma: no cover - s = "Token doesn't exist. Need to authorize" - return custom_exception_handler(401, s) - elif (timezone.now() > r.polartokenexpirydate): # pragma: no cover - s = "Token expired. Needs to refresh" - return custom_exception_handler(401, s) - - authorizationstring = 'Bearer {token}'.format(token=token) - headers = { - 'Content-Type': 'application/xml', - 'Authorization': authorizationstring, - 'Accept': 'application/json' - } - - payload = { - "member-id": encoder.encode_hex(user.id) - } - - headers = { - 'Content-Type': 'application/json', - 'Accept': 'application/json', - 'Authorization': 'Bearer {token}'.format(token=token) - } - - dologging('polar.log', 'Registering user') - - response = requests.post( - 'https://www.polaraccesslink.com/v3/users', - json=payload, - headers=headers - ) - - if response.status_code not in [200, 201]: # pragma: no cover - # dologging('polar.log',url) - dologging('polar.log', headers) - dologging('polar.log', payload) - dologging('polar.log', response.status_code) - dologging('polar.log', response.content) - try: - dologging('polar.log', response.reason) - dologging('polar.log', response.text) - except KeyError: - pass - - return {} - - polar_user_data = response.json() - - return polar_user_data - - -def get_polar_user_info(user, physical=False): # pragma: no cover - r = Rower.objects.get(user=user) - if (r.polartoken == '') or (r.polartoken is None): - s = "Token doesn't exist. Need to authorize" - return custom_exception_handler(401, s) - elif (timezone.now() > r.polartokenexpirydate): - s = "Token expired. Needs to refresh" - return custom_exception_handler(401, s) - - authorizationstring = str('Bearer ' + r.polartoken) - headers = { - 'Authorization': authorizationstring, - 'Accept': 'application/json' - } - - if not physical: - url = baseurl+'/users/{userid}'.format( - userid=r.polaruserid - ) - else: - url = 'https://www.polaraccesslink.com/v3/users/{userid}/physical-information-transactions/'.format( - userid=r.polaruserid - ) - - if physical: - response = requests.post(url, headers=headers) - else: - response = requests.get(url, headers=headers) - - return response - - -def get_polar_workout(user, id, transactionid): - - r = Rower.objects.get(user=user) - if (r.polartoken == '') or (r.polartoken is None): # pragma: no cover - s = "Token doesn't exist. Need to authorize" - return custom_exception_handler(401, s) - elif (timezone.now() > r.polartokenexpirydate): # pragma: no cover - s = "Token expired. Needs to refresh" - return custom_exception_handler(401, s) - else: - authorizationstring = str('Bearer ' + r.polartoken) - headers = { - 'Authorization': authorizationstring, - 'Accept': 'application/json' - } - - url = baseurl+'/users/{userid}/exercise-transactions'.format( - userid=r.polaruserid - ) - - response = requests.post(url, headers=headers) - - if response.status_code == 201: - transactionid = response.json()['transaction-id'] - url = baseurl+'/users/{userid}/exercise-transactions/{transactionid}'.format( - transactionid=transactionid, - userid=r.polaruserid - ) - - response = requests.get(url, headers=headers) - if response.status_code == 200: - exerciseurls = response.json()['exercises'] - for exerciseurl in exerciseurls: - response = requests.get(exerciseurl, headers=headers) - if response.status_code == 200: - exercise_dict = response.json() - thisid = exercise_dict['id'] - if thisid == id: - url = baseurl+'/users/{userid}/exercise-transactions/{transactionid}' \ - '/exercises/{exerciseid}/tcx'.format( - userid=r.polaruserid, - transactionid=transactionid, - exerciseid=id) - authorizationstring = str('Bearer ' + r.polartoken) - headers2 = { - 'Authorization': authorizationstring, - } - - response = requests.get(url, headers=headers2) - - if response.status_code == 200: - result = response.content - # commit transaction - url = baseurl+'/users/{userid}/exercise-transactions/{transactionid}'.format( - transactionid=transactionid, - userid=r.polaruserid - ) - response = requests.put(url, headers=headers) - dologging( - 'polar.log', 'Committing transaction on {url}'.format(url=url)) - else: # pragma: no cover - result = None - - return result - - return None # pragma: no cover From 3bc3cb1f46f20ddb900868279cdd56aac36d94d7 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Sat, 18 Feb 2023 19:39:53 +0100 Subject: [PATCH 26/27] passing tests --- rowers/templates/menu_other.html | 2 +- rowers/tests/test_emails.py | 7 +++---- rowers/tests/testdata/testdata.tcx.gz | Bin 4000 -> 3999 bytes 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/rowers/templates/menu_other.html b/rowers/templates/menu_other.html index f56facfa..9e409b62 100644 --- a/rowers/templates/menu_other.html +++ b/rowers/templates/menu_other.html @@ -37,7 +37,7 @@

        -
      • Concept2
      • +
      • Concept2
      • Strava
      • RunKeeper
      • SportTracks
      • diff --git a/rowers/tests/test_emails.py b/rowers/tests/test_emails.py index 2d3355ef..240efef7 100644 --- a/rowers/tests/test_emails.py +++ b/rowers/tests/test_emails.py @@ -202,11 +202,10 @@ class EmailTests(TestCase): pass @patch('rowers.dataprep.create_engine') - @patch('rowers.polarstuff.get_polar_notifications') - @patch('rowers.c2stuff.requests.get', side_effect=mocked_requests) - @patch('rowers.c2stuff.requests.post', side_effect=mocked_requests) + @patch('rowers.integrations.c2.requests.get', side_effect=mocked_requests) + @patch('rowers.integrations.c2.requests.post', side_effect=mocked_requests) def test_emailprocessing( - self, mocked_sqlalchemy,mocked_polar_notifications, mock_get, mock_post): + self, mocked_sqlalchemy,mocked_get, mock_post): out = StringIO() call_command('processemail', stdout=out,testing=True,mailbox='workouts4') self.assertIn('Successfully processed email attachments',out.getvalue()) diff --git a/rowers/tests/testdata/testdata.tcx.gz b/rowers/tests/testdata/testdata.tcx.gz index 8f8a0dc99dca2b5c97c3e1000a87574c6737bf2f..1f0308d79e32ff01486a94c6d8ec162bd08c13ba 100644 GIT binary patch delta 257 zcmV+c0sj7=AD0t@Vb9>c5&r3 zd2oJyvO4Pe^wTe2jLUO>b+Yu`Pp?-eFE4-YZ|QwZhhBL&AGSR0{*x}5ods8LKYzX3 zYMnNF`Ly45!{wv>;`|3M(oG-!x;$BSn`PpVKYe&~SMKZJNw+?J{OIuKXPM3R&)xQ4 z(p&uO_30nWbY}qf0CxW$EUtX9d-B`Iba-crdbLi^;bB6#ym*wJ#vkd@hj;!5N=#;e H#DD<+c*Klh delta 258 zcmV+d0sa1;AD|xxABzYG<@xZj2dNH!(L)SwnI``4j{6@D7P|xP?9TM@H|EcmpH{o? z4<245j%ODi_I=i+x98&Q?WSA5>`qR$Pb+;~t&d-JeV5ML?cT0L<Ch_==fjq#-G9<0v$NoF>gTU_ zTdmV(FQ4|?Zn%83U!4EoMY`$3UzaD#ZnI1r@~01v?#g`~Jn7cQj~^ZW{4BHC{<+)! zOL~i+y*~Y8neGhW9>DJZgT<9Ec29o$m=5o3QLonNIXp}#mlu!H)A%D@`tZ*G0PUC( I0mOg-0CobCT>t<8 From bd25aed986f9706aa6677feafaf19bf3d7209a81 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Sun, 19 Feb 2023 10:05:04 +0100 Subject: [PATCH 27/27] temp file --- rowers/tests/testdata/testdata.tcx.gz | Bin 3999 -> 4000 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/rowers/tests/testdata/testdata.tcx.gz b/rowers/tests/testdata/testdata.tcx.gz index 1f0308d79e32ff01486a94c6d8ec162bd08c13ba..5888a36b3719db38b7d5ee8bd6df0b655c921832 100644 GIT binary patch delta 3913 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 Wl%B>P>C%UH{s&4-W`M*4fB^uhBJnx^