diff --git a/rowers/admin.py b/rowers/admin.py index a314ebe6..f28aecae 100644 --- a/rowers/admin.py +++ b/rowers/admin.py @@ -44,7 +44,8 @@ class RowerInline(admin.StackedInline): 'polartoken','polartokenexpirydate', 'polarrefreshtoken','polaruserid', 'polar_auto_import', - 'stravatoken','stravaexportas','strava_auto_export', + 'stravatoken','stravatokenexpirydate','stravarefreshtoken', + 'stravaexportas','strava_auto_export', 'strava_auto_import', 'runkeepertoken','runkeeper_auto_export',)}), ('Team', diff --git a/rowers/c2stuff.py b/rowers/c2stuff.py index 5f5979bc..7f3d0abe 100644 --- a/rowers/c2stuff.py +++ b/rowers/c2stuff.py @@ -6,7 +6,7 @@ from rowers.imports import * import datetime from requests import Request, Session - +import mytypes from rowers.mytypes import otwtypes from iso8601 import ParseError @@ -428,7 +428,7 @@ def createc2workoutdata(w): startdate = datetime.datetime.combine(w.date,datetime.time()) data = { - "type": workouttype, + "type": mytypes.c2mapping[workouttype], "date": w.startdatetime.isoformat(), "timezone": w.timezone, "distance": int(w.distance), @@ -684,6 +684,12 @@ def process_callback(request): # Uploading workout def workout_c2_upload(user,w): message = 'trying C2 upload' + try: + if mytypes.c2mapping[w.workouttype] is None: + return "This workout type cannot be uploaded to Concept2",0 + except KeyError: + return "This workout type cannot be uploaded to Concept2",0 + thetoken = c2_open(user) r = Rower.objects.get(user=user) @@ -755,7 +761,7 @@ def add_workout_from_data(user,importid,data,strokedata, source='c2',splitdata=None, workoutsource='concept2'): try: - workouttype = data['type'] + workouttype = mytypes.c2mappinginv[data['type']] except KeyError: workouttype = 'rower' diff --git a/rowers/dataprep.py b/rowers/dataprep.py index 7d12f7f7..4467cb82 100644 --- a/rowers/dataprep.py +++ b/rowers/dataprep.py @@ -741,6 +741,7 @@ def fetchcp(rower,theworkouts,table='cpdata'): def create_row_df(r,distance,duration,startdatetime,workouttype='rower', avghr=None,avgpwr=None,avgspm=None, rankingpiece = False, + duplicate=False, title='Manual entry',notes='',weightcategory='hwt'): @@ -813,6 +814,7 @@ def create_row_df(r,distance,duration,startdatetime,workouttype='rower', title=title, notes=notes, rankingpiece=rankingpiece, + duplicate=duplicate, dosmooth=False, workouttype=workouttype, consistencychecks=False, @@ -829,6 +831,7 @@ def save_workout_database(f2, r, dosmooth=True, workouttype='rower', workoutsource='unknown', notes='', totaldist=0, totaltime=0, rankingpiece=False, + duplicate=False, summary='', makeprivate=False, oarlength=2.89, inboard=0.88, @@ -999,13 +1002,30 @@ def save_workout_database(f2, r, dosmooth=True, workouttype='rower', maxhr = np.nan_to_num(maxhr) averagehr = np.nan_to_num(averagehr) + + t = datetime.datetime.strptime(duration,"%H:%M:%S.%f") + delta = datetime.timedelta(hours=t.hour, minutes=t.minute, seconds=t.second) + + workoutenddatetime = workoutstartdatetime+delta + # check for duplicate start times and duration - ws = Workout.objects.filter(startdatetime=workoutstartdatetime, - distance=totaldist, - user=r) - if (len(ws) != 0): - message = "Warning: This workout probably already exists in the database" - privacy = 'hidden' + ws = Workout.objects.filter(user=r,date=workoutdate,duplicate=False).exclude( + startdatetime__gt=workoutenddatetime + ) + + ws2 = [] + + for ww in ws: + t = ww.duration + delta = datetime.timedelta(hours=t.hour, minutes=t.minute, seconds=t.second) + enddatetime = ww.startdatetime+delta + if enddatetime > workoutstartdatetime: + ws2.append(ww) + + + if (len(ws2) != 0): + message = "Warning: This workout overlaps with an existing one and was marked as a duplicate" + duplicate = True w = Workout(user=r, name=title, date=workoutdate, @@ -1014,6 +1034,7 @@ def save_workout_database(f2, r, dosmooth=True, workouttype='rower', duration=duration, distance=totaldist, weightcategory=r.weightcategory, starttime=workoutstarttime, + duplicate=duplicate, workoutsource=workoutsource, rankingpiece=rankingpiece, forceunit=forceunit, diff --git a/rowers/dataprepnodjango.py b/rowers/dataprepnodjango.py index da384fbd..8e830138 100644 --- a/rowers/dataprepnodjango.py +++ b/rowers/dataprepnodjango.py @@ -866,6 +866,8 @@ def update_agegroup_db(age,sex,weightcategory,wcdurations,wcpower, df['sex'] = sex df['age'] = age df['weightcategory'] = weightcategory + df.replace([np.inf,-np.inf],np.nan,inplace=True) + df.dropna(axis=0,inplace=True) if debug: engine = create_engine(database_url_debug, echo=False) diff --git a/rowers/imports.py b/rowers/imports.py index 70f765c0..0c7e3640 100644 --- a/rowers/imports.py +++ b/rowers/imports.py @@ -94,7 +94,7 @@ def imports_open(user,oauth_data): tokenname = oauth_data['tokenname'] refreshtokenname = oauth_data['refreshtokenname'] expirydatename = oauth_data['expirydatename'] - if tokenexpirydate and timezone.now()>tokenexpirydate: + if tokenexpirydate and timezone.now()+timedelta(seconds=3599)>tokenexpirydate: token = imports_token_refresh( user, tokenname, @@ -102,6 +102,15 @@ def imports_open(user,oauth_data): expirydatename, oauth_data, ) + elif tokenexpirydate is None and expirydatename is not None and 'strava' in expirydatename: + token = imports_token_refresh( + user, + tokenname, + refreshtokenname, + expirydatename, + oauth_data, + ) + return token @@ -156,7 +165,11 @@ def imports_do_refresh_token(refreshtoken,oauth_data,access_token=''): try: expires_in = token_json['expires_in'] except KeyError: - expires_in = 0 + try: + expires_at = arrow.get(token_json['expires_at']).timestamp + expires_in = expires_at - arrow.now().timestamp + except KeyError: + expires_in = 0 try: refresh_token = token_json['refresh_token'] except KeyError: @@ -266,6 +279,11 @@ def imports_token_refresh(user,tokenname,refreshtokenname,expirydatename,oauth_d r = Rower.objects.get(user=user) refreshtoken = getattr(r,refreshtokenname) + + # for Strava transition + if not refreshtoken: + refreshtoken = getattr(r,tokenname) + res = imports_do_refresh_token(refreshtoken,oauth_data) access_token = res[0] diff --git a/rowers/interactiveplots.py b/rowers/interactiveplots.py index 5d61220e..c08ef90d 100644 --- a/rowers/interactiveplots.py +++ b/rowers/interactiveplots.py @@ -237,8 +237,6 @@ def interactive_boxchart(datadf,fieldname,extratitle=''): def interactive_activitychart(workouts,startdate,enddate,stack='type'): - if len(workouts) == 0: - return "","" dates = [] dates_sorting = [] @@ -316,7 +314,10 @@ def interactive_activitychart(workouts,startdate,enddate,stack='type'): label = CatAttr(columns=['date'], sort=False), xlabel='Date', ylabel='Time', - title='Activity', + title='Activity {d1} to {d2}'.format( + d1 = startdate.strftime("%Y-%m-%d"), + d2 = enddate.strftime("%Y-%m-%d"), + ), stack=stack, plot_width=350, plot_height=250, diff --git a/rowers/models.py b/rowers/models.py index 5db5bb25..dfa1726a 100644 --- a/rowers/models.py +++ b/rowers/models.py @@ -653,6 +653,9 @@ class Rower(models.Model): polar_auto_import = models.BooleanField(default=False) stravatoken = models.CharField(default='',max_length=200,blank=True,null=True) + stravatokenexpirydate = models.DateTimeField(blank=True,null=True) + stravarefreshtoken = models.CharField(default='',max_length=1000, + blank=True,null=True) stravaexportas = models.CharField(default="Rowing", max_length=30, choices=stravatypes, @@ -2131,6 +2134,7 @@ class Workout(models.Model): privacy = models.CharField(default='visible',max_length=30, choices=privacychoices) rankingpiece = models.BooleanField(default=False,verbose_name='Ranking Piece') + duplicate = models.BooleanField(default=False,verbose_name='Duplicate Workout') def __unicode__(self): @@ -2178,7 +2182,42 @@ def auto_delete_file_on_delete(sender, instance, **kwargs): if instance.csvfilename+'.gz': if os.path.isfile(instance.csvfilename+'.gz'): os.remove(instance.csvfilename+'.gz') - + +@receiver(models.signals.post_delete,sender=Workout) +def update_duplicates_on_delete(sender, instance, **kwargs): + if instance.id: + + duplicates = Workout.objects.filter( + user=instance.user,date=instance.date, + duplicate=True) + + for d in duplicates: + t = d.duration + delta = datetime.timedelta(hours=t.hour, minutes=t.minute, seconds=t.second) + workoutenddatetime = d.startdatetime+delta + ws = Workout.objects.filter( + user=d.user,date=d.date, + ).exclude( + pk__in=[instance.pk,d.pk] + ).exclude( + startdatetime__gt=workoutenddatetime + ) + + + ws2 = [] + + for ww in ws: + t = ww.duration + delta = datetime.timedelta(hours=t.hour, minutes=t.minute, seconds=t.second) + enddatetime = ww.startdatetime+delta + if enddatetime > d.startdatetime: + ws2.append(ww) + + if len(ws2) == 0: + d.duplicate=False + d.save() + + # Delete stroke data from the database when a workout is deleted @receiver(models.signals.post_delete,sender=Workout) def auto_delete_strokedata_on_delete(sender, instance, **kwargs): @@ -2401,7 +2440,7 @@ class WorkoutForm(ModelForm): # duration = forms.TimeInput(format='%H:%M:%S.%f') class Meta: model = Workout - fields = ['name','date','starttime','timezone','duration','distance','workouttype','boattype','weightcategory','notes','rankingpiece'] + fields = ['name','date','starttime','timezone','duration','distance','workouttype','boattype','weightcategory','notes','rankingpiece','duplicate'] widgets = { 'date': AdminDateWidget(), 'notes': forms.Textarea, diff --git a/rowers/mytypes.py b/rowers/mytypes.py index df0709ba..e0b9fa7a 100644 --- a/rowers/mytypes.py +++ b/rowers/mytypes.py @@ -10,9 +10,214 @@ workouttypes = ( ('coastal','Coastal'), ('c-boat','Dutch C boat'), ('churchboat','Finnish Church boat'), + ('Ride','Ride'), + ('Run','Run'), + ('NordicSki','NordicSki'), + ('Swim','Swim'), + ('Hike','Hike'), + ('Walk','Walk'), + ('Canoeing','Canoeing'), + ('Crossfit','Crossfit'), + ('StandUpPaddling','StandUpPaddling'), + ('IceSkate','IceSkate'), + ('WeightTraining','WeightTraining'), + ('InlineSkate','InlineSkate'), + ('Kayaking','Kayaking'), + ('Workout','Workout'), ('other','Other'), ) +stravamapping = { + 'water':'Rowing', + 'rower':'Rowing', + 'skierg':'NordicSki', + 'bike':'Ride', + 'dynamic':'Rowing', + 'slides':'Rowing', + 'paddle':'StandUpPaddling', + 'snow':'NordicSki', + 'coastal':'Rowing', + 'c-boat':'Rowing', + 'churchboat':'Rowing', + 'Ride':'Ride', + 'Run':'Run', + 'NordicSki':'NordicSki', + 'Swim':'Swim', + 'Hike':'Hike', + 'Walk':'Walk', + 'Canoeing':'Canoeing', + 'Crossfit':'Crossfit', + 'StandUpPaddling':'StandUpPaddling', + 'IceSkate':'IceSkate', + 'WeightTraining':'WeightTraining', + 'InlineSkate':'InlineSkate', + 'Kayaking':'Kayaking', + 'Workout':'Workout', + 'other':'Workout', + + } + +stmapping = { + 'water':'Rowing', + 'rower':'Rowing', + 'skierg':'Skiing:Nordic', + 'bike':'Cycling', + 'dynamic':'Rowing', + 'slides':'Rowing', + 'paddle':'Other:Paddling', + 'snow':'Skiing:Nordic', + 'coastal':'Rowing', + 'c-boat':'Rowing', + 'churchboat':'Rowing', + 'Ride':'Cycling', + 'Run':'Running', + 'NordicSki':'Skiing:Nordic', + 'Swim':'Swimming', + 'Hike':'Hiking', + 'RollerSki':'Other:RollerSki', + 'Walk':'Other:Walk', + 'Canoeing':'Other:Canoeing', + 'Crossfit':'Other:Crossfit', + 'StandUpPaddling':'Other:StandUpPaddling', + 'IceSkate':'Skating', + 'WeightTraining':'Other:WeightTraining', + 'InlineSkate':'Skating:InlineSkate', + 'Kayaking':'Other:Kayaking', + 'Workout':'Other:Workout', + 'other':'Other', + + } + +rkmapping = { + 'water':'Rowing', + 'rower':'Rowing', + 'skierg':'Cross-Country Skiing', + 'bike':'Cycling', + 'dynamic':'Rowing', + 'slides':'Rowing', + 'paddle':'Other:Paddling', + 'snow':'Cross-Country Skiing', + 'coastal':'Rowing', + 'c-boat':'Rowing', + 'churchboat':'Rowing', + 'Ride':'Cycling', + 'Run':'Running', + 'NordicSki':'Cross-Country Skiing', + 'Swim':'Swimming', + 'Hike':'Hiking', + 'Walk':'Walking', + 'Canoeing':'Other', + 'Crossfit':'CrossFit', + 'StandUpPaddling':'Other', + 'IceSkate':'Skating', + 'WeightTraining':'Other', + 'InlineSkate':'Skating', + 'Kayaking':'Other', + 'Workout':'Other', + 'other':'Other', + + } + +polarmapping = { + 'water':'Rowing', + 'rower':'Rowing', + 'skierg':'Skiing', + 'bike':'Cycling', + 'dynamic':'Rowing', + 'slides':'Rowing', + 'paddle':'Other Outdoor', + 'snow':'Skiing', + 'coastal':'Rowing', + 'c-boat':'Rowing', + 'churchboat':'Rowing', + 'Ride':'Cycling', + 'Run':'Running', + 'NordicSki':'Skiing', + 'Swim':'Swimming', + 'Hike':'Hiking', + 'Walk':'Walking', + 'Canoeing':'Canoeing', + 'Crossfit':'Crossfit', + 'StandUpPaddling':'Other Outdoor', + 'IceSkate':'Skating', + 'WeightTraining':'Strength training', + 'InlineSkate':'Skating', + 'Kayaking':'Kayaking', + 'Workout':'Other Indoor', + 'other':'Other Indoor', + + } + +tpmapping = { + 'water':'rowing', + 'rower':'rowing', + 'skierg':'xc-ski', + 'bike':'bike', + 'dynamic':'rowing', + 'slides':'rowing', + 'paddle':'other', + 'snow':'xc-ski', + 'coastal':'rowing', + 'c-boat':'rowing', + 'churchboat':'rowing', + 'Ride':'cycling', + 'Run':'run', + 'NordicSki':'xc-ski', + 'Swim':'swim', + 'Hike':'other', + 'Walk':'walk', + 'Canoeing':'other', + 'Crossfit':'other', + 'StandUpPaddling':'other', + 'IceSkate':'other', + 'WeightTraining':'strength', + 'InlineSkate':'other', + 'Kayaking':'other', + 'Workout':'other', + 'other':'other', + + } + +c2mapping = { + 'water':'water', + 'rower':'rower', + 'skierg':'skierg', + 'bike':'bike', + 'dynamic':'dynamic', + 'slides':'slides', + 'paddle':'paddle', + 'snow':'snow', + 'coastal':'water', + 'c-boat':'water', + 'churchboat':'water', + 'Ride':'bike', + 'Run':None, + 'NordicSki':'snow', + 'Swim':None, + 'Hike':None, + 'Walk':None, + 'Canoeing':'paddle', + 'Crossfit':None, + 'StandUpPaddling':None, + 'IceSkate':None, + 'WeightTraining':None, + 'InlineSkate':None, + 'Kayaking':None, + 'Workout':None, + 'other':None, + + } + +c2mappinginv = {value:key for key,value in c2mapping.iteritems() if value is not None} + +stravamappinginv = {value:key for key,value in stravamapping.iteritems() if value is not None} + +stmappinginv = {value:key for key,value in stmapping.iteritems() if value is not None} + +rkmappinginv = {value:key for key,value in rkmapping.iteritems() if value is not None} + +polarmappinginv = {value:key for key,value in polarmapping.iteritems() if value is not None} + otwtypes = ( 'water', 'coastal', @@ -20,6 +225,17 @@ otwtypes = ( 'churchboat' ) +rowtypes = ( + 'water', + 'rower', + 'dynamic', + 'slides', + 'coastal', + 'c-boat', + 'churchboat' + ) + + checktypes = [i[0] for i in workouttypes] workoutsources = ( diff --git a/rowers/polarstuff.py b/rowers/polarstuff.py index a1962bcc..e6fe6025 100644 --- a/rowers/polarstuff.py +++ b/rowers/polarstuff.py @@ -52,7 +52,7 @@ baseurl = 'https://polaraccesslink.com/v3' from utils import NoTokenError, custom_exception_handler - +import mytypes # Exchange access code for long-lived access token def get_token(code): diff --git a/rowers/stravastuff.py b/rowers/stravastuff.py index 32eb785f..fdad80f4 100644 --- a/rowers/stravastuff.py +++ b/rowers/stravastuff.py @@ -18,7 +18,7 @@ from stravalib.exc import ActivityUploadFailed,TimeoutExceeded from iso8601 import ParseError from utils import myqueue - +import mytypes import gzip from rowsandall_app.settings import ( @@ -40,11 +40,11 @@ oauth_data = { 'autorization_uri': "https://www.strava.com/oauth/authorize", 'content_type': 'application/json', 'tokenname': 'stravatoken', - 'refreshtokenname': '', - 'expirydatename': '', + 'refreshtokenname': 'stravarefreshtoken', + 'expirydatename': 'stravatokenexpirydate', 'bearer_auth': True, 'base_url': "https://www.strava.com/oauth/token", - 'grant_type': None, + 'grant_type': 'refresh_token', } @@ -52,6 +52,27 @@ oauth_data = { def get_token(code): return imports_get_token(code, oauth_data) +def strava_open(user): + return imports_open(user, oauth_data) + +def do_refresh_token(refreshtoken): + return imports_do_refresh_token(refreshtoken, oauth_data) + +def rower_strava_token_refresh(user): + r = Rower.objects.get(user=user) + res = do_refresh_token(r.stravarefreshtoken) + access_token = res[0] + expires_in = res[1] + refresh_token = res[2] + expirydatetime = timezone.now()+timedelta(seconds=expires_in) + + r.stravatoken = access_token + r.stravatokenexpirydate = expirydatetime + r.stravarefreshtoken = refresh_token + r.save() + + return r.stravatoken + # Make authorization URL including random string def make_authorization_url(request): return imports_make_authorization_url(oauth_data) @@ -62,6 +83,9 @@ def get_strava_workout_list(user,limit_n=0): if (r.stravatoken == '') or (r.stravatoken is None): s = "Token doesn't exist. Need to authorize" return custom_exception_handler(401,s) + elif (r.stravatokenexpirydate is None or timezone.now()+timedelta(seconds=3599)>r.stravatokenexpirydate): + s = "Token expired. Needs to refresh." + return custom_exception_handler(401,s) else: # ready to fetch. Hurray authorizationstring = str('Bearer ' + r.stravatoken) @@ -78,6 +102,7 @@ def get_strava_workout_list(user,limit_n=0): s = requests.get(url,headers=headers,params=params) + return s @@ -86,10 +111,13 @@ def get_strava_workouts(rower): if not isprorower(rower): return 0 - - res = get_strava_workout_list(rower.user,limit_n=10) - print res.status_code + try: + thetoken = strava_open(rower.user) + except NoTokenError: + return 0 + + res = get_strava_workout_list(rower.user,limit_n=10) if (res.status_code != 200): return 0 @@ -135,9 +163,9 @@ def create_async_workout(alldata,user,stravaid,debug=False): distance = data['distance'] stravaid = data['id'] try: - workouttype = data['type'] + workouttype = mytypes.stravamappinginv[data['type']] except: - workouttype = 'rower' + workouttype = 'other' if workouttype.lower() == 'rowing': workouttype = 'rower' @@ -227,6 +255,9 @@ def get_workout(user,stravaid): if (r.stravatoken == '') or (r.stravatoken is None): s = "Token doesn't exist. Need to authorize" return custom_exception_handler(401,s) + elif (r.stravatokenexpirydate is not None and timezone.now()>r.stravatokenexpirydate): + s = "Token expired. Needs to refresh." + return custom_exception_handler(401,s) else: # ready to fetch. Hurray fetchresolution = 'high' @@ -413,14 +444,15 @@ def add_workout_from_data(user,importid,data,strokedata, source='strava',splitdata=None, workoutsource='strava'): try: - workouttype = data['type'] + workouttype = mytypes.stravamappinginv[data['type']] except KeyError: - workouttype = 'rower' + workouttype = 'other' if workouttype.lower() == 'rowing': workouttype = 'rower' - if 'summary_polyline' in data['map']: - workouttype = 'water' + + if 'summary_polyline' in data['map'] and workouttype=='rower': + workouttype = 'water' if workouttype not in [x[0] for x in Workout.workouttypes]: workouttype = 'other' diff --git a/rowers/templates/document_form.html b/rowers/templates/document_form.html index 38b76235..f285de40 100644 --- a/rowers/templates/document_form.html +++ b/rowers/templates/document_form.html @@ -83,18 +83,15 @@ $( document ).ready(function() { $('#id_workouttype').on('change', function(){ if ( - $(this).val() == 'rower' - || $(this).val() == 'skierg' - || $(this).val() == 'dynamic' - || $(this).val() == 'slides' - || $(this).val() == 'paddle' - || $(this).val() == 'bike' - || $(this).val() == 'snow' + $(this).val() == 'water' + || $(this).val() == 'coastal' + || $(this).val() == 'c-boat' + || $(this).val() == 'churchboat' ) { + $('#id_boattype').toggle(true); + } else { $('#id_boattype').toggle(false); $('#id_boattype').val('1x'); - } else { - $('#id_boattype').toggle(true); } }); $('#id_workouttype').change(); diff --git a/rowers/templates/manualadd.html b/rowers/templates/manualadd.html index f1baf6f2..a70040c1 100644 --- a/rowers/templates/manualadd.html +++ b/rowers/templates/manualadd.html @@ -16,18 +16,15 @@ $( document ).ready(function() { $('#id_workouttype').on('change', function(){ if ( - $(this).val() == 'rower' - || $(this).val() == 'skierg' - || $(this).val() == 'dynamic' - || $(this).val() == 'slides' - || $(this).val() == 'paddle' - || $(this).val() == 'bike' - || $(this).val() == 'snow' + $(this).val() == 'water' + || $(this).val() == 'coastal' + || $(this).val() == 'c-boat' + || $(this).val() == 'churchboat' ) { + $('#id_boattype').toggle(true); + } else { $('#id_boattype').toggle(false); $('#id_boattype').val('1x'); - } else { - $('#id_boattype').toggle(true); } }); $('#id_workouttype').change(); diff --git a/rowers/templates/plannedsessionview.html b/rowers/templates/plannedsessionview.html index fdb42941..dc96de68 100644 --- a/rowers/templates/plannedsessionview.html +++ b/rowers/templates/plannedsessionview.html @@ -88,7 +88,10 @@
+ Compare Workouts +
{% if coursescript %}