diff --git a/garminscript.py b/garminscript.py new file mode 100644 index 00000000..a946efc8 --- /dev/null +++ b/garminscript.py @@ -0,0 +1,83 @@ +from requests_oauthlib import OAuth1Session, OAuth1 +import hmac +import hashlib + +import requests +from urllib.parse import quote_plus as rawurlencode + +import logging + +logging.basicConfig() +logging.getLogger().setLevel(logging.DEBUG) +requests_log = logging.getLogger("requests.packages.urllib3") +requests_log.setLevel(logging.DEBUG) +requests_log.propagate = True + + +payload = { + 'workoutName': '30min at threshold', + 'sport': 'GENERIC', + 'description': 'Uploaded from Rowsandall.com', + 'estimatedDurationInSecs': 1800, + 'estimatedDistanceInMeters': 6936, + 'workoutProvider': 'Rowsandall.com', + 'workoutSourceId': 'Rowsandall.com', + 'steps': [ + { + 'type': 'Step', 'stepOrder': 0, + 'repeatType': 'Step', + 'repeatValue': 1, + 'intensity': 'Active', + 'description': '0', + 'durationType': 'TIME', + 'durationValue': 1800, + 'durationValueType': '', + 'targetType': 'Power', + 'targetValue': 1226, + 'targetValueLow': 0, 'targetValueHigh': (0,) + } + ] + } + +payload = {} + +oauth_consumer_key = b'ca29ba5e-6868-4468-987d-4ee60a1f04bf' +oauth_consumer_secret = b'SKRqjML9mOBV7BcPpN7LsbuDNDtvLOvRiyo' +oauth_token = b'79454eab-bf82-4329-9de2-82a6bd911498' +oauth_token_secret = b'DihdHJ2ThEdbsyoStpPTEmYh5F52L697HhD' + +authheaders = OAuth1(client_key=oauth_consumer_key, + client_secret=oauth_consumer_secret, + resource_owner_key=oauth_token, + resource_owner_secret=oauth_token_secret, + signature_method='HMAC-SHA1', + #encoding='base64' + ) + + +url = 'https://apis.garmin.com/training-api/workout/' + +response = requests.post(url,payload,auth=authheaders) + +# build base_string +base_string1 = b'POST&https%3A%2F%2Fapis.garmin.com%2Ftraining-api%2Fworkout%2F&description%3DUploaded%2520from%2520Rowsandall.com%26estimatedDistanceInMeters%3D6936%26estimatedDurationInSecs%3D1800%26oauth_consumer_key%3Dca29ba5e-6868-4468-987d-4ee60a1f04bf%26oauth_nonce%3D163208869057942765101620205416%26oauth_signature_method%3DHMAC-SHA1%26oauth_timestamp%3D1620205416%26oauth_token%3D79454eab-bf82-4329-9de2-82a6bd911498%26oauth_version%3D1.0%26sport%3DGENERIC%26steps%3Ddescription%26steps%3DdurationType%26steps%3DdurationValue%26steps%3DdurationValueType%26steps%3Dintensity%26steps%3DrepeatType%26steps%3DrepeatValue%26steps%3DstepOrder%26steps%3DtargetType%26steps%3DtargetValue%26steps%3DtargetValueHigh%26steps%3DtargetValueLow%26steps%3Dtype%26workoutName%3D30min%2520at%2520threshold%26workoutProvider%3DRowsandall.com%26workoutSourceId%3DRowsandall.com' + +base_stringa = b'POST&https%3A%2F%2Fapis.garmin.com%2Ftraining-api%2Fworkout%2F&oauth_consumer_key%3Dca29ba5e-6868-4468-987d-4ee60a1f04bf%26oauth_nonce%3D90559685229655402871620208938%26oauth_signature_method%3DHMAC-SHA1%26oauth_timestamp%3D1620208938%26oauth_token%3D79454eab-bf82-4329-9de2-82a6bd911498%26oauth_version%3D1.0' +base_string2 = b'POST&https%3A%2F%2Fapis.garmin.com%2Ftraining-api%2Fworkout%2F&oauth_consumer_key%3Dca29ba5e-6868-4468-987d-4ee60a1f04bf%26oauth_nonce%3DIyk9Ambokd2%26oauth_signature_method%3DHMAC-SHA1%26oauth_timestamp%3D1620138340%26oauth_token%3D673806b7-aa7b-4064-8290-2dd1b0236ae6%26oauth_version%3D1.0' + + +base_string3 = b'POST&https%3A%2F%2Fapis.garmin.com%2Ftraining-api%2Fworkout&oauth_consumer_key%3Dca29ba5e-6868-4468-987d-4ee60a1f04bf%26oauth_nonce%3D90559685229655402871620208938%26oauth_signature_method%3DHMAC-SHA1%26oauth_timestamp%3D1620208938%26oauth_token%3D79454eab-bf82-4329-9de2-82a6bd911498%26oauth_version%3D1.0' + +auth = b'&'.join([oauth_consumer_secret,oauth_token_secret]) +signingKey = bytes(rawurlencode(oauth_consumer_secret) + "&" + rawurlencode(oauth_token_secret),'utf-8') +print(auth) +print(signingKey) +print(base_string3) + +sig = hmac.new(signingKey, base_string3, hashlib.sha1) + +print(response.status_code) +print(response.text) + + +print(sig.hexdigest().encode()) diff --git a/rowers/c2stuff.py b/rowers/c2stuff.py index 496cc8d7..28084073 100644 --- a/rowers/c2stuff.py +++ b/rowers/c2stuff.py @@ -238,8 +238,8 @@ def create_async_workout(alldata,user,c2id): duration = dataprep.totaltime_sec_to_string(totaltime) try: - timezone_str = tz(data['timezone']) - except: + timezone_str = data['timezone'] + except: # pragma: no cover timezone_str = 'UTC' workoutdate = startdatetime.astimezone( @@ -848,6 +848,9 @@ def get_workout(user,c2id,do_async=False): data = s.json()['data'] splitdata = None + #with open('c2temp.json','w') as f: + # f.write(json.dumps(s.json())) + # print(s.json()) if 'workout' in data: if 'splits' in data['workout']: # pragma: no cover @@ -1076,20 +1079,21 @@ def add_workout_from_data(user,importid,data,strokedata, except: # pragma: no cover comments = ' ' - try: - thetimezone = tz(data['timezone']) - except: - thetimezone = 'UTC' + + thetimezone = pytz.timezone(data['timezone']) + r = Rower.objects.get(user=user) try: rowdatetime = iso8601.parse_date(data['date_utc']) + thetimezone = 'UTC' except KeyError: # pragma: no cover rowdatetime = iso8601.parse_date(data['start_date']) + rowdatetime = rowdatetime.make_aware(thetimezone) except ParseError: # pragma: no cover rowdatetime = iso8601.parse_date(data['date']) - + rowdatetime = rowdatetime.make_aware(thetimezone) try: @@ -1119,7 +1123,9 @@ def add_workout_from_data(user,importid,data,strokedata, cum_time = res[0] lapidx = res[1] - starttimeunix = starttimeunix - cum_time.max() + totaltime = data['time']/10. + starttimeunix = starttimeunix - totaltime + unixtime = cum_time+starttimeunix # unixtime[0] = starttimeunix @@ -1161,7 +1167,6 @@ def add_workout_from_data(user,importid,data,strokedata, velo = 1000./pace pace = 500./velo - # save csv # Create data frame with all necessary data to write to csv df = pd.DataFrame({'TimeStamp (sec)':unixtime, @@ -1223,15 +1228,18 @@ def add_workout_from_data(user,importid,data,strokedata, w = Workout.objects.get(id=id) + local_tz = pytz.timezone(data['timezone']) +# local_tz = pytz.timezone(thetimezone) + + w.startdatetime = w.startdatetime.astimezone(local_tz) + w.starttime = w.startdatetime.strftime('%H:%M:%S') + w.timezone = local_tz w.duration = dataprep.totaltime_sec_to_string(totaltime) w.distance = totaldist - w.startdatetime = rowdatetime - w.starttime = rowdatetime.time() - w.date = rowdatetime.date() + w.save() - return id,message diff --git a/rowers/dataprep.py b/rowers/dataprep.py index 790a83f1..2711a35d 100644 --- a/rowers/dataprep.py +++ b/rowers/dataprep.py @@ -1692,6 +1692,7 @@ def save_workout_database(f2, r, dosmooth=True, workouttype='rower', + try: latavg = row.df[' latitude'].mean() lonavg = row.df[' longitude'].mean() @@ -1717,6 +1718,7 @@ def save_workout_database(f2, r, dosmooth=True, workouttype='rower', except KeyError: timezone_str = r.defaulttimezone + duration = totaltime_sec_to_string(totaltime) workoutdate = workoutstartdatetime.astimezone( @@ -1785,6 +1787,8 @@ def save_workout_database(f2, r, dosmooth=True, workouttype='rower', except ValidationError: return (0,'Unable to create your workout') + + if privacy == 'visible': ts = Team.objects.filter(rower=r) for t in ts: @@ -2013,6 +2017,16 @@ def new_workout_from_file(r, f2, return -1, message, f2 # Some people try to upload Concept2 logbook summaries + if fileformat == 'imageformat': # pragma: no cover + os.remove(f2) + message = "You cannot upload image files here" + return (0, message, f2) + + if fileformat == 'json': # pragma: no cover + os.remove(f2) + message = "JSON format not supported in direct upload" + return (0, message, f2) + if fileformat == 'c2log': os.remove(f2) message = "This summary does not contain stroke data. Use the files containing stroke by stroke data." @@ -2435,7 +2449,7 @@ def getsmallrowdata_db(columns, ids=[], doclean=True,workstrokesonly=True,comput else: try: df = pd.read_parquet(csvfilenames[0],columns=columns) - except (OSError,ArrowInvalid): + except (OSError,ArrowInvalid,IndexError): rowdata,row = getrowdata(id=ids[0]) if rowdata and len(rowdata.df): # pragma: no cover data = dataprep(rowdata.df,id=ids[0],bands=True,otwpower=True,barchart=True) diff --git a/rowers/forms.py b/rowers/forms.py index c1957690..76d00219 100644 --- a/rowers/forms.py +++ b/rowers/forms.py @@ -994,32 +994,6 @@ class PowerIntervalUpdateForm(forms.Form): activeminutesmin = forms.IntegerField(required=False,initial=0,widget=forms.HiddenInput()) activeminutesmax = forms.IntegerField(required=False,initial=0,widget=forms.HiddenInput()) -# Form used to update interval stats -class IntervalUpdateForm(forms.Form): - - def __init__(self, *args, **kwargs): - typechoices = ( - (1,'single time'), - (2,'single distance'), - (3,'rest (time based)'), - (3,'rest (distance based)'), - (4,'work (time based)'), - (5,'work (distance based)'), - ) - aantal = int(kwargs.pop('aantal')) - super(IntervalUpdateForm, self).__init__(*args, **kwargs) - - for i in range(aantal): - self.fields['intervalt_%s' % i] = forms.DurationField(label='Time '+str(i+1)) - self.fields['intervald_%s' % i] = forms.IntegerField(label='Distance '+str(i+1)) - self.fields['type_%s' % i] = forms.ChoiceField(choices=typechoices, - required=True, - initial=4, - label = 'Type '+str(i+1)) - self.fields['intervalt_%s' % i].widget.attrs['style'] = 'width:76px; height: 16px;' - self.fields['intervald_%s' % i].widget.attrs['style'] = 'width:76px; height: 16px;' - self.fields['type_%s' % i].widget.attrs['style'] = 'width:156px; height: 22px;' - self.fields['intervald_%s' % i].widget = forms.TimeInput(format='%H:%M:%S.%f') boattypes = mytypes.boattypes workouttypes = mytypes.workouttypes diff --git a/rowers/garmin_stuff.py b/rowers/garmin_stuff.py index 16cfbc25..6d255b64 100644 --- a/rowers/garmin_stuff.py +++ b/rowers/garmin_stuff.py @@ -2,6 +2,7 @@ from rowers.imports import * import datetime import requests +from requests import Session, Request from requests_oauthlib import OAuth1,OAuth1Session from requests_oauthlib.oauth1_session import TokenRequestDenied from requests import Request, Session @@ -302,27 +303,17 @@ def ps_to_garmin(ps,r): lijst.append(gstep) payload['steps'] = lijst + url = 'https://apis.garmin.com/training-api/workout/' garmin = OAuth1Session(oauth_data['client_id'], client_secret=oauth_data['client_secret'], resource_owner_key=r.garmintoken, resource_owner_secret=r.garminrefreshtoken, - signature_method='HMAC-SHA1' + signature_method='HMAC-SHA1', + encoding='base64' ) - url = 'https://apis.garmin.com/training-api/workout/' - - - garminauth = OAuth1( - client_key=oauth_data['client_id'], - client_secret=oauth_data['client_secret'], - resource_owner_key=r.garmintoken, - resource_owner_secret=r.garminrefreshtoken, - signature_method='HMAC-SHA1' - ) - - response = garmin.post(url,data=payload) #POST /training-api/workout?undefined HTTP/1.1 diff --git a/rowers/interactiveplots.py b/rowers/interactiveplots.py index de8ef74c..56c2ac42 100644 --- a/rowers/interactiveplots.py +++ b/rowers/interactiveplots.py @@ -571,7 +571,6 @@ def interactive_activitychart(workouts,startdate,enddate,stack='type',toolbar_lo while d<=enddate: dd = d.strftime('%d') - if totaldays<30: dates.append(d.strftime('%m/%d')) dates_sorting.append(d.strftime('%Y/%m/%d')) @@ -582,7 +581,10 @@ def interactive_activitychart(workouts,startdate,enddate,stack='type',toolbar_lo rscores.append(0) trimps.append(0) links.append('') - types.append('rower') + try: + types.append(types[0]) + except IndexError: + types.append('rower') try: rowers.append(rowers[0]) diff --git a/rowers/models.py b/rowers/models.py index 6f73da0b..efac1c2d 100644 --- a/rowers/models.py +++ b/rowers/models.py @@ -3158,7 +3158,7 @@ class Workout(models.Model): timezone = models.CharField(default='UTC', choices=timezones, max_length=100) - distance = models.IntegerField(default=0,blank=True) + distance = models.IntegerField(default=0) duration = models.TimeField(blank=True) dragfactor = models.IntegerField(default=0,blank=True) diff --git a/rowers/mytypes.py b/rowers/mytypes.py index 1716d023..89ddb6af 100644 --- a/rowers/mytypes.py +++ b/rowers/mytypes.py @@ -21,7 +21,7 @@ workouttypes_ordered = collections.OrderedDict({ 'c-boat':'Dutch C boat', 'churchboat':'Finnish Church boat', 'Ride':'Ride', - 'Bike':'Bike', + 'bike':'Bike', 'Run':'Run', 'NordicSki':'NordicSki', 'Swim':'Swim', @@ -36,7 +36,7 @@ workouttypes_ordered = collections.OrderedDict({ 'Kayaking':'Kayaking', 'Workout':'Workout', 'Yoga':'Yoga', - 'bike':'Bike', +# 'bike':'Bike', 'other':'Other', } ) @@ -51,7 +51,7 @@ stravacollection = ( ('water','Rowing'), ('rower','Rowing'), ('skierg','NordicSki'), - ('Bike','Ride'), + ('bike','Ride'), ('bikeerg','Ride'), ('dynamic','Rowing'), ('slides','Rowing'), @@ -84,7 +84,7 @@ garmincollection = ( ('water','ROWING'), ('rower','INDOOR_ROWING'), ('skierg','CROSS_COUNTRY_SKIING'), - ('Bike','ROAD_BIKING'), + ('bike','ROAD_BIKING'), ('bikeerg','INDOOR_CYCLING'), ('dynamic','INDOOR_ROWING'), ('slides','INDOOR_ROWING'), @@ -117,7 +117,7 @@ fitcollection = ( ('water','rowing'), ('rower','rowing'), ('skierg','cross_country_skiing'), -('Bike','cycling'), +('bike','cycling'), ('bikeerg','cycling'), ('dynamic','rowing'), ('slides','rowing'), @@ -153,7 +153,7 @@ stcollection = ( ('water','Rowing'), ('rower','Rowing'), ('skierg','Skiing:Nordic'), - ('Bike','Cycling'), + ('bike','Cycling'), ('bikeerg','Cycling'), ('dynamic','Rowing'), ('slides','Rowing'), @@ -187,7 +187,7 @@ rkcollection = ( ('water','Rowing'), ('rower','Rowing'), ('skierg','Cross-Country Skiing'), - ('Bike','Cycling'), + ('bike','Cycling'), ('bikeerg','Cycling'), ('dynamic','Rowing'), ('slides','Rowing'), @@ -220,7 +220,7 @@ polarcollection = ( ('water','Rowing'), ('rower','Rowing'), ('skierg','Skiing'), - ('Bike','Cycling'), + ('bike','Cycling'), ('bikeerg','Cycling'), ('dynamic','Rowing'), ('slides','Rowing'), @@ -253,7 +253,7 @@ tpcollection = ( ('water','rowing'), ('rower','rowing'), ('skierg','xc-ski'), - ('Bike','bike'), + ('bike','bike'), ('Bikeerg','bike'), ('dynamic','rowing'), ('slides','rowing'), @@ -286,7 +286,7 @@ c2collection = ( ('water','water'), ('rower','rower'), ('skierg','skierg'), - ('Bike','bike'), + ('bike','bike'), ('bikeerg','bike'), ('dynamic','dynamic'), ('slides','slides'), diff --git a/rowers/rows.py b/rowers/rows.py index b5b8d854..8f1c3643 100644 --- a/rowers/rows.py +++ b/rowers/rows.py @@ -68,7 +68,8 @@ def validate_file_extension(value): ext = os.path.splitext(value.name)[1] valid_extensions = ['.tcx','.csv','.TCX','.gpx','.GPX', '.CSV','.fit','.FIT','.zip','.ZIP', - '.gz','.GZ','.xls'] + '.gz','.GZ','.xls', + '.jpg','.jpeg','.tiff','.png','.gif','.bmp'] if not ext in valid_extensions: # pragma: no cover raise ValidationError(u'File not supported!') diff --git a/rowers/stravastuff.py b/rowers/stravastuff.py index 8e9b7ebc..93d865ae 100644 --- a/rowers/stravastuff.py +++ b/rowers/stravastuff.py @@ -330,7 +330,7 @@ def create_async_workout(alldata,user,stravaid,debug=False): starttime = rowdatetime.astimezone( pytz.timezone(thetimezone) - ).strftime('%H:%m:%S') + ).strftime('%H:%M:%S') totaltime = data['elapsed_time'] duration = dataprep.totaltime_sec_to_string(totaltime) diff --git a/rowers/tasks.py b/rowers/tasks.py index fdaf22fe..b4d9b3b2 100644 --- a/rowers/tasks.py +++ b/rowers/tasks.py @@ -419,7 +419,7 @@ def handle_c2_import_stroke_data(c2token, duration = datetime.datetime.strptime(duration,'%H:%M:%S.%f').time() try: - timezone_str = tz(workoutdata['timezone']) + timezone_str = workoutdata['timezone'] except: timezone_str = 'UTC' @@ -2991,7 +2991,7 @@ def handle_c2_async_workout(alldata,userid,c2token,c2id,delaysec,defaulttimezone c2id = data['id'] workouttype = data['type'] verified = data['verified'] - startdatetime = iso8601.parse_date(data['date']) + startdatetime = iso8601.parse_date(data['date_utc']) weightclass = data['weight_class'] try: @@ -3013,21 +3013,18 @@ def handle_c2_async_workout(alldata,userid,c2token,c2id,delaysec,defaulttimezone totaltime = data['time']/10. duration = totaltime_sec_to_string(totaltime) + starttimeunix = arrow.get(startdatetime).timestamp()-totaltime + startdatetime = arrow.get(starttimeunix) - try: - timezone_str = tz(data['timezone']) - except: - timezone_str = defaulttimezone + timezone = pytz.timezone(data['timezone']) + startdatetime = startdatetime.astimezone(timezone) - startdatetime = startdatetime.replace(tzinfo=None) - tz = pytz.timezone(timezone_str) - startdatetime = tz.localize(startdatetime) workoutdate = startdatetime.astimezone( - pytz.timezone(timezone_str) + timezone ).strftime('%Y-%m-%d') starttime = startdatetime.astimezone( - pytz.timezone(timezone_str) + timezone ).strftime('%H:%M:%S') try: @@ -3056,8 +3053,6 @@ def handle_c2_async_workout(alldata,userid,c2token,c2id,delaysec,defaulttimezone cum_time = res[0] lapidx = res[1] - starttimeunix = arrow.get(startdatetime).timestamp() - starttimeunix = starttimeunix-cum_time.max() unixtime = cum_time+starttimeunix # unixtime[0] = starttimeunix @@ -3134,6 +3129,7 @@ def handle_c2_async_workout(alldata,userid,c2token,c2id,delaysec,defaulttimezone 'workouttype':workouttype, 'boattype':'1x', 'c2id':c2id, + 'timezone':str(timezone) } session = requests.session() @@ -3165,8 +3161,11 @@ def handle_c2_async_workout(alldata,userid,c2token,c2id,delaysec,defaulttimezone parkedids = [] with open('c2blocked.json','r') as c2blocked: - jsondata = json.load(c2blocked) - parkedids = jsondata['ids'] + try: + jsondata = json.load(c2blocked) + parkedids = jsondata['ids'] + except JSONDecodeError: # pragma: no cover + parkedids = [] newparkedids = [id for id in parkedids if id != newc2id] with open('c2blocked.json','wt') as c2blocked: @@ -3364,7 +3363,7 @@ def fetch_strava_workout(stravatoken,oauth_data,stravaid,csvfilename,userid,debu comments = ' ' try: - thetimezone = tz(workoutsummary['timezone']) + thetimezone = workoutsummary['timezone'] except: thetimezone = 'UTC' diff --git a/rowers/templates/summary_edit.html b/rowers/templates/summary_edit.html index 422f34bc..fb021a59 100644 --- a/rowers/templates/summary_edit.html +++ b/rowers/templates/summary_edit.html @@ -111,9 +111,6 @@ {% for key,value in formvalues.items %} {% endfor %} - {% for field in detailform %} - {{ field.as_hidden }} - {% endfor %}

@@ -130,38 +127,6 @@

  • -

    Detailed Summary Edit

    -

    This is still experimental and there are known bugs. Use at your own risk. Nothing is stored permanently until you hit Save in the Updated Summary section. You can use the restore original button to restore the original values.

    -
    - - - - - - - - {% for i in nrintervals|times %} - - - - - - - {% endfor %} - -
    #TimeDistanceType
    {{ i }}  - {% get_field_id i "intervalt_" detailform %} - - {% get_field_id i "intervald_" detailform %} - - {% get_field_id i "type_" detailform %} -
    - {% csrf_token %} - - -
    -
  • -
  • Interval Shorthand How-To

    This is a quick way to enter the intervals using a special mini-language.

    You enter something like 8x500m/3min, press "Update" and the site will interpret this for you and update the summary on the right. If you're happy with the result, press the green Save button to update the values. Nothing will be changed permanently until you hit Save.

    diff --git a/rowers/templatetags/rowerfilters.py b/rowers/templatetags/rowerfilters.py index 3c7a1624..2ee4bf9d 100644 --- a/rowers/templatetags/rowerfilters.py +++ b/rowers/templatetags/rowerfilters.py @@ -559,7 +559,7 @@ def iterrows(df): # pragma: no cover return df.iterrows() @register.filter(name='times') -def times(number): +def times(number): # pragma: no cover return range(number) @register.simple_tag @@ -567,7 +567,7 @@ def get_df_iloc(data,i,j): # pragma: no cover return data.iloc(i,j) @register.simple_tag -def get_field_id(id,s,form): +def get_field_id(id,s,form): # pragma: no cover field_name = s+str(id) return form.__getitem__(field_name) diff --git a/rowers/tests/mocks.py b/rowers/tests/mocks.py index 8c0d1f51..66ba56bf 100644 --- a/rowers/tests/mocks.py +++ b/rowers/tests/mocks.py @@ -713,6 +713,13 @@ def mocked_requests(*args, **kwargs): with open('rowers/tests/testdata/c2jsonworkoutdata.txt','r') as infile: c2workoutdata = json.load(infile) + with open('rowers/tests/testdata/c2_timezone.json','r') as infile: + c2timezoneworkoutdata = json.load(infile) + + with open('rowers/tests/testdata/c2_timezone2.json','r') as infile: + c2timezoneworkoutdata2 = json.load(infile) + + with open('rowers/tests/testdata/c2jsonstrokedata.txt','r') as infile: c2strokedata = json.load(infile) @@ -1181,7 +1188,12 @@ def mocked_requests(*args, **kwargs): if c2strokestester.match(args[0]): return MockResponse(c2strokedata,200) elif c2importtester.match(args[0]): - return MockResponse(c2workoutdata,200) + if '12' in args[0]: + return MockResponse(c2workoutdata,200) + elif '31' in args[0]: + return MockResponse(c2timezoneworkoutdata2,200) + else: + return MockResponse(c2timezoneworkoutdata,200) elif c2workoutlisttester.match(args[0]): return MockResponse(c2workoutlist,200) elif 'access_token' in args[0]: diff --git a/rowers/tests/test_emails.py b/rowers/tests/test_emails.py index 8b8bf8ca..ea6fdfec 100644 --- a/rowers/tests/test_emails.py +++ b/rowers/tests/test_emails.py @@ -427,7 +427,7 @@ workout bike self.assertEqual(len(ws),1) w = ws[0] - self.assertEqual(w.workouttype,'Bike') + self.assertEqual(w.workouttype,'bike') @@ -575,7 +575,7 @@ race 1 self.assertIn('Successfully processed email attachments',out.getvalue()) ws = Workout.objects.filter(name="johnsworkout") - + self.assertEqual(len(ws),1) w = ws[0] diff --git a/rowers/tests/test_imports.py b/rowers/tests/test_imports.py index 233ad051..bfef0eee 100644 --- a/rowers/tests/test_imports.py +++ b/rowers/tests/test_imports.py @@ -158,6 +158,7 @@ class C2Objects(DjangoTestCase): self.r.c2token = '12' self.r.c2refreshtoken = 'ab' self.r.tokenexpirydate = arrow.get(datetime.datetime.now()+datetime.timedelta(days=1)).datetime + self.r.defaulttimezone = 'Europe/Prague' self.r.save() self.c.login(username='john',password='koeinsloot') @@ -261,6 +262,39 @@ class C2Objects(DjangoTestCase): self.assertEqual(response.status_code, 200) + @patch('rowers.c2stuff.requests.get', side_effect=mocked_requests) + @patch('rowers.dataprep.create_engine') + def test_c2_import_tz(self, mock_get, mocked_sqlalchemy): + + response = self.c.get('/rowers/workout/c2import/22/',follow=True) + + self.assertRedirects(response, + expected_url='/rowers/workout/'+encoded2+'/edit/', + status_code=302,target_status_code=200) + + self.assertEqual(response.status_code, 200) + + w = Workout.objects.get(id=2) + self.assertEqual(w.timezone,'Europe/Prague') + + @patch('rowers.c2stuff.requests.get', side_effect=mocked_requests) + @patch('rowers.dataprep.create_engine') + def test_c2_import_tz2(self, mock_get, mocked_sqlalchemy): + + response = self.c.get('/rowers/workout/c2import/31/',follow=True) + + self.assertRedirects(response, + expected_url='/rowers/workout/'+encoded2+'/edit/', + status_code=302,target_status_code=200) + + self.assertEqual(response.status_code, 200) + + w = Workout.objects.get(id=2) + self.assertEqual(w.timezone,'Europe/Amsterdam') + + self.assertEqual(w.starttime.strftime("%H:%M:%S"),"20:04:56") + + @patch('rowers.dataprep.create_engine') def test_strokedata(self, mocked_sqlalchemy): @@ -309,6 +343,7 @@ class C2Objects(DjangoTestCase): res = tasks.handle_c2_async_workout(alldata,self.u.id,self.r.c2token,33991243,0,self.r.defaulttimezone) self.assertEqual(res,1) + @override_settings(TESTING=True) class C2ObjectsTokenExpired(DjangoTestCase): def setUp(self): diff --git a/rowers/utils.py b/rowers/utils.py index 9fa0825b..defdd0af 100644 --- a/rowers/utils.py +++ b/rowers/utils.py @@ -524,7 +524,6 @@ def get_strava_stream(r,metric,stravaid,series_type='time',fetchresolution='high if metric=='power': # pragma: no cover with open('data.txt', 'w') as outfile: json.dump(s.json(), outfile) - print('saved to file') for data in s.json(): y = None @@ -589,18 +588,15 @@ def step_to_time_dist(step,avgspeed = 3.2,ftp=200,ftspm=25,ftv=3.7): seconds = 0 distance = 0 rscore = 0 - durationtype = step['durationType'] + durationtype = step.get('durationType',0) + value = step.get('durationValue',0) - if step['durationValue'] == 0: # pragma: no cover + if value == 0: # pragma: no cover return 0,0,0 - try: - targettype = step['targetType'] - except KeyError: # pragma: no cover - targettype = 0 + targettype = step.get('targetType',0) if durationtype == 'Time': - value = step['durationValue'] seconds = value/1000. distance = avgspeed*seconds rscore = 60.*seconds/3600. @@ -616,6 +612,8 @@ def step_to_time_dist(step,avgspeed = 3.2,ftp=200,ftspm=25,ftv=3.7): elif valuelow != 0 and valuehigh != 0: # pragma: no cover distance = seconds*(valuelow+valuehigh)/2. velomid = (valuelow+valuehigh)/2000. + else: + velomid = avgspeed veloratio = (velomid/ftv)**(3.0) rscoreperhour = 100.*veloratio @@ -657,22 +655,21 @@ def step_to_time_dist(step,avgspeed = 3.2,ftp=200,ftspm=25,ftv=3.7): rscore = 100*(avgpower/ftp)*seconds/3600. return seconds,distance,rscore - elif durationtype == 'Distance': # pragma: no cover - value = step['durationValue'] + elif durationtype == 'Distance': distance = value/100. seconds = distance/avgspeed - rscore = 60*seconds/3600. + rscore = 60.*float(seconds)/3600. - if targettype == 'Speed': + if targettype == 'Speed': # pragma: no cover value = step.get('targetValue',0) valuelow = step.get('targetValueLow',0) valuehigh = step.get('targetValueHigh',0) velomid = 0 - if value != 0: # pragma: no cover + if value != 0: seconds = distance/value velomid = value/1000. - elif valuelow != 0 and valuehigh != 0: # pragma: no cover + elif valuelow != 0 and valuehigh != 0: velomid = (valuelow+valuehigh)/2000. seconds = distance/velomid @@ -680,7 +677,7 @@ def step_to_time_dist(step,avgspeed = 3.2,ftp=200,ftspm=25,ftv=3.7): rscoreperhour = 100.*veloratio rscore = rscoreperhour*seconds/3600. - if targettype == 'Power': + if targettype == 'Power': # pragma: no cover value = step.get('targetValue',0) valuelow = step.get('targetValueLow',0) valuehigh = step.get('targetValueHigh',0) @@ -702,7 +699,7 @@ def step_to_time_dist(step,avgspeed = 3.2,ftp=200,ftspm=25,ftv=3.7): rscore = 100.*(avgpower/ftp)*seconds/3600. - if targettype == 'Cadence': + if targettype == 'Cadence': # pragma: no cover value = step.get('targetValue',0) valuelow = step.get('targetValueLow',0) valuehigh = step.get('targetValueHigh',0) @@ -1167,3 +1164,53 @@ def request_is_ajax(request): # is_ajax = True return is_ajax + +def intervals_to_string(vals, units, typ): + if vals is None or units is None or typ is None: # pragma: no cover + return '' + if len(vals) != len(units) or len(vals) != len(typ): # pragma: no cover + return '' + + s = '' + previous = 'rest' + for i in range(len(vals)): + if typ[i] == 'rest' and previous == 'rest': + if units[i] == 'min': # pragma: no cover + val = int(vals[i])*60 + unit = 'sec' + else: + val = int(vals[i]) + if units[i] == 'meters': # pragma: no cover + unit = 'm' + if units[i] == 'seconds': + unit = 'sec' + s += '+0min/{val}{unit}'.format(val=val,unit=unit) + elif typ[i] == 'rest': # pragma: no cover + if units[i] == 'min': + val = int(vals[i])*60 + unit = 'sec' + else: + val = int(vals[i]) + if units[i] == 'meters': + unit = 'm' + if units[i] == 'seconds': + unit = 'sec' + s += '/{val}{unit}'.format(val=val,unit=unit) + previous = 'rest' + else: # pragma: no cover # work interval + if units[i] == 'min': + val = int(vals[i])*60 + unit = 'sec' + else: + val = int(vals[i]) + if units[i] == 'meters': + unit = 'm' + if units[i] == 'seconds': + unit = 'sec' + s += '+{val}{unit}'.format(val=val,unit=unit) + previous = 'work' + + if s[0] == '+': + s = s[1:] + + return s diff --git a/rowers/views/importviews.py b/rowers/views/importviews.py index 1993490b..08922a89 100644 --- a/rowers/views/importviews.py +++ b/rowers/views/importviews.py @@ -2099,6 +2099,7 @@ def workout_getimportview(request,externalid,source = 'c2',do_async=False): + workoutdate = startdatetime.astimezone( pytz.timezone(timezone_str) ).strftime('%Y-%m-%d') diff --git a/rowers/views/statements.py b/rowers/views/statements.py index 67f62298..b74f1616 100644 --- a/rowers/views/statements.py +++ b/rowers/views/statements.py @@ -91,7 +91,7 @@ from django.utils.datastructures import MultiValueDictKeyError from django.utils import timezone,translation from django.core.mail import send_mail, BadHeaderError from rowers.forms import ( - SummaryStringForm,IntervalUpdateForm,StrokeDataForm, + SummaryStringForm,StrokeDataForm, StatsOptionsForm,PredictedPieceForm,DateRangeForm,DeltaDaysForm, FitnessMetricForm,PredictedPieceFormNoDistance, EmailForm, RegistrationForm, RegistrationFormTermsOfService, @@ -1161,7 +1161,7 @@ def get_my_teams(user): return teams # Used for the interval editor - translates seconds to a time object -def get_time(second): +def get_time(second): # pragma: no cover if (second<=0) or (second>1e9): hours = 0 minutes=0 diff --git a/rowers/views/workoutviews.py b/rowers/views/workoutviews.py index a273601f..ec44ea1a 100644 --- a/rowers/views/workoutviews.py +++ b/rowers/views/workoutviews.py @@ -13,6 +13,7 @@ import numpy from rowers.mailprocessing import send_confirm import rowers.uploads as uploads import rowers.utils as utils +from rowers.utils import intervals_to_string from urllib.parse import urlparse, parse_qs from json.decoder import JSONDecodeError @@ -2297,7 +2298,7 @@ def workout_view(request,id=0,raceresult=0,sessionresult=0,nocourseraceresult=0) intervaldata['itime'] = itime intervaldata['itype'] = itype - rowdata.updateinterval_metric(' AverageBoatSpeed (m/s)',0.1,mode='larger', + vals, units, typ = rowdata.updateinterval_metric(' AverageBoatSpeed (m/s)',0.1,mode='larger', debug=False,smoothwindow=15., activewindow = [startsecond,endsecond]) summary = rowdata.allstats() @@ -2316,7 +2317,7 @@ def workout_view(request,id=0,raceresult=0,sessionresult=0,nocourseraceresult=0) intervaldata['itime'] = itime intervaldata['itype'] = itype - rowdata.updateinterval_metric(' AverageBoatSpeed (m/s)',0.1,mode='larger', + vals, units, typ = rowdata.updateinterval_metric(' AverageBoatSpeed (m/s)',0.1,mode='larger', debug=False,smoothwindow=15., activewindow = [startsecond,endsecond]) summary = rowdata.allstats() @@ -2336,7 +2337,7 @@ def workout_view(request,id=0,raceresult=0,sessionresult=0,nocourseraceresult=0) intervaldata['itime'] = itime intervaldata['itype'] = itype - rowdata.updateinterval_metric(' AverageBoatSpeed (m/s)',0.1,mode='larger', + vals, units, typ = rowdata.updateinterval_metric(' AverageBoatSpeed (m/s)',0.1,mode='larger', debug=False,smoothwindow=15., activewindow = [startsecond,endsecond]) summary = rowdata.allstats() @@ -4812,7 +4813,7 @@ def workout_upload_api(request): totalDistance = post_data.get('totalDistance',None) elapsedTime = post_data.get('elapsedTime',None) summary = post_data.get('summary',None) - + timezone = post_data.get('timezone',None) r = None if form.is_valid(): @@ -4904,6 +4905,13 @@ def workout_upload_api(request): return JSONResponse(status=200,data=message) w = Workout.objects.get(id=id) + if timezone is not None: # pragma: no cover + w.startdatetime = w.startdatetime.astimezone(pytz.timezone(timezone)) + w.workoutdate = w.startdatetime.strftime('%Y-%m-%d') + w.starttime = w.starttime.strftime('%H:%M:%S') + w.timezone = timezone + w.save() + if make_plot: # pragma: no cover @@ -6240,7 +6248,7 @@ def workout_summary_edit_view(request,id,message="",successmessage="" if powerorpace == 'power' and power is not None: try: - rowdata.updateinterval_metric( + vals, units, typ = rowdata.updateinterval_metric( ' Power (watts)',power,mode='larger', debug=False,smoothwindow=15., activewindow=[activesecondsmin,activesecondsmax], @@ -6250,7 +6258,7 @@ def workout_summary_edit_view(request,id,message="",successmessage="" elif powerorpace == 'pace': # pragma: no cover try: velo = 500./pace_secs - rowdata.updateinterval_metric( + vals, units, typ = rowdata.updateinterval_metric( ' AverageBoatSpeed (m/s)',velo,mode='larger', debug=False,smoothwindow=15., activewindow=[activesecondsmin,activesecondsmax], @@ -6259,7 +6267,7 @@ def workout_summary_edit_view(request,id,message="",successmessage="" messages.error(request,'Error updating pace') elif powerorpace == 'work': # pragma: no cover try: - rowdata.updateinterval_metric( + vals, units, typ = rowdata.updateinterval_metric( 'driveenergy',work,mode='larger', debug=False,smoothwindow=15., activewindow=[activesecondsmin,activesecondsmax], @@ -6268,13 +6276,17 @@ def workout_summary_edit_view(request,id,message="",successmessage="" messages.error(request,'Error updating Work per Stroke') elif powerorpace == 'spm': # pragma: no cover try: - rowdata.updateinterval_metric( + vals, units, typ = rowdata.updateinterval_metric( ' Cadence (stokes/min)',spm,mode='larger', debug=False,smoothwindow=2., activewindow=[activesecondsmin,activesecondsmax],) except: messages.error(request,'Error updating SPM') + intervalString = '' + if vals is not None: + intervalString = intervals_to_string(vals, units, typ) + intervalstats = rowdata.allstats() itime,idist,itype = rowdata.intervalstats_values() nrintervals = len(idist) @@ -6296,7 +6308,7 @@ def workout_summary_edit_view(request,id,message="",successmessage="" 'activeminutesmin': activeminutesmin, 'activeminutesmax': activeminutesmax, } - form = SummaryStringForm() + form = SummaryStringForm(initial={'intervalstring':intervalString}) powerupdateform = PowerIntervalUpdateForm(initial=data) savebutton = 'savepowerpaceform' formvalues = { @@ -6368,7 +6380,7 @@ def workout_summary_edit_view(request,id,message="",successmessage="" pace_secs = 120. if powerorpace == 'power' and power is not None: - rowdata.updateinterval_metric(' Power (watts)',power,mode='larger', + vals, units, typ = rowdata.updateinterval_metric(' Power (watts)',power,mode='larger', debug=False,smoothwindow=15, activewindow=[activesecondsmin,activesecondsmax], ) @@ -6376,7 +6388,7 @@ def workout_summary_edit_view(request,id,message="",successmessage="" elif powerorpace == 'pace': # pragma: no cover try: velo = 500./pace_secs - rowdata.updateinterval_metric(' AverageBoatSpeed (m/s)',velo,mode='larger', + vals, units, typ = rowdata.updateinterval_metric(' AverageBoatSpeed (m/s)',velo,mode='larger', debug=False,smoothwindow=15, activewindow=[activesecondsmin,activesecondsmax], ) @@ -6384,7 +6396,7 @@ def workout_summary_edit_view(request,id,message="",successmessage="" messages.error(request,'Error updating pace') elif powerorpace == 'work': # pragma: no cover try: - rowdata.updateinterval_metric( + vals, units, typ = rowdata.updateinterval_metric( 'driveenergy',work,mode='larger', debug=False,smoothwindow=15., activewindow=[activesecondsmin,activesecondsmax],) @@ -6392,7 +6404,7 @@ def workout_summary_edit_view(request,id,message="",successmessage="" messages.error(request,'Error updating Work per Stroke') elif powerorpace == 'spm': # pragma: no cover try: - rowdata.updateinterval_metric(' Cadence (stokes/min)',spm,mode='larger', + vals, units, typ = rowdata.updateinterval_metric(' Cadence (stokes/min)',spm,mode='larger', debug=False,smoothwindow=2., activewindow=[activesecondsmin,activesecondsmax], ) @@ -6400,6 +6412,11 @@ def workout_summary_edit_view(request,id,message="",successmessage="" messages.error(request,'Error updating SPM') + intervalString = '' + if vals is not None: + intervalString = intervals_to_string(vals, units, typ) + + intervalstats = rowdata.allstats() itime,idist,itype = rowdata.intervalstats_values() nrintervals = len(idist) @@ -6414,135 +6431,10 @@ def workout_summary_edit_view(request,id,message="",successmessage="" 'activeminutesmax': activeminutesmax, } powerupdateform = PowerIntervalUpdateForm(initial=cd) - form = SummaryStringForm() - - - form = SummaryStringForm() - - # we are saving the results obtained from the detailed form - elif request.method == 'POST' and "savedetailform" in request.POST: # pragma: no cover - savebutton = 'savedetailform' - form = SummaryStringForm() - nrintervals = int(request.POST['nrintervals']) - detailform = IntervalUpdateForm(request.POST,aantal=nrintervals) - itime = [] - idist = [] - itype = [] - ivalues = [] - iunits = [] - itypes = [] - iresults = [] - for i in range(nrintervals): - try: - t = datetime.datetime.strptime(request.POST['intervalt_%s' % i],"%H:%M:%S.%f") - except ValueError: - t = datetime.datetime.strptime(request.POST['intervalt_%s' % i],"%H:%M:%S") - - timesecs = 3600*t.hour+60*t.minute+t.second+t.microsecond/1.e6 - itime += [timesecs] - idist += [int(request.POST['intervald_%s' % i])] - itype += [int(request.POST['type_%s' % i])] - - if itype[i] == 3: # rest - itypes += ['rest'] - ivalues += [timesecs] - iresults += [idist[i]] - iunits += ['seconds'] - if itype[i] == 5 or itype[i] == 2: # distance based work - itypes += ['work'] - ivalues += [idist[i]] - iresults += [timesecs] - iunits += ['meters'] - if itype[i] == 4 or itype[i] == 1: # time based work - itypes += ['work'] - ivalues += [timesecs] - iresults += [idist[i]] - iunits += ['seconds'] - - - rowdata.updateintervaldata(ivalues,iunits,itypes,iresults=iresults) - intervalstats = rowdata.allstats() - row.summary = intervalstats - try: - row.notes += "\n"+s - except TypeError: - pass - - row.save() - rowdata.write_csv(f1,gzip=True) - dataprep.update_strokedata(encoder.decode_hex(id),rowdata.df) - messages.info(request,"Updated interval data saved") - - form = SummaryStringForm() - powerupdateform = PowerIntervalUpdateForm(initial={ - 'power': int(normp), - 'pace': avpace, - 'selector': 'power', - 'work': int(normw), - 'spm': int(normspm), - 'activeminutesmin': 0, - 'activeminutesmax': activeminutesmax, - }) - - - # we are processing the details form - elif request.method == 'POST' and "nrintervals" in request.POST: # pragma: no cover - savebutton = 'savedetailform' - nrintervals = int(request.POST['nrintervals']) - detailform = IntervalUpdateForm(request.POST,aantal=nrintervals) - if detailform.is_valid(): - cd = detailform.cleaned_data - itime = [] - idist = [] - itype = [] - ivalues = [] - iunits = [] - itypes = [] - iresults = [] - for i in range(nrintervals): - t = cd['intervalt_%s' % i] - timesecs = t.total_seconds() - itime += [timesecs] - idist += [cd['intervald_%s' % i]] - itype += [cd['type_%s' % i]] - - if itype[i] == '3': # rest - itypes += ['rest'] - ivalues += [timesecs] - iresults += [idist[i]] - iunits += ['seconds'] - if itype[i] == '5' or itype[i] == '2': # distance based work - itypes += ['work'] - ivalues += [idist[i]] - iresults += [timesecs] - iunits += ['meters'] - if itype[i] == '4' or itype[i] == '1': # time based work - itypes += ['work'] - ivalues += [timesecs] - iresults += [idist[i]] - iunits += ['seconds'] - - rowdata.updateintervaldata(ivalues,iunits, - itypes,iresults=iresults) - intervalstats = rowdata.allstats() - - - form = SummaryStringForm() - powerupdateform = PowerIntervalUpdateForm() + form = SummaryStringForm(initial={'intervalstring':intervalString}) - initial = {} - for i in range(nrintervals): - try: - initial['intervald_%s' % i] = idist[i] - initial['intervalt_%s' % i] = get_time(itime[i]) - initial['type_%s' % i] = itype[i] - except IndexError: # pragma: no cover - pass - - - detailform = IntervalUpdateForm(aantal=nrintervals,initial=initial) # create interactive plot try: @@ -6562,13 +6454,11 @@ def workout_summary_edit_view(request,id,message="",successmessage="" div = '' # render page - return render(request, 'summary_edit.html', {'form':form, 'activeminutesmax':activeminutesmax, 'activeminutesmin':activeminutesmin, 'maxminutes': maxminutes, - 'detailform':detailform, 'powerupdateform':powerupdateform, 'workout':row, 'rower':r,