Private
Public Access
1
0

Merge branch 'release/v22.4.0'

This commit is contained in:
2024-12-10 17:47:56 +01:00
23 changed files with 567 additions and 24 deletions

View File

@@ -1572,6 +1572,13 @@ def new_workout_from_file(r, f2,
# Get workout type from fit & tcx # Get workout type from fit & tcx
if (fileformat == 'fit'): # pragma: no cover if (fileformat == 'fit'): # pragma: no cover
workouttype = get_workouttype_from_fit(f2, workouttype=workouttype) 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'): # if (fileformat == 'tcx'):
# workouttype_from_tcx = get_workouttype_from_tcx(f2,workouttype=workouttype) # workouttype_from_tcx = get_workouttype_from_tcx(f2,workouttype=workouttype)
# if workouttype != 'rower' and workouttype_from_tcx not in mytypes.otwtypes: # if workouttype != 'rower' and workouttype_from_tcx not in mytypes.otwtypes:

View File

@@ -1350,6 +1350,39 @@ def handle_nonpainsled(f2, fileformat, summary='', startdatetime='', empowerfirm
# Create new workout from file and store it in the database # Create new workout from file and store it in the database
# This routine should be used everywhere in views.py # 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'): def get_workouttype_from_fit(filename, workouttype='water'):
try: try:
@@ -1359,16 +1392,27 @@ def get_workouttype_from_fit(filename, workouttype='water'):
records = fitfile.messages records = fitfile.messages
fittype = 'rowing' fittype = 'rowing'
subsporttype = ''
for record in records: for record in records:
if record.name in ['sport', 'lap']: if record.name in ['sport', 'lap','session']:
try: try:
fittype = record.get_values()['sport'].lower() fittype = record.get_values()['sport'].lower()
try:
subsporttype = record.get_values()['sub_sport'].lower()
except KeyError:
subsporttype = ''
except (KeyError, AttributeError): # pragma: no cover except (KeyError, AttributeError): # pragma: no cover
return 'water' pass
try: if subsporttype:
workouttype = mytypes.fitmappinginv[fittype] try:
except KeyError: # pragma: no cover workouttype = mytypes.fitmappinginv[subsporttype]
return workouttype except KeyError:
pass
else:
try:
workouttype = mytypes.fitmappinginv[fittype]
except KeyError:
pass
return workouttype return workouttype

View File

@@ -5,6 +5,7 @@ from .sporttracks import SportTracksIntegration
from .rp3 import RP3Integration from .rp3 import RP3Integration
from .trainingpeaks import TPIntegration from .trainingpeaks import TPIntegration
from .polar import PolarIntegration from .polar import PolarIntegration
from .intervals import IntervalsIntegration
importsources = { importsources = {
'c2': C2Integration, 'c2': C2Integration,
@@ -15,5 +16,6 @@ importsources = {
'tp':TPIntegration, 'tp':TPIntegration,
'rp3':RP3Integration, 'rp3':RP3Integration,
'polar': PolarIntegration, 'polar': PolarIntegration,
'intervals': IntervalsIntegration,
} }

View File

@@ -63,7 +63,7 @@ class C2Integration(SyncIntegration):
'client_id': C2_CLIENT_ID, 'client_id': C2_CLIENT_ID,
'client_secret': C2_CLIENT_SECRET, 'client_secret': C2_CLIENT_SECRET,
'redirect_uri': C2_REDIRECT_URI, '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', 'content_type': 'application/x-www-form-urlencoded',
'tokenname': 'c2token', 'tokenname': 'c2token',
'refreshtokenname': 'c2refreshtoken', 'refreshtokenname': 'c2refreshtoken',

View File

@@ -109,7 +109,7 @@ class SyncIntegration(metaclass=ABCMeta):
if 'grant_type' in self.oauth_data: if 'grant_type' in self.oauth_data:
if self.oauth_data['grant_type']: if self.oauth_data['grant_type']:
post_data['grant_type'] = 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" post_data['grant_type'] = "authorization_code"
if 'json' in self.oauth_data['content_type']: if 'json' in self.oauth_data['content_type']:

View File

@@ -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)

View File

@@ -35,7 +35,7 @@ class NKIntegration(SyncIntegration):
'client_id': NK_CLIENT_ID, 'client_id': NK_CLIENT_ID,
'client_secret': NK_CLIENT_SECRET, 'client_secret': NK_CLIENT_SECRET,
'redirect_uri': NK_REDIRECT_URI, 'redirect_uri': NK_REDIRECT_URI,
'autorization_uri': NK_OAUTH_LOCATION+"/oauth/authorize", 'authorization_uri': NK_OAUTH_LOCATION+"/oauth/authorize",
'content_type': 'application/json', 'content_type': 'application/json',
'tokenname': 'nktoken', 'tokenname': 'nktoken',
'refreshtokenname': 'nkrefreshtoken', 'refreshtokenname': 'nkrefreshtoken',

View File

@@ -30,7 +30,7 @@ class RP3Integration(SyncIntegration):
'client_id': RP3_CLIENT_ID, 'client_id': RP3_CLIENT_ID,
'client_secret': RP3_CLIENT_SECRET, 'client_secret': RP3_CLIENT_SECRET,
'redirect_uri': RP3_REDIRECT_URI, '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/x-www-form-urlencoded',
# 'content_type': 'application/json', # 'content_type': 'application/json',
'tokenname': 'rp3token', 'tokenname': 'rp3token',

View File

@@ -89,7 +89,7 @@ class StravaIntegration(SyncIntegration):
'client_id': STRAVA_CLIENT_ID, 'client_id': STRAVA_CLIENT_ID,
'client_secret': STRAVA_CLIENT_SECRET, 'client_secret': STRAVA_CLIENT_SECRET,
'redirect_uri': STRAVA_REDIRECT_URI, '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', 'content_type': 'application/json',
'tokenname': 'stravatoken', 'tokenname': 'stravatoken',
'refreshtokenname': 'stravarefreshtoken', 'refreshtokenname': 'stravarefreshtoken',

View File

@@ -41,7 +41,7 @@ class TPIntegration(SyncIntegration):
'client_id': TP_CLIENT_ID, 'client_id': TP_CLIENT_ID,
'client_secret': TP_CLIENT_SECRET, 'client_secret': TP_CLIENT_SECRET,
'redirect_uri': TP_REDIRECT_URI, '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', 'content_type': 'application/x-www-form-urlencoded',
'tokenname': 'tptoken', 'tokenname': 'tptoken',
'refreshtokenname': 'tprefreshtoken', 'refreshtokenname': 'tprefreshtoken',

View File

@@ -372,7 +372,7 @@ def update_records(url=c2url, verbose=True):
# Create a DataFrame # Create a DataFrame
df = pd.DataFrame(rows, columns=headers) df = pd.DataFrame(rows, columns=headers)
except: # pragma: no cover except: # pragma: no cover
df = pd.DataFrame() df = pd.DataFrame()
if not df.empty: if not df.empty:
@@ -1172,6 +1172,8 @@ class Rower(models.Model):
default='', max_length=200, blank=True, null=True) default='', max_length=200, blank=True, null=True)
c2_auto_export = models.BooleanField(default=False) c2_auto_export = models.BooleanField(default=False)
c2_auto_import = 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( sporttrackstoken = models.CharField(
default='', max_length=200, blank=True, null=True) default='', max_length=200, blank=True, null=True)
sporttrackstokenexpirydate = models.DateTimeField(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_import = models.BooleanField(default=False)
strava_auto_delete = 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 = ( privacychoices = (
('visible', 'Visible'), ('visible', 'Visible'),
('hidden', 'Hidden'), ('hidden', 'Hidden'),
@@ -3691,6 +3697,7 @@ class Workout(models.Model):
uploadedtogarmin = models.BigIntegerField(default=0) uploadedtogarmin = models.BigIntegerField(default=0)
uploadedtorp3 = models.BigIntegerField(default=0) uploadedtorp3 = models.BigIntegerField(default=0)
uploadedtonk = models.BigIntegerField(default=0) uploadedtonk = models.BigIntegerField(default=0)
uploadedtointervals = models.CharField(default=None,null=True, max_length=100)
forceunit = models.CharField(default='lbs', forceunit = models.CharField(default='lbs',
choices=( choices=(
('lbs', 'lbs'), ('lbs', 'lbs'),
@@ -3822,6 +3829,7 @@ class TombStone(models.Model):
uploadedtosporttracks = models.BigIntegerField(default=0) uploadedtosporttracks = models.BigIntegerField(default=0)
uploadedtotp = models.BigIntegerField(default=0) uploadedtotp = models.BigIntegerField(default=0)
uploadedtonk = 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) @receiver(models.signals.pre_delete, sender=Workout)
def create_tombstone_on_delete(sender, instance, **kwargs): def create_tombstone_on_delete(sender, instance, **kwargs):
@@ -3830,7 +3838,8 @@ def create_tombstone_on_delete(sender, instance, **kwargs):
uploadedtoc2=instance.uploadedtoc2, uploadedtoc2=instance.uploadedtoc2,
uploadedtostrava=instance.uploadedtostrava, uploadedtostrava=instance.uploadedtostrava,
uploadedtotp=instance.uploadedtotp, uploadedtotp=instance.uploadedtotp,
uploadedtonk=instance.uploadedtonk uploadedtonk=instance.uploadedtonk,
uploadedtointervals=instance.uploadedtointervals,
) )
t.save() t.save()
@@ -3846,6 +3855,7 @@ class SyncRecord(models.Model):
c2id = models.BigIntegerField(unique=True,null=True,default=None) c2id = models.BigIntegerField(unique=True,null=True,default=None)
tpid = 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) 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): def save(self, *args, **kwargs):
if self.workout: if self.workout:
@@ -3861,7 +3871,7 @@ class SyncRecord(models.Model):
str2 = '' str2 = ''
for field in ['stravaid', 'sporttracksid', 'nkid', 'c2id', 'tpid']: for field in ['stravaid', 'sporttracksid', 'nkid', 'c2id', 'tpid', 'intervalsid']:
value = getattr(self, field, None) value = getattr(self, field, None)
if value is not None: if value is not None:
str2 += '{w}: {v},'.format( str2 += '{w}: {v},'.format(

View File

@@ -148,6 +148,7 @@ garminmapping = {key: value for key, value in Reverse(garmincollection)}
fitcollection = ( fitcollection = (
('water', 'rowing'), ('water', 'rowing'),
('rower', 'rowing'), ('rower', 'rowing'),
('rower', 'indoor_rowing'),
('skierg', 'cross_country_skiing'), ('skierg', 'cross_country_skiing'),
('bike', 'cycling'), ('bike', 'cycling'),
('bikeerg', 'cycling'), ('bikeerg', 'cycling'),
@@ -180,6 +181,41 @@ fitcollection = (
fitmapping = {key: value for key, value in Reverse(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 = ( stcollection = (
('water', 'Rowing'), ('water', 'Rowing'),
('rower', 'Rowing'), ('rower', 'Rowing'),
@@ -332,6 +368,9 @@ garminmappinginv = {value: key for key, value in Reverse(
fitmappinginv = {value: key for key, value in Reverse( fitmappinginv = {value: key for key, value in Reverse(
fitcollection) if value is not None} fitcollection) if value is not None}
intervalsmappinginv = {value: key for key, value in Reverse(
intervalscollection) if value is not None}
otwtypes = ( otwtypes = (
'water', 'water',
'coastal', 'coastal',

View File

@@ -24,6 +24,7 @@ from rowers.courseutils import (
InvalidTrajectoryError InvalidTrajectoryError
) )
from rowers.emails import send_template_email from rowers.emails import send_template_email
from rowers.mytypes import intervalsmappinginv
from rowers.nkimportutils import ( from rowers.nkimportutils import (
get_nk_summary, get_nk_allstats, get_nk_intervalstats, getdict, strokeDataToDf, get_nk_summary, get_nk_allstats, get_nk_intervalstats, getdict, strokeDataToDf,
add_workout_from_data add_workout_from_data
@@ -59,6 +60,8 @@ import rowingdata
from rowingdata import make_cumvalues, make_cumvalues_array from rowingdata import make_cumvalues, make_cumvalues_array
from uuid import uuid4 from uuid import uuid4
from rowingdata import rowingdata as rdata from rowingdata import rowingdata as rdata
from rowingdata import FITParser as FP
from rowingdata.otherparsers import FitSummaryData
from datetime import timedelta from datetime import timedelta
@@ -3485,6 +3488,72 @@ def handle_nk_async_workout(alldata, userid, nktoken, nkid, delaysec, defaulttim
return workoutid 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 @app.task
def handle_c2_getworkout(userid, c2token, c2id, defaulttimezone, debug=False, **kwargs): def handle_c2_getworkout(userid, c2token, c2id, defaulttimezone, debug=False, **kwargs):

View File

@@ -231,6 +231,20 @@
</a> </a>
{% endif %} {% endif %}
</li> </li>
<li id="export-intervals">
{% if workout.uploadedtointervals and workout.uploadedtointervals != '0' %}
<a href="https://intervals.icu/activities/{{ workout.uploadedtointervals }}">
Intervals.icu <i class="fas fa-check"></i>
</a>
{% elif user.rower.intervals_token == None or user.rower.intervals_token == '' %}
<a href="/rowers/me/intervalsauthorize">
Connect to Intervals.icu
</a>
{% else %}
<a href="/rowers/workout/{{ workout.id|encode }}/intervalsuploadw/">
Intervals.icu
</a>
{% endif %}
<li id="export-csv"> <li id="export-csv">
<a href="/rowers/workout/{{ workout.id|encode }}/emailcsv/"> <a href="/rowers/workout/{{ workout.id|encode }}/emailcsv/">
CSV CSV

View File

@@ -57,6 +57,7 @@
<li id="sporttracks"><a href="/rowers/workout/sporttracksimport/">SportTracks</a></li> <li id="sporttracks"><a href="/rowers/workout/sporttracksimport/">SportTracks</a></li>
<li id="polar"><a href="/rowers/workout/polarimport/">Polar</a></li> <li id="polar"><a href="/rowers/workout/polarimport/">Polar</a></li>
<li id="rp3"><a href="/rowers/workout/rp3import/">RP3</a></li> <li id="rp3"><a href="/rowers/workout/rp3import/">RP3</a></li>
<li id="intervals"><a href="/rowers/workout/intervalsimport/">Intervals.icu</a></li>
</ul> </ul>
</li> </li>
</ul> <!-- cd-accordion-menu --> </ul> <!-- cd-accordion-menu -->

View File

@@ -123,6 +123,8 @@
alt="connect with RP3" width="130"></a></p> alt="connect with RP3" width="130"></a></p>
<p><a href="/rowers/me/rojaboauthorize"><img src="/static/img/rojabo.png" <p><a href="/rowers/me/rojaboauthorize"><img src="/static/img/rojabo.png"
alt="connect with Rojabo" width="130"></a></p> alt="connect with Rojabo" width="130"></a></p>
<p><a href="/rowers/me/intervalsauthorize"><img src="/static/img/intervals_icu.png"
alt="connect with intervals.icu" height="30"></a></p>
{% endblock %} {% endblock %}

Binary file not shown.

View File

@@ -146,6 +146,22 @@ def do_sync(w, options, quick=False):
except KeyError: except KeyError:
pass 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: try:
if options['nkid'] != 0 and options['nkid'] != '': # pragma: no cover if options['nkid'] != 0 and options['nkid'] != '': # pragma: no cover
w.uploadedtonk = options['nkid'] w.uploadedtonk = options['nkid']
@@ -232,14 +248,29 @@ def do_sync(w, options, quick=False):
except NoTokenError: # pragma: no cover except NoTokenError: # pragma: no cover
id = 0 id = 0
message = "Please connect to Strava first" message = "Please connect to Strava first"
except: except Exception as e:
e = sys.exc_info()[0] dologging('stravalog.log', e)
t = time.localtime()
timestamp = time.strftime('%b-%d-%Y_%H%M', t) if do_icu_export:
with open('stravalog.log', 'a') as f: intervals_integration = IntervalsIntegration(w.user.user)
f.write('\n') try:
f.write(timestamp) id = intervals_integration.workout_export(w)
f.write(str(e)) 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 do_st_export = w.user.sporttracks_auto_export

View File

@@ -24,6 +24,7 @@ importauthorizeviews = {
'nk': 'rower_integration_authorize', 'nk': 'rower_integration_authorize',
'rp3': 'rower_integration_authorize', 'rp3': 'rower_integration_authorize',
'garmin': 'rower_garmin_authorize', 'garmin': 'rower_garmin_authorize',
'intervals': 'rower_integration_authorize',
} }
@@ -173,6 +174,37 @@ def rower_process_twittercallback(request): # pragma: no cover
# Process Polar Callback # 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() @login_required()
def rower_process_polarcallback(request): def rower_process_polarcallback(request):
@@ -439,7 +471,10 @@ def workout_import_view(request, source='c2'):
try: try:
tdict = dict(request.POST.lists()) tdict = dict(request.POST.lists())
ids = tdict['workoutid'] 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: for nkid in nkids:
try: try:
_ = integration.get_workout(nkid, startdate=startdate, enddate=enddate) _ = integration.get_workout(nkid, startdate=startdate, enddate=enddate)

View File

@@ -296,6 +296,21 @@ C2_CLIENT_SECRET = CFG['c2_client_secret']
C2_REDIRECT_URI = CFG['c2_callback'] C2_REDIRECT_URI = CFG['c2_callback']
# C2_REDIRECT_URI = "http://localhost:8000/call_back" # 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
STRAVA_CLIENT_ID = CFG['strava_client_id'] STRAVA_CLIENT_ID = CFG['strava_client_id']

View File

@@ -94,6 +94,7 @@ urlpatterns += [
re_path(r'^rp3\_callback', rowersviews.rower_process_rp3callback), re_path(r'^rp3\_callback', rowersviews.rower_process_rp3callback),
re_path(r'^twitter\_callback', rowersviews.rower_process_twittercallback), re_path(r'^twitter\_callback', rowersviews.rower_process_twittercallback),
re_path(r'^idoklad\_callback', rowersviews.process_idokladcallback), 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'^i18n/', include('django.conf.urls.i18n')),
re_path(r'^tz_detect/', include('tz_detect.urls')), re_path(r'^tz_detect/', include('tz_detect.urls')),
re_path(r'^logo/', logoview), re_path(r'^logo/', logoview),

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB