diff --git a/rowers/dataprep.py b/rowers/dataprep.py index 55da70ec..c91ea326 100644 --- a/rowers/dataprep.py +++ b/rowers/dataprep.py @@ -1853,9 +1853,16 @@ parsers = { 'nklinklogbook': NKLiNKLogbookParser, } -def parsenonpainsled(fileformat,f2,summary,startdatetime=''): +def parsenonpainsled(fileformat,f2,summary,startdatetime='',empowerfirmware=None,inboard=None,oarlength=None): try: - row = parsers[fileformat](f2) + if fileformat == 'nklinklogbook' and empowerfirmware is not None: + if inboard is not None and oarlength is not None: + row = NKLiNKLogbookParser(f2,firmware=empowerfirmware,inboard=inboard,oarlength=oarlength) + print(row,'aap') + else: + row = NKLiNKLogbookParser(f2) + else: + row = parsers[fileformat](f2) if startdatetime != '': row.rowdatetime = arrow.get(startdatetime).datetime hasrecognized = True @@ -1875,6 +1882,8 @@ def parsenonpainsled(fileformat,f2,summary,startdatetime=''): summary = row.allstats() except ZeroDivisionError: summary = '' + else: + fileformat = fileformat+'v'+str(empowerfirmware) # handle FIT if (fileformat == 'fit'): @@ -1889,13 +1898,14 @@ def parsenonpainsled(fileformat,f2,summary,startdatetime=''): return row,hasrecognized,summary,fileformat -def handle_nonpainsled(f2, fileformat, summary='',startdatetime=''): +def handle_nonpainsled(f2, fileformat, summary='',startdatetime='',empowerfirmware=None,impeller=False): oarlength = 2.89 inboard = 0.88 hasrecognized = False - impeller = False - row,hasrecognized,summary,fileformat = parsenonpainsled(fileformat,f2,summary,startdatetime=startdatetime) + + row,hasrecognized,summary,fileformat = parsenonpainsled(fileformat,f2,summary,startdatetime=startdatetime, + empowerfirmware=empowerfirmware) # Handle c2log if (fileformat == 'c2log' or fileformat == 'rowprolog'): @@ -1909,13 +1919,15 @@ def handle_nonpainsled(f2, fileformat, summary='',startdatetime=''): f2 = f2[:-4] + 'o.csv' row2 = rrdata(df = row.df) - if 'speedcoach2' in fileformat: + if 'speedcoach2' in fileformat or 'nklinklogbook' in fileformat: # impeller consistency impellerdata, consistent, ratio = row.impellerconsistent(threshold=0.3) if impellerdata and consistent: impeller = True if impellerdata and not consistent: row2.use_gpsdata() + if impeller: + row2.use_impellerdata() row2.write_csv(f2, gzip=True) @@ -1990,9 +2002,13 @@ def new_workout_from_file(r, f2, makeprivate=False, startdatetime='', notes='', + oarlockfirmware='', + inboard=None, + oarlength=None, + impeller=False, uploadoptions={'boattype':'1x','workouttype':'rower'}): message = "" - impeller = False + try: fileformat = get_file_type(f2) except (IOError,UnicodeDecodeError): @@ -2009,7 +2025,7 @@ def new_workout_from_file(r, f2, uploadoptions['fromuploadform'] = True bodyyaml = yaml.safe_dump(uploadoptions,default_flow_style=False) f_to_be_deleted = f2 - impeller = False + workoutsbox = Mailbox.objects.filter(name='workouts')[0] msg = Message(mailbox=workoutsbox, from_header=r.user.email, @@ -2095,7 +2111,9 @@ def new_workout_from_file(r, f2, f2, fileformat, startdatetime=startdatetime, - summary=summary + summary=summary, + empowerfirmware=oarlockfirmware, + impeller=impeller, ) if not f2: message = 'Something went wrong' diff --git a/rowers/management/commands/processemail.py b/rowers/management/commands/processemail.py index d9ba789c..8a7d758d 100644 --- a/rowers/management/commands/processemail.py +++ b/rowers/management/commands/processemail.py @@ -33,6 +33,7 @@ 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.models import User,VirtualRace,Workout @@ -213,6 +214,11 @@ class Command(BaseCommand): if user_is_not_basic(r.user): res = rp3stuff.get_rp3_workouts(r) + rowers = Rower.objects.filter(nk_auto_import=True) + for r in rowers: + if user_is_not_basic(r.user): + res = nkstuff.get_nk_workouts(r) + messages = Message.objects.filter(mailbox_id = workoutmailbox.id) message_ids = [m.id for m in messages] attachments = MessageAttachment.objects.filter( diff --git a/rowers/models.py b/rowers/models.py index 9256b262..1ce31758 100644 --- a/rowers/models.py +++ b/rowers/models.py @@ -1002,7 +1002,7 @@ class Rower(models.Model): 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) + nk_auto_import = models.BooleanField(default=False,verbose_name='NK LiNK auto import') trainingpeaks_auto_export = models.BooleanField(default=False) @@ -3852,6 +3852,7 @@ class RowerExportForm(ModelForm): 'polar_auto_import', 'c2_auto_export', 'c2_auto_import', + 'nk_auto_import', 'mapmyfitness_auto_export', 'runkeeper_auto_export', 'sporttracks_auto_export', diff --git a/rowers/nkimportutils.py b/rowers/nkimportutils.py new file mode 100644 index 00000000..581b9a4d --- /dev/null +++ b/rowers/nkimportutils.py @@ -0,0 +1,264 @@ +import pandas as pd +import datetime +from datetime import timedelta +from uuid import uuid4 + +from rowsandall_app.settings import UPLOAD_SERVICE_SECRET, UPLOAD_SERVICE_URL +from rowsandall_app.settings import NK_API_LOCATION + +import requests + +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 add_workout_from_data(userid,nkid,data,strokedata,source='nk',splitdata=None, + workoutsource='nklinklogbook'): + + csvfilename = 'media/{code}_{nkid}.csv.gz'.format( + nkid=nkid, + code = uuid4().hex[:16] + ) + + try: + userid=int(userid) + except TypeError: + userid = userid.id + + strokedata.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 + + totalDistance = totalDistanceGps + useImpeller = False + if speedInput: + totdalDistance = totalDistanceImp + useImpeller = True + + summary = get_nk_allstats(data,strokedata) + + speedInput = data['speedInput'] # 0 = GPS; 1 = Impeller + + # 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"] + try: + oarlockfirmware = oarlocksession["firmwareVersion"] + except KeyError: + oarlockfirmware = '' + else: + boatName = '' + oarLength = 289 + oarInboardLength = 88 + seatNumber = 1 + oarlockfirmware = '' + + 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, + 'oarlockfirmware':oarlockfirmware, + 'elapsedTime': elapsedTime/1000., # in seconds + 'totalDistance': totalDistance, + 'useImpeller': useImpeller + } + + 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_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 {} + +def strokeDataToDf(strokeData): + + 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',inplace=True) + + if not df2.empty: + #df2.set_index('timestamp',inplace=True) + 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') + + + + return df diff --git a/rowers/nkstuff.py b/rowers/nkstuff.py index 709e9db7..4a3dbe2d 100644 --- a/rowers/nkstuff.py +++ b/rowers/nkstuff.py @@ -22,7 +22,7 @@ queuehigh = django_rq.get_queue('low') from rowers.rower_rules import is_workout_user, ispromember from iso8601 import ParseError -from rowers.utils import myqueue,get_nk_summary, get_nk_allstats, get_nk_intervalstats,getdict +from rowers.utils import myqueue import rowers.mytypes as mytypes import gzip @@ -42,6 +42,7 @@ except ImportError: JSONDecodeError = ValueError from rowers.imports import * +from rowers.nkimportutils import * oauth_data = { 'client_id': NK_CLIENT_ID, @@ -209,7 +210,7 @@ def rower_nk_token_refresh(user): def make_authorization_url(request): return imports_make_authorization_url(oauth_data) -def get_nk_workout_list(user,fake=False): +def get_nk_workout_list(user,fake=False,startTime=0,endTime=0): r = Rower.objects.get(user=user) if (r.nktoken == '') or (r.nktoken is None): @@ -220,10 +221,12 @@ def get_nk_workout_list(user,fake=False): 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())) + if not endTime: + endTime = int(arrow.now().timestamp())*1000 + endTime = str(endTime) + if not startTime: + startTime = arrow.now()-timedelta(days=30) + startTime = str(int(startTime.timestamp())*1000) authorizationstring = str('Bearer ' + r.nktoken) headers = {'Authorization': authorizationstring, 'user-agent': 'sanderroosendaal', @@ -244,81 +247,6 @@ def get_nk_workout_list(user,fake=False): # -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): @@ -338,8 +266,6 @@ def get_workout(user,nkid): 'Content-Type': 'application/json', } - - # get strokes url = NK_API_LOCATION+"api/v1/sessions/strokes" response = requests.get(url,headers=headers,params=params) @@ -353,22 +279,7 @@ def get_workout(user,nkid): 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') + df = strokeDataToDf(strokeData) # get workout data timestampbegin = df['timestamp'].min() @@ -379,6 +290,7 @@ def get_workout(user,nkid): 'startTime':timestampbegin-1, 'endTime': timestampend+1, } + response = requests.get(url, headers=headers,params=params) if response.status_code != 200: diff --git a/rowers/tasks.py b/rowers/tasks.py index 84957b55..c9dab841 100644 --- a/rowers/tasks.py +++ b/rowers/tasks.py @@ -111,7 +111,10 @@ def safetimedelta(x): siteurl = SITE_URL -from rowers.utils import get_nk_summary, get_nk_allstats, get_nk_intervalstats,getdict +from rowers.nkimportutils import ( + get_nk_summary, get_nk_allstats, get_nk_intervalstats,getdict,strokeDataToDf, + add_workout_from_data + ) # testing task @@ -2958,6 +2961,7 @@ def handle_rp3_async_workout(userid,rp3token,rp3id,startdatetime,max_attempts,de @app.task def handle_nk_async_workout(alldata,userid,nktoken,nkid,delaysec,defaulttimezone,debug=False,**kwargs): time.sleep(delaysec) + data = alldata[nkid] params = { @@ -2981,22 +2985,8 @@ def handle_nk_async_workout(alldata,userid,nktoken,nkid,delaysec,defaulttimezone 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') + df = strokeDataToDf(strokeData) # get workout data timestampbegin = df['timestamp'].min() @@ -3009,60 +2999,7 @@ def handle_nk_async_workout(alldata,userid,nktoken,nkid,delaysec,defaulttimezone 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 + workoutid,error = add_workout_from_data(userid,nkid,data,df) if debug: engine = create_engine(database_url_debug, echo=False) @@ -3081,9 +3018,12 @@ def handle_nk_async_workout(alldata,userid,nktoken,nkid,delaysec,defaulttimezone conn.close() parkedids = [] - with open('nkblocked.json','r') as nkblocked: - jsondata = json.load(nkblocked) - parkedids = jsondata['ids'] + try: + with open('nkblocked.json','r') as nkblocked: + jsondata = json.load(nkblocked) + parkedids = jsondata['ids'] + except FileNotFoundError: + pass newparkedids = [id for id in parkedids if id != newnkid] with open('nkblocked.json','wt') as nkblocked: diff --git a/rowers/templates/menu_workout.html b/rowers/templates/menu_workout.html index 51aefb8b..382f8d71 100644 --- a/rowers/templates/menu_workout.html +++ b/rowers/templates/menu_workout.html @@ -335,7 +335,7 @@  Remove Power Data - {% if 'speedcoach2' in workout.workoutsource %} + {% if 'speedcoach2' in workout.workoutsource or 'nklinklogbook' in workout.workoutsource %}
  • diff --git a/rowers/tests/mocks.py b/rowers/tests/mocks.py index e3f34f70..8c30723b 100644 --- a/rowers/tests/mocks.py +++ b/rowers/tests/mocks.py @@ -623,6 +623,12 @@ def mocked_requests(*args, **kwargs): with open('rowers/tests/testdata/nk_list.json','r') as infile: nkworkoutlist = json.load(infile) + with open('rowers/tests/testdata/example-session-strokes-with-impeller-data.json','r') as infile: + nkimpellerstrokedata = json.load(infile) + + with open('rowers/tests/testdata/example-session-with-impeller-data.json','r') as infile: + nkimpellerworkoutlist = json.load(infile) + polar_json = { 'available-user-data': [] } @@ -960,10 +966,14 @@ def mocked_requests(*args, **kwargs): } return MockResponse(json_data,200) if nkstrokestester.match(args[0]): - + params = kwargs.pop('params',{}) + if 'sessionIds' in params and params['sessionIds'] == '404': + return MockResponse(nkimpellerstrokedata, 200) return MockResponse(nkstrokedata,200) if nkworkoutlisttester.match(args[0]): - + params = kwargs.pop('params',{}) + if 'startTime' in params and params['startTime'] == 1267136372998: + return MockResponse(nkimpellerworkoutlist,200) return MockResponse(nkworkoutlist,200) diff --git a/rowers/tests/test_imports.py b/rowers/tests/test_imports.py index c61ffa8d..de36cc85 100644 --- a/rowers/tests/test_imports.py +++ b/rowers/tests/test_imports.py @@ -424,9 +424,44 @@ class NKObjects(DjangoTestCase): self.assertEqual(response.status_code,200) + @patch('rowers.nkimportutils.requests.session', side_effect=mocked_session) + @patch('rowers.tasks.requests.get', side_effects=mocked_requests) + def test_handle_nk_get_workouts(self, mocked_session, mock_get): + with open('rowers/tests/testdata/nk_list.json','r') as f: + data = json.load(f) + + with open('rowers/tests/testdata/nk_strokes.json','r') as f: + strokeData = json.load(f) + + alldata = {} + for item in data: + alldata[item['id']] = item + + result = rowers.nkstuff.rower_nk_token_refresh(self.u) + + res = tasks.handle_nk_async_workout( + alldata,self.r.user.id,self.r.nktoken,469,0,'UTC' + ) + + @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) + def test_nk_get_workouts(self, mock_get, mockpost): + result = rowers.nkstuff.nk_open(self.u) + self.assertEqual(result,"TA3n1vrNjuQJWw0TdCDHnjSmrjIPULhTlejMIWqq") + response = self.c.get('/rowers/workout/nkimport/all/',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.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, @@ -441,6 +476,34 @@ class NKObjects(DjangoTestCase): self.assertEqual(response.status_code, 200) + w = Workout.objects.get(id=1) + #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.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) + response = self.c.get('/rowers/workout/nkimport/404',follow=True) + + self.assertRedirects(response, + expected_url='/rowers/workout/'+encoded1+'/edit/', + status_code=301,target_status_code=200) + + self.assertEqual(response.status_code, 200) + + response = self.c.get('/rowers/workout/'+encoded1+'/otwuseimpeller/', follow=True) + self.assertRedirects(response, + expected_url='/rowers/workout/'+encoded1+'/edit/', + status_code=302,target_status_code=200) + + w = Workout.objects.get(id=1) + #self.assertTrue(w.impeller) #@pytest.mark.django_db diff --git a/rowers/tests/test_races.py b/rowers/tests/test_races.py index 2f1a3114..fe78d0d9 100644 --- a/rowers/tests/test_races.py +++ b/rowers/tests/test_races.py @@ -15,6 +15,8 @@ import rowers.tasks as tasks from rowers.views.racesviews import * from rowers.utils import calculate_age +# to do - add test for manual form with image + @override_settings(TESTING=True) class ChallengesTest(TestCase): def setUp(self): diff --git a/rowers/tests/test_unit_tests.py b/rowers/tests/test_unit_tests.py index bc1acac7..b481530e 100644 --- a/rowers/tests/test_unit_tests.py +++ b/rowers/tests/test_unit_tests.py @@ -12,6 +12,42 @@ nu = datetime.datetime.now() # interactive plots from rowers import interactiveplots from rowers import dataprep +from rowers.views.workoutviews import get_video_id + +class OtherUnitTests(TestCase): + def test_get_video_id(self): + url1 = 'http://youtu.be/_lOT2p_FCvA' + url2 = 'www.youtube.com/watch?v=_lOT2p_FCvA&feature=feedu' + url3 = 'http://www.youtube.com/embed/_lOT2p_FCvA' + url4 = 'http://www.youtube.com/v/_lOT2p_FCvA?version=3&hl=en_US' + url5 = 'https://www.youtube.com/watch?v=rTHlyTphWP0&index=6&list=PLjeDyYvG6-40qawYNR4juzvSOg-ezZ2a6' + url6 = 'youtube.com/watch?v=_lOT2p_FCvA' + expected = '_lOT2p_FCvA' + expected2 = 'rTHlyTphWP0' + + result = get_video_id(url1) + + self.assertEqual(result,expected) + + result = get_video_id(url2) + + self.assertEqual(result,expected) + + result = get_video_id(url3) + + self.assertEqual(result,expected) + + result = get_video_id(url4) + + self.assertEqual(result,expected) + + result = get_video_id(url5) + + self.assertEqual(result,expected2) + + result = get_video_id(url6) + + self.assertEqual(result,expected) class DataPrepTests(TestCase): def setUp(self): diff --git a/rowers/uploads.py b/rowers/uploads.py index 23546796..eb57dbb0 100644 --- a/rowers/uploads.py +++ b/rowers/uploads.py @@ -548,6 +548,19 @@ def do_sync(w,options, quick=False): except KeyError: pass + try: + if options['inboard'] != 0 and options['inboard'] != '': + w.inboard = options['inboard'] + except KeyError: + pass + + try: + if options['oarlength'] != 0 and options['oarlength'] != '': + w.oarlength = options['oarlength'] + 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 ad8dfe03..e36d55e0 100644 --- a/rowers/urls.py +++ b/rowers/urls.py @@ -568,6 +568,7 @@ urlpatterns = [ 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/all/$',views.workout_getnkworkout_all,name='workout_getnkworkout_all'), re_path(r'^workout/rp3import/(?P\d+)/$',views.workout_getrp3importview, name='workout_getrp3importview'), diff --git a/rowers/utils.py b/rowers/utils.py index 062f10c2..79b38130 100644 --- a/rowers/utils.py +++ b/rowers/utils.py @@ -1108,126 +1108,3 @@ def strfdelta(tdelta): ) 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 e3120a29..e644e53a 100644 --- a/rowers/views/importviews.py +++ b/rowers/views/importviews.py @@ -836,14 +836,14 @@ def workout_getnkworkout_all(request): @login_required() @permission_required('rower.is_coach',fn=get_user_by_userid, raise_exception=True) -def workout_nkimport_view(request,userid=0): +def workout_nkimport_view(request,userid=0,startTime=0,endTime=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) + res = nkstuff.get_nk_workout_list(request.user,startTime=startTime,endTime=endTime) if (res.status_code != 200): if (res.status_code == 401): @@ -882,6 +882,9 @@ def workout_nkimport_view(request,userid=0): 'start_date':arrow.get(item['startTime']), } for item in res.json()] + #for item in res.json(): + # print(item['startTime'],arrow.get(item['startTime']),item['name']) + workouts = [] for item in res.json(): diff --git a/rowers/views/paymentviews.py b/rowers/views/paymentviews.py index 8dd72317..51bb56cd 100644 --- a/rowers/views/paymentviews.py +++ b/rowers/views/paymentviews.py @@ -636,7 +636,6 @@ def upgrade_checkouts_view(request): @login_required() def downgrade_checkouts_view(request): - print('aap') if not PAYMENT_PROCESSING_ON: url = reverse('promembership') return HttpResponseRedirect(url) diff --git a/rowers/views/userviews.py b/rowers/views/userviews.py index 65d4b24d..2d1d85a6 100644 --- a/rowers/views/userviews.py +++ b/rowers/views/userviews.py @@ -336,6 +336,8 @@ def rower_exportsettings_view(request,userid=0): 'strava_auto_import':'stravatoken', 'strava_auto_delete':'stravatoken', 'trainingpeaks_auto_export':'tptoken', + 'rp3_auto_import':'rp3token', + 'nk_auto_import':'nktoken' } r = getrequestrowercoachee(request,userid=userid) if request.method == 'POST': diff --git a/rowers/views/workoutviews.py b/rowers/views/workoutviews.py index b1f1ef56..c26b1ac7 100644 --- a/rowers/views/workoutviews.py +++ b/rowers/views/workoutviews.py @@ -17,7 +17,7 @@ import rowers.utils as utils from urllib.parse import urlparse, parse_qs from json.decoder import JSONDecodeError -def default(o): +def default(o): # pragma: no cover if isinstance(o, numpy.int64): return int(o) if isinstance(o, numpy.int32): return int(o) raise TypeError @@ -52,7 +52,7 @@ def get_video_id(url): return query.path.split('/')[2] elif 'youtu.be' in query.hostname: return query.path[1:] - else: + else: # pragma: no cover raise ValueError # Show a video compared with data @@ -60,7 +60,7 @@ def workout_video_view_mini(request,id=''): try: id = encoder.decode_hex(id) analysis = VideoAnalysis.objects.get(id=id) - except VideoAnalysis.DoesNotExist: + except VideoAnalysis.DoesNotExist: # pragma: no cover raise Http404("Video Analysis does not exist") w = analysis.workout @@ -173,7 +173,7 @@ def workout_video_view(request,id=''): try: id = encoder.decode_hex(id) analysis = VideoAnalysis.objects.get(id=id) - except VideoAnalysis.DoesNotExist: + except VideoAnalysis.DoesNotExist: # pragma: no cover raise Http404("Video Analysis does not exist") w = analysis.workout @@ -199,7 +199,7 @@ def workout_video_view(request,id=''): video_id = form.cleaned_data['url'] try: video_id = get_video_id(form.cleaned_data['url']) - except (TypeError,ValueError): + except (TypeError,ValueError): # pragma: no cover pass delay = form.cleaned_data['delay'] metricsgroups = metricsform.cleaned_data['groups'] @@ -314,7 +314,7 @@ def workout_video_create_view(request,id=0): url = reverse('workout_video_view', kwargs={'id':encoder.encode_hex(analysis.id)}) return HttpResponseRedirect(url) - except IntegrityError: + except IntegrityError: # pragma: no cover messages.error(request,'You cannot save two video analysis with the same YouTube video and Workout. Redirecting to your existing analysis') analysis = VideoAnalysis.objects.filter(workout=w,video_id=video_id) if analysis: @@ -577,7 +577,7 @@ def addmanual_view(request,raceid=0): if form.is_valid() and metricsform.is_valid(): # Get values from form name = form.cleaned_data['name'] - if name == '': + if name == '': # pragma: no cover name = 'Manual Entry' date = form.cleaned_data['date'] starttime = form.cleaned_data['starttime'] @@ -590,7 +590,7 @@ def addmanual_view(request,raceid=0): rpe = form.cleaned_data['rpe'] if not rpe: rpe = -1 - except KeyError: + except KeyError: # pragma: no cover rpe = -1 notes = form.cleaned_data['notes'] thetimezone = form.cleaned_data['timezone'] @@ -601,25 +601,25 @@ def addmanual_view(request,raceid=0): try: ps = form.cleaned_data['plannedsession'] - except KeyError: + except KeyError: # pragma: no cover ps = None try: boattype = request.POST['boattype'] - except KeyError: + except KeyError: # pragma: no cover boattype = '1x' try: privacy = request.POST['privacy'] - except KeyError: + except KeyError: # pragma: no cover privacy = 'visible' try: rankingpiece = form.cleaned_data['rankingpiece'] - except KeyError: + except KeyError: # pragma: no cover rankingpiece = False try: duplicate = form.cleaned_data['duplicate'] - except KeyError: + except KeyError: # pragma: no cover duplicate = False if private: @@ -654,7 +654,7 @@ def addmanual_view(request,raceid=0): - if message: + if message: # pragma: no cover messages.error(request,message) if id: @@ -871,7 +871,7 @@ def workouts_join_view(request,userid=0): setprivate=set_private, killparents=killparents) - if message: + if message: # pragma: no cover messages.error(request,message) url = reverse(r.defaultlandingpage, @@ -920,13 +920,13 @@ def video_selectworkout(request,userid=0,teamid=0): options['userid'] = userid try: workouttypes = options['workouttypes'] - except KeyError: + except KeyError: # pragma: no cover workouttypes = ['rower','dynamic','slides'] try: modalities = options['modalities'] modality = modalities[0] - except KeyError: + except KeyError: # pragma: no cover modalities = [m[0] for m in mytypes.workouttypes_ordered.items()] modality = 'all' @@ -934,11 +934,11 @@ def video_selectworkout(request,userid=0,teamid=0): try: rankingonly = options['rankingonly'] - except KeyError: + except KeyError: # pragma: no cover rankingonly = False query = request.GET.get('q') - if query: + if query: # pragma: no cover query_list = query.split() workouts = workouts.filter( reduce(operator.and_, @@ -1005,7 +1005,7 @@ def video_selectworkout(request,userid=0,teamid=0): startdate = datetime.datetime.combine(startdate,datetime.time()) enddate = datetime.datetime.combine(enddate,datetime.time(23,59,59)) - if enddate < startdate: + if enddate < startdate: # pragma: no cover s = enddate enddate = startdate startdate = s @@ -1168,7 +1168,7 @@ def workouts_join_select(request, if enddatestring: enddate = iso8601.parse_date(enddatestring) - if enddate < startdate: + if enddate < startdate: # pragma: no cover s = enddate enddate = startdate startdate = s @@ -1279,7 +1279,7 @@ def remove_power_view(request,id=0): try: os.remove('media/cpdata_{id}.parquet.gz'.format(id=workout.id)) - except OSError: + except OSError: # pragma: no cover pass f = workout.csvfilename @@ -1412,7 +1412,7 @@ def team_comparison_select(request, if enddatestring: enddate = iso8601.parse_date(enddatestring) - if enddate < startdate: + if enddate < startdate: # pragma: no cover s = enddate enddate = startdate startdate = s @@ -1445,7 +1445,7 @@ def team_comparison_select(request, workouts = workouts.exclude(rankingpiece=False) query = request.GET.get('q') - if query: + if query: # pragma: no cover query_list = query.split() workouts = workouts.filter( reduce(operator.and_, @@ -2034,7 +2034,7 @@ def workouts_view(request,message='',successmessage='', #enddate = enddate+datetime.timedelta(days=1) - if enddate < startdate: + if enddate < startdate: # pragma: no cover s = enddate enddate = startdate startdate = s @@ -4859,8 +4859,6 @@ def workout_upload_api(request): message = {'status':'false','message':'this view cannot be accessed through GET'} return JSONResponse(status=403,data=message) - - # test if JSON try: json_data = json.loads(request.body) @@ -4907,38 +4905,24 @@ def workout_upload_api(request): message = {'status':'false','message':'could not find file'} return JSONResponse(status=400,data=message) - try: - stravaid = post_data['stravaid'] - except KeyError: - stravaid = '' + # sync related IDs + stravaid = post_data.get('stravaid','') + c2id = post_data.get('c2id','') + nkid = post_data.get('nkid','') + rp3id = post_data.get('nkid','') + garminid = post_data.get('garminid',0) - try: - c2id = post_data['c2id'] - except KeyError: - c2id = '' + startdatetime = post_data.get('startdatetime','') + oarlockfirmware = post_data.get('oarlockfirmware',None) + inboard = post_data.get('inboard',None) + oarlength = post_data.get('oarlength',None) + useImpeller = post_data.get('useImpeller',False) - try: - nkid = post_data['nkid'] - except KeyError: - nkid = '' + totalDistance = post_data.get('totalDistance',None) + elapsedTime = post_data.get('elapsedTime',None) + summary = post_data.get('summary',None) - try: - rp3id = post_data['rp3id'] - except KeyError: - rp3id = '' - - - try: - garminid = post_data['garminid'] - except KeyError: - garminid = 0 - - try: - startdatetime = post_data['startdatetime'] - except KeyError: - startdatetime = '' - r = None if form.is_valid(): t = form.cleaned_data['title'] @@ -5011,6 +4995,10 @@ def workout_upload_api(request): notes=notes, uploadoptions=post_data, startdatetime=startdatetime, + oarlockfirmware=oarlockfirmware, + inboard=inboard, + oarlength=oarlength, + impeller=useImpeller, ) if id == 0: @@ -5033,21 +5021,23 @@ 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'] + if inboard: w.inboard = inboard + w.save() + if oarlength: w.oarlength = oarlength w.save() - except KeyError: - pass - try: - summary = post_data['summary'] + if totalDistance: + w.distance = totalDistance + w.save() + + if elapsedTime: + w.duration = totaltime_sec_to_string(elapsedTime) + + if summary: w.summary = summary w.save() - except KeyError: - pass uploads.do_sync(w,post_data,quick=True)