452 lines
14 KiB
Python
452 lines
14 KiB
Python
from .integrations import SyncIntegration, NoTokenError, create_or_update_syncrecord, get_known_ids
|
|
from rowers.models import User, Rower, Workout, TombStone
|
|
from django.db.utils import IntegrityError
|
|
|
|
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
|
|
except:
|
|
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('c2_log.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, *args, **kwargs):
|
|
_ = self.open()
|
|
r = self.rower
|
|
|
|
record = create_or_update_syncrecord(r, None, c2id=id)
|
|
|
|
_ = 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)
|
|
count = 0
|
|
|
|
for workout in workouts:
|
|
c2id = workout['id']
|
|
if workout['new'] == 'NEW':
|
|
self.get_workout(c2id)
|
|
count+= 1
|
|
|
|
return count
|
|
|
|
# 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 = get_known_ids(r, 'c2id')
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
|
|
|