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..f68aed3e --- /dev/null +++ b/rowers/integrations/__init__.py @@ -0,0 +1,19 @@ +from .c2 import C2Integration +from .strava import StravaIntegration +from .nk import NKIntegration +from .sporttracks import SportTracksIntegration +from .rp3 import RP3Integration +from .trainingpeaks import TPIntegration +from .polar import PolarIntegration + +importsources = { + 'c2': C2Integration, + 'strava': StravaIntegration, + 'sporttracks': SportTracksIntegration, + 'trainingpeaks': TPIntegration, + 'nk': NKIntegration, + 'tp':TPIntegration, + 'rp3':RP3Integration, + 'polar': PolarIntegration +} + diff --git a/rowers/integrations/c2.py b/rowers/integrations/c2.py new file mode 100644 index 00000000..60a066e3 --- /dev/null +++ b/rowers/integrations/c2.py @@ -0,0 +1,453 @@ +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, custom_exception_handler +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__(*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_name(self): + return "Concept2 Logbook" + + def get_shortname(self): + return "c2" + + + + 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).token_refresh(*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) + + 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', 'name', 'new'] + values = [i, d, ttot, s, r, s2, c, nnn] + ress = dict(zip(keys, values)) + workouts.append(ress) + + return workouts + + + + + + diff --git a/rowers/integrations/integrations.py b/rowers/integrations/integrations.py new file mode 100644 index 00000000..df11b674 --- /dev/null +++ b/rowers/integrations/integrations.py @@ -0,0 +1,263 @@ +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 + +import json + +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[0] + 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 get_name(self): + raise NotImplementedError + + @abstractmethod + def get_shortname(self): + raise NotImplementedError + + @abstractmethod + def createworkoutdata(self, w, *args, **kwargs): + return None + + @abstractmethod + def workout_export(self, workout, *args, **kwargs) -> str: + pass + + @abstractmethod + def get_workouts(self, *args, **kwargs) -> int: + pass + + @abstractmethod + def get_workout(self, id) -> int: + return 0 + + # need to unify workout list + @abstractmethod + def get_workout_list(self, *args, **kwargs) -> list: + return [] + + @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 = self.oauth_data['authorization_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 + 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") + + 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): + 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'], + "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, self.oauth_data['refreshtokenname']) + + if not refreshtoken: + 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, 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() + + return access_token + + diff --git a/rowers/integrations/nk.py b/rowers/integrations/nk.py new file mode 100644 index 00000000..987b8972 --- /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 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__(*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 get_name(self): + return "NK Logbook" + + def get_shortname(self): + return "nk" + + def createworkoutdata(self, w, *args, **kwargs): + return None + + def workout_export(self, workout, *args, **kwargs) -> str: + return "" # there is no export + + def get_workouts(self, *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 workouts: + 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 + 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/rp3.py b/rowers/integrations/rp3.py new file mode 100644 index 00000000..02c8f8d0 --- /dev/null +++ b/rowers/integrations/rp3.py @@ -0,0 +1,259 @@ +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 get_name(self): + return "RP3 Logbook" + + def get_shortname(self): + return "rp3" + + 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',inplace=True) + except (KeyError, IndexError): + return 0 + + knownrp3ids = uniqify([ + w.uploadedtorp3 for w in Workout.objects.filter(user=r) + ]) + + 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_iso8601'] + 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(): + 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 + 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 = { + "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) + + + diff --git a/rowers/integrations/sporttracks.py b/rowers/integrations/sporttracks.py new file mode 100644 index 00000000..199f95a9 --- /dev/null +++ b/rowers/integrations/sporttracks.py @@ -0,0 +1,325 @@ +from .integrations import SyncIntegration, NoTokenError +from rowers.models import User, Rower, Workout, TombStone + +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 ( + SPORTTRACKS_CLIENT_SECRET, SPORTTRACKS_CLIENT_ID, + SPORTTRACKS_REDIRECT_URI +) + +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, 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): + super(SportTracksIntegration, self).__init__(*args, **kwargs) + + self.oauth_data = { + 'client_id': SPORTTRACKS_CLIENT_ID, + 'client_secret': SPORTTRACKS_CLIENT_SECRET, + 'redirect_uri': SPORTTRACKS_REDIRECT_URI, + 'authorization_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 get_name(self): + return "SportTracks" + + def get_shortname(self): + return "sporttracks" + + def open(self, *args, **kwargs) -> str: + return super(SportTracksIntegration, self).open(*args, **kwargs) + + def createworkoutdata(self, w, *args, **kwargs): + 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: + 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: + 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 + + + + def get_workout(self, id) -> int: + _ = self.open() + + r = self.rower + + + job = myqueue( + queue, + handle_sporttracks_workout_from_data, + self.user, + id, + 'sporttracks', + 'sporttracks' + ) + + return job.id + + def get_workout_list_json(self, *args, **kwargs) -> dict: + _ = 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) + + 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 workouts_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 super(SportTracksIntegration, self).get_token(code, *args, **kwargs) + + + + def token_refresh(self, *args, **kwargs) -> str: + return super(SportTracksIntegration, self).token_refresh(*args, **kwargs) + + diff --git a/rowers/integrations/strava.py b/rowers/integrations/strava.py new file mode 100644 index 00000000..6344fd1c --- /dev/null +++ b/rowers/integrations/strava.py @@ -0,0 +1,346 @@ +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__(*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 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) + 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}_{id}.csv'.format( + code=uuid4().hex[:16], id=id) + job = myqueue(queue, + fetch_strava_workout, + self.rower.stravatoken, + self.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, self).token_refresh(*args, **kwargs) + + 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) + 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 + + + diff --git a/rowers/integrations/trainingpeaks.py b/rowers/integrations/trainingpeaks.py new file mode 100644 index 00000000..c816ee6a --- /dev/null +++ b/rowers/integrations/trainingpeaks.py @@ -0,0 +1,149 @@ +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 get_name(self): + return "TrainingPeaks" + + def get_shortname(self): + return "trainingpeaks" + + 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 + 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) + 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/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 33f3427a..593a4e9c 100644 --- a/rowers/management/commands/processemail.py +++ b/rowers/management/commands/processemail.py @@ -27,13 +27,8 @@ 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 @@ -79,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) @@ -91,7 +87,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) @@ -101,7 +99,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) @@ -112,7 +111,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/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/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/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 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/sporttracksstuff.py b/rowers/sporttracksstuff.py deleted file mode 100644 index 4b4f2dd8..00000000 --- a/rowers/sporttracksstuff.py +++ /dev/null @@ -1,510 +0,0 @@ -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 ( - 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, message = add_workout_from_data( - user, - sporttracksid, data, - strokedata, - source='sporttracks', - workoutsource='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 - - -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/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/tasks.py b/rowers/tasks.py index 5c318741..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] @@ -447,6 +521,189 @@ 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, 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 + 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): @@ -2975,6 +3232,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 = """{ @@ -3056,6 +3316,7 @@ def handle_rp3_async_workout(userid, rp3token, rp3id, startdatetime, max_attempt 'boattype': '1x', 'rp3id': int(rp3id), 'startdatetime': startdatetime, + 'timezone': timezone, } session = requests.session() @@ -3086,7 +3347,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/templates/list_import.html b/rowers/templates/list_import.html new file mode 100644 index 00000000..fc4373a7 --- /dev/null +++ b/rowers/templates/list_import.html @@ -0,0 +1,95 @@ +{% extends "newbase.html" %} +{% load static %} +{% load rowerfilters %} + +{% block title %}Workouts{% endblock %} + +{% block main %} +

Available on {{ integration }}

+ +
    +
  • +
    + + {{ dateform.as_table }} +
    + {% csrf_token %} + +
    +
  • + {% 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. +

    +
  • +
  • +

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

    +
  • + {% endif %} +
  • +
    + {% csrf_token %} + + Select All New + + + + + + + + + + + + + + + {% for workout in workouts %} + + + + + + + + + + + + {% endfor %} + +
    Import Date/Time Duration Total Distance Type Source Name 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:'name' }} + {{ workout|lookup:'new' }} +
    +
    +
  • + {% else %} +

    No workouts found

    + {% endif %} +
