From 2981c59a5d1f28b60546db43b6fb7baa39a1b845 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Fri, 10 Feb 2023 16:55:47 +0100 Subject: [PATCH] first attempt on c2 --- rowers/c2stuff.py | 920 --------------------- rowers/imports.py | 6 +- rowers/integrations/__init__.py | 1 + rowers/integrations/c2.py | 450 ++++++++++ rowers/integrations/integrations.py | 250 ++++++ rowers/management/commands/processemail.py | 8 +- rowers/ownapistuff.py | 1 - rowers/templates/list_import.html | 84 ++ rowers/templatetags/rowerfilters.py | 10 +- rowers/tests/mocks.py | 3 +- rowers/tests/statements.py | 1 - rowers/tests/test_imports.py | 47 +- rowers/tests/test_permissions2.py | 16 +- rowers/tests/test_unit_tests.py | 7 +- rowers/uploads.py | 7 +- rowers/urls.py | 5 - rowers/views/importviews.py | 179 +--- rowers/views/statements.py | 4 +- rowers/views/workoutviews.py | 3 +- 19 files changed, 873 insertions(+), 1129 deletions(-) create mode 100644 rowers/integrations/__init__.py create mode 100644 rowers/integrations/c2.py create mode 100644 rowers/integrations/integrations.py create mode 100644 rowers/templates/list_import.html diff --git a/rowers/c2stuff.py b/rowers/c2stuff.py index 92aed939..0f933238 100644 --- a/rowers/c2stuff.py +++ b/rowers/c2stuff.py @@ -101,923 +101,3 @@ def getagegrouprecord(age, sex='male', weightcategory='hwt', return power - -oauth_data = { - 'client_id': C2_CLIENT_ID, - 'client_secret': C2_CLIENT_SECRET, - 'redirect_uri': C2_REDIRECT_URI, - 'autorization_uri': "https://log.concept2.com/oauth/authorize", - 'content_type': 'application/x-www-form-urlencoded', - 'tokenname': 'c2token', - 'refreshtokenname': 'c2refreshtoken', - 'expirydatename': 'tokenexpirydate', - 'bearer_auth': True, - 'base_url': "https://log.concept2.com/oauth/access_token", - 'scope': 'write', -} - - -# Checks if user has Concept2 tokens, resets tokens if they are -# expired. -def c2_open(user): - r = Rower.objects.get(user=user) - if (r.c2token == '') or (r.c2token is None): - raise NoTokenError("User has no token") - else: - if (timezone.now() > r.tokenexpirydate): - res = rower_c2_token_refresh(user) - if res is None: # pragma: no cover - raise NoTokenError("User has no token") - if res[0] is not None: - thetoken = res[0] - else: # pragma: no cover - raise NoTokenError("User has no token") - else: - thetoken = r.c2token - - return thetoken - - -def get_c2_workouts(rower, page=1, do_async=True): - try: - _ = c2_open(rower.user) - except NoTokenError: # pragma: no cover - return 0 - - res = get_c2_workout_list(rower.user, page=page) - - if (res.status_code != 200): # pragma: no cover - return 0 - else: - c2ids = [item['id'] for item in res.json()['data']] - alldata = {} - for item in res.json()['data']: - alldata[item['id']] = item - - knownc2ids = [ - w.uploadedtoc2 for w in Workout.objects.filter(user=rower) - ] - - tombstones = [ - t.uploadedtoc2 for t in TombStone.objects.filter(user=rower) - ] - - # get "blocked" c2ids - parkedids = [] - try: - with open('c2blocked.json', 'r') as c2blocked: - jsondata = json.load(c2blocked) - parkedids = jsondata['ids'] - except FileNotFoundError: # pragma: no cover - pass - - knownc2ids = uniqify(knownc2ids+tombstones+parkedids) - - newids = [c2id for c2id in c2ids if c2id not in knownc2ids] - if settings.TESTING: - newids = c2ids - - newparkedids = uniqify(newids+parkedids) - - with open('c2blocked.json', 'wt') as c2blocked: - data = {'ids': newparkedids} - json.dump(data, c2blocked) - - counter = 0 - for c2id in newids: - if do_async: # pragma: no cover - _ = myqueue(queuehigh, - handle_c2_async_workout, - alldata, - rower.user.id, - rower.c2token, - c2id, - counter, - rower.defaulttimezone) - - counter = counter+1 - else: - _ = create_async_workout(alldata, rower.user, c2id) - - return 1 - -# get workout metrics, then relay stroke data to an asynchronous task - - -def create_async_workout(alldata, user, c2id): - data = alldata[c2id] - splitdata = None - - c2id = data['id'] - workouttype = data['type'] - startdatetime = iso8601.parse_date(data['date']) - - try: - title = data['name'] - except KeyError: - title = "" - try: - t = data['comments'].split('\n', 1)[0] - title += t[:40] - except: - title = '' - - # Create CSV file name and save data to CSV file - csvfilename = 'media/Import_'+str(c2id)+'.csv.gz' - - r = Rower.objects.get(user=user) - - authorizationstring = str('Bearer ' + r.c2token) - headers = {'Authorization': authorizationstring, - 'user-agent': 'sanderroosendaal', - 'Content-Type': 'application/json'} - # url2 = "https://log.concept2.com/api/users/me/results"+str(c2id) - url = "https://log.concept2.com/api/users/me/results/"+str(c2id)+"/strokes" - try: - s = requests.get(url, headers=headers) - except ConnectionError: # pragma: no cover - return 0 - - if s.status_code != 200: # pragma: no cover - return 0 - - strokedata = pd.DataFrame.from_dict(s.json()['data']) - - res = make_cumvalues(0.1*strokedata['t']) - cum_time = res[0] - lapidx = res[1] - - starttimeunix = arrow.get(startdatetime).timestamp() - starttimeunix = starttimeunix-cum_time.max() - - unixtime = cum_time+starttimeunix - # unixtime[0] = starttimeunix - seconds = 0.1*strokedata.loc[:, 't'] - - nr_rows = len(unixtime) - - try: # pragma: no cover - latcoord = strokedata.loc[:, 'lat'] - loncoord = strokedata.loc[:, 'lon'] - except: - latcoord = np.zeros(nr_rows) - loncoord = np.zeros(nr_rows) - - try: - strokelength = strokedata.loc[:, 'strokelength'] - except: - strokelength = np.zeros(nr_rows) - - dist2 = 0.1*strokedata.loc[:, 'd'] - - try: - spm = strokedata.loc[:, 'spm'] - except KeyError: # pragma: no cover - spm = 0*dist2 - - try: - hr = strokedata.loc[:, 'hr'] - except KeyError: # pragma: no cover - hr = 0*spm - - pace = strokedata.loc[:, 'p']/10. - pace = np.clip(pace, 0, 1e4) - pace = pace.replace(0, 300) - - velo = 500./pace - power = 2.8*velo**3 - if workouttype == 'bike': # pragma: no cover - velo = 1000./pace - - df = pd.DataFrame({'TimeStamp (sec)': unixtime, - ' Horizontal (meters)': dist2, - ' Cadence (stokes/min)': spm, - ' HRCur (bpm)': hr, - ' longitude': loncoord, - ' latitude': latcoord, - ' Stroke500mPace (sec/500m)': pace, - ' Power (watts)': power, - ' DragFactor': np.zeros(nr_rows), - ' DriveLength (meters)': np.zeros(nr_rows), - ' StrokeDistance (meters)': strokelength, - ' DriveTime (ms)': np.zeros(nr_rows), - ' StrokeRecoveryTime (ms)': np.zeros(nr_rows), - ' AverageDriveForce (lbs)': np.zeros(nr_rows), - ' PeakDriveForce (lbs)': np.zeros(nr_rows), - ' lapIdx': lapidx, - ' WorkoutState': 4, - ' ElapsedTime (sec)': seconds, - 'cum_dist': dist2 - }) - - df.sort_values(by='TimeStamp (sec)', ascending=True) - - res = df.to_csv(csvfilename, index_label='index', - compression='gzip') - - userid = r.user.id - - uploadoptions = { - 'secret': UPLOAD_SERVICE_SECRET, - 'user': userid, - 'file': csvfilename, - 'title': title, - 'workouttype': workouttype, - 'boattype': '1x', - 'c2id': c2id, - } - - session = requests.session() - newHeaders = {'Content-type': 'application/json', 'Accept': 'text/plain'} - session.headers.update(newHeaders) - - response = session.post(UPLOAD_SERVICE_URL, json=uploadoptions) - - if response.status_code != 200: # pragma: no cover - return 0 - - try: - workoutid = response.json()['id'] - except KeyError: # pragma: no cover - workoutid = 1 - - newc2id = Workout.objects.get(id=workoutid).uploadedtoc2 - - parkedids = [] - with open('c2blocked.json', 'r') as c2blocked: - jsondata = json.load(c2blocked) - parkedids = jsondata['ids'] - - newparkedids = [id for id in parkedids if id != newc2id] - with open('c2blocked.json', 'wt') as c2blocked: - data = {'ids': newparkedids} - c2blocked.seek(0) - json.dump(data, c2blocked) - - # summary - if 'workout' in data: # pragma: no cover - if 'splits' in data['workout']: - splitdata = data['workout']['splits'] - elif 'intervals' in data['workout']: - splitdata = data['workout']['intervals'] - else: - splitdata = False - else: - splitdata = False - - if splitdata: # pragma: no cover - summary, sa, results = c2stuff.summaryfromsplitdata( - splitdata, data, csvfilename, workouttype=workouttype) - w = Workout.objects.get(id=workoutid) - w.summary = summary - w.save() - - from rowingdata.trainingparser import getlist - if sa: - values = getlist(sa) - units = getlist(sa, sel='unit') - types = getlist(sa, sel='type') - - rowdata = rdata(w.csvfilename) - if rowdata: - rowdata.updateintervaldata(values, - units, types, results) - - rowdata.write_csv(w.csvfilename, gzip=True) - dataprep.update_strokedata(w.id, rowdata.df) - - return workoutid - - -# convert datetime object to seconds -def makeseconds(t): - seconds = t.hour*3600.+t.minute*60.+t.second+0.1*int(t.microsecond/1.e5) - return seconds - -# convert our weight class code to Concept2 weight class code - - -def c2wc(weightclass): - if (weightclass == "lwt"): # pragma: no cover - res = "L" - else: - res = "H" - - return res - -# Concept2 logbook sends over split data for each interval -# We use it here to generate a custom summary -# Some users complained about small differences - - -def summaryfromsplitdata(splitdata, data, filename, sep='|', workouttype='rower'): - workouttype = workouttype.lower() - - totaldist = data['distance'] - totaltime = data['time']/10. - try: - spm = data['stroke_rate'] - except KeyError: # pragma: no cover - spm = 0 - try: - resttime = data['rest_time']/10. - except KeyError: # pragma: no cover - resttime = 0 - try: - restdistance = data['rest_distance'] - except KeyError: # pragma: no cover - restdistance = 0 - try: - avghr = data['heart_rate']['average'] - except KeyError: # pragma: no cover - avghr = 0 - try: - maxhr = data['heart_rate']['max'] - except KeyError: # pragma: no cover - maxhr = 0 - - try: - avgpace = 500.*totaltime/totaldist - except (ZeroDivisionError, OverflowError): # pragma: no cover - avgpace = 0. - - try: - restpace = 500.*resttime/restdistance - except (ZeroDivisionError, OverflowError): # pragma: no cover - restpace = 0. - - velo = totaldist/totaltime - avgpower = 2.8*velo**(3.0) - if workouttype in ['bike', 'bikeerg']: # pragma: no cover - velo = velo/2. - avgpower = 2.8*velo**(3.0) - velo = velo*2 - - try: - restvelo = restdistance/resttime - except (ZeroDivisionError, OverflowError): # pragma: no cover - restvelo = 0 - - restpower = 2.8*restvelo**(3.0) - if workouttype in ['bike', 'bikeerg']: # pragma: no cover - restvelo = restvelo/2. - restpower = 2.8*restvelo**(3.0) - restvelo = restvelo*2 - - try: - avgdps = totaldist/data['stroke_count'] - except (ZeroDivisionError, OverflowError, KeyError): # pragma: no cover - avgdps = 0 - - from rowingdata import summarystring, workstring, interval_string - - sums = summarystring(totaldist, totaltime, avgpace, spm, avghr, maxhr, - avgdps, avgpower, readFile=filename, - separator=sep) - - sums += workstring(totaldist, totaltime, avgpace, spm, avghr, maxhr, - avgdps, avgpower, separator=sep, symbol='W') - - sums += workstring(restdistance, resttime, restpace, 0, 0, 0, 0, restpower, - separator=sep, - symbol='R') - - sums += '\nWorkout Details\n' - sums += '#-{sep}SDist{sep}-Split-{sep}-SPace-{sep}-Pwr-{sep}SPM-{sep}AvgHR{sep}MaxHR{sep}DPS-\n'.format( - sep=sep - ) - - intervalnr = 0 - sa = [] - results = [] - - try: - timebased = data['workout_type'] in [ - 'FixedTimeSplits', 'FixedTimeInterval'] - except KeyError: # pragma: no cover - timebased = False - - for interval in splitdata: - try: - idist = interval['distance'] - except KeyError: # pragma: no cover - idist = 0 - - try: - itime = interval['time']/10. - except KeyError: # pragma: no cover - itime = 0 - try: - ipace = 500.*itime/idist - except (ZeroDivisionError, OverflowError): # pragma: no cover - ipace = 180. - - try: - ispm = interval['stroke_rate'] - except KeyError: # pragma: no cover - ispm = 0 - try: - irest_time = interval['rest_time']/10. - except KeyError: # pragma: no cover - irest_time = 0 - try: - iavghr = interval['heart_rate']['average'] - except KeyError: # pragma: no cover - iavghr = 0 - try: - imaxhr = interval['heart_rate']['average'] - except KeyError: # pragma: no cover - imaxhr = 0 - - # create interval values - iarr = [idist, 'meters', 'work'] - resarr = [itime] - if timebased: # pragma: no cover - iarr = [itime, 'seconds', 'work'] - resarr = [idist] - - if irest_time > 0: - iarr += [irest_time, 'seconds', 'rest'] - try: - resarr += [interval['rest_distance']] - except KeyError: # pragma: no cover - resarr += [np.nan] - - sa += iarr - results += resarr - - if itime != 0: - ivelo = idist/itime - ipower = 2.8*ivelo**(3.0) - if workouttype in ['bike', 'bikeerg']: # pragma: no cover - ipower = 2.8*(ivelo/2.)**(3.0) - else: # pragma: no cover - ivelo = 0 - ipower = 0 - - sums += interval_string(intervalnr, idist, itime, ipace, ispm, - iavghr, imaxhr, 0, ipower, separator=sep) - intervalnr += 1 - - return sums, sa, results - - -# Create the Data object for the stroke data to be sent to Concept2 logbook -# API -def createc2workoutdata(w): - filename = w.csvfilename - try: - row = rowingdata(csvfile=filename) - except IOError: # pragma: no cover - return 0 - - try: - averagehr = int(row.df[' HRCur (bpm)'].mean()) - maxhr = int(row.df[' HRCur (bpm)'].max()) - except (ValueError, KeyError): # pragma: no cover - averagehr = 0 - maxhr = 0 - - # Calculate intervalstats - itime, idist, itype = row.intervalstats_values() - try: - lapnames = row.df[' lapIdx'].unique() - except KeyError: # pragma: no cover - lapnames = range(len(itime)) - nrintervals = len(itime) - if len(lapnames) != nrintervals: - newlapnames = [] - for name in lapnames: - newlapnames += [name, name] - lapnames = newlapnames - intervaldata = [] - for i in range(nrintervals): - if itime[i] > 0: - mask = (row.df[' lapIdx'] == lapnames[i]) & ( - row.df[' WorkoutState'] == itype[i]) - try: - spmav = int(row.df[' Cadence (stokes/min)'] - [mask].mean().astype(int)) - hrav = int(row.df[' HRCur (bpm)'][mask].mean().astype(int)) - except AttributeError: # pragma: no cover - try: - spmav = int(row.df[' Cadence (stokes/min)'][mask].mean()) - hrav = int(row.df[' HRCur (bpm)'][mask].mean()) - except ValueError: - spmav = 0 - try: - hrav = int(row.df[' HRCur (bpm)'][mask].mean()) - except ValuError: - hrav = 0 - intervaldict = { - 'type': 'distance', - 'time': int(10*itime[i]), - 'distance': int(idist[i]), - 'heart_rate': { - 'average': hrav, - }, - 'stroke_rate': spmav, - } - intervaldata.append(intervaldict) - - # adding diff, trying to see if this is valid - t = 10*row.df.loc[:, 'TimeStamp (sec)'].values - \ - 10*row.df.loc[:, 'TimeStamp (sec)'].iloc[0] - try: - t[0] = t[1] - except IndexError: # pragma: no cover - pass - - d = 10*row.df.loc[:, ' Horizontal (meters)'].values - try: - d[0] = d[1] - except IndexError: # pragma: no cover - pass - - p = abs(10*row.df.loc[:, ' Stroke500mPace (sec/500m)'].values) - p = np.clip(p, 0, 3600) - if w.workouttype == 'bike': # pragma: no cover - p = 2.0*p - - t = t.astype(int) - d = d.astype(int) - p = p.astype(int) - spm = row.df[' Cadence (stokes/min)'].astype(int) - - try: - spm[0] = spm[1] - except (KeyError, IndexError): # pragma: no cover - spm = 0*t - try: - hr = row.df[' HRCur (bpm)'].astype(int) - except ValueError: # pragma: no cover - hr = 0*d - stroke_data = [] - - t = t.tolist() - d = d.tolist() - p = p.tolist() - spm = spm.tolist() - hr = hr.tolist() - - for i in range(len(t)): - thisrecord = {"t": t[i], - "d": d[i], - "p": p[i], - "spm": spm[i], - "hr": hr[i]} - stroke_data.append(thisrecord) - - try: - durationstr = datetime.datetime.strptime( - str(w.duration), "%H:%M:%S.%f") - except ValueError: - durationstr = datetime.datetime.strptime(str(w.duration), "%H:%M:%S") - - workouttype = w.workouttype - if workouttype in otwtypes: - workouttype = 'water' - - if w.timezone == 'tzutc()': # pragma: no cover - w.timezone = 'UTC' - w.save() - - try: - wendtime = w.startdatetime.astimezone(pytz.timezone( - w.timezone))+datetime.timedelta(seconds=makeseconds(durationstr)) - except UnknownTimeZoneError: - wendtime = w.startdatetime.astimezone(pytz.utc)+datetime.timedelta(seconds=makeseconds(durationstr)) - - data = { - "type": mytypes.c2mapping[workouttype], - # w.startdatetime.isoformat(), - "date": wendtime.strftime('%Y-%m-%d %H:%M:%S'), - "stroke_count": int(row.stroke_count), - "timezone": w.timezone, - "distance": int(w.distance), - "time": int(10*makeseconds(durationstr)), - "weight_class": c2wc(w.weightcategory), - "comments": w.notes, - 'stroke_rate': int(row.df[' Cadence (stokes/min)'].mean()), - 'drag_factor': int(row.dragfactor), - "heart_rate": { - "average": averagehr, - "max": maxhr, - }, - "stroke_data": stroke_data, - 'workout': { - 'splits': intervaldata, - } - } - - return data - -# Refresh Concept2 authorization token - - -def do_refresh_token(refreshtoken): - scope = "results:write,user:read" - # client_auth = requests.auth.HTTPBasicAuth(C2_CLIENT_ID, C2_CLIENT_SECRET) - post_data = {"grant_type": "refresh_token", - "client_secret": C2_CLIENT_SECRET, - "client_id": C2_CLIENT_ID, - "refresh_token": refreshtoken, - } - headers = {'user-agent': 'sanderroosendaal'} - url = "https://log.concept2.com/oauth/access_token" - s = Session() - req = Request('POST', url, data=post_data, headers=headers) - - prepped = req.prepare() - prepped.body += "&scope=" - prepped.body += scope - - response = s.send(prepped) - - try: - token_json = response.json() - except JSONDecodeError: # pragma: no cover - return [None, None, None] - - try: - thetoken = token_json['access_token'] - expires_in = token_json['expires_in'] - refresh_token = token_json['refresh_token'] - except: # pragma: no cover - with open("media/c2errors.log", "a") as errorlog: - errorstring = str(sys.exc_info()[0]) - timestr = time.strftime("%Y%m%d-%H%M%S") - errorlog.write(timestr+errorstring+"\r\n") - errorlog.write(str(token_json)+"\r\n") - thetoken = None - expires_in = None - refresh_token = None - - return [thetoken, expires_in, refresh_token] - -# Exchange authorization code for authorization token - - -def get_token(code): - messg = '' - scope = "user:read,results:write" - # client_auth = requests.auth.HTTPBasicAuth(C2_CLIENT_ID, C2_CLIENT_SECRET) - post_data = {"grant_type": "authorization_code", - "code": code, - "redirect_uri": C2_REDIRECT_URI, - "client_secret": C2_CLIENT_SECRET, - "client_id": C2_CLIENT_ID, - } - headers = {'user-agent': 'sanderroosendaal'} - url = "https://log.concept2.com/oauth/access_token" - s = Session() - req = Request('POST', url, data=post_data, headers=headers) - - prepped = req.prepare() - prepped.body += "&scope=" - prepped.body += scope - - response = s.send(prepped) - - token_json = response.json() - - try: - status_code = response.status_code - # status_code = token_json['status_code'] - except AttributeError: # pragma: no cover - # except KeyError: - return (0, response.text) - try: - status_code = token_json.status_code - except AttributeError: # pragma: no cover - return (0, 'Attribute Error on c2_get_token') - - if status_code == 200: - thetoken = token_json['access_token'] - expires_in = token_json['expires_in'] - refresh_token = token_json['refresh_token'] - else: # pragma: no cover - return (0, token_json['message']) - - return (thetoken, expires_in, refresh_token, messg) - -# Make URL for authorization and load it - - -def make_authorization_url(request): # pragma: no cover - # Generate a random string for the state parameter - # Save it for use later to prevent xsrf attacks - from uuid import uuid4 - # state = str(uuid4()) - scope = "user:read,results:write" - - params = {"client_id": C2_CLIENT_ID, - "response_type": "code", - "redirect_uri": C2_REDIRECT_URI} - url = "https://log.concept2.com/oauth/authorize?" + \ - urllib.parse.urlencode(params) - url += "&scope="+scope - - return HttpResponseRedirect(url) - -# Get workout from C2 ID - - -def get_workout(user, c2id, do_async=True): - r = Rower.objects.get(user=user) - _ = c2_open(user) - - _ = myqueue(queuehigh, - handle_c2_getworkout, - user.id, - r.c2token, - c2id, - r.defaulttimezone) - - return 1 - - -# Get list of C2 workouts. We load only the first page, -# assuming that users don't want to import their old workouts -def get_c2_workout_list(user, page=1): - r = Rower.objects.get(user=user) - if (r.c2token == '') or (r.c2token is None): # pragma: no cover - s = "Token doesn't exist. Need to authorize" - return custom_exception_handler(401, s) - elif (timezone.now() > r.tokenexpirydate): # pragma: no cover - s = "Token expired. Needs to refresh." - - return custom_exception_handler(401, s) - - # ready to fetch. Hurray - authorizationstring = str('Bearer ' + r.c2token) - headers = {'Authorization': authorizationstring, - 'user-agent': 'sanderroosendaal', - 'Content-Type': 'application/json'} - url = "https://log.concept2.com/api/users/me/results" - url += "?page={page}".format(page=page) - - s = requests.get(url, headers=headers) - - return s - - -# Get username, having access token. -# Handy for checking if the API access is working -def get_username(access_token): # pragma: no cover - authorizationstring = str('Bearer ' + access_token) - headers = {'Authorization': authorizationstring, - 'user-agent': 'sanderroosendaal', - 'Content-Type': 'application/json'} - import urllib - url = "https://log.concept2.com/api/users/me" - response = requests.get(url, headers=headers) - - me_json = response.json() - - try: - res = me_json['data']['username'] - _ = me_json['data']['id'] - except KeyError: - res = None - - return res - -# Get user id, having access token -# Handy for checking if the API access is working - - -def get_userid(access_token): - authorizationstring = str('Bearer ' + access_token) - headers = {'Authorization': authorizationstring, - 'user-agent': 'sanderroosendaal', - 'Content-Type': 'application/json'} - import urllib - url = "https://log.concept2.com/api/users/me" - try: - response = requests.get(url, headers=headers) - except: # pragma: no cover - return 0 - - try: - me_json = response.json() - except: # pragma: no cover - return 0 - try: - res = me_json['data']['id'] - except KeyError: # pragma: no cover - return 0 - - return res - -# For debugging purposes - - -def process_callback(request): # pragma: no cover - # need error handling - - code = request.GET['code'] - - access_token = get_token(code) - - username, id = get_username(access_token) - - return HttpResponse("got a user name: %s" % username) - - -def default(o): # pragma: no cover - if isinstance(o, numpy.int64): - return int(o) - raise TypeError - -# Uploading workout - - -def workout_c2_upload(user, w, asynchron=False): - message = 'trying C2 upload' - - try: - if mytypes.c2mapping[w.workouttype] is None: # pragma: no cover - return "This workout type cannot be uploaded to Concept2", 0 - except KeyError: # pragma: no cover - return "This workout type cannot be uploaded to Concept2", 0 - - _ = c2_open(user) - - r = Rower.objects.get(user=user) - - # ready to upload. Hurray - - if (is_workout_user(user, w)): - - c2userid = get_userid(r.c2token) - if not c2userid: # pragma: no cover - raise NoTokenError("User has no token") - - dologging('debuglog.log', - 'Upload to C2 user {userid}'.format(userid=user.id)) - data = createc2workoutdata(w) - dologging('debuglog.log', json.dumps(data)) - - if data == 0: # pragma: no cover - return "Error: No data file. Contact info@rowsandall.com if the problem persists", 0 - - authorizationstring = str('Bearer ' + r.c2token) - headers = {'Authorization': authorizationstring, - 'user-agent': 'sanderroosendaal', - 'Content-Type': 'application/json'} - import urllib - url = "https://log.concept2.com/api/users/%s/results" % (c2userid) - if not asynchron: # pragma: no cover - response = requests.post( - url, headers=headers, data=json.dumps(data, default=default)) - - if (response.status_code == 409): # pragma: no cover - message = "Concept2 Duplicate error" - w.uploadedtoc2 = -1 - c2id = -1 - w.save() - elif (response.status_code == 201 or response.status_code == 200): - # s= json.loads(response.text) - s = response.json() - c2id = s['data']['id'] - w.uploadedtoc2 = c2id - w.save() - message = "Upload to Concept2 was successful" - else: # pragma: no cover - message = "Something went wrong in workout_c2_upload_view." \ - " Response code 200/201 but C2 sync failed: "+response.text - c2id = 0 - else: # pragma: no cover - - _ = myqueue(queue, - handle_c2_sync, - w.id, - url, - headers, - json.dumps(data, default=default)) - c2id = 0 - - return message, c2id - -# This is token refresh. Looks for tokens in our database, then refreshes - - -def rower_c2_token_refresh(user): - r = Rower.objects.get(user=user) - res = do_refresh_token(r.c2refreshtoken) - if res[0]: - access_token = res[0] - expires_in = res[1] - refresh_token = res[2] - expirydatetime = timezone.now()+datetime.timedelta(seconds=expires_in) - - r = Rower.objects.get(user=user) - r.c2token = access_token - r.tokenexpirydate = expirydatetime - r.c2refreshtoken = refresh_token - - r.save() - return r.c2token - else: # pragma: no cover - return None diff --git a/rowers/imports.py b/rowers/imports.py index 6e40e0b5..38cac687 100644 --- a/rowers/imports.py +++ b/rowers/imports.py @@ -66,7 +66,7 @@ def splitstdata(lijst): return [np.array(t), np.array(latlong)] - +# covered in integrations def imports_open(user, oauth_data): r = Rower.objects.get(user=user) token = getattr(r, oauth_data['tokenname']) @@ -101,7 +101,7 @@ def imports_open(user, oauth_data): return token - +# covered in integrations # Refresh token using refresh token def imports_do_refresh_token(refreshtoken, oauth_data, access_token=''): # client_auth = requests.auth.HTTPBasicAuth( @@ -175,7 +175,7 @@ def imports_do_refresh_token(refreshtoken, oauth_data, access_token=''): # Exchange ST access code for long-lived ST access token - +# implemented in integrations def imports_get_token( code, oauth_data ): diff --git a/rowers/integrations/__init__.py b/rowers/integrations/__init__.py new file mode 100644 index 00000000..56112937 --- /dev/null +++ b/rowers/integrations/__init__.py @@ -0,0 +1 @@ +from .c2 import C2Integration diff --git a/rowers/integrations/c2.py b/rowers/integrations/c2.py new file mode 100644 index 00000000..316af562 --- /dev/null +++ b/rowers/integrations/c2.py @@ -0,0 +1,450 @@ +from .integrations import SyncIntegration, NoTokenError +from rowers.models import User, Rower, Workout, TombStone +from rowingdata import rowingdata +import numpy as np +import datetime +import json +import urllib +from rowers.utils import dologging, uniqify +from django.utils import timezone +import requests +from pytz.exceptions import UnknownTimeZoneError +import pytz + +from rowsandall_app.settings import ( + C2_CLIENT_ID, C2_REDIRECT_URI, C2_CLIENT_SECRET, + UPLOAD_SERVICE_URL, UPLOAD_SERVICE_SECRET +) + +from rowers.tasks import ( + handle_c2_import_stroke_data, handle_c2_sync, handle_c2_async_workout, + handle_c2_getworkout +) +import django_rq +queue = django_rq.get_queue('default') +queuelow = django_rq.get_queue('low') +queuehigh = django_rq.get_queue('high') + +from rowers.utils import myqueue, dologging + +from requests import Request, Session + +import rowers.mytypes as mytypes +from rowers.rower_rules import is_workout_user, ispromember + +def default(o): # pragma: no cover + if isinstance(o, numpy.int64): + return int(o) + raise TypeError + +# convert datetime object to seconds +def makeseconds(t): + seconds = t.hour*3600.+t.minute*60.+t.second+0.1*int(t.microsecond/1.e5) + return seconds + +# convert our weight class code to Concept2 weight class code + + +def c2wc(weightclass): + if (weightclass == "lwt"): # pragma: no cover + res = "L" + else: + res = "H" + + return res + + +class C2Integration(SyncIntegration): + def __init__(self, *args, **kwargs): + super(C2Integration, self).__init__(self, *args, **kwargs) + self.oauth_data = { + 'client_id': C2_CLIENT_ID, + 'client_secret': C2_CLIENT_SECRET, + 'redirect_uri': C2_REDIRECT_URI, + 'autorization_uri': "https://log.concept2.com/oauth/authorize", + 'content_type': 'application/x-www-form-urlencoded', + 'tokenname': 'c2token', + 'refreshtokenname': 'c2refreshtoken', + 'expirydatename': 'tokenexpirydate', + 'bearer_auth': True, + 'base_url': "https://log.concept2.com/oauth/access_token", + 'scope': 'write', + } + + def get_token(self, code, *args, **kwargs): + messg = '' + scope = "user:read,results:write" + + post_data = {"grant_type": "authorization_code", + "code": code, + "redirect_uri": C2_REDIRECT_URI, + "client_secret": C2_CLIENT_SECRET, + "client_id": C2_CLIENT_ID, + } + headers = {'user-agent': 'sanderroosendaal'} + url = "https://log.concept2.com/oauth/access_token" + s = Session() + req = Request('POST', url, data=post_data, headers=headers) + + prepped = req.prepare() + prepped.body += "&scope=" + prepped.body += scope + + response = s.send(prepped) + + token_json = response.json() + + try: + status_code = response.status_code + except AttributeError: # pragma: no cover + return (0, response.text) + + if status_code == 200: + thetoken = token_json['access_token'] + expires_in = token_json['expires_in'] + refresh_token = token_json['refresh_token'] + else: # pragma: no cover + raise NoTokenError("No Token") + + + return (thetoken, expires_in, refresh_token, messg) + + + def open(self, *args, **kwargs): + return super(C2Integration, self).open(*args, **kwargs) + + def token_refresh(self, *args, **kwargs): + return super(C2Integration, self).open(*args, **kwargs) + + def make_authorization_url(self, *args, **kwargs): + scope = "user:read,results:write" + + params = {"client_id": C2_CLIENT_ID, + "response_type": "code", + "redirect_uri": C2_REDIRECT_URI} + url = "https://log.concept2.com/oauth/authorize?" + \ + urllib.parse.urlencode(params) + url += "&scope="+scope + return url + + def get_userid(self, *args, **kwargs): + _ = self.open() + access_token = self.rower.c2token + authorizationstring = str('Bearer ' + access_token) + headers = {'Authorization': authorizationstring, + 'user-agent': 'sanderroosendaal', + 'Content-Type': 'application/json'} + url = "https://log.concept2.com/api/users/me" + try: + response = requests.get(url, headers=headers) + except: # pragma: no cover + return 0 + + try: + me_json = response.json() + except: # pragma: no cover + return 0 + try: + res = me_json['data']['id'] + except KeyError: # pragma: no cover + return 0 + + return res + + def createworkoutdata(self, w, *args, **kwargs) -> dict: + filename = w.csvfilename + try: + row = rowingdata(csvfile=filename) + except IOError: # pragma: no cover + return 0 + + try: + averagehr = int(row.df[' HRCur (bpm)'].mean()) + maxhr = int(row.df[' HRCur (bpm)'].max()) + except (ValueError, KeyError): # pragma: no cover + averagehr = 0 + maxhr = 0 + + # Calculate intervalstats + itime, idist, itype = row.intervalstats_values() + lapnames = range(len(itime)) + nrintervals = len(itime) + + try: + lapnames = row.df[' lapIdx'].unique() + except KeyError: # pragma: no cover + pass + + if len(lapnames) != nrintervals: + newlapnames = [] + for name in lapnames: + newlapnames += [name, name] + lapnames = newlapnames + intervaldata = [] + for i in range(nrintervals): + if itime[i] > 0: + mask = (row.df[' lapIdx'] == lapnames[i]) & ( + row.df[' WorkoutState'] == itype[i]) + try: + spmav = int(row.df[' Cadence (stokes/min)'] + [mask].mean().astype(int)) + hrav = int(row.df[' HRCur (bpm)'][mask].mean().astype(int)) + except AttributeError: # pragma: no cover + try: + spmav = int(row.df[' Cadence (stokes/min)'][mask].mean()) + hrav = int(row.df[' HRCur (bpm)'][mask].mean()) + except ValueError: + spmav = 0 + try: + hrav = int(row.df[' HRCur (bpm)'][mask].mean()) + except ValuError: + hrav = 0 + intervaldict = { + 'type': 'distance', + 'time': int(10*itime[i]), + 'distance': int(idist[i]), + 'heart_rate': { + 'average': hrav, + }, + 'stroke_rate': spmav, + } + intervaldata.append(intervaldict) + + # adding diff, trying to see if this is valid + t = 10*row.df.loc[:, 'TimeStamp (sec)'].values - \ + 10*row.df.loc[:, 'TimeStamp (sec)'].iloc[0] + try: + t[0] = t[1] + except IndexError: # pragma: no cover + pass + + d = 10*row.df.loc[:, ' Horizontal (meters)'].values + try: + d[0] = d[1] + except IndexError: # pragma: no cover + pass + + p = abs(10*row.df.loc[:, ' Stroke500mPace (sec/500m)'].values) + p = np.clip(p, 0, 3600) + if w.workouttype == 'bike': # pragma: no cover + p = 2.0*p + + t = t.astype(int) + d = d.astype(int) + p = p.astype(int) + spm = row.df[' Cadence (stokes/min)'].astype(int) + + try: + spm[0] = spm[1] + except (KeyError, IndexError): # pragma: no cover + spm = 0*t + try: + hr = row.df[' HRCur (bpm)'].astype(int) + except ValueError: # pragma: no cover + hr = 0*d + stroke_data = [] + + t = t.tolist() + d = d.tolist() + p = p.tolist() + spm = spm.tolist() + hr = hr.tolist() + + for i in range(len(t)): + thisrecord = {"t": t[i], + "d": d[i], + "p": p[i], + "spm": spm[i], + "hr": hr[i]} + stroke_data.append(thisrecord) + + try: + durationstr = datetime.datetime.strptime( + str(w.duration), "%H:%M:%S.%f") + except ValueError: + durationstr = datetime.datetime.strptime(str(w.duration), "%H:%M:%S") + + workouttype = w.workouttype + if workouttype in mytypes.otwtypes: + workouttype = 'water' + + if w.timezone == 'tzutc()': # pragma: no cover + w.timezone = 'UTC' + w.save() + + try: + wendtime = w.startdatetime.astimezone(pytz.timezone( + w.timezone))+datetime.timedelta(seconds=makeseconds(durationstr)) + except UnknownTimeZoneError: + wendtime = w.startdatetime.astimezone(pytz.utc)+datetime.timedelta(seconds=makeseconds(durationstr)) + + data = { + "type": mytypes.c2mapping[workouttype], + # w.startdatetime.isoformat(), + "date": wendtime.strftime('%Y-%m-%d %H:%M:%S'), + "stroke_count": int(row.stroke_count), + "timezone": w.timezone, + "distance": int(w.distance), + "time": int(10*makeseconds(durationstr)), + "weight_class": c2wc(w.weightcategory), + "comments": w.notes, + 'stroke_rate': int(row.df[' Cadence (stokes/min)'].mean()), + 'drag_factor': int(row.dragfactor), + "heart_rate": { + "average": averagehr, + "max": maxhr, + }, + "stroke_data": stroke_data, + 'workout': { + 'splits': intervaldata, + } + } + + return data + + + def workout_export(self, workout, *args, **kwargs) -> str: + try: + if mytypes.c2mapping[workout.workouttype] is None: # pragma: no cover + return "0" + except KeyError: # pragma: no cover + return "0" + + _ = self.open() + r = self.rower + user = self.user + + if (is_workout_user(user, workout)): + c2userid = self.get_userid() + if not c2userid: # pragma: no cover + raise NoTokenError("User has no token") + + dologging('debuglog.log', + 'Upload to C2 user {userid}'.format(userid=user.id)) + data = self.createworkoutdata(workout) + dologging('c2_log.log', json.dumps(data)) + + if data == 0: # pragma: no cover + return 0 + + authorizationstring = str('Bearer ' + r.c2token) + headers = {'Authorization': authorizationstring, + 'user-agent': 'sanderroosendaal', + 'Content-Type': 'application/json'} + + url = "https://log.concept2.com/api/users/%s/results" % (c2userid) + + _ = myqueue(queue, + handle_c2_sync, + workout.id, + url, + headers, + json.dumps(data, default=default)) + c2id = 0 + + return c2id + + + + def get_workout(self, id): + _ = self.open() + _ = myqueue(queuehigh, + handle_c2_getworkout, + self.user.id, + self.rower.c2token, + id, + self.rower.defaulttimezone) + + return 1 + + + def get_workouts(self, *args, **kwargs): + page = kwargs.get('page',1) + + try: + _ = self.open() + except NoTokenError: + return 0 + + # this part to get_workout_list + workouts = self.get_workout_list(page=page) + + counter = 0 + for workout in workouts: + c2id = workout['id'] + if workout['new'] == 'NEW': + self.get_workout(c2id) + + return 1 + + # should be unified to workout list + def get_workout_list(self, *args, **kwargs): + page = kwargs.get('page',1) + r = self.rower + if (r.c2token == '') or (r.c2token is None): # pragma: no cover + s = "Token doesn't exist. Need to authorize" + return custom_exception_handler(401, s) + elif (timezone.now() > r.tokenexpirydate): # pragma: no cover + s = "Token expired. Needs to refresh." + + return custom_exception_handler(401, s) + + # ready to fetch. Hurray + authorizationstring = str('Bearer ' + r.c2token) + headers = {'Authorization': authorizationstring, + 'user-agent': 'sanderroosendaal', + 'Content-Type': 'application/json'} + url = "https://log.concept2.com/api/users/me/results" + url += "?page={page}".format(page=page) + + res = requests.get(url, headers=headers) + + if (res.status_code != 200): # pragma: no cover + return [] + + workouts = [] + c2ids = [item['id'] for item in res.json()['data']] + knownc2ids = uniqify([ + w.uploadedtoc2 for w in Workout.objects.filter(user=self.rower) + ]) + tombstones = [ + t.uploadedtoc2 for t in TombStone.objects.filter(user=self.rower) + ] + parkedids = [] + try: + with open('c2blocked.json', 'r') as c2blocked: + jsondata = json.load(c2blocked) + parkedids = jsondata['ids'] + except: # pragma: no cover + pass + + knownc2ids = uniqify(knownc2ids+tombstones+parkedids) + for item in res.json()['data']: + d = item['distance'] + i = item['id'] + ttot = item['time_formatted'] + s = item['date'] + r = item['type'] + s2 = item['source'] + c = item['comments'] + if i in knownc2ids: + nnn = '' + else: # pragma: no cover + nnn = 'NEW' + keys = ['id', 'distance', 'duration', 'starttime', + 'rowtype', 'source', 'comment', 'new'] + values = [i, d, ttot, s, r, s2, c, nnn] + ress = dict(zip(keys, values)) + workouts.append(ress) + + return workouts + + + + + + +# just as a quick test during development +u = User.objects.get(id=1) + +c2_integration_1 = C2Integration(u) diff --git a/rowers/integrations/integrations.py b/rowers/integrations/integrations.py new file mode 100644 index 00000000..6ca777f9 --- /dev/null +++ b/rowers/integrations/integrations.py @@ -0,0 +1,250 @@ +from abc import ABCMeta, ABC, abstractmethod +from importlib import import_module +from rowers.models import Rower, User +from rowers.utils import NoTokenError + +import requests +from django.utils import timezone +from datetime import timedelta +import arrow +import urllib +from uuid import uuid4 + + + +class SyncIntegration(metaclass=ABCMeta): + oauth_data = { + 'tokenname':'token', + 'expirydatename':'exp', + 'refreshtokenname':'r', + 'redirect_uri': 'r', + 'client_secret': 's', + 'base_uri': 's' + } + + user = User() + rower = Rower() + + def __init__(self, *args, **kwargs): + user = args[1] + self.user = user + self.rower = user.rower + + @classmethod + def __subclasshook__(cls, subclass): + return (hasattr(subclass, 'get_token') and + callable(subclass.get_token) or + NotImplemented) + + + @abstractmethod + def createworkoutdata(w, *args, **kwargs) -> dict: + return {} + + @abstractmethod + def workout_export(workout, *args, **kwargs) -> str: + pass + + @abstractmethod + def get_workouts(*args, **kwargs) -> int: + pass + + @abstractmethod + def get_workout(id) -> int: + pass + + # need to unify workout list + @abstractmethod + def get_workout_list(*args, **kwargs) -> list: + pass + + @abstractmethod + def make_authorization_url(self, *args, **kwargs) -> str: # pragma: no cover + # Generate a random string for the state parameter + # Save it for use later to prevent xsrf attacks + + state = str(uuid4()) + + params = {"client_id": self.oauth_data['client_id'], + "response_type": "code", + "redirect_uri": self.oauth_data['redirect_uri'], + "scope": self.oauth_data['scope'], + "state": state} + + url = oauth_data['authorizaton_uri']+urllib.parse.urlencode(params) + + return url + + + @abstractmethod + def get_token(self, code, *args, **kwargs) -> (str, int, str): + redirect_uri = self.oauth_data['redirect_uri'] + client_secret = self.oauth_data['client_secret'] + client_id = self.oauth_data['client_id'] + base_uri = self.oauth_data['base_url'] + + post_data = {"grant_type": "authorization_code", + "code": code, + "redirect_uri": redirect_uri, + "client_secret": client_secret, + "client_id": client_id, + } + + try: + headers = self.oauth_data['headers'] + except KeyError: + headers = {'Accept': 'application/json', + 'Api-Key': client_id, + 'Content-Type': 'application/json', + 'user-agent': 'sanderroosendaal'} + + if 'grant_type' in self.oauth_data: + if self.oauth_data['grant_type']: + post_data['grant_type'] = self.oauth_data['grant_type'] + if 'strava' in self.oauth_data['autorization_uri']: + post_data['grant_type'] = "authorization_code" + + if 'json' in self.oauth_data['content_type']: + response = requests.post( + base_uri, + data=json.dumps(post_data), + headers=headers) + else: # pragma: no cover + response = requests.post( + base_uri, + data=post_data, + headers=headers, verify=False) + + if response.status_code == 200 or response.status_code == 201: + token_json = response.json() + try: + thetoken = token_json['access_token'] + except KeyError: # pragma: no cover + raise NoTokenError("Failed to obtain token") + try: + refresh_token = token_json['refresh_token'] + except KeyError: # pragma: no cover + refresh_token = '' + try: + expires_in = token_json['expires_in'] + except KeyError: # pragma: no cover + expires_in = 0 + try: + expires_in = int(expires_in) + except (ValueError, TypeError): # pragma: no cover + expires_in = 0 + else: # pragma: no cover + raise NoTokenError("Failed to obtain token") + + return [thetoken, expires_in, refresh_token] + + + @abstractmethod + def open(self, *args, **kwargs) -> str: + token = getattr(self.rower, self.oauth_data['tokenname']) + + try: + tokenexpirydate = getattr(self.rower, self.oauth_data['expirydatename']) + except (TypeError, AttributeError, KeyError): # pragma: no cover + tokenexpirydate = None + + if (token == '') or (token is None): + raise NoTokenError("User has no token") + else: + tokenname = self.oauth_data['tokenname'] + refreshtokenname = self.oauth_data['refreshtokenname'] + expirydatename = self.oauth_data['expirydatename'] + if tokenexpirydate and timezone.now()+timedelta(seconds=60) > tokenexpirydate: + token = self.token_refresh() + elif tokenexpirydate is None and expirydatename is not None and 'strava' in expirydatename: # pragma: no cover + token = self.token_refresh() + + return token + + def do_refresh_token(self, *args, **kwargs) -> (str, int, str): + post_data = {"grant_type": "refresh_token", + "client_secret": self.oauth_data['client_secret'], + "client_id": self.oauth_data['client_id'], + "refresh_token": refreshtoken, + } + headers = {'user-agent': 'sanderroosendaal', + 'Accept': 'application/json', + 'Content-Type': self.oauth_data['content_type']} + + # for Strava + if 'grant_type' in self.oauth_data: + if self.oauth_data['grant_type']: + post_data['grant_type'] = self.oauth_data['grant_type'] + + if self.oauth_data['bearer_auth']: + headers['authorization'] = 'Bearer %s' % access_token + + baseurl = self.oauth_data['base_url'] + + if 'json' in self.oauth_data['content_type']: + try: + response = requests.post(baseurl, + data=json.dumps(post_data), + headers=headers, verify=False) + except: # pragma: no cover + raise NoTokenError("Failed to get token") + else: + try: + response = requests.post(baseurl, + data=post_data, + headers=headers, verify=False, + ) + except: # pragma: no cover + raise NoTokenError("Failed to get token") + + if response.status_code == 200 or response.status_code == 201: + token_json = response.json() + else: # pragma: no cover + raise NoTokenError("User has no token") + + try: + thetoken = token_json['access_token'] + except KeyError: # pragma: no cover + raise NoTokenError("User has no token") + + try: + expires_in = token_json['expires_in'] + except KeyError: + try: + expires_at = arrow.get(token_json['expires_at']).timestamp() + expires_in = expires_at - arrow.now().timestamp() + except KeyError: # pragma: no cover + expires_in = 0 + try: + refresh_token = token_json['refresh_token'] + except KeyError: # pragma: no cover + refresh_token = refreshtoken + try: + expires_in = int(expires_in) + except (TypeError, ValueError): # pragma: no cover + expires_in = 0 + + return [thetoken, expires_in, refresh_token] + + + @abstractmethod + def token_refresh(self, *args, **kwargs) -> str: + refreshtoken = getattr(self.rower, oauth['refreshtokenname']) + + if not refreshtoken: + refreshtoken = getattr(self.rower, oauth['tokenname']) + + access_token, expires_in, refresh_token = self.do_refresh_token() + expirydatetime = timezone.now()+timedelta(seconds=expires_in) + + setattr(self.rower, tokenname, access_token) + if expirydatename is not None: + setattr(self.rower, expirydatename, expirydatetime) + if refreshtokenname is not None: + setattr(self.rower, refreshtokenname, refresh_token) + + self.rower.save() + + return access_token + + diff --git a/rowers/management/commands/processemail.py b/rowers/management/commands/processemail.py index 33f3427a..c9ff2cb1 100644 --- a/rowers/management/commands/processemail.py +++ b/rowers/management/commands/processemail.py @@ -28,12 +28,12 @@ from rowingdata import rowingdata as rrdata import rowers.uploads as uploads import rowers.polarstuff as polarstuff -import rowers.c2stuff as c2stuff + import rowers.rp3stuff as rp3stuff import rowers.stravastuff as stravastuff import rowers.nkstuff as nkstuff from rowers.opaque import encoder - +from rowers.integrations import * from rowers.rower_rules import user_is_not_basic, user_is_coachee from rowers.utils import dologging @@ -91,7 +91,9 @@ class Command(BaseCommand): rowers = Rower.objects.filter(c2_auto_import=True) for r in rowers: # pragma: no cover if user_is_not_basic(r.user) or user_is_coachee(r.user): - c2stuff.get_c2_workouts(r) + c2integration = C2Integration(r.user) + _ = c2integration.get_workouts() + except: # pragma: no cover exc_type, exc_value, exc_traceback = sys.exc_info() lines = traceback.format_exception(exc_type, exc_value, exc_traceback) diff --git a/rowers/ownapistuff.py b/rowers/ownapistuff.py index d222986d..cf81c148 100644 --- a/rowers/ownapistuff.py +++ b/rowers/ownapistuff.py @@ -15,7 +15,6 @@ import math from math import sin, cos, atan2, sqrt import urllib -import rowers.c2stuff as c2stuff # Django from django.http import HttpResponseRedirect, HttpResponse, JsonResponse diff --git a/rowers/templates/list_import.html b/rowers/templates/list_import.html new file mode 100644 index 00000000..27e1e36c --- /dev/null +++ b/rowers/templates/list_import.html @@ -0,0 +1,84 @@ +{% extends "newbase.html" %} +{% load static %} +{% load rowerfilters %} + +{% block title %}Workouts{% endblock %} + +{% block main %} +

Available on {{ integration }}

+ +
    + {% if workouts %} +
  • + Import all NEW +

    This imports all workouts that have not been imported to rowsandall.com. + The action may take a longer time to process, so please be patient. Click on Import in the list below to import an individual workout. +

    +
  • +
  • +

    + + {% if page > 1 %} + + + + {% endif %} + + + + +

    +
  • +
  • +
    + {% csrf_token %} + + Select All New + + + + + + + + + + + + + + + {% for workout in workouts %} + + + + + + + + + + + + {% endfor %} + +
    Import Date/Time Duration Total Distance Type Source Comment New
    + {% if workout|lookup:'new' == 'NEW' and checknew == 'true' %} + + {% else %} + + {% endif %} + {{ workout|lookup:'starttime' }}{{ workout|lookup:'duration' }}{{ workout|lookup:'distance' }}{{ workout|lookup:'rowtype' }}{{ workout|lookup:'source' }}{{ workout|lookup:'comment' }} + {{ workout|lookup:'new' }} +
    +
    +
  • + {% else %} +

    No workouts found

    + {% endif %} +
+{% endblock %} + +{% block sidebar %} +{% include 'menu_workouts.html' %} +{% endblock %} diff --git a/rowers/templatetags/rowerfilters.py b/rowers/templatetags/rowerfilters.py index 40924683..21ca77ad 100644 --- a/rowers/templatetags/rowerfilters.py +++ b/rowers/templatetags/rowerfilters.py @@ -24,8 +24,7 @@ from rowers.models import PlannedSession from rowers.teams import coach_getcoachees from rowers import credits -from rowers import c2stuff -from rowers.c2stuff import c2_open +from rowers.integrations import C2Integration from rowers.rower_rules import * from rowers.mytypes import ( otwtypes, adaptivetypes, sexcategories, weightcategories, workouttypes, @@ -521,12 +520,9 @@ def deltatimeprint(d): # pragma: no cover @register.filter def c2userid(user): # pragma: no cover - try: - thetoken = c2_open(user) - except NoTokenError: # pragma: no cover - return 0 + c2integration = C2Integration(user) - c2userid = c2stuff.get_userid(thetoken) + c2userid = c2integration.get_userid(thetoken) return c2userid diff --git a/rowers/tests/mocks.py b/rowers/tests/mocks.py index 89ac6292..e65d770a 100644 --- a/rowers/tests/mocks.py +++ b/rowers/tests/mocks.py @@ -18,7 +18,6 @@ from django.test import TestCase, Client,override_settings from django.core.management import call_command from django.test.client import RequestFactory -from rowers.views import c2_open from rowers.models import Workout, User, Rower, WorkoutForm,RowerForm,GraphImage from rowers.forms import DocumentsForm,CNsummaryForm,RegistrationFormUniqueEmail import rowers.plots as plots @@ -39,7 +38,7 @@ from nose.tools import assert_true from mock import Mock, patch #from minimocktest import MockTestCase import pandas as pd -import rowers.c2stuff as c2stuff + import arrow from django.http import HttpResponseRedirect diff --git a/rowers/tests/statements.py b/rowers/tests/statements.py index aa4821ea..5945c690 100644 --- a/rowers/tests/statements.py +++ b/rowers/tests/statements.py @@ -37,7 +37,6 @@ from django.core.files.uploadedfile import SimpleUploadedFile from io import StringIO from django.test.client import RequestFactory -from rowers.views import c2_open from rowers.forms import ( diff --git a/rowers/tests/test_imports.py b/rowers/tests/test_imports.py index 42a133c7..46bc63af 100644 --- a/rowers/tests/test_imports.py +++ b/rowers/tests/test_imports.py @@ -14,7 +14,7 @@ import numpy as np import rowers from rowers import dataprep from rowers import tasks -from rowers import c2stuff + from rowers import stravastuff from rowers import polarstuff import urllib @@ -22,7 +22,7 @@ import json import rowers.utils as utils import rowers.rojabo_stuff as rojabo_stuff - +from rowers.integrations import * from django.db import transaction import rowers.garmin_stuff as gs @@ -312,7 +312,9 @@ class C2Objects(DjangoTestCase): ) def test_timezone_c2(self): - data = c2stuff.createc2workoutdata(self.w) + c2integration = C2Integration(self.u) + data = c2integration.createworkoutdata(self.w) + wenddtime = self.w.startdatetime+datetime.timedelta(seconds=self.totaltime) t1 = arrow.get(wenddtime).timestamp() t2 = arrow.get(data['date']).timestamp() @@ -328,35 +330,34 @@ class C2Objects(DjangoTestCase): #self.assertEqual(data['date'],wenddtime.strftime('%Y-%m-%d %H:%M:%S')) - @patch('rowers.c2stuff.Session', side_effect=mocked_requests) + @patch('rowers.integrations.c2.Session', side_effect=mocked_requests) def test_c2_callback(self, mock_Session): response = self.c.get('/call_back?code=dsdoij232s',follow=True) self.assertEqual(response.status_code, 200) - @patch('rowers.c2stuff.Session', side_effect=mocked_requests) + @patch('rowers.integrations.c2.Session', side_effect=mocked_requests) def test_c2_token_refresh(self, mock_Session): response = self.c.get('/rowers/me/c2refresh/',follow=True) self.assertEqual(response.status_code, 200) - @patch('rowers.c2stuff.requests.post', side_effect=mocked_requests) - @patch('rowers.c2stuff.requests.get', side_effect=mocked_requests) - @patch('rowers.c2stuff.requests.session', side_effect=mocked_requests) + @patch('rowers.integrations.c2.requests.post', side_effect=mocked_requests) + @patch('rowers.integrations.c2.requests.get', side_effect=mocked_requests) + @patch('rowers.integrations.c2.requests.session', side_effect=mocked_requests) def test_c2_auto_import(self, mock_get, mock_post,MockSession): self.r.sporttracks_auto_export = True self.r.save() - res = c2stuff.get_c2_workouts(self.r) - self.assertEqual(res,1) + c2integration = C2Integration(self.u) + res = c2integration.get_workouts() - res = c2stuff.get_c2_workouts(self.r,do_async=False) self.assertEqual(res,1) - @patch('rowers.c2stuff.requests.post', side_effect=mocked_requests) - @patch('rowers.c2stuff.requests.get', side_effect=mocked_requests) + @patch('rowers.integrations.c2.requests.post', side_effect=mocked_requests) + @patch('rowers.integrations.c2.requests.get', side_effect=mocked_requests) def test_c2_upload(self, mock_get, mock_post): response = self.c.get('/rowers/workout/'+encoded1+'/c2uploadw/') @@ -367,14 +368,14 @@ class C2Objects(DjangoTestCase): self.assertEqual(response.url, '/rowers/workout/'+encoded1+'/edit/') self.assertEqual(response.status_code, 302) - @patch('rowers.c2stuff.requests.post', side_effect=mocked_requests) - @patch('rowers.c2stuff.requests.get', side_effect=mocked_requests) + @patch('rowers.integrations.c2.requests.post', side_effect=mocked_requests) + @patch('rowers.integrations.c2.requests.get', side_effect=mocked_requests) def test_c2_list(self, mock_get, mock_post): response = self.c.get('/rowers/workout/c2list',follow=True) self.assertEqual(response.status_code,200) - @patch('rowers.c2stuff.requests.get', side_effect=mocked_requests) + @patch('rowers.integrations.c2.requests.get', side_effect=mocked_requests) @patch('rowers.dataprep.create_engine') def test_c2_import(self, mock_get, mocked_sqlalchemy): @@ -506,7 +507,7 @@ class C2Objects(DjangoTestCase): self.assertEqual(got, want) - @patch('rowers.c2stuff.requests.get', side_effect=mocked_requests) + @patch('rowers.integrations.c2.requests.get', side_effect=mocked_requests) @patch('rowers.dataprep.create_engine') @patch('rowers.tasks.requests.session', side_effect=mocked_session) def test_c2_import_tz(self, mock_get, mocked_sqlalchemy, mock_session): @@ -526,7 +527,7 @@ class C2Objects(DjangoTestCase): self.assertEqual(timezone,'Europe/Prague') - @patch('rowers.c2stuff.requests.get', side_effect=mocked_requests) + @patch('rowers.integrations.c2.requests.get', side_effect=mocked_requests) @patch('rowers.dataprep.create_engine') def test_c2_import_tz3(self, mock_get, mocked_sqlalchemy): @@ -547,7 +548,7 @@ class C2Objects(DjangoTestCase): @patch('rowers.tasks.requests.get', side_effect=mocked_requests) @patch('rowers.dataprep.create_engine') - @patch('rowers.c2stuff.requests.session', side_effect=mocked_requests) + @patch('rowers.integrations.c2.requests.session', side_effect=mocked_requests) def test_c2_import_tz2(self, mock_get, mocked_sqlalchemy, MockSession): response = self.c.get('/rowers/workout/c2import/31/',follow=True) @@ -643,15 +644,15 @@ class C2ObjectsTokenExpired(DjangoTestCase): - @patch('rowers.c2stuff.requests.post', side_effect=mocked_requests) - @patch('rowers.c2stuff.requests.get', side_effect=mocked_requests) - @patch('rowers.c2stuff.Session',side_effect=mocked_requests) + @patch('rowers.integrations.c2.requests.post', side_effect=mocked_requests) + @patch('rowers.integrations.c2.requests.get', side_effect=mocked_requests) + @patch('rowers.integrations.c2.Session',side_effect=mocked_requests) def test_c2_list(self, mock_get, mock_post, mock_Session): response = self.c.get('/rowers/workout/c2list',follow=True) self.assertEqual(response.status_code,200) - @patch('rowers.c2stuff.requests.get', side_effect=mocked_requests) + @patch('rowers.integrations.c2.requests.get', side_effect=mocked_requests) @patch('rowers.dataprep.create_engine') def test_c2_import(self, mock_get, mocked_sqlalchemy): diff --git a/rowers/tests/test_permissions2.py b/rowers/tests/test_permissions2.py index 1bd5c3d0..8baa8c28 100644 --- a/rowers/tests/test_permissions2.py +++ b/rowers/tests/test_permissions2.py @@ -214,8 +214,8 @@ class PermissionsViewTests(TestCase): # Test access for anonymous users @parameterized.expand(viewstotest) - @patch('rowers.c2stuff.Session', side_effect=mocked_requests) - @patch('rowers.c2stuff.c2_open') + @patch('rowers.integrations.c2.Session', side_effect=mocked_requests) + @patch('rowers.integrations.c2.C2Integration.open') @patch('rowers.dataprep.create_engine') @patch('rowers.dataprep.read_df_sql') @patch('rowers.dataprep.getsmallrowdata_db') @@ -249,8 +249,8 @@ class PermissionsViewTests(TestCase): # Test access for logged in users - accessing own objects @parameterized.expand(viewstotest) - @patch('rowers.c2stuff.Session', side_effect=mocked_requests) - @patch('rowers.c2stuff.c2_open') + @patch('rowers.integrations.c2.Session', side_effect=mocked_requests) + @patch('rowers.integrations.c2.C2Integration.open') @patch('rowers.dataprep.create_engine') @patch('rowers.dataprep.read_df_sql') @patch('rowers.dataprep.getsmallrowdata_db') @@ -331,8 +331,8 @@ class PermissionsViewTests(TestCase): # Test access for logged in users - accessing team member objects @parameterized.expand(viewstotest) - @patch('rowers.c2stuff.Session', side_effect=mocked_requests) - @patch('rowers.c2stuff.c2_open') + @patch('rowers.integrations.c2.Session', side_effect=mocked_requests) + @patch('rowers.integrations.c2.C2Integration.open') @patch('rowers.dataprep.create_engine') @patch('rowers.dataprep.read_df_sql') @patch('rowers.dataprep.getsmallrowdata_db') @@ -414,8 +414,8 @@ class PermissionsViewTests(TestCase): # Test access for logged in users - accessing coachee @parameterized.expand(viewstotest) - @patch('rowers.c2stuff.Session', side_effect=mocked_requests) - @patch('rowers.c2stuff.c2_open') + @patch('rowers.integrations.c2.Session', side_effect=mocked_requests) + @patch('rowers.integrations.c2.C2Integration.open') @patch('rowers.dataprep.create_engine') @patch('rowers.dataprep.read_df_sql') @patch('rowers.dataprep.getsmallrowdata_db') diff --git a/rowers/tests/test_unit_tests.py b/rowers/tests/test_unit_tests.py index 854185df..aad12551 100644 --- a/rowers/tests/test_unit_tests.py +++ b/rowers/tests/test_unit_tests.py @@ -17,12 +17,13 @@ import pytz # interactive plots from rowers import interactiveplots from rowers import dataprep - +from rowers import tasks from rowers import plannedsessions from rowers.views.workoutviews import get_video_id from rowers import stravastuff import rowingdata +from rowers.c2stuff import getagegrouprecord class OtherUnitTests(TestCase): def setUp(self): @@ -120,7 +121,7 @@ class OtherUnitTests(TestCase): s = f.read() data = json.loads(s) splitdata = data['workout']['intervals'] - summary = c2stuff.summaryfromsplitdata(splitdata,data,'aap.txt') + summary = tasks.summaryfromsplitdata(splitdata,data,'aap.txt') self.assertEqual(len(summary),3) sums = summary[0] @@ -592,7 +593,7 @@ class DataPrepTests(TestCase): def test_getagegrouprecord(self): records = C2WorldClassAgePerformance.objects.filter(distance=2000,sex=self.r.sex,weightcategory=self.r.weightcategory) - result = c2stuff.getagegrouprecord(25) + result = getagegrouprecord(25) self.assertEqual(int(result),590) @patch('rowers.dataprep.getsmallrowdata_db',side_effect=mocked_getsmallrowdata_uh) diff --git a/rowers/uploads.py b/rowers/uploads.py index eff59345..bbf63aab 100644 --- a/rowers/uploads.py +++ b/rowers/uploads.py @@ -1,9 +1,10 @@ from rowers.mytypes import workouttypes, boattypes, otwtypes, workoutsources, workouttypes_ordered -import rowers.c2stuff as c2stuff + from rowers.rower_rules import is_promember import rowers.tpstuff as tpstuff import rowers.sporttracksstuff as sporttracksstuff import rowers.stravastuff as stravastuff +from rowers.integrations import * from rowers.utils import ( geo_distance, serialize_list, deserialize_list, uniqify, str2bool, range_to_color_hex, absolute, myqueue, NoTokenError @@ -199,9 +200,9 @@ def do_sync(w, options, quick=False): if do_c2_export: # pragma: no cover dologging('c2_log.log','Exporting workout to C2 for user {user}'.format(user=w.user.user.id)) + c2_integration = C2Integration(w.user.user) try: - message, id = c2stuff.workout_c2_upload( - w.user.user, w, asynchron=True) + id = c2_integration.workout_export(w) dologging('c2_log.log','C2 upload succeeded') except NoTokenError: id = 0 diff --git a/rowers/urls.py b/rowers/urls.py index c9760f9a..ea9448b3 100644 --- a/rowers/urls.py +++ b/rowers/urls.py @@ -1169,8 +1169,3 @@ urlpatterns = [ name="braintree_webhook_view"), ] -if settings.DEBUG: # pragma: no cover - urlpatterns += [ - re_path(r'^c2listug/(?P\d+)/$', views.c2listdebug_view), - re_path(r'^c2listug/$', views.c2listdebug_view), - ] diff --git a/rowers/views/importviews.py b/rowers/views/importviews.py index 6d7b2a65..6943ee1d 100644 --- a/rowers/views/importviews.py +++ b/rowers/views/importviews.py @@ -7,6 +7,8 @@ from rowers.views.statements import * from rowers.plannedsessions import get_dates_timeperiod from rowers.tasks import fetch_strava_workout +from rowers.integrations.c2 import C2Integration + import numpy @@ -109,9 +111,10 @@ def workout_c2_upload_view(request, id=0): ) dologging('c2_log.log', s) + c2_integration = C2Integration(request.user) + try: - message, c2id = c2stuff.workout_c2_upload( - request.user, w, asynchron=True) + c2id = c2_integration.workout_export(w) except NoTokenError: # pragma: no cover return HttpResponseRedirect("/rowers/me/c2authorize/") @@ -187,18 +190,9 @@ def rower_nk_authorize(request): # pragma: no cover # Concept2 authorization @login_required() def rower_c2_authorize(request): # pragma: no cover - # Generate a random string for the state parameter - # Save it for use later to prevent xsrf attacks - - # state = str(uuid4()) - scope = "user:read,results:write" - params = {"client_id": C2_CLIENT_ID, - "response_type": "code", - "redirect_uri": C2_REDIRECT_URI} - url = "http://log.concept2.com/oauth/authorize?" + \ - urllib.parse.urlencode(params) - url += "&scope="+scope - + c2_integration = C2Integration(request.user) + url = c2_integration.make_authorization_url() + return HttpResponseRedirect(url) # Garmin authorization @@ -312,26 +306,12 @@ def rower_tp_authorize(request): # pragma: no cover # Concept2 token refresh. URL for manual refresh. Not visible to users @login_required() def rower_c2_token_refresh(request): - r = getrower(request.user) - res = c2stuff.do_refresh_token(r.c2refreshtoken) - - if res[0] is not None: - access_token = res[0] - expires_in = res[1] - refresh_token = res[2] - expirydatetime = timezone.now()+datetime.timedelta(seconds=expires_in) - r = getrower(request.user) - r.c2token = access_token - r.tokenexpirydate = expirydatetime - r.c2refreshtoken = refresh_token - - r.save() - - successmessage = "Tokens refreshed. Good to go" - messages.info(request, successmessage) - else: # pragma: no cover - message = "Something went wrong (refreshing tokens). Please reauthorize:" - messages.error(request, message) + c2_integration = C2Integration(request.user) + try: + token = c2_integration.token_refresh() + messages.info(request, "Tokens refreshed. Good to go") + except NoTokenError: + messages.error(request, "Something went wrong refreshing C2 tokens. Please reauthorize") url = reverse('workouts_view') @@ -395,10 +375,11 @@ def rower_sporttracks_token_refresh(request): # Concept2 Callback @login_required() def rower_process_callback(request): + c2_integration = C2Integration(request.user) try: code = request.GET['code'] - res = c2stuff.get_token(code) - except MultiValueDictKeyError: # pragma: no cover + res = c2_integration.get_token(code) + except (MultiValueDictKeyError, NoTokenError): # pragma: no cover message = "The resource owner or authorization server denied the request" messages.error(request, message) @@ -1677,61 +1658,18 @@ def workout_sporttracksimport_view(request, message="", userid=0): # List of workouts on Concept2 logbook. This view only used for debugging -@login_required() -def c2listdebug_view(request, page=1, message=""): # pragma: no cover - try: - _ = c2_open(request.user) - except NoTokenError: # pragma: no cover - return HttpResponseRedirect("/rowers/me/c2authorize/") - - r = getrower(request.user) - - res = c2stuff.get_c2_workout_list(request.user, page=page) - - if (res.status_code != 200): - message = "Something went wrong in workout_c2import_view (C2 token renewal)" - messages.error(request, message) - if settings.DEBUG: - return HttpResponse(res) - else: - url = reverse('workouts_view') - return HttpResponseRedirect(url) - else: - workouts = [] - - for item in res.json()['data']: - d = item['distance'] - i = item['id'] - ttot = item['time_formatted'] - s = item['date'] - r = item['type'] - s2 = item['source'] - c = item['comments'] - keys = ['id', 'distance', 'duration', - 'starttime', 'rowtype', 'source', 'comment'] - values = [i, d, ttot, s, r, s2, c] - res = dict(zip(keys, values)) - workouts.append(res) - - return render(request, - 'c2_list_import2.html', - {'workouts': workouts, - 'teams': get_my_teams(request.user), - }) # Import all unknown workouts available on Concept2 logbook - - @login_required() def workout_getc2workout_all(request, page=1, message=""): # pragma: no cover + r = getrequestrower(request) + c2_integration = C2Integration(request.user) try: - _ = c2_open(request.user) + _ = c2_integration.open() except NoTokenError: # pragma: no cover return HttpResponseRedirect("/rowers/me/c2authorize/") - r = getrequestrower(request) - - result = c2stuff.get_c2_workouts(r, page=page, do_async=True) + result = c2_integration.get_workouts(page=page) if result: messages.info( @@ -1777,76 +1715,23 @@ def workout_c2import_view(request, page=1, userid=0, message=""): 'userid': request.user.id}) return HttpResponseRedirect(url) + c2_integration = C2Integration(request.user) try: - _ = c2_open(request.user) + _ = c2_integration.open() except NoTokenError: # pragma: no cover return HttpResponseRedirect("/rowers/me/c2authorize/") - res = c2stuff.get_c2_workout_list(request.user, page=page) + workouts = c2_integration.get_workout_list(page=1) - if (res.status_code != 200): # pragma: no cover - message = "Something went wrong in workout_c2import_view (C2 token refresh)" - messages.error(request, message) - url = reverse('workouts_view') - return HttpResponseRedirect(url) - - workouts = [] - c2ids = [item['id'] for item in res.json()['data']] - knownc2ids = uniqify([ - w.uploadedtoc2 for w in Workout.objects.filter(user=rower) - ]) - tombstones = [ - t.uploadedtoc2 for t in TombStone.objects.filter(user=rower) - ] - parkedids = [] - try: - with open('c2blocked.json', 'r') as c2blocked: - jsondata = json.load(c2blocked) - parkedids = jsondata['ids'] - except: # pragma: no cover - pass - - knownc2ids = uniqify(knownc2ids+tombstones+parkedids) - for item in res.json()['data']: - d = item['distance'] - i = item['id'] - ttot = item['time_formatted'] - s = item['date'] - r = item['type'] - s2 = item['source'] - c = item['comments'] - if i in knownc2ids: - nnn = '' - else: # pragma: no cover - nnn = 'NEW' - keys = ['id', 'distance', 'duration', 'starttime', - 'rowtype', 'source', 'comment', 'new'] - values = [i, d, ttot, s, r, s2, c, nnn] - ress = dict(zip(keys, values)) - workouts.append(ress) if request.method == "POST": try: # pragma: no cover tdict = dict(request.POST.lists()) ids = tdict['workoutid'] c2ids = [int(id) for id in ids] - alldata = {} - for item in res.json()['data']: - alldata[item['id']] = item - counter = 0 for c2id in c2ids: - _ = myqueue( - queue, - handle_c2_async_workout, - alldata, - rower.user.id, - rower.c2token, - c2id, - counter, - rower.defaulttimezone - ) - counter = counter+1 + c2_integration.get_workout(c2id) # done, redirect to workouts list messages.info( request, @@ -1877,7 +1762,7 @@ def workout_c2import_view(request, page=1, userid=0, message=""): checknew = request.GET.get('selectallnew', False) return render(request, - 'c2_list_import2.html', + 'list_import.html', {'workouts': workouts, 'rower': rower, 'active': 'nav-workouts', @@ -1885,6 +1770,7 @@ def workout_c2import_view(request, page=1, userid=0, message=""): 'teams': get_my_teams(request.user), 'page': page, 'checknew': checknew, + 'integration': 'C2 Logbook' }) @@ -1909,7 +1795,7 @@ importauthorizeviews = { } importsources = { - 'c2': c2stuff, + 'c2': C2Integration, 'strava': stravastuff, 'polar': polarstuff, 'ownapi': ownapistuff, @@ -1953,19 +1839,18 @@ def workout_getimportview(request, externalid, source='c2', do_async=True): startdate = request.session.get('startdate') enddate = request.session.get('enddate') + integration = importsources[source](request.user) + try: - result = importsources[source].get_workout(request.user, externalid, do_async=do_async, - startdate=startdate, enddate=enddate) + result = integration.get_workout(externalid, startdate=startdate, enddate=enddate) except NoTokenError: return HttpResponseRedirect(reverse(importauthorizeviews[source])) url = reverse(importlistviews[source]) return HttpResponseRedirect(url) try: - result = importsources[source].get_workout(request.user, externalid, - do_async=do_async) + result = integration.get_workout(externalid) except NoTokenError: - return HttpResponseRedirect(reverse(importauthorizeviews[source])) if result: # pragma: no cover diff --git a/rowers/views/statements.py b/rowers/views/statements.py index 8770ed20..118fc2c5 100644 --- a/rowers/views/statements.py +++ b/rowers/views/statements.py @@ -1,5 +1,6 @@ import rowers.teams as teams from rowers.serializers import RowerSerializer, WorkoutSerializer +from rowers.integrations import integrations from rq import Queue, cancel_job from redis import StrictRedis, Redis from rowers.models import C2WorldClassAgePerformance @@ -191,10 +192,8 @@ import os import sys import datetime import iso8601 -import rowers.c2stuff as c2stuff import rowers.nkstuff as nkstuff import rowers.rojabo_stuff as rojabo_stuff -from rowers.c2stuff import c2_open from rowers.nkstuff import nk_open from rowers.rp3stuff import rp3_open from rowers.sporttracksstuff import sporttracks_open @@ -208,6 +207,7 @@ from rowers.rojabo_stuff import rojabo_open import rowers.polarstuff as polarstuff import rowers.sporttracksstuff as sporttracksstuff +from rowers.integrations import * import rowers.tpstuff as tpstuff import rowers.rp3stuff as rp3stuff diff --git a/rowers/views/workoutviews.py b/rowers/views/workoutviews.py index d5df4565..1e7cf862 100644 --- a/rowers/views/workoutviews.py +++ b/rowers/views/workoutviews.py @@ -5630,7 +5630,8 @@ def workout_upload_view(request, # upload to C2 if (upload_to_c2): # pragma: no cover try: - message, id = c2stuff.workout_c2_upload(request.user, w) + c2integration = C2Integration(request.user) + id = c2integration.workout_export(w) except NoTokenError: id = 0 message = "Something went wrong with the Concept2 sync"