diff --git a/rowers/dataprep.py b/rowers/dataprep.py index 8d6ed877..827a9717 100644 --- a/rowers/dataprep.py +++ b/rowers/dataprep.py @@ -1572,6 +1572,13 @@ def new_workout_from_file(r, f2, # Get workout type from fit & tcx if (fileformat == 'fit'): # pragma: no cover workouttype = get_workouttype_from_fit(f2, workouttype=workouttype) + new_title = get_title_from_fit(f2) + if new_title: + title = new_title + new_notes = get_notes_from_fit(f2) + if new_notes: + notes = new_notes + # if (fileformat == 'tcx'): # workouttype_from_tcx = get_workouttype_from_tcx(f2,workouttype=workouttype) # if workouttype != 'rower' and workouttype_from_tcx not in mytypes.otwtypes: diff --git a/rowers/dataroutines.py b/rowers/dataroutines.py index d5f1e168..d34a1a3e 100644 --- a/rowers/dataroutines.py +++ b/rowers/dataroutines.py @@ -1350,6 +1350,39 @@ def handle_nonpainsled(f2, fileformat, summary='', startdatetime='', empowerfirm # Create new workout from file and store it in the database # This routine should be used everywhere in views.py +def get_notes_from_fit(filename): + try: + fitfile = FitFile(filename, check_crc=False) + except FitHeaderError: # pragma: no cover + return '' + + records = fitfile.messages + notes = '' + for record in records: + if record.name == 'session': + try: + notes = ' '.join(record.get_values()['description'].split()) + except KeyError: + pass + + return notes + +def get_title_from_fit(filename): + try: + fitfile = FitFile(filename, check_crc=False) + except FitHeaderError: # pragma: no cover + return '' + + records = fitfile.messages + title = '' + for record in records: + if record.name == 'workout': + try: + title = ' '.join(record.get_values()['wkt_name'].split()) + except KeyError: + pass + + return title def get_workouttype_from_fit(filename, workouttype='water'): try: @@ -1359,16 +1392,27 @@ def get_workouttype_from_fit(filename, workouttype='water'): records = fitfile.messages fittype = 'rowing' + subsporttype = '' for record in records: - if record.name in ['sport', 'lap']: + if record.name in ['sport', 'lap','session']: try: fittype = record.get_values()['sport'].lower() + try: + subsporttype = record.get_values()['sub_sport'].lower() + except KeyError: + subsporttype = '' except (KeyError, AttributeError): # pragma: no cover - return 'water' - try: - workouttype = mytypes.fitmappinginv[fittype] - except KeyError: # pragma: no cover - return workouttype + pass + if subsporttype: + try: + workouttype = mytypes.fitmappinginv[subsporttype] + except KeyError: + pass + else: + try: + workouttype = mytypes.fitmappinginv[fittype] + except KeyError: + pass return workouttype diff --git a/rowers/integrations/__init__.py b/rowers/integrations/__init__.py index dc76fdb1..409a1ce5 100644 --- a/rowers/integrations/__init__.py +++ b/rowers/integrations/__init__.py @@ -5,6 +5,7 @@ from .sporttracks import SportTracksIntegration from .rp3 import RP3Integration from .trainingpeaks import TPIntegration from .polar import PolarIntegration +from .intervals import IntervalsIntegration importsources = { 'c2': C2Integration, @@ -15,5 +16,6 @@ importsources = { 'tp':TPIntegration, 'rp3':RP3Integration, 'polar': PolarIntegration, + 'intervals': IntervalsIntegration, } diff --git a/rowers/integrations/c2.py b/rowers/integrations/c2.py index 2a4a57ab..c2d21332 100644 --- a/rowers/integrations/c2.py +++ b/rowers/integrations/c2.py @@ -63,7 +63,7 @@ class C2Integration(SyncIntegration): 'client_id': C2_CLIENT_ID, 'client_secret': C2_CLIENT_SECRET, 'redirect_uri': C2_REDIRECT_URI, - 'autorization_uri': "https://log.concept2.com/oauth/authorize", + 'authorization_uri': "https://log.concept2.com/oauth/authorize", 'content_type': 'application/x-www-form-urlencoded', 'tokenname': 'c2token', 'refreshtokenname': 'c2refreshtoken', diff --git a/rowers/integrations/integrations.py b/rowers/integrations/integrations.py index 0cfaf0ad..2ff231a2 100644 --- a/rowers/integrations/integrations.py +++ b/rowers/integrations/integrations.py @@ -109,7 +109,7 @@ class SyncIntegration(metaclass=ABCMeta): if 'grant_type' in self.oauth_data: if self.oauth_data['grant_type']: post_data['grant_type'] = self.oauth_data['grant_type'] - if 'strava' in self.oauth_data['autorization_uri']: + if 'strava' in self.oauth_data['authorization_uri']: post_data['grant_type'] = "authorization_code" if 'json' in self.oauth_data['content_type']: diff --git a/rowers/integrations/intervals.py b/rowers/integrations/intervals.py new file mode 100644 index 00000000..2669907f --- /dev/null +++ b/rowers/integrations/intervals.py @@ -0,0 +1,273 @@ +from .integrations import SyncIntegration, NoTokenError, create_or_update_syncrecord, get_known_ids +from rowers.models import Rower, User, Workout, TombStone +from rowingdata import rowingdata + +from rowers import mytypes + +from rowers.rower_rules import is_workout_user, ispromember +from rowers.utils import myqueue, dologging, custom_exception_handler +from rowers.tasks import handle_intervals_getworkout + +import urllib +import gzip +import requests +import arrow +import datetime +import os +from uuid import uuid4 +from django.utils import timezone +from datetime import timedelta + +from rowsandall_app.settings import ( + INTERVALS_CLIENT_ID, INTERVALS_REDIRECT_URI, INTERVALS_CLIENT_SECRET, SITE_URL +) + +import django_rq +queue = django_rq.get_queue('default', default_timeout=3600) +queuelow = django_rq.get_queue('low', default_timeout=3600) +queuehigh = django_rq.get_queue('high', default_timeout=3600) + + +def seconds_to_duration(seconds): + hours = seconds // 3600 + minutes = (seconds % 3600) // 60 + remaining_seconds = seconds % 60 + + # Format as "H:MM:SS" or "MM:SS" if no hours + if hours > 0: + return f"{int(hours)}:{int(minutes):02}:{int(remaining_seconds):02}" + else: + return f"{int(minutes)}:{int(remaining_seconds):02}" + +headers = { + 'Content-Type': 'application/json', + 'Accept': 'application/json' +} + +intervals_authorize_url = 'https://intervals.icu/oauth/authorize?' +intervals_token_url = 'https://intervals.icu/api/oauth/token' + +class IntervalsIntegration(SyncIntegration): + def __init__(self, *args, **kwargs): + super(IntervalsIntegration, self).__init__(*args, **kwargs) + self.oauth_data = { + 'client_id': INTERVALS_CLIENT_ID, + 'client_secret': INTERVALS_CLIENT_SECRET, + 'redirect_uri': INTERVALS_REDIRECT_URI, + 'authorization_uri': intervals_authorize_url, + 'content_type': 'application/json', + 'tokenname': 'intervals_token', + 'expirydatename': 'intervals_exp', + 'refreshtokenname': 'intervals_r', + 'bearer_auth': True, + 'base_url': 'https://intervals.icu/api/v1/', + 'grant_type': 'refresh_token', + 'headers': headers, + 'scope': 'ACTIVITY:WRITE, LIBRARY:READ', + } + + def get_token(self, code, *args, **kwargs): + post_data = { + 'client_id': str(self.oauth_data['client_id']), + 'client_secret': self.oauth_data['client_secret'], + 'code': code, + } + + response = requests.post( + intervals_token_url, + data=post_data, + ) + + if response.status_code not in [200, 201]: + dologging('intervals.icu.log',response.text) + return [0,"Failed to get token. ",0] + + token_json = response.json() + access_token = token_json['access_token'] + athlete = token_json['athlete'] + + return [access_token, athlete, ''] + + def get_name(self): + return 'Intervals' + + def get_shortname(self): + return 'intervals' + + def open(self, *args, **kwargs): + # dologging('intervals.icu.log', "Getting token for user {id}".format(id=self.rower.id)) + token = super(IntervalsIntegration, self).open(*args, **kwargs) + return token + + def createworkoutdata(self, w, *args, **kwargs) -> str: + dozip = kwargs.get('dozip', True) + filename = w.csvfilename + try: + row = rowingdata(csvfile=filename) + except IOError: # pragma: no cover + data = dataprep.read_df_sql(w.id) + try: + datalength = len(data) + except AttributeError: + datalength = 0 + + if datalength == 0: + data.rename(columns=columndict, inplace=True) + _ = data.to_csv(w.csvfilename+'.gz', index_label='index', compression='gzip') + + try: + row = rowingdata(csvfile=filename) + except IOError: # pragma: no cover + return '' # pragma: no cover + else: + return '' + + tcxfilename = w.csvfilename[:-4] + '.tcx' + try: + newnotes = w.notes + '\n from'+w.workoutsource+' via rowsandall.com' + except TypeError: + newnotes = 'from'+w.workoutsource+' via rowsandall.com' + + row.exporttotcx(tcxfilename, notes=newnotes) + if dozip: + gzfilename = tcxfilename + '.gz' + try: + with open(tcxfilename, 'rb') as inF: + s = inF.read() + with gzip.GzipFile(gzfilename, 'wb') as outF: + outF.write(s) + try: + os.remove(tcxfilename) + except WindowsError: # pragma: no cover + pass + except FileNotFoundError: + return '' + + return gzfilename + + return tcxfilename + + + def workout_export(self, workout, *args, **kwargs) -> str: + token = self.open() + + filename = self.createworkoutdata(workout) + if not filename: + return 0 + + params = { + 'name': workout.name, + 'description': workout.notes, + } + + + authorizationstring = str('Bearer ' + token) + # headers with authorization string and content type multipart/form-data + headers = { + 'Authorization': authorizationstring, + } + + url = "https://intervals.icu/api/v1/athlete/{athleteid}/activities".format(athleteid=0) + + with open(filename, 'rb') as f: + files = {'file': f} + response = requests.post(url, params=params, headers=headers, files=files) + + if response.status_code not in [200, 201]: + dologging('intervals.icu.log', response.reason) + return 0 + + id = response.json()['id'] + # set workout type to workouttype + url = "https://intervals.icu/api/v1/activity/{activityid}".format(activityid=id) + + thetype = mytypes.intervalsmapping[workout.workouttype] + response = requests.put(url, headers=headers, json={'type': thetype}) + if response.status_code not in [200, 201]: + return 0 + + workout.uploadedtointervals = id + workout.save() + + os.remove(filename) + + return id + + def get_workout_list(self, *args, **kwargs) -> int: + url = self.oauth_data['base_url'] + 'athlete/0/activities?' + startdate = timezone.now() - timedelta(days=30) + enddate = timezone.now() + timedelta(days=1) + startdatestring = kwargs.get("startdate","") + enddatestring = kwargs.get("enddate","") + + try: + startdate = arrow.get(startdatestring).datetime + except: + pass + try: + enddate = arrow.get(enddatestring).datetime + except: + pass + + url += 'oldest=' + startdate.strftime('%Y-%m-%d') + '&newest=' + enddate.strftime('%Y-%m-%d') + headers = { + 'accept': '*/*', + 'authorization': 'Bearer ' + self.open(), + } + + response = requests.get(url, headers=headers) + if response.status_code != 200: + dologging('intervals.icu.log', response.text) + return [] + + data = response.json() + known_interval_ids = get_known_ids(self.rower, 'intervalsid') + workouts = [] + + for item in data: + i = item['id'] + r = item['type'] + d = item['distance'] + ttot = seconds_to_duration(item['moving_time']) + s = item['start_date'] + s2 = '' + c = item['name'] + if i in known_interval_ids: + nnn = '' + else: + nnn = 'NEW' + + keys = ['id','distance','duration','starttime', + 'rowtype','source','name','new'] + + values = [i, d, ttot, s, r, s2, c, nnn] + + ress = dict(zip(keys, values)) + workouts.append(ress) + + return workouts + + + def get_workout(self, id, *args, **kwargs) -> int: + _ = self.open() + r = self.rower + + record = create_or_update_syncrecord(r, None, intervalsid=id) + + _ = myqueue(queuehigh, + handle_intervals_getworkout, + self.rower, + self.rower.intervals_token, + id) + + def get_workouts(workout, *args, **kwargs) -> list: + return NotImplemented + + def make_authorization_url(self, *args, **kwargs): + return super(IntervalsIntegration, self).make_authorization_url(*args, **kwargs) + + def token_refresh(self, *args, **kwargs): + return super(IntervalsIntegration, self).token_refresh(*args, **kwargs) + + + diff --git a/rowers/integrations/nk.py b/rowers/integrations/nk.py index 960b2f51..a26a7cc9 100644 --- a/rowers/integrations/nk.py +++ b/rowers/integrations/nk.py @@ -35,7 +35,7 @@ class NKIntegration(SyncIntegration): 'client_id': NK_CLIENT_ID, 'client_secret': NK_CLIENT_SECRET, 'redirect_uri': NK_REDIRECT_URI, - 'autorization_uri': NK_OAUTH_LOCATION+"/oauth/authorize", + 'authorization_uri': NK_OAUTH_LOCATION+"/oauth/authorize", 'content_type': 'application/json', 'tokenname': 'nktoken', 'refreshtokenname': 'nkrefreshtoken', diff --git a/rowers/integrations/rp3.py b/rowers/integrations/rp3.py index 245c7615..bdbae35e 100644 --- a/rowers/integrations/rp3.py +++ b/rowers/integrations/rp3.py @@ -30,7 +30,7 @@ class RP3Integration(SyncIntegration): 'client_id': RP3_CLIENT_ID, 'client_secret': RP3_CLIENT_SECRET, 'redirect_uri': RP3_REDIRECT_URI, - 'autorization_uri': "https://rp3rowing-app.com/oauth/authorize?", + 'authorization_uri': "https://rp3rowing-app.com/oauth/authorize?", 'content_type': 'application/x-www-form-urlencoded', # 'content_type': 'application/json', 'tokenname': 'rp3token', diff --git a/rowers/integrations/strava.py b/rowers/integrations/strava.py index b78281a0..8df2cb31 100644 --- a/rowers/integrations/strava.py +++ b/rowers/integrations/strava.py @@ -89,7 +89,7 @@ class StravaIntegration(SyncIntegration): 'client_id': STRAVA_CLIENT_ID, 'client_secret': STRAVA_CLIENT_SECRET, 'redirect_uri': STRAVA_REDIRECT_URI, - 'autorization_uri': "https://www.strava.com/oauth/authorize", + 'authorization_uri': "https://www.strava.com/oauth/authorize", 'content_type': 'application/json', 'tokenname': 'stravatoken', 'refreshtokenname': 'stravarefreshtoken', diff --git a/rowers/integrations/trainingpeaks.py b/rowers/integrations/trainingpeaks.py index 14e3307a..404801de 100644 --- a/rowers/integrations/trainingpeaks.py +++ b/rowers/integrations/trainingpeaks.py @@ -41,7 +41,7 @@ class TPIntegration(SyncIntegration): 'client_id': TP_CLIENT_ID, 'client_secret': TP_CLIENT_SECRET, 'redirect_uri': TP_REDIRECT_URI, - 'autorization_uri': "https://oauth.trainingpeaks.com/oauth/authorize?", + 'authorization_uri': "https://oauth.trainingpeaks.com/oauth/authorize?", 'content_type': 'application/x-www-form-urlencoded', 'tokenname': 'tptoken', 'refreshtokenname': 'tprefreshtoken', diff --git a/rowers/models.py b/rowers/models.py index 857a2680..259f9f55 100644 --- a/rowers/models.py +++ b/rowers/models.py @@ -372,7 +372,7 @@ def update_records(url=c2url, verbose=True): # Create a DataFrame df = pd.DataFrame(rows, columns=headers) - except: # pragma: no cover + except: # pragma: no cover df = pd.DataFrame() if not df.empty: @@ -1172,6 +1172,8 @@ class Rower(models.Model): default='', max_length=200, blank=True, null=True) c2_auto_export = models.BooleanField(default=False) c2_auto_import = models.BooleanField(default=False) + intervals_auto_export = models.BooleanField(default=False) + intervals_auto_import = models.BooleanField(default=False) sporttrackstoken = models.CharField( default='', max_length=200, blank=True, null=True) sporttrackstokenexpirydate = models.DateTimeField(blank=True, null=True) @@ -1240,6 +1242,10 @@ class Rower(models.Model): strava_auto_import = models.BooleanField(default=False) strava_auto_delete = models.BooleanField(default=False) + intervals_token = models.CharField( + default='', max_length=200, blank=True, null=True) + intervals_owner_id = models.CharField(default='', max_length=200,blank=True, null=True) + privacychoices = ( ('visible', 'Visible'), ('hidden', 'Hidden'), @@ -3691,6 +3697,7 @@ class Workout(models.Model): uploadedtogarmin = models.BigIntegerField(default=0) uploadedtorp3 = models.BigIntegerField(default=0) uploadedtonk = models.BigIntegerField(default=0) + uploadedtointervals = models.CharField(default=None,null=True, max_length=100) forceunit = models.CharField(default='lbs', choices=( ('lbs', 'lbs'), @@ -3822,6 +3829,7 @@ class TombStone(models.Model): uploadedtosporttracks = models.BigIntegerField(default=0) uploadedtotp = models.BigIntegerField(default=0) uploadedtonk = models.BigIntegerField(default=0) + uploadedtointervals = models.CharField(default=None,null=True, max_length=100) @receiver(models.signals.pre_delete, sender=Workout) def create_tombstone_on_delete(sender, instance, **kwargs): @@ -3830,7 +3838,8 @@ def create_tombstone_on_delete(sender, instance, **kwargs): uploadedtoc2=instance.uploadedtoc2, uploadedtostrava=instance.uploadedtostrava, uploadedtotp=instance.uploadedtotp, - uploadedtonk=instance.uploadedtonk + uploadedtonk=instance.uploadedtonk, + uploadedtointervals=instance.uploadedtointervals, ) t.save() @@ -3846,6 +3855,7 @@ class SyncRecord(models.Model): c2id = models.BigIntegerField(unique=True,null=True,default=None) tpid = models.BigIntegerField(unique=True,null=True,default=None) rp3id = models.BigIntegerField(unique=True,null=True,default=None) + intervalsid = models.CharField(unique=True, null=True, default=None, max_length=100) def save(self, *args, **kwargs): if self.workout: @@ -3861,7 +3871,7 @@ class SyncRecord(models.Model): str2 = '' - for field in ['stravaid', 'sporttracksid', 'nkid', 'c2id', 'tpid']: + for field in ['stravaid', 'sporttracksid', 'nkid', 'c2id', 'tpid', 'intervalsid']: value = getattr(self, field, None) if value is not None: str2 += '{w}: {v},'.format( diff --git a/rowers/mytypes.py b/rowers/mytypes.py index afc90c8b..3f19525c 100644 --- a/rowers/mytypes.py +++ b/rowers/mytypes.py @@ -148,6 +148,7 @@ garminmapping = {key: value for key, value in Reverse(garmincollection)} fitcollection = ( ('water', 'rowing'), ('rower', 'rowing'), + ('rower', 'indoor_rowing'), ('skierg', 'cross_country_skiing'), ('bike', 'cycling'), ('bikeerg', 'cycling'), @@ -180,6 +181,41 @@ fitcollection = ( fitmapping = {key: value for key, value in Reverse(fitcollection)} + + +intervalscollection = ( + ('water', 'Rowing'), + ('rower', 'VirtualRow'), + ('skierg', 'NordicSki'), + ('bike', 'Ride'), + ('bikeerg', 'VirtualRide'), + ('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'), + ('Yoga', 'Yoga'), + ('other', 'Other'), +) + +intervalsmapping = {key: value for key, value in Reverse(intervalscollection)} + stcollection = ( ('water', 'Rowing'), ('rower', 'Rowing'), @@ -332,6 +368,9 @@ garminmappinginv = {value: key for key, value in Reverse( fitmappinginv = {value: key for key, value in Reverse( fitcollection) if value is not None} +intervalsmappinginv = {value: key for key, value in Reverse( + intervalscollection) if value is not None} + otwtypes = ( 'water', 'coastal', diff --git a/rowers/tasks.py b/rowers/tasks.py index bad2cc3c..a43a225d 100644 --- a/rowers/tasks.py +++ b/rowers/tasks.py @@ -24,6 +24,7 @@ from rowers.courseutils import ( InvalidTrajectoryError ) from rowers.emails import send_template_email +from rowers.mytypes import intervalsmappinginv from rowers.nkimportutils import ( get_nk_summary, get_nk_allstats, get_nk_intervalstats, getdict, strokeDataToDf, add_workout_from_data @@ -59,6 +60,8 @@ import rowingdata from rowingdata import make_cumvalues, make_cumvalues_array from uuid import uuid4 from rowingdata import rowingdata as rdata +from rowingdata import FITParser as FP +from rowingdata.otherparsers import FitSummaryData from datetime import timedelta @@ -3485,6 +3488,72 @@ def handle_nk_async_workout(alldata, userid, nktoken, nkid, delaysec, defaulttim return workoutid +@app.task +def handle_intervals_getworkout(rower, intervalstoken, workoutid, debug=False, **kwargs): + authorizationstring = str('Bearer '+intervalstoken) + headers = { + 'authorization': authorizationstring, + } + + url = "https://intervals.icu/api/v1/activity/{}".format(workoutid) + + response = requests.get(url, headers=headers) + if response.status_code != 200: + return 0 + + data = response.json() + try: + title = data['name'] + except KeyError: + title = 'Intervals workout' + + try: + workouttype = intervalsmappinginv[data['type']] + except KeyError: + workouttype = 'water' + + + url = "https://intervals.icu/api/v1/activity/{workoutid}/fit-file".format(workoutid=workoutid) + + response = requests.get(url, headers=headers) + + if response.status_code != 200: + return 0 + + try: + fit_data = response.content + fit_filename = 'media/'+f'{uuid4().hex[:16]}.fit' + with open(fit_filename, 'wb') as fit_file: + fit_file.write(fit_data) + except Exception as e: + return 0 + + try: + row = FP(fit_filename) + rowdata = rowingdata.rowingdata(df=row.df) + rowsummary = FitSummaryData(fit_filename) + duration = totaltime_sec_to_string(rowdata.duration) + distance = rowdata.df[" Horizontal (meters)"].iloc[-1] + except Exception as e: + return 0 + + uploadoptions = { + 'secret': UPLOAD_SERVICE_SECRET, + 'user': rower.user.id, + 'boattype': '1x', + 'workouttype': workouttype, + 'file': fit_filename, + 'intervalsid': workoutid, + 'title': title, + 'rpe': 0, + 'notes': '', + 'offline': False, + } + + url = UPLOAD_SERVICE_URL + handle_request_post(url, uploadoptions) + + return 1 @app.task def handle_c2_getworkout(userid, c2token, c2id, defaulttimezone, debug=False, **kwargs): diff --git a/rowers/templates/menu_workout.html b/rowers/templates/menu_workout.html index f61797f6..ea138baf 100644 --- a/rowers/templates/menu_workout.html +++ b/rowers/templates/menu_workout.html @@ -231,6 +231,20 @@ {% endif %} +
  • + {% if workout.uploadedtointervals and workout.uploadedtointervals != '0' %} + + Intervals.icu + + {% elif user.rower.intervals_token == None or user.rower.intervals_token == '' %} + + Connect to Intervals.icu + + {% else %} + + Intervals.icu + + {% endif %}
  • CSV diff --git a/rowers/templates/menu_workouts.html b/rowers/templates/menu_workouts.html index b85f728f..2beafb52 100644 --- a/rowers/templates/menu_workouts.html +++ b/rowers/templates/menu_workouts.html @@ -57,6 +57,7 @@
  • SportTracks
  • Polar
  • RP3
  • +
  • Intervals.icu
  • diff --git a/rowers/templates/rower_exportsettings.html b/rowers/templates/rower_exportsettings.html index 4990ce37..866a771a 100644 --- a/rowers/templates/rower_exportsettings.html +++ b/rowers/templates/rower_exportsettings.html @@ -123,6 +123,8 @@ alt="connect with RP3" width="130">

    connect with Rojabo

    +

    connect with intervals.icu

    {% endblock %} diff --git a/rowers/tests/testdata/testdata.tcx.gz b/rowers/tests/testdata/testdata.tcx.gz index 1c63fdbf..4ab2f8bd 100644 Binary files a/rowers/tests/testdata/testdata.tcx.gz and b/rowers/tests/testdata/testdata.tcx.gz differ diff --git a/rowers/uploads.py b/rowers/uploads.py index 781271f7..b13881a9 100644 --- a/rowers/uploads.py +++ b/rowers/uploads.py @@ -146,6 +146,22 @@ def do_sync(w, options, quick=False): except KeyError: pass + do_icu_export = w.user.intervals_auto_export + try: + do_icu_export = options['upload_to_Intervals'] or do_icu_export + except KeyError: + pass + + try: + if options['intervalsid'] != 0 and options['intervalsid'] != '': # pragma: no cover + w.uploadedtointervals = options['intervalsid'] + # upload_to_icu = False + do_icu_export = False + w.save() + record = create_or_update_syncrecord(w.user, w, intervalsid=options['intervalsid']) + except KeyError: + pass + try: if options['nkid'] != 0 and options['nkid'] != '': # pragma: no cover w.uploadedtonk = options['nkid'] @@ -232,14 +248,29 @@ def do_sync(w, options, quick=False): except NoTokenError: # pragma: no cover id = 0 message = "Please connect to Strava first" - except: - e = sys.exc_info()[0] - t = time.localtime() - timestamp = time.strftime('%b-%d-%Y_%H%M', t) - with open('stravalog.log', 'a') as f: - f.write('\n') - f.write(timestamp) - f.write(str(e)) + except Exception as e: + dologging('stravalog.log', e) + + if do_icu_export: + intervals_integration = IntervalsIntegration(w.user.user) + try: + id = intervals_integration.workout_export(w) + dologging( + 'intervals.icu.log', + 'exporting workout {id} as {type}'.format( + id=w.id, + type=w.workouttype, + ) + ) + except NoTokenError: + id = 0 + message = "Please connect to Intervals.icu first" + except Exception as e: + dologging( + 'intervals.icu.log', + e + ) + do_st_export = w.user.sporttracks_auto_export diff --git a/rowers/views/importviews.py b/rowers/views/importviews.py index f5270e17..a02527b0 100644 --- a/rowers/views/importviews.py +++ b/rowers/views/importviews.py @@ -24,6 +24,7 @@ importauthorizeviews = { 'nk': 'rower_integration_authorize', 'rp3': 'rower_integration_authorize', 'garmin': 'rower_garmin_authorize', + 'intervals': 'rower_integration_authorize', } @@ -173,6 +174,37 @@ def rower_process_twittercallback(request): # pragma: no cover # Process Polar Callback +@login_required() +def rower_process_intervalscallback(request): + integration = importsources['intervals'](request.user) + r = getrower(request.user) + try: + code = request.GET['code'] + res = integration.get_token(code) + except MultiValueDictKeyError: + message = "The resource owner or authorization server denied the request" + messages.error(request, message) + + url = reverse('rower_exportsettings_view') + return HttpResponseRedirect(url) + + access_token = res[0] + athlete = res[1] + if access_token == 0: + message = res[1] + message += 'Connection to intervals.icu failed.' + messages.error(request, message) + url = reverse('rower_exportsettings_view') + return HttpResponseRedirect(url) + + r.intervals_token = access_token + r.intervals_owner_id = athlete['id'] + r.save() + + successmessage = "Tokens stored. Good to go. Please check your import/export settings" + messages.info(request, successmessage) + url = reverse('rower_exportsettings_view') + return HttpResponseRedirect(url) @login_required() def rower_process_polarcallback(request): @@ -439,7 +471,10 @@ def workout_import_view(request, source='c2'): try: tdict = dict(request.POST.lists()) ids = tdict['workoutid'] - nkids = [int(id) for id in ids] + try: + nkids = [int(id) for id in ids] + except ValueError: + nkids = ids for nkid in nkids: try: _ = integration.get_workout(nkid, startdate=startdate, enddate=enddate) diff --git a/rowsandall_app/settings.py b/rowsandall_app/settings.py index 5e1aae1f..5500b293 100644 --- a/rowsandall_app/settings.py +++ b/rowsandall_app/settings.py @@ -296,6 +296,21 @@ C2_CLIENT_SECRET = CFG['c2_client_secret'] C2_REDIRECT_URI = CFG['c2_callback'] # C2_REDIRECT_URI = "http://localhost:8000/call_back" +# Intervals.icu +try: + INTERVALS_CLIENT_ID = CFG['intervals_client_id'] +except KeyError: + INTERVALS_CLIENT_ID = '0' + +try: + INTERVALS_CLIENT_SECRET = CFG['intervals_client_secret'] +except KeyError: + INTERVALS_CLIENT_SECRET = 'aa' +try: + INTERVALS_REDIRECT_URI = CFG['intervals_callback'] +except KeyError: + INTERVALS_REDIRECT_URI = 'http://localhost:8000/intervals_icu_callback' + # Strava STRAVA_CLIENT_ID = CFG['strava_client_id'] diff --git a/rowsandall_app/urls.py b/rowsandall_app/urls.py index caf20824..67c26a3d 100644 --- a/rowsandall_app/urls.py +++ b/rowsandall_app/urls.py @@ -94,6 +94,7 @@ urlpatterns += [ re_path(r'^rp3\_callback', rowersviews.rower_process_rp3callback), re_path(r'^twitter\_callback', rowersviews.rower_process_twittercallback), re_path(r'^idoklad\_callback', rowersviews.process_idokladcallback), + re_path(r'^intervals\_icu\_callback', rowersviews.rower_process_intervalscallback), re_path(r'^i18n/', include('django.conf.urls.i18n')), re_path(r'^tz_detect/', include('tz_detect.urls')), re_path(r'^logo/', logoview), diff --git a/static/img/intervals_icu.png b/static/img/intervals_icu.png new file mode 100644 index 00000000..a333c8b8 Binary files /dev/null and b/static/img/intervals_icu.png differ diff --git a/static/img/ms-icon-120x120.png b/static/img/ms-icon-120x120.png new file mode 100644 index 00000000..7416d941 Binary files /dev/null and b/static/img/ms-icon-120x120.png differ