+{% endblock %} + +{% block sidebar %} +{% include 'menu_workouts.html' %} +{% endblock %} 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/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/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
    • NK Logbook
    • Strava
    • SportTracks
    • diff --git a/rowers/templatetags/rowerfilters.py b/rowers/templatetags/rowerfilters.py index 40924683..6fc2fee0 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(user) return c2userid @@ -570,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/mocks.py b/rowers/tests/mocks.py index 89ac6292..64f9d64e 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 @@ -1367,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/statements.py b/rowers/tests/statements.py index aa4821ea..7a18725d 100644 --- a/rowers/tests/statements.py +++ b/rowers/tests/statements.py @@ -37,8 +37,7 @@ 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.integrations import * from rowers.forms import ( DocumentsForm,CNsummaryForm,RegistrationFormUniqueEmail, @@ -65,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_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_async_tasks.py b/rowers/tests/test_async_tasks.py index 66aa36dd..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): @@ -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) @@ -452,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, @@ -462,15 +463,17 @@ 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)) + res = tasks.handle_sporttracks_sync(1,url,headers,json.dumps(data,default=stdefault)) 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/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_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/test_imports.py b/rowers/tests/test_imports.py index 42a133c7..e4baa269 100644 --- a/rowers/tests/test_imports.py +++ b/rowers/tests/test_imports.py @@ -14,18 +14,17 @@ 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 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 +import rowers.integrations.strava as strava @pytest.mark.django_db @override_settings(TESTING=True) @@ -312,7 +311,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,38 +329,37 @@ 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/') - + 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) @@ -367,20 +367,20 @@ 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) + response = self.c.get('/rowers/workout/c2import',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): 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, @@ -506,13 +506,13 @@ 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): 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, @@ -526,12 +526,12 @@ 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): 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, @@ -547,11 +547,11 @@ 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) - 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) @@ -643,15 +643,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) + response = self.c.get('/rowers/workout/c2import/',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): @@ -715,10 +715,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/') @@ -741,39 +743,39 @@ 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) + 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.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') + expected_url = reverse('workout_import_view',kwargs={'source':'nk'}) self.assertRedirects(response, expected_url=expected_url, @@ -785,18 +787,19 @@ 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') + expected_url = reverse('workout_import_view',kwargs={'source':'nk'}) self.assertRedirects(response, expected_url=expected_url, @@ -871,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) @@ -963,15 +967,15 @@ 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): 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, @@ -994,7 +998,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) @@ -1058,14 +1062,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) @@ -1116,21 +1120,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, @@ -1141,23 +1142,24 @@ 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): 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, @@ -1165,16 +1167,12 @@ class StravaObjects(DjangoTestCase): self.assertEqual(response.status_code, 200) - @patch('rowers.stravastuff.requests.post', side_effect=mocked_requests) - def test_strava_callback(self, mock_post): + @patch('rowers.integrations.integrations.requests.post', side_effect=mocked_requests) + @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) - @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 @@ -1234,7 +1232,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) @@ -1242,15 +1240,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/') @@ -1261,7 +1259,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) @@ -1294,36 +1292,10 @@ 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) - @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 @@ -1391,15 +1363,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/test_permissions2.py b/rowers/tests/test_permissions2.py index 1bd5c3d0..52d495aa 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') @@ -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 854185df..a4984a1a 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/tests/testdata/testdata.tcx.gz b/rowers/tests/testdata/testdata.tcx.gz new file mode 100644 index 00000000..5888a36b Binary files /dev/null and b/rowers/tests/testdata/testdata.tcx.gz differ diff --git a/rowers/tests/viewnames.csv b/rowers/tests/viewnames.csv index 34c2b7cd..9fa22e20 100644 --- a/rowers/tests/viewnames.csv +++ b/rowers/tests/viewnames.csv @@ -84,15 +84,9 @@ 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, -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, -104,127,workout_sporttracksimport_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, -105,129,workout_getsporttracksworkout_all,gets all C2 workouts (now redirects due to NoTokenError,TRUE,302,basic,302,302,FALSE,302,302,FALSE,302,302,FALSE,FALSE,FALSE,TRUE,TRUE, -106,130,workout_polarimport_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, -109,135,workout_c2_upload_view,uploads workout to C2,TRUE,200,basic,200,302,basic,200,302,coach,200,302,FALSE,FALSE,TRUE,FALSE,FALSE, -110,136,workout_strava_upload_view,uploads workout to C2,TRUE,200,basic,200,302,basic,200,302,coach,200,302,FALSE,FALSE,TRUE,FALSE,FALSE, +104,127,workout_import_view,list workouts to be imported (test stops at notokenerror),TRUE,200,basic,200,302,basic,403,403,coach,403,403,FALSE,FALSE,FALSE,FALSE,FALSE, +109,135,workout_export_view,upload workout to integration,TRUE,200,basic,200,302,basic,200,302,coach,200,302,FALSE,FALSE,TRUE,FALSE,FALSE, 111,137,workout_recalcsummary_view,recalculates workout summary,TRUE,403,basic,302,403,basic,403,403,coach,302,403,FALSE,FALSE,TRUE,TRUE,TRUE, 112,138,workout_sporttracks_upload_view,uploads workout to C2,TRUE,200,basic,200,302,basic,200,302,coach,200,302,FALSE,FALSE,TRUE,FALSE,FALSE, 115,141,workout_tp_upload_view,uploads workout to C2,TRUE,200,basic,200,302,basic,200,302,coach,200,302,FALSE,FALSE,TRUE,FALSE,FALSE, 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/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/uploads.py b/rowers/uploads.py index eff59345..606d5a9e 100644 --- a/rowers/uploads.py +++ b/rowers/uploads.py @@ -1,9 +1,8 @@ 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 @@ -130,7 +129,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 @@ -199,9 +197,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 @@ -212,10 +210,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( @@ -236,6 +233,12 @@ def do_sync(w, options, quick=False): f.write(str(e)) do_st_export = w.user.sporttracks_auto_export + + sporttracksid = options.get('sporttracksid','') + if sporttracksid != 0 and sporttracksid != '': + w.uploadedtosporttracks = 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 @@ -244,9 +247,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, @@ -266,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/urls.py b/rowers/urls.py index c9760f9a..6baf4b8d 100644 --- a/rowers/urls.py +++ b/rowers/urls.py @@ -618,74 +618,16 @@ 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/(?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, - 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/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+)/$', - 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)/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[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'), - 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'), @@ -795,32 +737,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+)/$', @@ -1169,8 +1097,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 b7f6f3e9..06275ff1 100644 --- a/rowers/views/importviews.py +++ b/rowers/views/importviews.py @@ -7,6 +7,11 @@ from rowers.views.statements import * from rowers.plannedsessions import get_dates_timeperiod 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 @@ -15,108 +20,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 = "" +def workout_export_view(request, id=0, source='c2'): r = getrower(request.user) - res = -1 - try: - _ = tp_open(r.user) - except NoTokenError: # pragma: no cover - return HttpResponseRedirect("/rowers/me/tpauthorize/") - - # ready to upload. Hurray w = get_workout_by_opaqueid(request, id) - r = w.user + integration = importsources[source](request.user) + try: + id = integration.workout_export(w) + except NoTokenError: + return HttpResponseRedirect(integration.make_authorization_url()) - tcxfile = tpstuff.createtpworkoutdata(w) - if tcxfile: - res, reason, status_code, headers = tpstuff.uploadactivity( - r.tptoken, tcxfile, - name=w.name + messages.info( + request, + "Your workout will be synchronized to {name} in the background".format( + name=integration.get_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 - - 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={ - '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): - r = getrower(request.user) - w = get_workout_by_opaqueid(request, id) - result = -1 - comment, result = stravastuff.workout_strava_upload( - r.user, w, asynchron=True) - 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 ) - dologging('c2_log.log', s) - - try: - message, c2id = c2stuff.workout_c2_upload( - request.user, w, asynchron=True) - 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={ @@ -126,38 +44,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): - message = "" - # ready to upload. Hurray - w = get_workout(id) - r = w.user - - message, res = sporttracksstuff.workout_sporttracks_upload( - r.user, w, asynchron=True) - - 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, } @@ -165,44 +57,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 - 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) - +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 - # 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 - - return HttpResponseRedirect(url) - -# Garmin authorization - @login_required() def rower_garmin_authorize(request): # pragma: no cover @@ -211,181 +72,29 @@ 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 - # 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) - - return HttpResponseRedirect(url) # 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) - -# 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 - - 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) - - 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): - 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) - - 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) - 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() - - 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): - 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) +def rower_integration_token_refresh(request, source='c2'): + 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") + except NoTokenError: + messages.error(request, + "Something went wrong refreshing your access tokens to {name}. Please reauthorize".format( + name=integration.get_name() + )) url = reverse('workouts_view') @@ -395,10 +104,11 @@ def rower_sporttracks_token_refresh(request): # Concept2 Callback @login_required() def rower_process_callback(request): + c2_integration = importsources['c2'](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) @@ -443,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) @@ -465,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 @@ -475,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 @@ -567,7 +294,8 @@ def rower_process_nkcallback(request): # pragma: no cover # do stuff try: code = request.GET.get('code', None) - res = nkstuff.get_token(code) + 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" messages.error(request, message) @@ -575,7 +303,6 @@ def rower_process_nkcallback(request): # pragma: no cover url = reverse('rower_exportsettings_view') return HttpResponseRedirect(url) - access_token = res[0] if access_token == 0: message = res[1] message += ' Contact support@rowsandall.com if this behavior persists' @@ -585,16 +312,14 @@ def rower_process_nkcallback(request): # pragma: no cover return HttpResponseRedirect(url) - expires_in = res[1] - refresh_token = res[2] - nk_owner_id = res[3] + # nk_owner_id = res[3] expirydatetime = timezone.now()+datetime.timedelta(seconds=expires_in) r = getrower(request.user) r.nktoken = access_token r.nktokenexpirydate = expirydatetime r.nkrefreshtoken = refresh_token - r.nk_owner_id = nk_owner_id + # r.nk_owner_id = nk_owner_id r.save() @@ -604,60 +329,27 @@ def rower_process_nkcallback(request): # pragma: no cover return HttpResponseRedirect(url) -@login_required() -def workout_getnkworkout_all(request, startdatestring='', enddatestring=''): - startdate, enddate = get_dates_timeperiod( - request, startdatestring=startdatestring, enddatestring=enddatestring) - startdate = startdate.date() - enddate = enddate.date() - - before = arrow.get(enddate) - before = str(int(before.timestamp()*1000)) - - after = arrow.get(startdate) - after = str(int(after.timestamp()*1000)) - - try: - _ = nk_open(request.user) - except NoTokenError: # pragma: no cover - return HttpResponseRedirect("rower_nk_authorize") - - r = getrequestrower(request) - - result = nkstuff.get_nk_workouts( - r, do_async=True, 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): +def workout_import_view(request, source='c2'): startdate, enddate = get_dates_timeperiod( request, defaulttimeperiod='last30') startdate = startdate.date() enddate = enddate.date() - r = getrequestrower(request, userid=userid) - #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') - # url = reverse('workout_nkimport_view', kwargs={ - # 'userid': request.user.id}) - # return HttpResponseRedirect(url) + + r = getrequestrower(request) + integration = importsources[source](request.user) try: - _ = nk_open(r.user) + _ = 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(): @@ -687,88 +379,20 @@ def workout_nkimport_view(request, userid=0, after=0, before=0): after = arrow.get(startdate) after = str(int(after.timestamp()*1000)) - res = nkstuff.get_nk_workout_list(r.user, before=before, after=after) + workouts = integration.get_workout_list(before=before, after=after, startdate=startdate, enddate=enddate) - 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/nkauthorize/") - message = "Something went wrong in workout_nkimport_view" - messages.error(request, message) - url = reverse('workouts_view') - return HttpResponseRedirect(url) - - # get NK IDs - nkids = [item['id'] for item in res.json()] - 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: - jsondata = json.load(nkblocked) - except: - jsondata = { - 'ids':[] - } - parkedids = jsondata['ids'] - except FileNotFoundError: # pragma: no cover - pass - - knownnkids = uniqify(knownnkids+tombstones+parkedids) - workouts = [] - - for item in res.json(): - 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', 'name', 'new'] - values = [i, d, ttot, s, n, nnn] - rs = dict(zip(keys, values)) - workouts.append(rs) - - workouts = workouts[::-1] if request.method == 'POST': # pragma: no cover try: tdict = dict(request.POST.lists()) ids = tdict['workoutid'] nkids = [int(id) for id in ids] - alldata = {} - - for item in res.json(): - alldata[item['id']] = item - counter = 0 for nkid in nkids: - _ = myqueue( - queue, - handle_nk_async_workout, - alldata, - r.user.id, - r.nktoken, - nkid, - counter, - r.defaulttimezone - ) - counter = counter+1 + _ = 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: @@ -780,31 +404,34 @@ 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, 'nk_list_import.html', + + return render(request, 'list_import.html', { 'workouts': workouts, 'rower': r, - 'dateform': dateform, - 'startdate': startdate, - 'enddate': enddate, + 'dateform':dateform, 'active': 'nav-workouts', 'breadcrumbs': breadcrumbs, 'teams': get_my_teams(request.user), 'checknew': checknew, + 'startdate':startdate, + 'enddate': enddate, + 'integration': integration.get_name() }) + -# Process Strava Callback @login_required() def rower_process_stravacallback(request): + strava_integration = importsources['strava'](request.user) try: code = request.GET['code'] _ = request.GET['scope'] @@ -819,7 +446,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] @@ -834,7 +461,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) @@ -858,20 +485,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 = importsources['sporttracks'](request.user) + token = st_integration.get_token(code) successmessage = "Tokens stored. Good to go. Please check your import/export settings" messages.info(request, successmessage) @@ -894,11 +509,9 @@ def rower_process_rp3callback(request): # pragma: no cover url = reverse('rower_exportsettings_view') return HttpResponseRedirect(url) - res = rp3stuff.get_token(code) + rp3_integration = importsources['rp3'](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) @@ -930,11 +543,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 = importsources['trainingpeaks'](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) @@ -968,79 +578,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) - - try: - _ = rp3stuff.rp3_open(request.user) - except NoTokenError: # pragma: no cover - url = reverse('rower_rp3_authorize') - return HttpResponseRedirect(url) - - res = rp3stuff.get_rp3_workout_list(request.user) - - 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']) - - knownrp3ids = uniqify([ - w.uploadedtorp3 for w in Workout.objects.filter(user=r) - ]) - - workouts = [] - - for key, data in workouts_list.iterrows(): - try: - i = data['id'] - 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) - - breadcrumbs = [ - { - 'url': '/rowers/list-workouts/', - 'name': 'Workouts' - }, - { - 'url': reverse('workout_rp3import_view'), - 'name': 'RP3' - }, - ] - - return render(request, 'rp3_list_import.html', - { - 'workouts': workouts, - 'rower': r, - 'active': 'nav-workouts', - 'breadcrumbs': breadcrumbs, - 'teams': get_my_teams(request.user) - }) # The page where you select which Strava workout to import @login_required() @@ -1188,7 +725,7 @@ def workout_rojaboimport_view(request, message="", userid=0): }, { 'url': reverse('workout_rojaboimport_view'), - 'name': 'Strava' + 'name': 'Rojabo' }, ] @@ -1203,143 +740,7 @@ 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) - # if r.user != request.user: - # messages.info(request,"You cannot import other people's workouts from Strava") - try: - _ = strava_open(request.user) - except NoTokenError: # pragma: no cover - return HttpResponseRedirect("/rowers/me/stravaauthorize/") - - res = stravastuff.get_strava_workout_list(request.user) - - 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()] - - wfailed = Workout.objects.filter(user=r, 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=r) - ]) - - 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 @@ -1349,7 +750,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) @@ -1393,8 +794,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 = importsources['strava'](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 @@ -1544,434 +946,41 @@ 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), - }) - - -# 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): - 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) - - 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) - - r = getrower(request.user) - - breadcrumbs = [ - { - 'url': '/rowers/list-workouts/', - 'name': 'Workouts' - }, - { - 'url': reverse('workout_sporttracksimport_view'), - 'name': 'SportTracks' - }, - ] - - return render(request, 'sporttracks_list_import.html', - {'workouts': workouts, - 'breadcrumbs': breadcrumbs, - 'active': 'nav-workouts', - 'rower': r, - 'teams': get_my_teams(request.user), - }) - - return HttpResponse(res) # pragma: no cover - -# 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 - try: - _ = c2_open(request.user) - except NoTokenError: # pragma: no cover - return HttpResponseRedirect("/rowers/me/c2authorize/") - - r = getrequestrower(request) - - result = c2stuff.get_c2_workouts(r, page=page, do_async=True) - - 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) - - result = rp3stuff.get_rp3_workouts(r, do_async=True) - - 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) - - try: - _ = c2_open(request.user) - except NoTokenError: # pragma: no cover - return HttpResponseRedirect("/rowers/me/c2authorize/") - - res = c2stuff.get_c2_workout_list(request.user, page=page) - - 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 - # 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, - 'c2_list_import2.html', - {'workouts': workouts, - 'rower': rower, - 'active': 'nav-workouts', - 'breadcrumbs': breadcrumbs, - 'teams': get_my_teams(request.user), - 'page': page, - 'checknew': checknew, - }) - - -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', -} 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', + 'sporttracks': 'rower_integration_authorize', + 'trainingpeaks': 'rower_integration_authorize', + 'nk': 'rower_integration_authorize', + 'rp3': 'rower_integration_authorize', } -importsources = { - 'c2': c2stuff, - 'strava': stravastuff, - 'polar': polarstuff, - 'ownapi': ownapistuff, - 'sporttracks': sporttracksstuff, - 'trainingpeaks': tpstuff, - 'nk': nkstuff, -} - - -@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 NK 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, - ) - - messages.info(request, 'The workout will be imported in the background') - - url = reverse('workout_rp3import_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 reverse("workouts_view") 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) + 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 = importsources[source].get_workout(request.user, externalid, - do_async=do_async) + 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( @@ -1980,56 +989,20 @@ 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) # 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 = importsources['sporttracks'](request.user) + 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) - -# 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 8770ed20..a0ef5835 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,26 +192,18 @@ 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 -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 + +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 ( diff --git a/rowers/views/workoutviews.py b/rowers/views/workoutviews.py index d5df4565..4a72c3e0 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) @@ -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', '') @@ -5630,53 +5631,37 @@ 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" - if id > 1: - messages.info(request, message) - else: 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" - if id > 1: - messages.info(request, message) - else: 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 - 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