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 @@ {{ workout.distance }}m - {{ workout.duration |durationprint:"%H:%M:%S.%f" }} + {{ workout.duration |durationprint:"%H:%M:%S.%f" }} + + Detach + {% endfor %} @@ -127,6 +130,10 @@ {% endfor %} +

+ Compare Workouts +

{% if coursescript %}
  • diff --git a/rowers/templates/team_compare_select.html b/rowers/templates/team_compare_select.html index 4b6b2796..63c3ae41 100644 --- a/rowers/templates/team_compare_select.html +++ b/rowers/templates/team_compare_select.html @@ -112,7 +112,7 @@
  • {% if workouts %}
    Toggle All
    diff --git a/rowers/templates/team_document_form.html b/rowers/templates/team_document_form.html index 4db8e2fb..899e62da 100644 --- a/rowers/templates/team_document_form.html +++ b/rowers/templates/team_document_form.html @@ -76,25 +76,22 @@ {% endblock %} diff --git a/rowers/templates/workout_form.html b/rowers/templates/workout_form.html index 72a8a0fe..add74b3d 100644 --- a/rowers/templates/workout_form.html +++ b/rowers/templates/workout_form.html @@ -23,18 +23,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() == 'snow' - || $(this).val() == 'bike' + $(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/tests.py b/rowers/tests.py index 4c61f86d..3a1b2dd8 100644 --- a/rowers/tests.py +++ b/rowers/tests.py @@ -321,6 +321,15 @@ def mocked_requests(*args, **kwargs): return MockResponse(json_data,200) elif stravasummarytester.match(args[0]): return MockResponse(stravasummaryjson,200) + elif 'token' in args[0]: + json_data = { + "token_type": "Bearer", + "access_token": "987654321234567898765432123456789", + "refresh_token": "1234567898765432112345678987654321", + "expires_at": 1531385304 + } + return MockResponse(json_data,200) + if c2tester.match(args[0]): if c2uploadtester.match(args[0]): @@ -403,7 +412,7 @@ class C2Objects(DjangoTestCase): workoutstarttime = row.rowdatetime.strftime('%H:%M:%S') self.w = Workout.objects.create( - name='testworkout',workouttype='On-water', + name='testworkout',workouttype='water', user=self.r,date=self.nu.strftime('%Y-%m-%d'), starttime=workoutstarttime, startdatetime=row.rowdatetime, @@ -574,7 +583,7 @@ class STObjects(DjangoTestCase): workoutstarttime = row.rowdatetime.strftime('%H:%M:%S') self.w = Workout.objects.create( - name='testworkout',workouttype='On-water', + name='testworkout',workouttype='water', user=self.r,date=self.nu.strftime('%Y-%m-%d'), starttime=workoutstarttime, startdatetime=row.rowdatetime, @@ -690,7 +699,7 @@ class RunKeeperObjects(DjangoTestCase): workoutstarttime = row.rowdatetime.strftime('%H:%M:%S') self.w = Workout.objects.create( - name='testworkout',workouttype='On-water', + name='testworkout',workouttype='water', user=self.r,date=self.nu.strftime('%Y-%m-%d'), starttime=workoutstarttime, startdatetime=row.rowdatetime, @@ -782,7 +791,7 @@ class UAObjects(DjangoTestCase): workoutstarttime = row.rowdatetime.strftime('%H:%M:%S') self.w = Workout.objects.create( - name='testworkout',workouttype='On-water', + name='testworkout',workouttype='water', user=self.r,date=self.nu.strftime('%Y-%m-%d'), starttime=workoutstarttime, startdatetime=row.rowdatetime, @@ -882,7 +891,7 @@ class TPObjects(DjangoTestCase): workoutstarttime = row.rowdatetime.strftime('%H:%M:%S') self.w = Workout.objects.create( - name='testworkout',workouttype='On-water', + name='testworkout',workouttype='water', user=self.r,date=self.nu.strftime('%Y-%m-%d'), starttime=workoutstarttime, startdatetime=row.rowdatetime, @@ -1074,7 +1083,7 @@ class WorkoutTests(TestCase): ) nu = datetime.datetime.now() self.w = Workout.objects.create(name='testworkout', - workouttype='On-water', + workouttype='water', user=self.r,date=nu.strftime('%Y-%m-%d'), starttime=nu.strftime('%H:%M:%S'), duration="0:55:00",distance=8000) @@ -1092,7 +1101,7 @@ class C2Tests(TestCase): gdproptindate=timezone.now() ) self.nu = datetime.datetime.now() - self.w = Workout.objects.create(name='testworkout',workouttype='On-water', + self.w = Workout.objects.create(name='testworkout',workouttype='water', user=r,date=nu.strftime('%Y-%m-%d'), starttime=nu.strftime('%H:%M:%S'), duration="0:55:00",distance=8000) @@ -1255,7 +1264,7 @@ class DataTest(TestCase): workoutdate = row.rowdatetime.strftime('%Y-%m-%d') workoutstarttime = row.rowdatetime.strftime('%H:%M:%S') - w = Workout.objects.create(name='testworkout',workouttype='On-water', + w = Workout.objects.create(name='testworkout',workouttype='water', user=r,date=self.nu.strftime('%Y-%m-%d'), starttime=workoutstarttime, duration=duration,distance=totaldist, @@ -1999,7 +2008,7 @@ class URLTests(TestCase): self.nu = datetime.datetime.now() filename = 'rowers/testdata/testdata.csv' self.wotw = Workout.objects.create(name='testworkout', - workouttype='On-water', + workouttype='water', user=r,date=self.nu.strftime('%Y-%m-%d'), starttime=self.nu.strftime('%H:%M:%S'), duration="0:55:00",distance=8000, @@ -2241,7 +2250,7 @@ class subroutinetests(TestCase): nu = datetime.datetime.now() filename = 'rowers/testdata/testdata.csv' self.w = Workout.objects.create(name='testworkout', - workouttype='On-water', + workouttype='water', user=r,date=nu.strftime('%Y-%m-%d'), starttime=nu.strftime('%H:%M:%S'), duration="0:55:00",distance=8000, @@ -2266,7 +2275,7 @@ class PlotTests(TestCase): self.nu = datetime.datetime.now() filename = 'rowers/testdata/testdata.csv' self.wotw = Workout.objects.create(name='testworkout', - workouttype='On-water', + workouttype='water', user=r,date=self.nu.strftime('%Y-%m-%d'), starttime=self.nu.strftime('%H:%M:%S'), duration="0:55:00",distance=8000, diff --git a/rowers/urls.py b/rowers/urls.py index 32a32256..c46a3ae1 100644 --- a/rowers/urls.py +++ b/rowers/urls.py @@ -342,8 +342,9 @@ urlpatterns = [ url(r'^workout/(?P\d+)/runkeeperuploadw/$',views.workout_runkeeper_upload_view), url(r'^workout/(?P\d+)/underarmouruploadw/$',views.workout_underarmour_upload_view), url(r'^workout/(?P\d+)/tpuploadw/$',views.workout_tp_upload_view), - url(r'^multi-compare/workout/(?P\d+)$',views.multi_compare_view), - url(r'^multi-compare$',views.multi_compare_view), + url(r'^multi-compare/workout/(?P\d+)/user/(?P\d+)/$',views.multi_compare_view), + url(r'^multi-compare/workout/(?P\d+)/$',views.multi_compare_view), + url(r'^multi-compare/$',views.multi_compare_view), url(r'^user-boxplot/user/(?P\d+)$',views.boxplot_view), url(r'^user-boxplot$',views.boxplot_view), url(r'^user-boxplot-data$',views.boxplot_view_data), @@ -486,12 +487,16 @@ urlpatterns = [ url(r'^sessions/multicreate/user/(?P\d+)/$', views.plannedsession_multicreate_view), url(r'^sessions/(?P\d+)/edit/$',views.plannedsession_edit_view), + url(r'^sessions/(?P\d+)/compare/$',views.plannedsession_compare_view), + url(r'^sessions/(?P\d+)/compare/user/(?P\d+)/$',views.plannedsession_compare_view), url(r'^sessions/(?P\d+)/edit/user/(?P\d+)/$',views.plannedsession_edit_view), url(r'^sessions/(?P\d+)/clone/user/(?P\d+)/$',views.plannedsession_clone_view), url(r'^sessions/(?P\d+)/clone/$',views.plannedsession_clone_view), - url(r'^sessions/(?P\d+)$',views.plannedsession_view, + url(r'^sessions/(?P\d+)/detach/(?P\d+)/user/(?P\d+)/$',views.plannedsession_detach_view), + url(r'^sessions/(?P\d+)/detach/(?P\d+)/$',views.plannedsession_detach_view), + url(r'^sessions/(?P\d+)/$',views.plannedsession_view, name='plannedsession_view'), - url(r'^sessions/(?P\d+)/user/(?P\d+)$',views.plannedsession_view, + url(r'^sessions/(?P\d+)/user/(?P\d+)/$',views.plannedsession_view, name='plannedsession_view'), url(r'^sessions/(?P\d+)/deleteconfirm$',views.PlannedSessionDelete.as_view()), url(r'^sessions/(?P\d+)/delete$',views.PlannedSessionDelete.as_view(), diff --git a/rowers/views.py b/rowers/views.py index cc9cdd8d..e288989c 100644 --- a/rowers/views.py +++ b/rowers/views.py @@ -116,6 +116,7 @@ from sporttracksstuff import sporttracks_open from tpstuff import tp_open from iso8601 import ParseError import stravastuff +from stravastuff import strava_open import polarstuff import sporttracksstuff import underarmourstuff @@ -1732,6 +1733,12 @@ def workout_strava_upload_view(request,id=0): message = "" r = getrower(request.user) res = -1 + + try: + thetoken = strava_open(request.user) + except NoTokenError: + return HttpResponseRedirect("/rowers/me/stravaauthorize") + if (r.stravatoken == '') or (r.stravatoken is None): s = "Token doesn't exist. Need to authorize" return HttpResponseRedirect("/rowers/me/stravaauthorize/") @@ -1748,11 +1755,16 @@ def workout_strava_upload_view(request,id=0): newnotes = w.notes+'\n from '+w.workoutsource+' via rowsandall.com' except TypeError: newnotes = 'from '+w.workoutsource+' via rowsandall.com' - activity_type = r.stravaexportas - res,mes = stravastuff.handle_stravaexport(f,w.name, - r.stravatoken, - description=newnotes, - activity_type=activity_type) + if w.workouttype in mytypes.rowtypes: + activity_type = r.stravaexportas + else: + activity_type = mytypes.stravamapping[w.workouttype] + + res,mes = stravastuff.handle_stravaexport( + f,w.name, + r.stravatoken, + description=newnotes, + activity_type=activity_type) if res==0: messages.error(request,mes) w.uploadedtostrava = -1 @@ -2421,15 +2433,22 @@ def rower_process_stravacallback(request): if res[0]: access_token = res[0] - + expires_in = res[1] + refresh_token = res[2] + + expirydatetime = timezone.now()+datetime.timedelta(seconds=expires_in) + r = getrower(request.user) r.stravatoken = access_token + r.stravatokenexpirydate = expirydatetime + r.stravarefreshtoken = refresh_token r.save() successmessage = "Tokens stored. Good to go" messages.info(request,successmessage) - return imports_view(request) + url = reverse(workouts_view) + return HttpResponseRedirect(url) else: message = "Something went wrong with the Strava authorization" messages.error(request,message) @@ -3372,6 +3391,11 @@ def addmanual_view(request): rankingpiece = form.cleaned_data['rankingpiece'] except KeyError: rankingpiece = False + + try: + duplicate = form.cleaned_data['duplicate'] + except KeyError: + duplicate = False if private: privacy = 'private' @@ -3395,6 +3419,7 @@ def addmanual_view(request): avghr=avghr, rankingpiece=rankingpiece, avgpwr=avgpwr, + duplicate=duplicate, avgspm=avgspm, title = name, notes=notes, @@ -5325,6 +5350,8 @@ def team_comparison_select(request, r = getrequestrower(request,userid=userid) requestrower = getrower(request.user) + request.session.pop('ps',None) + if 'waterboattype' in request.session: waterboattype = request.session['waterboattype'] else: @@ -5521,10 +5548,53 @@ def team_comparison_select(request, 'teams':get_my_teams(request.user), }) +@login_required() +def plannedsession_compare_view(request,id=0,userid=0): + r = getrequestrower(request,userid=userid) + + try: + ps = PlannedSession.objects.get(id=id) + except PlannedSession.DoesNotExist: + raise Http404("Planned session does not exist") + + m = ps.manager + mm = m.rower + + if ps.manager != request.user: + if r.rowerplan == 'coach': + teams = Team.objects.filter(manager=request.user) + members = Rower.objects.filter(team__in=teams).distinct() + teamusers = [m.user for m in members] + if ps.manager not in teamusers: + raise PermissionDenied("You do not have access to this session") + elif r not in ps.rower.all(): + raise PermissionDenied("You do not have access to this session") + + workouts = Workout.objects.filter(plannedsession=ps) + + ids = [int(w.id) for w in workouts] + + labeldict = { + int(w.id): w.__unicode__() for w in workouts + } + + xparam = 'time' + yparam = 'hr' + plottype = 'line' + + request.session['ids'] = ids + request.session['xparam'] = xparam + request.session['yparam'] = yparam + request.session['plottype'] = plottype + request.session['ps'] = ps.id + + url = reverse(multi_compare_view,kwargs={'userid':userid,'id':ids[0]}) + + return HttpResponseRedirect(url) # Team comparison @login_required() -def multi_compare_view(request,id=0): +def multi_compare_view(request,id=0,userid=0): promember=0 if not request.user.is_anonymous(): r = getrower(request.user) @@ -5549,45 +5619,9 @@ def multi_compare_view(request,id=0): int(w.id): w.__unicode__() for w in workouts } - res = interactive_multiple_compare_chart(ids,xparam,yparam, - promember=promember, - plottype=plottype, - labeldict=labeldict) - script = res[0] - div = res[1] - errormessage = res[3] - if errormessage != '': - messages.error(request,errormessage) - - r = getrower(request.user) - breadcrumbs = [ - { - 'url':'/rowers/list-workouts', - 'name':'Workouts' - }, - { - 'url':reverse(team_comparison_select,kwargs={'teamid':teamid}), - 'name': 'Compare Select' - }, - { - 'url':reverse(multi_compare_view), - 'name': 'Comparison Chart' - } - ] - return render(request,'multicompare.html', - {'interactiveplot':script, - 'the_div':div, - 'breadcrumbs':breadcrumbs, - 'rower':r, - 'active':'nav-workouts', - 'promember':promember, - 'teamid':teamid, - 'chartform':chartform, - 'teams':get_my_teams(request.user), - }) else: return HttpResponse("Form is not valid") - if request.method == 'POST' and 'ids' in request.session: + elif request.method == 'POST' and 'ids' in request.session: chartform = ChartParamChoiceForm(request.POST) if chartform.is_valid(): xparam = chartform.cleaned_data['xparam'] @@ -5601,42 +5635,24 @@ def multi_compare_view(request,id=0): labeldict = { int(w.id): w.__unicode__() for w in workouts } - - res = interactive_multiple_compare_chart(ids,xparam,yparam, - promember=promember, - plottype=plottype, - labeldict=labeldict) - script = res[0] - div = res[1] - - r = getrower(request.user) - breadcrumbs = [ - { - 'url':'/rowers/list-workouts', - 'name':'Workouts' - }, - { - 'url':reverse(team_comparison_select,kwargs={'teamid':teamid}), - 'name': 'Compare Select' - }, - { - 'url':reverse(multi_compare_view), - 'name': 'Comparison Chart' + elif 'ids' in request.session and 'plottype' in request.session: + xparam = request.session['xparam'] + yparam = request.session['yparam'] + plottype = request.session['plottype'] + teamid = 0 + ids = request.session['ids'] + workouts = [Workout.objects.get(id=id) for id in ids] + labeldict = { + int(w.id): w.__unicode__() for w in workouts + } + chartform = ChartParamChoiceForm( + initial = { + 'xparam':xparam, + 'yparam':yparam, + 'plottype':plottype, + 'teamid':teamid } - ] - - - return render(request,'multicompare.html', - {'interactiveplot':script, - 'the_div':div, - 'breadcrumbs':breadcrumbs, - 'rower':r, - 'active':'nav-workouts', - 'promember':promember, - 'teamid':teamid, - 'chartform':chartform, - 'teams':get_my_teams(request.user), - }) + ) else: url = reverse(team_comparison_select, @@ -5645,6 +5661,76 @@ def multi_compare_view(request,id=0): 'teamid':0}) return HttpResponseRedirect(url) + + res = interactive_multiple_compare_chart(ids,xparam,yparam, + promember=promember, + plottype=plottype, + labeldict=labeldict) + script = res[0] + div = res[1] + errormessage = res[3] + if errormessage != '': + messages.error(request,errormessage) + + r = getrower(request.user) + + breadcrumbs = [ + { + 'url':'/rowers/list-workouts', + 'name':'Workouts' + }, + { + 'url':reverse(team_comparison_select,kwargs={'teamid':teamid}), + 'name': 'Compare Select' + }, + { + 'url':reverse(multi_compare_view), + 'name': 'Comparison Chart' + } + ] + + if 'ps' in request.session: + ps = PlannedSession.objects.get(id=int(request.session['ps'])) + breadcrumbs = [ + { + 'url': reverse(plannedsessions_view, + kwargs={'userid':userid}), + 'name': 'Sessions' + }, + { + 'url':reverse(plannedsession_view, + kwargs={ + 'userid':userid, + 'id':ps.id, + } + ), + 'name': ps.id + }, + { + 'url':reverse(plannedsession_compare_view, + kwargs={ + 'userid':userid, + 'id':ps.id, + } + ), + 'name': 'Compare' + } + ] + + + return render(request,'multicompare.html', + {'interactiveplot':script, + 'the_div':div, + 'breadcrumbs':breadcrumbs, + 'rower':r, + 'active':'nav-workouts', + 'promember':promember, + 'teamid':teamid, + 'chartform':chartform, + 'teams':get_my_teams(request.user), + }) + + # Multi Flex Chart with Grouping @user_passes_test(ispromember,login_url="/rowers/promembership", message="This functionality requires a Pro plan or higher", @@ -6740,6 +6826,7 @@ def workouts_view(request,message='',successmessage='', enddate = startdate startdate = s + startdatestring = startdate.strftime('%Y-%m-%d') enddatestring = enddate.strftime('%Y-%m-%d') @@ -6755,6 +6842,10 @@ def workouts_view(request,message='',successmessage='', except ValueError: activity_enddate = enddate + g_startdate = activity_startdate + g_enddate = activity_enddate + + if teamid: try: theteam = Team.objects.get(id=teamid) @@ -6771,6 +6862,7 @@ def workouts_view(request,message='',successmessage='', team=theteam, startdatetime__gte=activity_startdate, startdatetime__lte=activity_enddate, + duplicate=False, privacy='visible').order_by("-date", "-starttime") elif theteam.viewing == 'coachonly': workouts = Workout.objects.filter( @@ -6782,6 +6874,7 @@ def workouts_view(request,message='',successmessage='', team=theteam,user=r, startdatetime__gte=activity_startdate, enddatetime__lte=activity_enddate, + duplicate=False, privacy='visible').order_by("-startdatetime") @@ -6797,6 +6890,7 @@ def workouts_view(request,message='',successmessage='', user=r, startdatetime__gte=activity_startdate, startdatetime__lte=activity_enddate, + duplicate=False, privacy='visible').order_by("-startdatetime") else: theteam = None @@ -6806,9 +6900,18 @@ def workouts_view(request,message='',successmessage='', startdatetime__lte=enddate).order_by("-date", "-starttime") g_workouts = Workout.objects.filter( user=r, + duplicate=False, startdatetime__gte=activity_startdate, startdatetime__lte=activity_enddate).order_by("-startdatetime") + + if len(g_workouts) == 0: + g_workouts = Workout.objects.filter( + user=r, + startdatetime__gte=timezone.now()-timedelta(days=15)).order_by("-startdatetime") + g_enddate = timezone.now() + g_startdate = (timezone.now()-timedelta(days=15)) + if rankingonly: workouts = workouts.exclude(rankingpiece=False) @@ -6846,9 +6949,10 @@ def workouts_view(request,message='',successmessage='', else: stack='type' + script,div = interactive_activitychart(g_workouts, - activity_startdate, - activity_enddate, + g_startdate, + g_enddate, stack=stack) messages.info(request,successmessage) @@ -9845,6 +9949,11 @@ def workout_edit_view(request,id=0,message="",successmessage=""): except KeyError: rankingpiece =- Workout.objects.get(id=id).rankingpiece + try: + duplicate = form.cleaned_data['duplicate'] + except KeyError: + duplicate = Workout.objects.get(id=id).duplicate + if private: privacy = 'private' else: @@ -9884,6 +9993,7 @@ def workout_edit_view(request,id=0,message="",successmessage=""): row.duration = duration row.distance = distance row.boattype = boattype + row.duplicate = duplicate row.privacy = privacy row.rankingpiece = rankingpiece row.timezone = thetimezone @@ -10268,6 +10378,11 @@ def workout_add_chart_view(request,id,plotnr=1): # The page where you select which Strava workout to import @login_required() def workout_stravaimport_view(request,message="",userid=0): + try: + thetoken = strava_open(request.user) + except NoTokenError: + return HttpResponseRedirect("/rowers/me/stravaauthorize/") + res = stravastuff.get_strava_workout_list(request.user) r = getrequestrower(request,userid=userid) @@ -10277,6 +10392,7 @@ def workout_stravaimport_view(request,message="",userid=0): r = getrower(request.user) + if (res.status_code != 200): if (res.status_code == 401): r = getrower(request.user) @@ -10420,15 +10536,7 @@ def workout_runkeeperimport_view(request,message="",userid=0): def workout_underarmourimport_view(request,message="",userid=0): res = underarmourstuff.get_underarmour_workout_list(request.user) if (res.status_code != 200): - if (res.status_code == 401): - r = getrower(request.user) - if (r.underarmourtoken == '') or (r.underarmourtoken is None): - s = "Token doesn't exist. Need to authorize" - return HttpResponseRedirect("/rowers/me/underarmourauthorize/") - message = "Something went wrong in workout_underarmourimport_view" - messages.error(request,message) - url = reverse(workouts_view) - return HttpResponseRedirect(url) + return HttpResponseRedirect("/rowers/me/underarmourauthorize/") workouts = [] items = res.json()['_embedded']['workouts'] @@ -10778,7 +10886,7 @@ def workout_getimportview(request,externalid,source = 'c2'): if strokedata.empty: distance = data['distance'] c2id = data['id'] - workouttype = data['type'] + workouttype = mytypes.c2mappinginv[data['type']] verified = data['verified'] startdatetime = iso8601.parse_date(data['date']) weightclass = data['weight_class'] @@ -10827,7 +10935,7 @@ def workout_getimportview(request,externalid,source = 'c2'): return HttpResponseRedirect(url) - # strokdata not empty - continue + # strokedata not empty - continue id,message = importsources[source].add_workout_from_data( request.user, externalid,data, @@ -15302,6 +15410,26 @@ def plannedsession_edit_view(request,id=0,userid=0): }) +@login_required() +def plannedsession_detach_view(request,id=0,psid=0): + + r = getrequestrower(request) + + try: + ps = PlannedSession.objects.get(id=psid) + except PlannedSession.DoesNotExist: + raise Http404("Planned Session does not exist") + + w = get_workout(id) + + if (checkworkoutuser(request.user,w)==False): + return HttpResponseForbidden("Permission Error") + + remove_workout_plannedsession(w,ps) + + url = reverse(plannedsession_view,kwargs={'id':psid}) + + return HttpResponseRedirect(url) @login_required() def plannedsession_view(request,id=0,userid=0):