Merge branch 'release/v22.4.0'
This commit is contained in:
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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']:
|
||||||
|
|||||||
273
rowers/integrations/intervals.py
Normal file
273
rowers/integrations/intervals.py
Normal 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)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -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',
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 -->
|
||||||
|
|||||||
@@ -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 %}
|
||||||
|
|
||||||
|
|||||||
BIN
rowers/tests/testdata/testdata.tcx.gz
vendored
BIN
rowers/tests/testdata/testdata.tcx.gz
vendored
Binary file not shown.
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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']
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
BIN
static/img/intervals_icu.png
Normal file
BIN
static/img/intervals_icu.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.7 KiB |
BIN
static/img/ms-icon-120x120.png
Normal file
BIN
static/img/ms-icon-120x120.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
Reference in New Issue
Block a user