diff --git a/rowers/dataprep.py b/rowers/dataprep.py index 07778df4..55da70ec 100644 --- a/rowers/dataprep.py +++ b/rowers/dataprep.py @@ -57,7 +57,7 @@ from rowingdata import ( MysteryParser, BoatCoachOTWParser,QuiskeParser, painsledDesktopParser, speedcoachParser, ErgStickParser, SpeedCoach2Parser, FITParser, fitsummarydata, - RitmoTimeParser,KinoMapParser, + RitmoTimeParser,KinoMapParser,NKLiNKLogbookParser, make_cumvalues,cumcpdata,ExcelTemplate, summarydata, get_file_type, ) @@ -1850,6 +1850,7 @@ parsers = { 'ergdata': ErgDataParser, 'humon': HumonParser, 'eth': ETHParser, + 'nklinklogbook': NKLiNKLogbookParser, } def parsenonpainsled(fileformat,f2,summary,startdatetime=''): diff --git a/rowers/imports.py b/rowers/imports.py index 821300fc..cb1cb83f 100644 --- a/rowers/imports.py +++ b/rowers/imports.py @@ -201,6 +201,7 @@ def imports_get_token( code,oauth_data ): + redirect_uri = oauth_data['redirect_uri'] client_secret = oauth_data['client_secret'] client_id = oauth_data['client_id'] @@ -266,7 +267,7 @@ def imports_get_token( except (ValueError,TypeError): expires_in = 0 else: - return [0,0,0] + return [0,response.text,0] return [thetoken,expires_in,refresh_token] diff --git a/rowers/models.py b/rowers/models.py index e0222fd7..9256b262 100644 --- a/rowers/models.py +++ b/rowers/models.py @@ -997,6 +997,13 @@ class Rower(models.Model): rp3_auto_import = models.BooleanField(default=False) + nktoken = models.TextField(default='',max_length=1000,blank=True,null=True) + nktokenexpirydate = models.DateTimeField(blank=True,null=True) + nkrefreshtoken = models.TextField(default='',max_length=1000, + blank=True,null=True) + nk_owner_id = models.BigIntegerField(default=0) + nk_auto_import = models.BooleanField(default=False) + trainingpeaks_auto_export = models.BooleanField(default=False) polartoken = models.CharField(default='',max_length=1000,blank=True,null=True) @@ -3182,6 +3189,7 @@ class Workout(models.Model): uploadedtorunkeeper = models.BigIntegerField(default=0) uploadedtogarmin = models.BigIntegerField(default=0) uploadedtorp3 = models.BigIntegerField(default=0) + uploadedtonk = models.BigIntegerField(default=0) forceunit = models.CharField(default='lbs', choices = ( ('lbs','lbs'), @@ -3261,6 +3269,7 @@ class TombStone(models.Model): uploadedtounderarmour = models.BigIntegerField(default=0) uploadedtotp = models.BigIntegerField(default=0) uploadedtorunkeeper = models.BigIntegerField(default=0) + uploadedtonk = models.BigIntegerField(default=0) @receiver(models.signals.pre_delete,sender=Workout) def create_tombstone_on_delete(sender, instance, **kwargs): @@ -3271,6 +3280,7 @@ def create_tombstone_on_delete(sender, instance, **kwargs): uploadedtounderarmour = instance.uploadedtounderarmour, uploadedtotp = instance.uploadedtotp, uploadedtorunkeeper = instance.uploadedtorunkeeper, + uploadedtonk = instance.uploadedtonk ) t.save() diff --git a/rowers/nkstuff.py b/rowers/nkstuff.py index 37d91aba..709e9db7 100644 --- a/rowers/nkstuff.py +++ b/rowers/nkstuff.py @@ -9,8 +9,8 @@ from time import strftime import requests -#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 +#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 @@ -22,16 +22,20 @@ queuehigh = django_rq.get_queue('low') from rowers.rower_rules import is_workout_user, ispromember from iso8601 import ParseError -from rowers.utils import myqueue +from rowers.utils import myqueue,get_nk_summary, get_nk_allstats, get_nk_intervalstats,getdict import rowers.mytypes as mytypes import gzip from rowsandall_app.settings import ( NK_CLIENT_ID, NK_REDIRECT_URI, NK_CLIENT_SECRET, - SITE_URL, NK_API_LOCATION + SITE_URL, NK_API_LOCATION, + UPLOAD_SERVICE_URL, UPLOAD_SERVICE_SECRET, ) +from rowers.tasks import handle_nk_async_workout + + try: from json.decoder import JSONDecodeError except ImportError: @@ -53,29 +57,139 @@ oauth_data = { 'scope':'read', } +from requests.auth import HTTPBasicAuth + def get_token(code): - #client_id = oauth_data['client_id'] - #client_secret = oauth_data['client_secret'] - #base_uri = oauth_data['base_url'] + url = oauth_data['base_url'] - #callbackuri = 'https://'+callbackuri[9:] - #print(callbackuri) - #nk = OAuth2Session(client_id) - #token = nk.fetch_token(base_uri,client_secret=client_secret,authorization_response=callbackuri) + headers = {'Accept': 'application/json', + #'Authorization': auth_header, + 'Content-Type': 'application/x-www-form-urlencoded', + #'user-agent': 'sanderroosendaal' + } + + + + 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] - #print(token) - #return [0,0,0] - return imports_get_token(code, oauth_data) def nk_open(user): - t = time.localtime() - timestamp = time.strftime('%b-%d-%Y_%H%M', t) - token = imports_open(user,oauth_data) - return token + r = Rower.objects.get(user=user) + + if (r.nktoken == '') or (r.nktoken is None): + s = "Token doesn't exist. Need to authorize" + raise NoTokenError("User has no token") + else: + if (timezone.now()>r.nktokenexpirydate): + + thetoken = rower_nk_token_refresh(user) + if thetoken == None: + raise NoTokenError("User has no token") + return thetoken + else: + thetoken = r.nktoken + + return thetoken + +def get_nk_workouts(rower, do_async=True): + try: + thetoken = nk_open(rower.user) + except NoTokenError: + return 0 + + res = get_nk_workout_list(rower.user) + + if res.status_code != 200: + return 0 + + nkids = [item['id'] for item in res.json()] + 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: + pass + + knownnkids = uniqify(knownnkids+tombstones+parkedids) + newids = [nkid for nkid in nkids if not nkid in knownnkids] + + newparkedids = uniqify(newids+parkedids) + + with open('nkblocked.json','wt') as nkblocked: + data = {'ids':newparkedids} + json.dump(data,nkblocked) + + counter = 0 + for nkid in newids: + 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): - return imports_do_refresh_token(refreshtoken, oauth_data) + 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: + 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) @@ -101,42 +215,180 @@ def get_nk_workout_list(user,fake=False): if (r.nktoken == '') or (r.nktoken is None): s = "Token doesn't exist. Need to authorize" return custom_exception_handler(401,s) - elif (r.nktokenexpirydate is None or timezone.now()+timedelta(seconds=3599)>r.nktokenexpirydate): + elif (r.nktokenexpirydate is None or timezone.now()+timedelta(seconds=10)>r.nktokenexpirydate): s = "Token expired. Needs to refresh." return custom_exception_handler(401,s) else: # ready to fetch. Hurray + endTime = int(arrow.now().timestamp())*1000 + endTime = str(endTime) + startTime = arrow.now()-timedelta(days=30)*1000 + startTime = str(int(startTime.timestamp())) authorizationstring = str('Bearer ' + r.nktoken) headers = {'Authorization': authorizationstring, 'user-agent': 'sanderroosendaal', - 'Content-Type': 'application/json'} + 'Content-Type': 'application/json', + } url = NK_API_LOCATION+"api/v1/sessions" - params = {} # start / end time + params = { + 'startTime':startTime, + 'endTime':endTime, + } # start / end time s = requests.get(url,headers=headers,params=params) - return s - -def get_nk_workout_strokes(user,nkid): - r = Rower.objects.get(user=user) - - if (r.nktoken == '') or (r.nktoken is None): - return custom_exception_handler(401,s) - s = "Token doesn't exist. Need to authorize" - elif (timezone.now()>r.tokenexpirydate): - s = "Token expired. Needs to refresh." - return custom_exception_handler(401,s) - else: - # ready to fetch. Hurray - authorizationstring = str('Bearer ' + r.nktoken) - headers = {'Authorization': authorizationstring, - 'user-agent': 'sanderroosendaal', - 'Content-Type': 'application/json'} - url = "https://log.concept2.com/api/users/me/results/"+str(nkid)+"/strokes" - s = requests.get(url,headers=headers) return s + # -#def get_workout(user,nkid): + +def add_workout_from_data(user,nkid,data,strokedata,source='nk',splitdata=None, + workoutsource='nklinklogbook'): + + csvfilename = 'media/{code}_{nkid}.csv.gz'.format( + nkid=nkid, + code = uuid4().hex[:16] + ) + + strokedata.to_csv(csvfilename, index_label='index', compression='gzip') + + userid = user.id + + title = data["name"] + speedInput = data["speedInput"] + elapsedTime = data["elapsedTime"] + totalDistanceGps = data["totalDistanceGps"] + totalDistanceImp = data["totalDistanceImp"] + intervals = data["intervals"] # add intervals + oarlockSessions = data["oarlockSessions"] + deviceId = data["deviceId"] # you could get the firmware version + + summary = get_nk_allstats(data,strokedata) + + # oarlock inboard, length, boat name + if oarlockSessions: + oarlocksession = oarlockSessions[0] # should take seatIndex + boatName = oarlocksession["boatName"] + oarLength = oarlocksession["oarLength"] # cm + oarInboardLength = oarlocksession["oarInboardLength"] # cm + seatNumber = oarlocksession["seatNumber"] + else: + boatName = '' + oarLength = 289 + oarInboardLength = 88 + seatNumber = 1 + + workouttype = "water" + boattype = "1x" + + uploadoptions = { + 'secret': UPLOAD_SERVICE_SECRET, + 'user':userid, + 'file': csvfilename, + 'title': title, + 'workouttype': workouttype, + 'boattype': boattype, + 'nkid':nkid, + 'inboard': oarInboardLength/100., + 'oarlength': oarLength/100., + 'summary':summary, + } + + 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,response.text + + try: + workoutid = response.json()['id'] + except KeyError: + workoutid = 1 + + + # evt update workout summary + + # return + return workoutid,"" + + + + +def get_workout(user,nkid): + r = Rower.objects.get(user=user) + if (r.nktoken == '') or (r.nktoken is None): + s = "Token doesn't exist. Need to authorize" + return custom_exception_handler(401,s) ,0 + elif (timezone.now()>r.nktokenexpirydate): + s = "Token expired. Needs to refresh." + return custom_exception_handler(401,s),0 + + params = { + 'sessionIds': nkid, + } + + authorizationstring = str('Bearer ' + r.nktoken) + headers = {'Authorization': authorizationstring, + 'user-agent': 'sanderroosendaal', + 'Content-Type': 'application/json', + } + + + + # get strokes + url = NK_API_LOCATION+"api/v1/sessions/strokes" + response = requests.get(url,headers=headers,params=params) + + + if response.status_code != 200: + # error handling and logging + return {},pd.DataFrame() + + jsonData = response.json() + + strokeData = jsonData[str(nkid)] + + df = pd.DataFrame.from_dict(strokeData) + oarlockData = df['oarlockStrokes'] + + oarlockData = oarlockData.apply(lambda x:getdict(x, seatIndex=1)) + df2 = pd.DataFrame.from_records(oarlockData.values) + + df.set_index('timestamp') + + if not df2.empty: + df2.set_index('timestamp') + df = df.merge(df2,left_index=True,right_index=True) + df = df.rename(columns={"timestamp_x":"timestamp"}) + + df = df.drop('oarlockStrokes',axis=1) + df.sort_values(by='timestamp',ascending=True,inplace=True) + df.fillna(inplace=True,method='ffill') + + # get workout data + timestampbegin = df['timestamp'].min() + timestampend = df['timestamp'].max() + + url = NK_API_LOCATION+"api/v1/sessions/" + params = { + 'startTime':timestampbegin-1, + 'endTime': timestampend+1, + } + response = requests.get(url, headers=headers,params=params) + + if response.status_code != 200: + # error handling and logging + return {},df + + jsondata = response.json() + workoutdata = {} + for w in jsondata: + if str(w['id']) == str(nkid): + workoutdata = w + + return workoutdata, df diff --git a/rowers/stravastuff.py b/rowers/stravastuff.py index 2cef3373..d0914b77 100644 --- a/rowers/stravastuff.py +++ b/rowers/stravastuff.py @@ -131,8 +131,6 @@ def strava_establish_push(): response = requests.post(url,data=post_data) - print(response.json()) - return response.status_code def strava_list_push(): diff --git a/rowers/tasks.py b/rowers/tasks.py index c841185f..84957b55 100644 --- a/rowers/tasks.py +++ b/rowers/tasks.py @@ -53,6 +53,7 @@ from rowsandall_app.settings import PROGRESS_CACHE_SECRET from rowsandall_app.settings import SETTINGS_NAME from rowsandall_app.settings import workoutemailbox from rowsandall_app.settings import UPLOAD_SERVICE_SECRET, UPLOAD_SERVICE_URL +from rowsandall_app.settings import NK_API_LOCATION from requests_oauthlib import OAuth1, OAuth1Session @@ -110,6 +111,8 @@ def safetimedelta(x): siteurl = SITE_URL +from rowers.utils import get_nk_summary, get_nk_allstats, get_nk_intervalstats,getdict + # testing task from rowers.emails import send_template_email @@ -2952,6 +2955,149 @@ def handle_rp3_async_workout(userid,rp3token,rp3id,startdatetime,max_attempts,de return workoutid +@app.task +def handle_nk_async_workout(alldata,userid,nktoken,nkid,delaysec,defaulttimezone,debug=False,**kwargs): + time.sleep(delaysec) + data = alldata[nkid] + + params = { + 'sessionIds': nkid, + } + + authorizationstring = str('Bearer ' + nktoken) + headers = {'Authorization': authorizationstring, + 'user-agent': 'sanderroosendaal', + 'Content-Type': 'application/json', + } + + # get strokes + url = NK_API_LOCATION+"api/v1/sessions/strokes" + response = requests.get(url,headers=headers,params=params) + + + if response.status_code != 200: + # error handling and logging + return 0 + + jsonData = response.json() + strokeData = jsonData[str(nkid)] + df = pd.DataFrame.from_dict(strokeData) + oarlockData = df['oarlockStrokes'] + + oarlockData = oarlockData.apply(lambda x:getdict(x, seatIndex=1)) + df2 = pd.DataFrame.from_records(oarlockData.values) + + df.set_index('timestamp') + + if not df2.empty: + df2.set_index('timestamp') + df = df.merge(df2,left_index=True,right_index=True) + df = df.rename(columns={"timestamp_x":"timestamp"}) + + df = df.drop('oarlockStrokes',axis=1) + df.sort_values(by='timestamp',ascending=True,inplace=True) + df.fillna(inplace=True,method='ffill') + + # get workout data + timestampbegin = df['timestamp'].min() + timestampend = df['timestamp'].max() + + csvfilename = 'media/{code}_{nkid}.csv.gz'.format( + nkid=nkid, + code = uuid4().hex[:16] + ) + + df.to_csv(csvfilename, index_label='index', compression='gzip') + + + title = data["name"] + speedInput = data["speedInput"] + elapsedTime = data["elapsedTime"] + totalDistanceGps = data["totalDistanceGps"] + totalDistanceImp = data["totalDistanceImp"] + intervals = data["intervals"] # add intervals + oarlockSessions = data["oarlockSessions"] + deviceId = data["deviceId"] # you could get the firmware version + + summary = get_nk_allstats(data,df) + + # oarlock inboard, length, boat name + if oarlockSessions: + oarlocksession = oarlockSessions[0] # should take seatIndex + boatName = oarlocksession["boatName"] + oarLength = oarlocksession["oarLength"] # cm + oarInboardLength = oarlocksession["oarInboardLength"] # cm + seatNumber = oarlocksession["seatNumber"] + else: + boatName = '' + oarLength = 289 + oarInboardLength = 88 + seatNumber = 1 + + workouttype = "water" + boattype = "1x" + + uploadoptions = { + 'secret': UPLOAD_SERVICE_SECRET, + 'user':userid, + 'file': csvfilename, + 'title': title, + 'workouttype': workouttype, + 'boattype': boattype, + 'nkid':nkid, + 'inboard': oarInboardLength/100., + 'oarlength': oarLength/100., + 'summary':summary, + } + + 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,response.text + + try: + workoutid = response.json()['id'] + except KeyError: + workoutid = 1 + + if debug: + engine = create_engine(database_url_debug, echo=False) + else: + engine = create_engine(database_url, echo=False) + + query = 'SELECT uploadedtonk from rowers_workout WHERE id ={workoutid}'.format(workoutid=workoutid) + + newnkid = 0 + with engine.connect() as conn, conn.begin(): + result = conn.execute(query) + tdata = result.fetchall() + if tdata: + newnkid = tdata[0][0] + + conn.close() + + parkedids = [] + with open('nkblocked.json','r') as nkblocked: + jsondata = json.load(nkblocked) + parkedids = jsondata['ids'] + + newparkedids = [id for id in parkedids if id != newnkid] + with open('nkblocked.json','wt') as nkblocked: + tdata = {'ids':newparkedids} + nkblocked.seek(0) + json.dump(tdata,nkblocked) + + # evt update workout summary + + # return + return workoutid + + + @app.task def handle_c2_async_workout(alldata,userid,c2token,c2id,delaysec,defaulttimezone,debug=False,**kwargs): time.sleep(delaysec) diff --git a/rowers/templates/menu_workout.html b/rowers/templates/menu_workout.html index 8c0d8385..51aefb8b 100644 --- a/rowers/templates/menu_workout.html +++ b/rowers/templates/menu_workout.html @@ -155,6 +155,13 @@ {% endif %} +
  • + {% if workout.uploadedtonk %} + + NK LiNK + + {% endif %} +
  • {% if workout.uploadedtostrava %} diff --git a/rowers/templates/menu_workouts.html b/rowers/templates/menu_workouts.html index aea1c84f..73c22bdf 100644 --- a/rowers/templates/menu_workouts.html +++ b/rowers/templates/menu_workouts.html @@ -38,6 +38,7 @@
    • Concept2
    • +
    • NK LiNK
    • Strava
    • RunKeeper
    • SportTracks
    • diff --git a/rowers/templates/nk_list_import.html b/rowers/templates/nk_list_import.html index 44ae0554..f8689b06 100644 --- a/rowers/templates/nk_list_import.html +++ b/rowers/templates/nk_list_import.html @@ -5,7 +5,7 @@ {% block title %}Workouts{% endblock %} {% block main %} -

      Available on nk

      +

      Available on NK LiNK Logbook

      {% if workouts %}
      • @@ -34,8 +34,8 @@ Import - {{ workout|lookup:'name' }} - {{ workout|lookup:'starttime' }} + {{ workout|lookuplong:'name' }} + {{ workout|lookuplong:'starttime' }} {{ workout|lookup:'duration' }} {{ workout|lookup:'distance' }} m {{ workout|lookup:'new' }} diff --git a/rowers/templates/rower_exportsettings.html b/rowers/templates/rower_exportsettings.html index 81179cb4..6cabf41f 100644 --- a/rowers/templates/rower_exportsettings.html +++ b/rowers/templates/rower_exportsettings.html @@ -11,6 +11,9 @@ {% if rower.c2token is not None and rower.c2token != '' %} Concept2 Logbook, {% endif %} + {% if rower.nktoken is not None and rower.nktoken != '' %} + NK LiNK, + {% endif %} {% if rower.sporttrackstoken is not None and rower.sporttrackstoken != '' %} SportTracks, {% endif %} @@ -71,6 +74,7 @@ choice or to renew the authorization.

        connect with strava

        connect with Concept2

        +

        connect with NK LiNK

        connect with SportTracks

        connect with RunKeeper

        connect with Under Armour

        diff --git a/rowers/templatetags/rowerfilters.py b/rowers/templatetags/rowerfilters.py index 89f5c1c6..baa1646b 100644 --- a/rowers/templatetags/rowerfilters.py +++ b/rowers/templatetags/rowerfilters.py @@ -30,6 +30,8 @@ from rowers.utils import NoTokenError, step_to_string import rowers.payments as payments +from rowsandall_app.settings import NK_VIEWER_LOCATION + from rowers.opaque import encoder from rowers.plannedsessions import ps_dict_get_description_html @@ -106,6 +108,15 @@ def adaptive(s): return u +@register.filter +def nkviewerlink(workout): + url = "{nkviewer}{nkid}".format( + nkid=workout.uploadedtonk, + nkviewer=NK_VIEWER_LOCATION) + + return url + + @register.filter def boatclass(s): u = s diff --git a/rowers/tests/mocks.py b/rowers/tests/mocks.py index 35adee66..e3f34f70 100644 --- a/rowers/tests/mocks.py +++ b/rowers/tests/mocks.py @@ -101,6 +101,30 @@ def mock_c2open(*args, **kwargs): print('mock') return('aap') +def mocked_session(*args, **kwargs): + + class MockEngine: + def __init__(self, *args, **kwargs): + self.headers = MockHeaders() + + def post(self, *args, **kwargs): + return MockResponse({},200) + + class MockHeaders: + def update(*args, **kwargs): + return None + + class MockResponse: + def __init__(self, json_data, status_code): + self.json_data = json_data + self.status_code = status_code + self.ok = True + + def json(self): + return self.json_data + + return MockEngine() + def mocked_sqlalchemy(*args, **kwargs): # return object with method @@ -593,6 +617,12 @@ def mocked_requests(*args, **kwargs): with open('rowers/tests/testdata/c2jsonstrokedata.txt','r') as infile: c2strokedata = json.load(infile) + with open('rowers/tests/testdata/nk_strokes.json','r') as infile: + nkstrokedata = json.load(infile) + + with open('rowers/tests/testdata/nk_list.json','r') as infile: + nkworkoutlist = json.load(infile) + polar_json = { 'available-user-data': [] } @@ -621,6 +651,8 @@ def mocked_requests(*args, **kwargs): stravaworkoutlist = json.load(open('rowers/tests/testdata/stravaworkoutlist.txt')) sporttracksworkoutlist = json.load(open('rowers/tests/testdata/sporttracksworkouts.txt')) + #nkworkoutlist = json.load(open('rowers/tests/testdata/nkworkouts.txt')) + rkworkoutlistjson = json.load(open('rowers/tests/testdata/rkworkoutslist.txt','r')) uaworkoutlistjson = json.load(open('rowers/tests/testdata/uaworkoutlist.txt','r')) @@ -732,6 +764,7 @@ def mocked_requests(*args, **kwargs): rktester = re.compile('.*?runkeeper\.com') uatester = re.compile('.*?mapmyfitness\.com') tptester = re.compile('.*?trainingpeaks\.com') + nktester = re.compile('.*?nkrowlink\.com') garmintester = re.compile('.*?garmin\.com') c2importregex = '.*?concept2.com\/api\/users\/me\/results\/\d+' @@ -746,6 +779,12 @@ def mocked_requests(*args, **kwargs): c2workoutlistregex = '.*?concept2\.com\/api\/users\/me\/results\?page=\d' c2workoutlisttester = re.compile(c2workoutlistregex) + nkworkoutlistregex = '.*?nkrowlink\.com\/api\/v1\/sessions' + nkworkoutlisttester = re.compile(nkworkoutlistregex) + + nkstrokesregex = '.*?nkrowlink\.com\/api\/v1\/sessions\/strokes' + nkstrokestester = re.compile(nkstrokesregex) + stravaathleteregex = '.*?strava\.com\/api\/v3\/athlete$' stravaathletetester = re.compile(stravaathleteregex) @@ -796,6 +835,7 @@ def mocked_requests(*args, **kwargs): garmindownloadregex = '.*?garmin\.com\/mockfile?id=1' garmindownloadtester = re.compile(garmindownloadregex) + if garmintester.match(args[0]): if garmindownloadtester.match(args[0]): return MockStreamResponse('rowers/tests/testdata/3x250m.fit',200) @@ -910,6 +950,24 @@ def mocked_requests(*args, **kwargs): return MockResponse(sporttracksworkoutlist,200) + if nktester.match(args[0]): + + if 'token' in args[0]: + json_data = { + 'access_token': 'TA3n1vrNjuQJWw0TdCDHnjSmrjIPULhTlejMIWqq', + 'expires_in': 604800, + 'refresh_token': 'jHJhFzCfOOKB8oyiayubhLAlxaMkG3ruC1E8YxaR' + } + return MockResponse(json_data,200) + if nkstrokestester.match(args[0]): + + return MockResponse(nkstrokedata,200) + if nkworkoutlisttester.match(args[0]): + + return MockResponse(nkworkoutlist,200) + + + if stravatester.match(args[0]): if stravaworkoutlisttester.match(args[0]): return MockResponse(stravaworkoutlist,200) diff --git a/rowers/tests/test_imports.py b/rowers/tests/test_imports.py index a83d534e..c61ffa8d 100644 --- a/rowers/tests/test_imports.py +++ b/rowers/tests/test_imports.py @@ -360,6 +360,87 @@ class C2ObjectsTokenExpired(DjangoTestCase): self.assertEqual(response.status_code, 200) +#@pytest.mark.django_db +@override_settings(TESTING=True) +class NKObjects(DjangoTestCase): + def setUp(self): + self.c = Client() + self.u = User.objects.create_user('john', + 'sander@ds.ds', + 'koeinsloot') + + self.u.first_name = 'John' + self.u.last_name = 'Sander' + self.u.save() + self.r = Rower.objects.create(user=self.u,gdproptin=True,surveydone=True, + gdproptindate=timezone.now() + ) + + self.r.nktoken = '12' + self.r.nkrefreshtoken = '123' + self.r.nktokenexpirydate = datetime.datetime.now()-datetime.timedelta(days=1) + + self.r.save() + self.c.login(username='john',password='koeinsloot') + self.nu = datetime.datetime.now() + + filename = 'rowers/tests/testdata/testdata.csv' + + rr = rrower(hrmax=self.r.max,hrut2=self.r.ut2, + hrut1=self.r.ut1,hrat=self.r.at, + hrtr=self.r.tr,hran=self.r.an,ftp=self.r.ftp) + row = rdata(filename,rower=rr) + totaldist = row.df['cum_dist'].max() + totaltime = row.df['TimeStamp (sec)'].max()-row.df['TimeStamp (sec)'].min() + totaltime = totaltime+row.df.loc[:,' ElapsedTime (sec)'].iloc[0] + + + hours = int(totaltime/3600.) + minutes = int((totaltime - 3600.*hours)/60.) + seconds = int(totaltime - 3600.*hours - 60.*minutes) + tenths = int(10*(totaltime - 3600.*hours - 60.*minutes - seconds)) + + duration = "%s:%s:%s.%s" % (hours,minutes,seconds,tenths) + + + workoutdate = row.rowdatetime.strftime('%Y-%m-%d') + workoutstarttime = row.rowdatetime.strftime('%H:%M:%S') + + self.w = Workout.objects.create( + name='testworkout',workouttype='water', + user=self.r,date=self.nu.strftime('%Y-%m-%d'), + starttime=workoutstarttime, + startdatetime=row.rowdatetime, + duration=duration,distance=totaldist, + csvfilename=filename + ) + + @patch('rowers.nkstuff.requests.get', side_effect=mocked_requests) + @patch('rowers.nkstuff.requests.post', side_effect=mocked_requests) + def test_nk_list(self, mock_get, mockpost): + result = rowers.nkstuff.rower_nk_token_refresh(self.u) + self.assertEqual(result,"TA3n1vrNjuQJWw0TdCDHnjSmrjIPULhTlejMIWqq") + response = self.c.get('/rowers/workout/nkimport/') + + 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.nkstuff.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) + response = self.c.get('/rowers/workout/nkimport/469',follow=True) + + self.assertRedirects(response, + expected_url='/rowers/workout/'+encoded1+'/edit/', + status_code=301,target_status_code=200) + + self.assertEqual(response.status_code, 200) + #@pytest.mark.django_db diff --git a/rowers/tests/testdata/nkworkoutlist.txt b/rowers/tests/testdata/nkworkoutlist.txt new file mode 100644 index 00000000..320a47cc --- /dev/null +++ b/rowers/tests/testdata/nkworkoutlist.txt @@ -0,0 +1,84 @@ +[ + { + "id": 1, + "name": "JustGo-7189M", + "type": 0, + "speedInput": 0, + "startTime": 1614264826, + "endTime": 1614269826, + "location": "Some Location", + "deviceId": 11, + "elapsedTime": 1140000, + "totalDistanceImp": 2920.69, + "totalDistanceGps": 2286.5, + "avgPaceImp": 195159.36302723, + "avgPaceGps": 250466.812537141, + "avgStrokeRate": 19.5, + "distStrokeImp": 8.51, + "distStrokeGps": 6.77, + "avgHeartRate": 158, + "totalStrokeCount": 343, + "totalCalories": 4959900, + "avgCalHour": 4482062.35252774, + "avgSpeedGps": 2, + "avgSpeedImp": 2.56, + "avgPower": 68.6979270660324, + "avgCatch": -46.7961491475831, + "avgSlip": 16.1616225246003, + "avgFinish": 48.1512290049444, + "avgWash": 22.9514686031976, + "avgForceAvg": 114.502194258813, + "avgWork": 210.785493336831, + "avgForceMax": 249.481436977143, + "avgMaxForceAngle": 4.75378911974861, + "startGpsLat": 39.7356346, + "startGpsLon": -75.5581928, + "intervals": [ + { + "id": 1, + "sessionId": 1, + "startTime": 1614264826, + "intervalNumber": 1, + "sessionStrokeStartIndex": 473, + "sessionStrokeEndIndex": 674, + "sessionStrokeCount": 91, + "elapsedTime": 1140000, + "totalDistanceImp": 2920.69, + "totalDistanceGps": 2286.5, + "avgPaceImp": 195159.36302723, + "avgPaceGps": 250466.812537141, + "avgStrokeRate": 19.5, + "distStrokeImp": 8.51, + "distStrokeGps": 6.77, + "avgHeartRate": 158, + "totalStrokeCount": 343, + "totalCalories": 4959900, + "avgCalHour": 4482062.35252774, + "avgSpeedGps": 2, + "avgSpeedImp": 2.56, + "avgPower": 68.6979270660324, + "avgCatch": -46.7961491475831, + "avgSlip": 16.1616225246003, + "avgFinish": 48.1512290049444, + "avgWash": 22.9514686031976, + "avgForceAvg": 114.502194258813, + "avgWork": 210.785493336831, + "avgForceMax": 249.481436977143, + "avgMaxForceAngle": 4.75378911974861, + "startGpsLat": 39.7356346, + "startGpsLon": -75.5581928 + } + ], + "oarlockSessions": [ + { + "id": 1, + "sessionId": 1, + "boatName": "Fast Boat", + "seatNumber": 3, + "portStarboard": 0, + "oarLength": 284, + "oarInboardLength": 85 + } + ] + } +] diff --git a/rowers/uploads.py b/rowers/uploads.py index adf2688f..23546796 100644 --- a/rowers/uploads.py +++ b/rowers/uploads.py @@ -541,6 +541,13 @@ def do_sync(w,options, quick=False): except KeyError: pass + try: + if options['nkid'] != 0 and options['nkid'] != '': + w.uploadedtonk = options['nkid'] + w.save() + except KeyError: + pass + try: if options['garminid'] != 0 and options['garminid'] != '': w.uploadedtogarmin = options['garminid'] diff --git a/rowers/urls.py b/rowers/urls.py index 434c291d..ad8dfe03 100644 --- a/rowers/urls.py +++ b/rowers/urls.py @@ -567,6 +567,8 @@ urlpatterns = [ 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/all/$',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/all/$',views.workout_getrp3workout_all,name='workout_getrp3workout_all'), @@ -671,6 +673,7 @@ urlpatterns = [ re_path(r'^me/preferences/user/(?P\d+)/$',views.rower_prefs_view,name='rower_prefs_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/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'), @@ -729,7 +732,7 @@ urlpatterns = [ re_path(r'^legal', TemplateView.as_view(template_name='legal.html'),name='legal'), re_path(r'^register/$',views.rower_register_view,name='rower_register_view'), re_path(r'^coachregister/$',views.freecoach_register_view,name='freecoach_register_view'), - path('activate///',views.useractivate, name='useractivate'), + path('activate///',views.useractivate, name='useractivate'), re_path(r'^register/thankyou/$', TemplateView.as_view(template_name='registerthankyou.html'), name='registerthankyou'), re_path(r'^workout/(?P\b[0-9A-Fa-f]+\b)/workflow/$',views.workout_workflow_view, name='workout_workflow_view'), diff --git a/rowers/utils.py b/rowers/utils.py index c1e69aab..062f10c2 100644 --- a/rowers/utils.py +++ b/rowers/utils.py @@ -1092,3 +1092,142 @@ def step_to_string(step,short=False): s = 'Repeat {duration}'.format(duration=duration) return s,type, nr, repeatID, repeatValue + +def strfdelta(tdelta): + try: + minutes, seconds = divmod(tdelta.seconds, 60) + tenths = int(tdelta.microseconds / 1e5) + except AttributeError: + minutes, seconds = divmod(tdelta.view(np.int64), 60e9) + seconds, rest = divmod(seconds, 1e9) + tenths = int(rest / 1e8) + res = "{minutes:0>2}:{seconds:0>2}.{tenths:0>1}".format( + minutes=minutes, + seconds=seconds, + tenths=tenths, + ) + + return res + + +def get_nk_intervalstats(workoutdata,strokedata): + intervals = workoutdata['intervals'] + separator = "|" + stri = "Workout Details\n" + stri += "#-{sep}SDist{sep}-Split-{sep}-SPace-{sep}-Pwr-{sep}-SPM--{sep}-AvgHR-{sep}DPS-\n".format( + sep=separator) + + i = 0 + + for interval in intervals: + id = interval['id'] + sdist = interval['totalDistanceGps'] + avgpace = interval['avgPaceGps']/1000. + avgpacetd = timedelta(seconds=avgpace) + newpacestring = strfdelta(avgpacetd) + + elapsedSeconds = interval['elapsedTime']/1000. # secs + elapsedTime = datetime.datetime.utcfromtimestamp(elapsedSeconds) + + newsplitstring = "%s.%i" % (elapsedTime.strftime("%M:%S"), elapsedTime.microsecond/100000) + + pwr = interval['avgPower'] + avghr = interval['avgHeartRate'] + spm = interval['avgStrokeRate'] + dps = interval['distStrokeGps'] + + stri += "{i:0>2}{sep}{sdist:0>5}{sep}{split}{sep}{space}{sep} {pwr:0>3} {sep}".format( + i=i + 1, + sdist=int(float(sdist)), + split=newsplitstring, + space=newpacestring, + pwr=int(pwr), + sep=separator, + ) + stri += " {spm} {sep} {avghr:0>3} {sep}{dps:0>4.1f}\n".format( + sep=separator, + avghr=avghr, + spm=spm, + dps=dps, + ) + + i += 1 + + return stri + +def get_nk_summary(workoutdata,strokedata): + + name = workoutdata['name'] + + avgpace = workoutdata['avgPaceGps']/1000. # secs + avgpacetd = timedelta(seconds=avgpace) + avgpacestring = strfdelta(avgpacetd) + + elapsedSeconds = workoutdata['elapsedTime']/1000. # secs + elapsedTime = datetime.datetime.utcfromtimestamp(elapsedSeconds) + + timestring = "%s.%i" % (elapsedTime.strftime("%H:%M:%S"), elapsedTime.microsecond/100000) + + dist = workoutdata['totalDistanceGps'] + spm = workoutdata['avgStrokeRate'] + avghr = workoutdata['avgHeartRate'] + avgdps = workoutdata['distStrokeGps'] + maxhr = strokedata['heartRate'].max() + pwr = workoutdata['avgPower'] + + sep = "|" + + + stri1 = "Workout Summary - " + name + "\n" + stri1 += "--{sep}Total{sep}--Total---{sep}--Avg--{sep}-Avg-{sep}-Avg--{sep}-Avg-{sep}-Max-{sep}-Avg\n".format( + sep=sep) + stri1 += "--{sep}Dist-{sep}--Time----{sep}-Pace--{sep}-Pwr-{sep}-SPM--{sep}-HR--{sep}-HR--{sep}-DPS\n".format( + sep=sep) + + + + stri1 += "--{sep}{dist:0>5.0f}{sep}".format( + sep=sep, + dist=dist, + ) + + stri1 += timestring + sep + avgpacestring + + stri1 += "{sep}{avgpower:0>5.1f}".format( + sep=sep, + avgpower=pwr, + ) + + stri1 += "{sep} {avgsr:2.1f} {sep}{avghr:0>5.1f}{sep}".format( + avgsr=spm, + sep=sep, + avghr=avghr + ) + + stri1 += "{maxhr:0>5.1f}{sep}{avgdps:0>4.1f}\n".format( + sep=sep, + maxhr=maxhr, + avgdps=avgdps + ) + + return stri1 + + + + return stri1 + +def get_nk_allstats(data,workoutdata): + stri = get_nk_summary(data, workoutdata) + \ + get_nk_intervalstats(data, workoutdata) + return stri + +#def get_workout(user,nkid): +def getdict(x, seatIndex=1): + seatStrokes = pd.DataFrame(x) + try: + seatStrokes = seatStrokes.set_index('seatIndex') + return dict(seatStrokes.loc[seatIndex]) + except KeyError: + pass + + return {} diff --git a/rowers/views/importviews.py b/rowers/views/importviews.py index 4dfc0ab4..e3120a29 100644 --- a/rowers/views/importviews.py +++ b/rowers/views/importviews.py @@ -392,6 +392,27 @@ def workout_sporttracks_upload_view(request,id=0): return HttpResponseRedirect(url) + +# NK LiNK authorization +@login_required() +def rower_nk_authorize(request): + 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 = "https://oauth-stage.nkrowlink.com/oauth/authorize?"+urllib.parse.urlencode(params) + + return HttpResponseRedirect(url) + + + # Concept2 authorization @login_required() def rower_c2_authorize(request): @@ -660,7 +681,7 @@ def rower_process_callback(request): message = "The resource owner or authorization server denied the request" messages.error(request,message) - url = reverse('workouts_view') + url = reverse('rower_exportsettings_view') return HttpResponseRedirect(url) @@ -670,7 +691,7 @@ def rower_process_callback(request): message += ' Contact info@rowsandall.com if this behavior persists.' messages.error(request,message) - url = reverse('workouts_view') + url = reverse('rower_exportsettings_view') return HttpResponseRedirect(url) @@ -756,11 +777,150 @@ def rower_process_garmincallback(request): @login_required() def rower_process_nkcallback(request): # do stuff + try: + code = request.GET.get('code',None) + res = nkstuff.get_token(code) + except MultiValueDictKeyError: + message = "The resource owner or authorization server denied the request" + messages.error(request,message) + 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' + messages.error(request,message) + + url = reverse('rower_exportsettings_view') + + return HttpResponseRedirect(url) + + expires_in = res[1] + refresh_token = res[2] + 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.save() + + successmessage = "Tokens stored. Good to go. Please check your import/export settings" + messages.info(request,successmessage) url = reverse('rower_exportsettings_view') - return HttpResponseRedirect(url) +@login_required() +def workout_getnkworkout_all(request): + try: + thetoken = nk_open(request.user) + except NoTokenError: + return HttpResponseRedirect("rower_nk_authorize") + + r = getrequestrower(request) + + result = nkstuff.get_nk_workouts(r,do_async=True) + + if result: + messages.info(request,"Your NK workouts will be imported in the coming few minutes") + else: + 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) +def workout_nkimport_view(request,userid=0): + r = getrequestrower(request,userid=userid) + try: + thetoken = nk_open(request.user) + except NoTokenError: + return HttpResponseRedirect("/rowers/me/nkauthorize/") + + res = nkstuff.get_nk_workout_list(request.user) + + if (res.status_code != 200): + 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: + jsondata = json.load(nkblocked) + parkedids = jsondata['ids'] + except FileNotFoundError: + pass + + knownnkids = uniqify(knownnkids+tombstones+parkedids) + newids = [nkid for nkid in nkids if not nkid in knownnkids] + + + nkdata = [{ + 'id':int(item['id']), + 'elapsed_time':item['elapsedTime'], + 'start_date':arrow.get(item['startTime']), + } for item in res.json()] + + 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: + nnn = 'NEW' + ttot = str(datetime.timedelta(seconds=int(float(item['elapsedTime'])/1000.))) + s = arrow.get(item['startTime'],tzinfo=r.defaulttimezone).format(arrow.FORMAT_RFC850) + #s = arrow.get(item['startTime']).to(r.defaulttimezone).isoformat() + keys = ['id','distance','duration','starttime','name','new'] + values = [i,d,ttot,s,n,nnn] + res = dict(zip(keys, values)) + workouts.append(res) + + workouts = workouts[::-1] + + breadcrumbs = [ + { + 'url':'/rowers/list-workouts/', + 'name':'Workouts' + }, + { + 'url':reverse('workout_nkimport_view'), + 'name':'Strava' + }, + ] + + return render(request,'nk_list_import.html', + { + 'workouts':workouts, + 'rower':r, + 'active':'nav-workouts', + 'breadcrumbs':breadcrumbs, + 'teams':get_my_teams(request.user) + }) # Process Strava Callback @login_required() @@ -1811,7 +1971,8 @@ importsources = { 'runkeeper':runkeeperstuff, 'sporttracks':sporttracksstuff, 'trainingpeaks':tpstuff, - 'underarmour':underarmourstuff + 'underarmour':underarmourstuff, + 'nk':nkstuff, } @login_required() @@ -1840,7 +2001,7 @@ def workout_getrp3importview(request,externalid): def workout_getimportview(request,externalid,source = 'c2'): data,strokedata = importsources[source].get_workout(request.user,externalid) if not data: - messages.error(request,res[1]) + messages.error(request,"No strokedata received") url = reverse('workouts_view') return HttpResponseRedirect(url) @@ -1924,6 +2085,7 @@ def workout_getimportview(request,externalid,source = 'c2'): return HttpResponseRedirect(url) + # strokedata not empty - continue id,message = importsources[source].add_workout_from_data( request.user, diff --git a/rowers/views/statements.py b/rowers/views/statements.py index e2dd74d9..b323beb1 100644 --- a/rowers/views/statements.py +++ b/rowers/views/statements.py @@ -154,7 +154,9 @@ import os,sys import datetime import iso8601 import rowers.c2stuff as c2stuff +import rowers.nkstuff as nkstuff from rowers.c2stuff import c2_open +from rowers.nkstuff import nk_open from rowers.rp3stuff import rp3_open from rowers.runkeeperstuff import runkeeper_open from rowers.sporttracksstuff import sporttracks_open @@ -185,7 +187,8 @@ from rowsandall_app.settings import ( RP3_CLIENT_ID,RP3_REDIRECT_URI,RP3_CLIENT_KEY,RP3_CLIENT_SECRET, BRAINTREE_MERCHANT_ID,BRAINTREE_PUBLIC_KEY,BRAINTREE_PRIVATE_KEY, PAYMENT_PROCESSING_ON, - RECAPTCHA_SITE_KEY, RECAPTCHA_SITE_SECRET + RECAPTCHA_SITE_KEY, RECAPTCHA_SITE_SECRET, + NK_REDIRECT_URI, NK_CLIENT_ID, NK_CLIENT_SECRET ) #from rowers.tasks_standalone import addcomment2 diff --git a/rowers/views/workoutviews.py b/rowers/views/workoutviews.py index 6553b0fa..b1f1ef56 100644 --- a/rowers/views/workoutviews.py +++ b/rowers/views/workoutviews.py @@ -4870,6 +4870,7 @@ def workout_upload_api(request): q = request.POST post_data = {k: q.getlist(k) if len(q.getlist(k))>1 else v for k, v in q.items()} + # only allow local host hostt = request.get_host().split(':') if hostt[0] not in ['localhost','127.0.0.1','dev.rowsandall.com','rowsandall.com']: @@ -4916,6 +4917,12 @@ def workout_upload_api(request): except KeyError: c2id = '' + try: + nkid = post_data['nkid'] + except KeyError: + nkid = '' + + try: rp3id = post_data['rp3id'] except KeyError: @@ -5026,6 +5033,21 @@ def workout_upload_api(request): plottype = r.staticchartonupload res, jobid = uploads.make_plot(r,w,f1,f2,plottype,t) + try: + oarlength = post_data['oarlength'] + inboard = post_data['inboard'] + w.inboard = inboard + w.oarlength = oarlength + w.save() + except KeyError: + pass + + try: + summary = post_data['summary'] + w.summary = summary + w.save() + except KeyError: + pass uploads.do_sync(w,post_data,quick=True) diff --git a/rowsandall_app/settings.py b/rowsandall_app/settings.py index a358260d..80e47a5f 100644 --- a/rowsandall_app/settings.py +++ b/rowsandall_app/settings.py @@ -329,6 +329,11 @@ RP3_REDIRECT_URI = CFG["rp3_redict_uri"] RP3_CLIENT_KEY = RP3_CLIENT_ID # Nielsen-Kellerman +NK_CLIENT_ID = CFG["nk_client_id"] +NK_CLIENT_SECRET = CFG["nk_client_secret"] +NK_REDIRECT_URI = CFG["nk_redirect_uri"] +NK_API_LOCATION = CFG["nk_api_location"] +NK_VIEWER_LOCATION = CFG["nk_viewer_location"] # Full Site URL SITE_URL = CFG['site_url'] diff --git a/static/img/NKLiNK.jpg b/static/img/NKLiNK.jpg new file mode 100644 index 00000000..38b9326b Binary files /dev/null and b/static/img/NKLiNK.jpg differ diff --git a/static/img/NK_white.png b/static/img/NK_white.png new file mode 100644 index 00000000..0c35d7d9 Binary files /dev/null and b/static/img/NK_white.png differ diff --git a/static/img/myimage.png b/static/img/myimage.png new file mode 100644 index 00000000..5c0211ff Binary files /dev/null and b/static/img/myimage.png differ