Merge branch 'develop' into feature/idoklad
This commit is contained in:
+28
-3
@@ -217,6 +217,9 @@ def workout_goldmedalstandard(workout, reset=False):
|
||||
|
||||
def check_marker(workout):
|
||||
r = workout.user
|
||||
if workout.workoutsource == 'strava':
|
||||
return None
|
||||
|
||||
gmstandard, gmseconds = workout_goldmedalstandard(workout)
|
||||
if gmseconds < 60:
|
||||
return None
|
||||
@@ -369,8 +372,20 @@ def workout_summary_to_df(
|
||||
return df
|
||||
|
||||
|
||||
def resample(id, r, parent, overwrite='copy'):
|
||||
def resample(id, r, parent, overwrite=False):
|
||||
data, row = getrowdata_db(id=id)
|
||||
rowdata = rrdata(csvfile=parent.csvfilename).df
|
||||
# drop all columns except ' latitude' and ' longitude' and 'TimeStamp (sec)' from rowdata
|
||||
allowedcolumns = [' latitude', ' longitude', 'TimeStamp (sec)']
|
||||
rowdata = rowdata.filter(allowedcolumns)
|
||||
rowdata.rename(columns={'TimeStamp (sec)': 'time'}, inplace=True)
|
||||
rowdata['time'] = (rowdata['time']-rowdata.loc[0,'time'])*1000.
|
||||
rowdata.set_index('time', inplace=True)
|
||||
data.set_index('time', inplace=True)
|
||||
rowdata_interpolated = rowdata.reindex(data.index.union(rowdata.index)).interpolate('index')
|
||||
data = data.merge(rowdata_interpolated, left_index=True, right_index=True, how='left')
|
||||
data = data.reset_index()
|
||||
|
||||
messages = []
|
||||
|
||||
# resample
|
||||
@@ -393,7 +408,7 @@ def resample(id, r, parent, overwrite='copy'):
|
||||
data['pace'] = data['pace'] / 1000.
|
||||
data['time'] = data['time'] / 1000.
|
||||
|
||||
if overwrite == 'overwrite':
|
||||
if overwrite == True:
|
||||
# remove CP data
|
||||
try:
|
||||
cpfile = 'media/cpdata_{id}.parquet.gz'.format(id=parent.id)
|
||||
@@ -1304,8 +1319,11 @@ def save_workout_database(f2, r, dosmooth=True, workouttype='rower',
|
||||
|
||||
if makeprivate: # pragma: no cover
|
||||
privacy = 'hidden'
|
||||
else:
|
||||
elif workoutsource != 'strava':
|
||||
privacy = 'visible'
|
||||
else:
|
||||
privacy = 'hidden'
|
||||
|
||||
|
||||
# checking for inf values
|
||||
|
||||
@@ -1572,6 +1590,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:
|
||||
|
||||
+68
-15
@@ -1289,10 +1289,10 @@ def parsenonpainsled(fileformat, f2, summary, startdatetime='', empowerfirmware=
|
||||
# handle FIT
|
||||
if (fileformat == 'fit'): # pragma: no cover
|
||||
try:
|
||||
s = fitsummarydata(f2)
|
||||
s = FitSummaryData(f2)
|
||||
s.setsummary()
|
||||
summary = s.summarytext
|
||||
except:
|
||||
except Exception as e:
|
||||
pass
|
||||
hasrecognized = True
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1605,9 +1649,13 @@ def read_data(columns, ids=[], doclean=True, workstrokesonly=True, debug=False,
|
||||
existing_columns = [col for col in columns if col in datadf.columns]
|
||||
datadf = datadf.select(existing_columns)
|
||||
except (ShapeError, SchemaError):
|
||||
data = [
|
||||
df.select(columns)
|
||||
for df in data]
|
||||
try:
|
||||
data = [
|
||||
df.select(columns)
|
||||
for df in data]
|
||||
except ColumnNotFoundError:
|
||||
existing_columns = [col for col in columns if col in df.columns]
|
||||
df = df.select(existing_columns)
|
||||
|
||||
# float columns
|
||||
floatcolumns = []
|
||||
@@ -1642,14 +1690,19 @@ def read_data(columns, ids=[], doclean=True, workstrokesonly=True, debug=False,
|
||||
]
|
||||
except ComputeError:
|
||||
pass
|
||||
except ColumnNotFoundError:
|
||||
pass
|
||||
|
||||
try:
|
||||
datadf = pl.concat(data)
|
||||
except SchemaError:
|
||||
data = [
|
||||
df.with_columns(cs.integer().cast(pl.Float64)) for df in data
|
||||
]
|
||||
datadf = pl.concat(data)
|
||||
try:
|
||||
data = [
|
||||
df.with_columns(cs.integer().cast(pl.Float64)) for df in data
|
||||
]
|
||||
datadf = pl.concat(data)
|
||||
except ShapeError:
|
||||
return pl.DataFrame()
|
||||
|
||||
|
||||
|
||||
|
||||
+17
-6
@@ -67,13 +67,12 @@ class FlexibleDecimalField(forms.DecimalField):
|
||||
|
||||
|
||||
class ResampleForm(forms.Form):
|
||||
resamplechoices = (
|
||||
('overwrite', 'Overwrite Workout'),
|
||||
('copy', 'Create a Duplicate Workout')
|
||||
)
|
||||
|
||||
# add resamplechoice field, the result is a True or False boolean, labels are "overwrite" and "create copy"
|
||||
resamplechoice = forms.ChoiceField(
|
||||
initial='copy', choices=resamplechoices, label='Copy behavior')
|
||||
required=True,
|
||||
choices=((True, 'overwrite'), (False, 'create copy')),
|
||||
label='Resample choice',
|
||||
widget=forms.RadioSelect)
|
||||
|
||||
|
||||
class TrainingZonesForm(forms.Form):
|
||||
@@ -554,6 +553,9 @@ class UploadOptionsForm(forms.Form):
|
||||
upload_to_TrainingPeaks = forms.BooleanField(initial=False,
|
||||
required=False,
|
||||
label='Export to TrainingPeaks')
|
||||
upload_to_Intervals = forms.BooleanField(initial=False,
|
||||
required=False,
|
||||
label='Export to Intervals')
|
||||
# do_physics = forms.BooleanField(initial=False,required=False,label='Power Estimate (OTW)')
|
||||
makeprivate = forms.BooleanField(initial=False, required=False,
|
||||
label='Make Workout Private')
|
||||
@@ -579,6 +581,11 @@ class UploadOptionsForm(forms.Form):
|
||||
races = VirtualRace.objects.filter(
|
||||
registration_closure__gt=timezone.now())
|
||||
|
||||
# set upload_to_X based on r.X_auto_export
|
||||
for field in ['C2', 'Strava', 'SportTracks', 'TrainingPeaks', 'Intervals']:
|
||||
if getattr(r, field.lower()+'_auto_export') and r.rowerplan in ['pro', 'plan','coach']:
|
||||
self.fields['upload_to_'+field].initial = True
|
||||
|
||||
registrations = IndoorVirtualRaceResult.objects.filter(
|
||||
race__in=races,
|
||||
userid=r.id)
|
||||
@@ -662,6 +669,10 @@ class TeamUploadOptionsForm(forms.Form):
|
||||
upload_to_TrainingPeaks = forms.BooleanField(initial=False,
|
||||
required=False,
|
||||
label='Export to TrainingPeaks')
|
||||
|
||||
upload_to_Intervals = forms.BooleanField(initial=False,
|
||||
required=False,
|
||||
label='Export to TrainingPeaks')
|
||||
# do_physics = forms.BooleanField(initial=False,required=False,label='Power Estimate (OTW)')
|
||||
makeprivate = forms.BooleanField(initial=False, required=False,
|
||||
label='Make Workout Private')
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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']:
|
||||
|
||||
@@ -0,0 +1,319 @@
|
||||
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
|
||||
import rowers.dataprep as dataprep
|
||||
from rowers.opaque import encoder
|
||||
|
||||
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)
|
||||
# resample if wanted by user, not tested
|
||||
if w.user.intervals_resample_to_1s:
|
||||
datadf, id, msgs = dataprep.resample(
|
||||
w.id, w.user, w, overwrite=False
|
||||
)
|
||||
w_resampled = Workout.objects.get(id=id)
|
||||
filename = w_resampled.csvfilename
|
||||
else:
|
||||
w_resampled = None
|
||||
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'
|
||||
|
||||
if w.user.intervals_resample_to_1s and w_resampled:
|
||||
w_resampled.delete()
|
||||
row.exporttotcx(tcxfilename, notes=newnotes, sport=mytypes.intervalsmapping[w.workouttype])
|
||||
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()
|
||||
dologging('intervals.icu.log', "Exporting workout {id}".format(id=workout.id))
|
||||
|
||||
filename = self.createworkoutdata(workout)
|
||||
if not filename:
|
||||
return 0
|
||||
|
||||
params = {
|
||||
'name': workout.name,
|
||||
'description': workout.notes,
|
||||
'external_id': encoder.encode_hex(workout.id),
|
||||
}
|
||||
|
||||
|
||||
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)
|
||||
|
||||
dologging('intervals.icu.log', "Exported workout {id}".format(id=workout.id))
|
||||
|
||||
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:
|
||||
try:
|
||||
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)
|
||||
except KeyError:
|
||||
dologging('intervals.icu.log', item)
|
||||
|
||||
|
||||
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)
|
||||
|
||||
return 1
|
||||
|
||||
def get_workouts(self, *args, **kwargs):
|
||||
startdate = timezone.now() - timedelta(days=7)
|
||||
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
|
||||
|
||||
count = 0
|
||||
workouts = self.get_workout_list(startdate=startdate, enddate=enddate)
|
||||
for workout in workouts:
|
||||
if workout['new'] == 'NEW':
|
||||
self.get_workout(workout['id'])
|
||||
count +=1
|
||||
|
||||
return count
|
||||
|
||||
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_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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
@@ -214,7 +214,7 @@ class StravaIntegration(SyncIntegration):
|
||||
def get_workout(self, id, *args, **kwargs) -> int:
|
||||
try:
|
||||
_ = self.open()
|
||||
except NoTokenError("Strava error"):
|
||||
except NoTokenError:
|
||||
return 0
|
||||
|
||||
record = create_or_update_syncrecord(self.rower, None, stravaid=id)
|
||||
|
||||
@@ -15,6 +15,7 @@ from rowingdata import rowingdata
|
||||
from rowers.rower_rules import is_workout_user
|
||||
import time
|
||||
from django_rq import job
|
||||
from rowers.mytypes import tpmapping
|
||||
|
||||
from rowers.tasks import check_tp_workout_id, handle_workout_tp_upload
|
||||
|
||||
@@ -41,7 +42,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',
|
||||
@@ -66,7 +67,10 @@ class TPIntegration(SyncIntegration):
|
||||
except TypeError:
|
||||
newnotes = 'from '+w.workoutsource+' via rowsandall.com'
|
||||
|
||||
row.exporttotcx(tcxfilename, notes=newnotes)
|
||||
try:
|
||||
row.exporttotcx(tcxfilename, notes=newnotes, sport=tpmapping[w.workouttype])
|
||||
except KeyError:
|
||||
row.exporttotcx(tcxfilename, notes=newnotes, sport='other')
|
||||
|
||||
return tcxfilename
|
||||
|
||||
|
||||
@@ -550,7 +550,7 @@ def goldmedalscorechart(user, startdate=None, enddate=None):
|
||||
workouts = Workout.objects.filter(user=user.rower, date__gte=startdate,
|
||||
date__lte=enddate,
|
||||
workouttype__in=mytypes.rowtypes,
|
||||
duplicate=False).order_by('date')
|
||||
duplicate=False).order_by('date').exclude(workoutsource='strava')
|
||||
|
||||
markerworkouts = workouts.filter(rankingpiece=True)
|
||||
outids = [w.id for w in markerworkouts]
|
||||
|
||||
@@ -24,7 +24,8 @@ class Command(BaseCommand):
|
||||
record.sporttracksid = w.uploadedtosporttracks
|
||||
if w.uploadedtoc2:
|
||||
record.c2id = w.uploadedtoc2
|
||||
|
||||
if w.uploadedtointervals:
|
||||
record.intervalsid = w.uploadedtointervals
|
||||
try:
|
||||
record.save()
|
||||
except IntegrityError:
|
||||
@@ -52,7 +53,8 @@ class Command(BaseCommand):
|
||||
record.sporttracksid = w.uploadedtosporttracks
|
||||
if w.uploadedtoc2:
|
||||
record.c2id = w.uploadedtoc2
|
||||
|
||||
if w.uploadedtointervals:
|
||||
record.intervalsid = w.uploadedtointervals
|
||||
try:
|
||||
record.save()
|
||||
except IntegrityError:
|
||||
|
||||
@@ -117,5 +117,16 @@ class Command(BaseCommand):
|
||||
lines = traceback.format_exception(exc_type, exc_value, exc_traceback)
|
||||
dologging('processemail.log', ''.join('!! ' + line for line in lines))
|
||||
|
||||
rowers = Rower.objects.filter(intervals_auto_import=True)
|
||||
for r in rowers:
|
||||
try:
|
||||
if user_is_not_basic(r.user) or user_is_coachee(r.user):
|
||||
intervals_integration = IntervalsIntegration(r.user)
|
||||
_ = intervals_integration.get_workouts()
|
||||
except:
|
||||
exc_type, exc_value, exc_traceback = sys.exc_info()
|
||||
lines = traceback.format_exception(exc_type, exc_value, exc_traceback)
|
||||
dologging('processemail.log', ''.join('!! ' + line for line in lines))
|
||||
|
||||
self.stdout.write(self.style.SUCCESS(
|
||||
'Successfully processed email attachments'))
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
#!/srv/venv/bin/python
|
||||
import sys
|
||||
import os
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from django.conf import settings
|
||||
|
||||
import time
|
||||
|
||||
from rowers.models import (
|
||||
Workout, User, Rower, WorkoutForm,
|
||||
RowerForm, GraphImage, AdvancedWorkoutForm)
|
||||
from django.core.files.base import ContentFile
|
||||
|
||||
from rowsandall_app.settings import BASE_DIR
|
||||
|
||||
from rowers.dataprep import *
|
||||
|
||||
# If you find a solution that does not need the two paths, please comment!
|
||||
sys.path.append('$path_to_root_of_project$')
|
||||
sys.path.append('$path_to_root_of_project$/$project_name$')
|
||||
|
||||
os.environ['DJANGO_SETTINGS_MODULE'] = '$project_name$.settings'
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
def handle(self, *args, **options):
|
||||
# find all Workout instances with uploadedtostrava not 0 or None, workoutsource not 'strava'
|
||||
workouts = Workout.objects.filter(uploadedtostrava__gt=0)
|
||||
# report the number of workouts found to the console
|
||||
self.stdout.write(self.style.SUCCESS('Found {} Strava workouts.'.format(workouts.count())))
|
||||
# set workout.privacy to hidden and workout.workoutsource to 'strava, report percentage complete to console'
|
||||
for workout in workouts:
|
||||
workout.privacy = 'hidden'
|
||||
workout.workoutsource = 'strava'
|
||||
workout.save()
|
||||
self.stdout.write(self.style.SUCCESS('Set workout {} private.'.format(workout.id)))
|
||||
|
||||
self.stdout.write(self.style.SUCCESS('Successfully set all Strava data private.'))
|
||||
+133
-8
@@ -21,7 +21,7 @@ from django.forms import ModelForm
|
||||
from django.dispatch import receiver
|
||||
from django.forms.widgets import SplitDateTimeWidget, SelectDateWidget
|
||||
from django.forms.formsets import BaseFormSet
|
||||
|
||||
from django.db.models.signals import post_save
|
||||
from django.contrib.admin.widgets import AdminDateWidget, AdminTimeWidget, AdminSplitDateTime
|
||||
|
||||
import os
|
||||
@@ -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,9 @@ 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)
|
||||
intervals_resample_to_1s = models.BooleanField(default=False, verbose_name='Resample to 1s on export')
|
||||
sporttrackstoken = models.CharField(
|
||||
default='', max_length=200, blank=True, null=True)
|
||||
sporttrackstokenexpirydate = models.DateTimeField(blank=True, null=True)
|
||||
@@ -1238,8 +1241,12 @@ class Rower(models.Model):
|
||||
|
||||
strava_auto_export = models.BooleanField(default=False)
|
||||
strava_auto_import = models.BooleanField(default=False)
|
||||
strava_auto_delete = models.BooleanField(default=False)
|
||||
strava_auto_delete = models.BooleanField(default=True)
|
||||
|
||||
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'),
|
||||
@@ -1248,6 +1255,8 @@ class Rower(models.Model):
|
||||
getemailnotifications = models.BooleanField(default=False,
|
||||
verbose_name='Receive email notifications')
|
||||
|
||||
imports_are_private = models.BooleanField(default=False, verbose_name='Make imports private by default')
|
||||
|
||||
# Friends/Team
|
||||
friends = models.ManyToManyField("self", blank=True)
|
||||
mycoachgroup = models.ForeignKey(
|
||||
@@ -1434,9 +1443,26 @@ parchoicesy1 = list(sorted(favchartlabelsy1.items(), key=lambda x: x[1]))
|
||||
parchoicesy2 = list(sorted(favchartlabelsy2.items(), key=lambda x: x[1]))
|
||||
parchoicesx = list(sorted(favchartlabelsx.items(), key=lambda x: x[1]))
|
||||
|
||||
# special filter for workouts to exclude strava workouts by default
|
||||
class WorkoutQuerySet(models.QuerySet):
|
||||
def filter(self, *args, exclude_strava=True, **kwargs):
|
||||
queryset = super().filter(*args, **kwargs)
|
||||
if exclude_strava:
|
||||
queryset = queryset.exclude(workoutsource='strava')
|
||||
|
||||
return queryset
|
||||
|
||||
def get(self, *args, **kwargs):
|
||||
queryset = self
|
||||
|
||||
return super().get(*args, **kwargs)
|
||||
|
||||
|
||||
class WorkoutManager(models.Manager):
|
||||
def get_queryset(self):
|
||||
return WorkoutQuerySet(self.model, using=self._db)
|
||||
|
||||
# Saving a chart as a favorite chart
|
||||
|
||||
|
||||
class FavoriteChart(models.Model):
|
||||
workouttypechoices = [
|
||||
('ote', 'Erg/SkiErg'),
|
||||
@@ -3691,6 +3717,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'),
|
||||
@@ -3715,6 +3742,9 @@ class Workout(models.Model):
|
||||
default=False, verbose_name='Duplicate Workout')
|
||||
impeller = models.BooleanField(default=False, verbose_name='Impeller')
|
||||
|
||||
# attach the WorkoutManager
|
||||
#objects = WorkoutManager()
|
||||
|
||||
def url(self):
|
||||
str = '/rowers/workout/{id}/'.format(
|
||||
id=encoder.encode_hex(self.id)
|
||||
@@ -3752,6 +3782,15 @@ class Workout(models.Model):
|
||||
|
||||
super(Workout, self).save(*args, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def post_create(cls, sender, instance, created, *args, **kwargs):
|
||||
if created:
|
||||
user = instance.user
|
||||
if user.imports_are_private:
|
||||
instance.privacy = 'hidden'
|
||||
instance.save()
|
||||
|
||||
|
||||
def __str__(self):
|
||||
|
||||
try:
|
||||
@@ -3810,6 +3849,8 @@ class Workout(models.Model):
|
||||
|
||||
return stri
|
||||
|
||||
post_save.connect(Workout.post_create, sender=Workout)
|
||||
|
||||
class WorkoutRPEForm(ModelForm):
|
||||
class Meta:
|
||||
model = Workout
|
||||
@@ -3822,6 +3863,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 +3872,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 +3889,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 +3905,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(
|
||||
@@ -4547,9 +4591,90 @@ class RowerExportForm(ModelForm):
|
||||
'strava_auto_import',
|
||||
'strava_auto_delete',
|
||||
'trainingpeaks_auto_export',
|
||||
'rp3_auto_import'
|
||||
'rp3_auto_import',
|
||||
'intervals_auto_import',
|
||||
'intervals_auto_export',
|
||||
'intervals_resample_to_1s',
|
||||
'imports_are_private'
|
||||
]
|
||||
|
||||
class RowerPrivateImportForm(ModelForm):
|
||||
class Meta:
|
||||
model = Rower
|
||||
fields = [
|
||||
'imports_are_private'
|
||||
]
|
||||
|
||||
class RowerExportFormStrava(ModelForm):
|
||||
class Meta:
|
||||
model = Rower
|
||||
fields = [
|
||||
'stravaexportas',
|
||||
'strava_auto_export',
|
||||
'strava_auto_import',
|
||||
'strava_auto_delete',
|
||||
]
|
||||
|
||||
class RowerExportFormIntervals(ModelForm):
|
||||
class Meta:
|
||||
model = Rower
|
||||
fields = [
|
||||
'intervals_auto_import',
|
||||
'intervals_auto_export',
|
||||
'intervals_resample_to_1s',
|
||||
]
|
||||
|
||||
class RowerExportFormGarmin(ModelForm):
|
||||
class Meta:
|
||||
model = Rower
|
||||
fields = [
|
||||
'garminactivity',
|
||||
]
|
||||
|
||||
class RowerExportFormPolar(ModelForm):
|
||||
class Meta:
|
||||
model = Rower
|
||||
fields = [
|
||||
'polar_auto_import',
|
||||
]
|
||||
|
||||
class RowerExportFormConcept2(ModelForm):
|
||||
class Meta:
|
||||
model = Rower
|
||||
fields = [
|
||||
'c2_auto_export',
|
||||
'c2_auto_import',
|
||||
]
|
||||
|
||||
class RowerExportFormSportTracks(ModelForm):
|
||||
class Meta:
|
||||
model = Rower
|
||||
fields = [
|
||||
'sporttracks_auto_export',
|
||||
]
|
||||
|
||||
class RowerExportFormTrainingPeaks(ModelForm):
|
||||
class Meta:
|
||||
model = Rower
|
||||
fields = [
|
||||
'trainingpeaks_auto_export',
|
||||
]
|
||||
|
||||
class RowerExportFormRP3(ModelForm):
|
||||
class Meta:
|
||||
model = Rower
|
||||
fields = [
|
||||
'rp3_auto_import',
|
||||
]
|
||||
|
||||
class RowerExportFormNK(ModelForm):
|
||||
class Meta:
|
||||
model = Rower
|
||||
fields = [
|
||||
'nk_auto_import'
|
||||
]
|
||||
|
||||
|
||||
# Simple form to set rower's Functional Threshold Power
|
||||
class SimpleRowerPowerForm(ModelForm):
|
||||
otwftp = forms.IntegerField(initial=0,required=True, label='FTP on water')
|
||||
|
||||
@@ -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,74 @@ fitcollection = (
|
||||
|
||||
fitmapping = {key: value for key, value in Reverse(fitcollection)}
|
||||
|
||||
tcxcollection = (
|
||||
('water', 'Rowing'),
|
||||
('rower', 'Rowing'),
|
||||
('skierg', 'CrossCountrySkiing'),
|
||||
('bike', 'Biking'),
|
||||
('bikeerg', 'Biking'),
|
||||
('dynamic', 'Rowing'),
|
||||
('slides', 'Rowing'),
|
||||
('paddle', 'Other'),
|
||||
('snow', 'CrossCountrySkiing'),
|
||||
('coastal', 'Rowing'),
|
||||
('c-boat', 'Rowing'),
|
||||
('churchboat', 'Rowing'),
|
||||
('Ride', 'Biking'),
|
||||
('Run', 'Running'),
|
||||
('NordicSki', 'CrossCountrySkiing'),
|
||||
('Swim', 'Swimming'),
|
||||
('Hike', 'Hiking'),
|
||||
('Walk', 'Walking'),
|
||||
('Canoeing', 'Other'),
|
||||
('Crossfit', 'Other'),
|
||||
('StandUpPaddling', 'Other'),
|
||||
('IceSkate', 'Other'),
|
||||
('WeightTraining', 'Other'),
|
||||
('InlineSkate', 'Other'),
|
||||
('Kayaking', 'Other'),
|
||||
('Workout', 'Other'),
|
||||
('Yoga', 'Other'),
|
||||
('other', 'Other'),
|
||||
)
|
||||
|
||||
tcxmapping = {key: value for key, value in Reverse(tcxcollection)}
|
||||
|
||||
tcxmappinginv = {value: key for key, value in Reverse(tcxcollection) if value is not None}
|
||||
|
||||
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 +401,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',
|
||||
|
||||
@@ -1597,13 +1597,28 @@ def add_workout_fastestrace(ws, race, r, recordid=0, doregister=False):
|
||||
enddatetime
|
||||
)
|
||||
|
||||
# from ws, remove any w where w.workoutsource = 'strava'. For each removal add an error "strava workout not permitted" to the errors list and if there are no workouts left, return 0, comments, errors, 0
|
||||
ws2 = []
|
||||
for w in ws:
|
||||
if w.workoutsource != 'strava':
|
||||
ws2.append(w)
|
||||
else:
|
||||
errors.append('Strava workouts are not permitted')
|
||||
|
||||
ws = ws2
|
||||
|
||||
if len(ws) == 0:
|
||||
return result, comments, errors, 0
|
||||
|
||||
ids = [w.id for w in ws]
|
||||
ids = list(set(ids))
|
||||
|
||||
|
||||
if len(ids) > 1 and race.sessiontype in ['test', 'coursetest', 'race', 'indoorrace', 'fastest_time', 'fastest_distance']: # pragma: no cover
|
||||
errors.append('For tests, you can only attach one workout')
|
||||
return result, comments, errors, 0
|
||||
|
||||
|
||||
if r.birthdate:
|
||||
age = calculate_age(r.birthdate)
|
||||
else: # pragma: no cover
|
||||
@@ -1759,6 +1774,19 @@ def add_workout_indoorrace(ws, race, r, recordid=0, doregister=False):
|
||||
enddatetime
|
||||
)
|
||||
|
||||
# from ws, remove any w where w.workoutsource = 'strava'. For each removal add an error "strava workout not permitted" to the errors list and if there are no workouts left, return 0, comments, errors, 0
|
||||
ws2 = []
|
||||
for w in ws:
|
||||
if w.workoutsource != 'strava':
|
||||
ws2.append(w)
|
||||
else:
|
||||
errors.append('Strava workouts are not permitted')
|
||||
|
||||
ws = ws2
|
||||
|
||||
if len(ws) == 0:
|
||||
return result, comments, errors, 0
|
||||
|
||||
# check if all sessions have same date
|
||||
dates = [w.date for w in ws]
|
||||
if (not all(d == dates[0] for d in dates)) and race.sessiontype not in ['challenge', 'cycletarget']: # pragma: no cover
|
||||
@@ -1906,6 +1934,19 @@ def add_workout_race(ws, race, r, splitsecond=0, recordid=0, doregister=False):
|
||||
enddatetime
|
||||
)
|
||||
|
||||
# from ws, remove any w where w.workoutsource = 'strava'. For each removal add an error "strava workout not permitted" to the errors list and if there are no workouts left, return 0, comments, errors, 0
|
||||
ws2 = []
|
||||
for w in ws:
|
||||
if w.workoutsource != 'strava':
|
||||
ws2.append(w)
|
||||
else:
|
||||
errors.append('Strava workouts are not permitted')
|
||||
|
||||
ws = ws2
|
||||
|
||||
if len(ws) == 0:
|
||||
return result, comments, errors, 0
|
||||
|
||||
# check if all sessions have same date
|
||||
dates = [w.date for w in ws]
|
||||
if (not all(d == dates[0] for d in dates)) and race.sessiontype not in ['challenge', 'cycletarget']: # pragma: no cover
|
||||
|
||||
+16
-1
@@ -451,6 +451,11 @@ def is_workout_user(user, workout):
|
||||
except AttributeError: # pragma: no cover
|
||||
return False
|
||||
|
||||
if workout.privacy == 'hidden':
|
||||
return user == workout.user.user
|
||||
if workout.workoutsource == 'strava':
|
||||
return user == workout.user.user
|
||||
|
||||
if workout.user == r:
|
||||
return True
|
||||
|
||||
@@ -458,6 +463,9 @@ def is_workout_user(user, workout):
|
||||
|
||||
# check if user is in same team as owner of workout
|
||||
|
||||
@rules.predicate
|
||||
def workout_is_strava(workout):
|
||||
return workout.workoutsource == 'strava'
|
||||
|
||||
@rules.predicate
|
||||
def is_workout_team(user, workout):
|
||||
@@ -469,6 +477,11 @@ def is_workout_team(user, workout):
|
||||
except AttributeError: # pragma: no cover
|
||||
return False
|
||||
|
||||
if workout.privacy == 'hidden':
|
||||
return user == workout.user.user
|
||||
if workout.workoutsource == 'strava':
|
||||
return user == workout.user.user
|
||||
|
||||
if workout.user == r:
|
||||
return True
|
||||
|
||||
@@ -479,7 +492,9 @@ def is_workout_team(user, workout):
|
||||
|
||||
@rules.predicate
|
||||
def can_view_workout(user, workout):
|
||||
if workout.privacy != 'private':
|
||||
if workout.workoutsource == 'strava':
|
||||
return user == workout.user.user
|
||||
if workout.privacy not in ('hidden', 'private'):
|
||||
return True
|
||||
if user.is_anonymous: # pragma: no cover
|
||||
return False
|
||||
|
||||
+73
-2
@@ -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):
|
||||
@@ -3626,7 +3695,8 @@ def handle_c2_async_workout(alldata, userid, c2token, c2id, delaysec,
|
||||
code=uuid4().hex[:16], c2id=c2id)
|
||||
|
||||
startdatetime, starttime, workoutdate, duration, starttimeunix, timezone = utils.get_startdatetime_from_c2data(
|
||||
data)
|
||||
data
|
||||
)
|
||||
|
||||
s = 'Time zone {timezone}, startdatetime {startdatetime}, duration {duration}'.format(
|
||||
timezone=timezone, startdatetime=startdatetime,
|
||||
@@ -3686,6 +3756,7 @@ def handle_c2_async_workout(alldata, userid, c2token, c2id, delaysec,
|
||||
strokelength = np.zeros(nr_rows)
|
||||
|
||||
dist2 = 0.1*strokedata.loc[:, 'd']
|
||||
cumdist, intervals = make_cumvalues(dist2)
|
||||
|
||||
try:
|
||||
spm = strokedata.loc[:, 'spm']
|
||||
@@ -3727,7 +3798,7 @@ def handle_c2_async_workout(alldata, userid, c2token, c2id, delaysec,
|
||||
' lapIdx': lapidx,
|
||||
' WorkoutState': 4,
|
||||
' ElapsedTime (sec)': seconds,
|
||||
'cum_dist': dist2
|
||||
'cum_dist': cumdist
|
||||
})
|
||||
|
||||
df.sort_values(by='TimeStamp (sec)', ascending=True)
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<ul class="main-content">
|
||||
<li class="grid_4">
|
||||
|
||||
<p>On this page, a work in progress, I will collect useful information
|
||||
<p>On this page, I will collect useful information
|
||||
for developers of rowing data apps and hardware.</p>
|
||||
|
||||
<p>I presume you have an app (smartphone app, dedicated hardware, web site)
|
||||
@@ -61,11 +61,11 @@
|
||||
</ul></p>
|
||||
<h2>Using the REST API</h2>
|
||||
|
||||
<p>We are building a REST API which will allow you to post and
|
||||
<p>We have a REST API which will allow you to post and
|
||||
receive stroke
|
||||
data from the site directly.</p>
|
||||
|
||||
<p>The REST API is a work in progress. We are open to improvement
|
||||
<p>We are open to improvement
|
||||
suggestions (provided they don't break existing apps). Please send
|
||||
email to <a href="mailto:info@rowsandall.com">info@rowsandall.com</a>
|
||||
with questions and/or suggestions. We
|
||||
@@ -84,7 +84,6 @@
|
||||
|
||||
<li>Disadvantages
|
||||
<p><ul class="contentli">
|
||||
<li>The API is not stable and not fully tested yet.</li>
|
||||
<li>You need to register your app with us. We can revoke your
|
||||
permissions if you misuse them.</li>
|
||||
<li>The user user must grant permissions to your app.</li>
|
||||
@@ -114,7 +113,7 @@
|
||||
|
||||
|
||||
<p>We have disabled the self service app link for security reasons.
|
||||
We will replace it with a secure self service app link soon. If you
|
||||
If you
|
||||
need to register an app, please send email to info@rowsandall.com</p>
|
||||
|
||||
<h3>Authentication</h3>
|
||||
@@ -728,11 +727,11 @@
|
||||
<li><b>peakdriveforce</b>: Peak handle force (lbs)</li>
|
||||
<li><b>lapidx</b>: Lap identifier</li>
|
||||
<li><b>hr</b>: Heart rate (beats per minute)</li>
|
||||
<li><b>wash</b>: Wash as defined per Empower oarlock (degrees)</li>
|
||||
<li><b>catch</b>: Catch angle per Empower oarlock (degrees)</li>
|
||||
<li><b>finish</b>: Finish angle per Empower oarlock (degrees)</li>
|
||||
<li><b>peakforceangle</b>: Peak Force Angle per Empower oarlock (degrees)</li>
|
||||
<li><b>slip</b>: Slip as defined per Empower oarlock (degrees)</li>
|
||||
<li><b>wash</b>: Wash as defined for your smart power measuring oarlock (degrees)</li>
|
||||
<li><b>catch</b>: Catch angle for your smart power measuring oarlock (degrees)</li>
|
||||
<li><b>finish</b>: Finish angle for your smart power measuring oarlock (degrees)</li>
|
||||
<li><b>peakforceangle</b>: Peak Force Angle for your smart power measuring oarlock (degrees)</li>
|
||||
<li><b>slip</b>: Slip as defined for your smart power measuring oarlock (degrees)</li>
|
||||
|
||||
</ul>
|
||||
</p>
|
||||
|
||||
@@ -231,6 +231,20 @@
|
||||
</a>
|
||||
{% endif %}
|
||||
</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">
|
||||
<a href="/rowers/workout/{{ workout.id|encode }}/emailcsv/">
|
||||
CSV
|
||||
|
||||
@@ -57,6 +57,7 @@
|
||||
<li id="sporttracks"><a href="/rowers/workout/sporttracksimport/">SportTracks</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="intervals"><a href="/rowers/workout/intervalsimport/">Intervals.icu</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul> <!-- cd-accordion-menu -->
|
||||
|
||||
@@ -11,12 +11,16 @@
|
||||
<th>Distance:</th><td>{{ workout.distance }}m</td>
|
||||
</tr><tr>
|
||||
<th>Duration:</th><td>{{ workout.duration |durationprint:"%H:%M:%S.%f" }}</td>
|
||||
</tr><tr>
|
||||
</tr>
|
||||
{% if workout.privacy != 'hidden' %}
|
||||
<tr>
|
||||
<th>Public link to this workout</th>
|
||||
<td>
|
||||
<a href="/rowers/workout/{{ workout.id|encode }}">https://rowsandall.com/rowers/workout/{{ workout.id|encode }}</a>
|
||||
</td>
|
||||
</tr><tr>
|
||||
</tr>
|
||||
{% endif %}
|
||||
<tr>
|
||||
<th>Comments</th>
|
||||
<td>
|
||||
<a href="/rowers/workout/{{ workout.id|encode }}/comment">Comment ({{ aantalcomments }})</a>
|
||||
|
||||
@@ -7,8 +7,10 @@
|
||||
{% block main %}
|
||||
<h1>Import and Export Settings for {{ rower.user.first_name }} {{ rower.user.last_name }}</h1>
|
||||
|
||||
<form enctype="multipart/form-data" action="" method="post">
|
||||
{% csrf_token %}
|
||||
<ul class="main-content">
|
||||
<li class="grid_2">
|
||||
<li class="grid_4">
|
||||
<p>You are currently connected to:
|
||||
{% if rower.c2token is not None and rower.c2token != '' %}
|
||||
Concept2 Logbook,
|
||||
@@ -32,50 +34,158 @@
|
||||
Strava,
|
||||
{% endif %}
|
||||
{% if rower.rp3token is not None and rower.rp3token != '' %}
|
||||
RP3
|
||||
RP3,
|
||||
{% endif %}
|
||||
{% if rower.rojabo_token is not None and rower.rojabo_token != '' %}
|
||||
Rojabo
|
||||
Rojabo,
|
||||
{% endif %}
|
||||
{% if rower.intervals_token is not None and rower.intervals_token != '' %}
|
||||
Intervals.icu
|
||||
{% endif %}
|
||||
</p>
|
||||
|
||||
{% if form.errors %}
|
||||
<p style="color: red;">
|
||||
Please correct the error{{ form.errors|pluralize }} below.
|
||||
</p>
|
||||
{% endif %}
|
||||
<p>
|
||||
<form enctype="multipart/form-data" action="" method="post">
|
||||
<table>
|
||||
{{ form.as_table }}
|
||||
</table>
|
||||
{% csrf_token %}
|
||||
<input type="submit" value="Save">
|
||||
</form>
|
||||
</p>
|
||||
{% if rower.garmintoken and rower.garmintoken != '' %}
|
||||
<p>
|
||||
<em>You are connected to Garmin.</em> Switching off Garmin Connect sync is on the
|
||||
<a href="https://connect.garmin.com/modern/settings/accountInformation">Account settings</a>
|
||||
page. Look for the "Rowsandall" app.
|
||||
</p>
|
||||
{% endif %}
|
||||
<p>
|
||||
Garmin Connnect has no manual sync, so connecting your account to your Garmin account will
|
||||
automatically auto-sync workouts from Garmin to Rowsandall (but not in the other direction). If you
|
||||
want to export our structured workout sessions to your Garmin device, you have to set the "Garmin Activity"
|
||||
to a activity type that is supported by your watch. Not all watches support "Custom" activities, so
|
||||
you may have to set your activity to Run or Ride while rowing.
|
||||
</p>
|
||||
<p>
|
||||
Strava Auto Import also imports activity changes on Strava to Rowsandall, except when you delete
|
||||
a workout on Strava. If you want Deletions to propagate to Rowsandall, tick the Strava Auto Delete
|
||||
check box.
|
||||
</p>
|
||||
<p>
|
||||
Click on the icons to establish the connection or to renew the authorization.
|
||||
</p>
|
||||
<p>
|
||||
By default, imported workouts are set to have a public URL. However, new workouts can be set to
|
||||
private by default with the following setting:
|
||||
<table>
|
||||
{{ forms.imports_are_private.as_table }}
|
||||
<input type="submit" value="Save">
|
||||
</table>
|
||||
</p>
|
||||
</li>
|
||||
<li class="grid_4">
|
||||
<h2>API Key</h2>
|
||||
<p>{{ apikey }}</p>
|
||||
<p>
|
||||
<a href="/rowers/me/regenerateapikey/">Regenerate</a>
|
||||
</p>
|
||||
<p>This API key can be used to access the Rowsandall API. It is used by some third party applications to access your data. Keep it secret.</p>
|
||||
</li>
|
||||
|
||||
{% if form.errors %}
|
||||
<li class="rounder">
|
||||
<p style="color: red;">
|
||||
Please correct the error{{ form.errors|pluralize }} below.
|
||||
</p>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li class="rounder">
|
||||
<h2>NK</h2>
|
||||
<table>
|
||||
{{ forms.nk.as_table }}
|
||||
<input type="submit" value="Save">
|
||||
</table>
|
||||
<p><a href="/rowers/me/nkauthorize/"><img src="/static/img/NKLiNKLogbook.png" alt="connect with NK Logbook" width="120"></a></p>
|
||||
</li>
|
||||
<li class="rounder">
|
||||
<h2>Concept2</h2>
|
||||
<table>
|
||||
{{ forms.c2.as_table }}
|
||||
<input type="submit" value="Save">
|
||||
</table>
|
||||
<p><a href="/rowers/me/c2authorize/"><img src="/static/img/blueC2logo.png" alt="connect with Concept2" width="120"></a></p>
|
||||
</li>
|
||||
<li class="rounder">
|
||||
<h2>RP3</h2>
|
||||
<table>
|
||||
{{ forms.rp3.as_table }}
|
||||
<input type="submit" value="Save">
|
||||
</table>
|
||||
<p><a href="/rowers/me/rp3authorize"><img src="/static/img/logo-rp3-full-black.png"
|
||||
alt="connect with RP3" width="130"></a></p>
|
||||
</li>
|
||||
<li class="rounder">
|
||||
<h2>Rojabo</h2>
|
||||
<p><a href="/rowers/me/rojaboauthorize"><img src="/static/img/rojabo.png"
|
||||
alt="connect with Rojabo" width="130"></a></p>
|
||||
</li>
|
||||
<li class="rounder">
|
||||
<h2>Intervals.icu</h2>
|
||||
<table>
|
||||
{{ forms.intervals.as_table }}
|
||||
<input type="submit" value="Save">
|
||||
</table>
|
||||
<p><a href="/rowers/me/intervalsauthorize"><img src="/static/img/intervals_logo_with_name.png"
|
||||
alt="connect with intervals.icu"></a></p>
|
||||
</li>
|
||||
<li class="rounder">
|
||||
<h2>SportTracks</h2>
|
||||
<table>
|
||||
{{ forms.sporttracks.as_table }}
|
||||
<input type="submit" value="Save">
|
||||
</table>
|
||||
<p><a href="/rowers/me/sporttracksauthorize/"><img src="/static/img/sporttracks-button.png" alt="connect with SportTracks" width="120"></a></p>
|
||||
</li>
|
||||
<li class="rounder">
|
||||
<h2>TrainingPeaks</h2>
|
||||
<table>
|
||||
{{ forms.trainingpeaks.as_table }}
|
||||
<input type="submit" value="Save">
|
||||
</table>
|
||||
<p><a href="/rowers/me/tpauthorize/"><img src="/static/img/TP_logo_horz_2_color.png"
|
||||
alt="connect with Polar" width="130"></a></p>
|
||||
</li>
|
||||
<li class="rounder">
|
||||
<h2>Polar</h2>
|
||||
<table>
|
||||
{{ forms.polar.as_table }}
|
||||
<input type="submit" value="Save">
|
||||
</table>
|
||||
<p><a href="/rowers/me/polarauthorize/"><img src="/static/img/Polar_connectwith_btn_white.png"
|
||||
alt="connect with Polar" width="130"></a></p>
|
||||
</li>
|
||||
<li class="rounder">
|
||||
<h2>Garmin Connect</h2>
|
||||
<table>
|
||||
{{ forms.garmin.as_table }}
|
||||
<input type="submit" value="Save">
|
||||
</table>
|
||||
<p><a href="/rowers/me/garminauthorize"><img src="/static/img/garmin_badge_130.png"
|
||||
alt="connect with Garmin" width="130"></a></p>
|
||||
|
||||
<p>
|
||||
Garmin Connnect has no manual sync, so connecting your account to your Garmin account will
|
||||
automatically auto-sync workouts from Garmin to Rowsandall (but not in the other direction). If you
|
||||
want to export our structured workout sessions to your Garmin device, you have to set the "Garmin Activity"
|
||||
to a activity type that is supported by your watch. Not all watches support "Custom" activities, so
|
||||
you may have to set your activity to Run or Ride while rowing.
|
||||
</p>
|
||||
{% if rower.garmintoken and rower.garmintoken != '' %}
|
||||
<p>
|
||||
<em>You are connected to Garmin.</em> Switching off Garmin Connect sync is on the
|
||||
<a href="https://connect.garmin.com/modern/settings/accountInformation">Account settings</a>
|
||||
page. Look for the "Rowsandall" app.
|
||||
</p>
|
||||
{% endif %}
|
||||
</li>
|
||||
<li class="rounder">
|
||||
<h2>Strava</h2>
|
||||
<p><em>Warning: API restrictions!</em></p>
|
||||
<p><input type="submit" value="Save"></p>
|
||||
{{ forms.strava.as_p }}
|
||||
<p><a href="/rowers/me/stravaauthorize/"><img src="/static/img/ConnectWithStrava.png" alt="connect with strava" width="120"></a></p>
|
||||
<p>
|
||||
Strava Auto Import also imports activity changes on Strava to Rowsandall, except when you delete
|
||||
a workout on Strava. If you want Deletions to propagate to Rowsandall, tick the Strava Auto Delete
|
||||
check box.
|
||||
</p>
|
||||
{% if rower.stravatoken and rower.stravatoken != '' %}
|
||||
<p>
|
||||
<em>You are connected to Strava.</em> Workouts imported from Strava will not be synced
|
||||
to other platforms and the data will only be visible to you, not your team members or coaches.
|
||||
We have to respect the terms and conditions of the Strava API, which do not allow us to sync
|
||||
data to other platforms or to share the data with others.
|
||||
</p>
|
||||
{% endif %}
|
||||
</li>
|
||||
<li class="grid_2">
|
||||
{% if grants %}
|
||||
<li class="rounder">
|
||||
<h2>Applications</h2>
|
||||
<p>
|
||||
These applications have access to your Rowsandall data.
|
||||
</p>
|
||||
<table width="100%">
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -96,36 +206,11 @@
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<li>
|
||||
{% endif %}
|
||||
<h2>API Key</h2>
|
||||
<p>{{ apikey }}</p>
|
||||
<p>
|
||||
<a href="/rowers/me/regenerateapikey/">Regenerate</a>
|
||||
</p>
|
||||
This API key can be used to access the Rowsandall API. It is used by some third party applications to access your data. Keep it secret.
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
<p>Click on one of the icons below to connect to the service of your
|
||||
choice or to renew the authorization.</p>
|
||||
<p><a href="/rowers/me/stravaauthorize/"><img src="/static/img/ConnectWithStrava.png" alt="connect with strava" width="120"></a></p>
|
||||
<p><a href="/rowers/me/c2authorize/"><img src="/static/img/blueC2logo.png" alt="connect with Concept2" width="120"></a></p>
|
||||
<p><a href="/rowers/me/nkauthorize/"><img src="/static/img/NKLiNKLogbook.png" alt="connect with NK Logbook" width="120"></a></p>
|
||||
<p><a href="/rowers/me/sporttracksauthorize/"><img src="/static/img/sporttracks-button.png" alt="connect with SportTracks" width="120"></a></p>
|
||||
<p><a href="/rowers/me/polarauthorize/"><img src="/static/img/Polar_connectwith_btn_white.png"
|
||||
alt="connect with Polar" width="130"></a></p>
|
||||
<p><a href="/rowers/me/tpauthorize/"><img src="/static/img/TP_logo_horz_2_color.png"
|
||||
alt="connect with Polar" width="130"></a></p>
|
||||
|
||||
<p><a href="/rowers/me/garminauthorize"><img src="/static/img/garmin_badge_130.png"
|
||||
alt="connect with Garmin" width="130"></a></p>
|
||||
<p><a href="/rowers/me/rp3authorize"><img src="/static/img/logo-rp3-full-black.png"
|
||||
alt="connect with RP3" width="130"></a></p>
|
||||
<p><a href="/rowers/me/rojaboauthorize"><img src="/static/img/rojabo.png"
|
||||
alt="connect with Rojabo" width="130"></a></p>
|
||||
{% if user.is_staff %}
|
||||
<p><a href="/rowers/me/idokladauthorize/">iDoklad authorize</a></p>
|
||||
{% endif %}
|
||||
</form>
|
||||
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
<form enctype="multipart/form-data" action="" method="post">
|
||||
<table>
|
||||
{{ userform.as_table }}
|
||||
{{ privateform.as_table }}
|
||||
{{ accountform.as_table }}
|
||||
<tr>
|
||||
<th> </th><td></td>
|
||||
|
||||
@@ -150,25 +150,28 @@
|
||||
<th>Duration:</th><td>{{ workout.duration |durationprint:"%H:%M:%S.%f" }}</td>
|
||||
</tr><tr>
|
||||
<th>Source:</th><td>{{ workout.workoutsource }}</td>
|
||||
</tr><tr>
|
||||
</tr>
|
||||
{% if workout.privacy != 'hidden' %}
|
||||
<tr>
|
||||
<th>Public link to this workout:</th>
|
||||
<td>
|
||||
<a href="/rowers/workout/{{ workout.id|encode }}/">https://rowsandall.com/rowers/workout/{{ workout.id|encode }}/</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% for course in courses %}
|
||||
<tr>
|
||||
<th>
|
||||
Timed Course:
|
||||
</th>
|
||||
<td>
|
||||
<a href="/rowers/courses/{{ course.id }}"/>{{ course }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</li>
|
||||
<li class="grid_2">
|
||||
{% endif %}
|
||||
{% for course in courses %}
|
||||
<tr>
|
||||
<th>
|
||||
Timed Course:
|
||||
</th>
|
||||
<td>
|
||||
<a href="/rowers/courses/{{ course.id }}"/>{{ course }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</li>
|
||||
<li class="grid_2">
|
||||
{% if form.errors %}
|
||||
<p style="color: red;">
|
||||
Please correct the error{{ form.errors|pluralize }} below.
|
||||
|
||||
+443
-3
@@ -22,12 +22,454 @@ from rowers.opaque import encoder
|
||||
|
||||
from rest_framework.test import APIRequestFactory, force_authenticate
|
||||
|
||||
UPLOAD_SERVICE_URL = '/rowers/workout/api/upload/'
|
||||
UPLOAD_SERVICE_SECRET = "FoYezZWLSyfAVimumpHEeYsJjsNCerxV"
|
||||
|
||||
import json
|
||||
|
||||
# import BeautifulSoup
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
from rowers.ownapistuff import *
|
||||
from rowers.views.apiviews import *
|
||||
from rowers.models import APIKey
|
||||
|
||||
from rowers.teams import add_member, add_coach
|
||||
from rowers.views.analysisviews import histodata
|
||||
|
||||
class TeamFactory(factory.DjangoModelFactory):
|
||||
class Meta:
|
||||
model = Team
|
||||
|
||||
name = factory.LazyAttribute(lambda _: faker.word())
|
||||
notes = faker.text()
|
||||
private = 'open'
|
||||
viewing = 'allmembers'
|
||||
|
||||
class StravaPrivacy(TestCase):
|
||||
def setUp(self):
|
||||
self.u = UserFactory()
|
||||
self.u2 = UserFactory()
|
||||
self.u3 = UserFactory()
|
||||
|
||||
self.r = Rower.objects.create(user=self.u,
|
||||
birthdate=faker.profile()['birthdate'],
|
||||
gdproptin=True, ftpset=True,surveydone=True,
|
||||
gdproptindate=timezone.now(),
|
||||
rowerplan='coach',subscription_id=1)
|
||||
|
||||
self.r.stravatoken = '12'
|
||||
self.r.stravarefreshtoken = '123'
|
||||
self.r.stravatokenexpirydate = arrow.get(datetime.datetime.now()-datetime.timedelta(days=1)).datetime
|
||||
self.r.strava_owner_id = 4
|
||||
|
||||
self.r.save()
|
||||
|
||||
self.c = Client()
|
||||
|
||||
self.factory = RequestFactory()
|
||||
self.password = faker.word()
|
||||
self.u.set_password(self.password)
|
||||
self.u.save()
|
||||
self.factory = APIRequestFactory()
|
||||
|
||||
self.r2 = Rower.objects.create(user=self.u2,
|
||||
birthdate=faker.profile()['birthdate'],
|
||||
gdproptin=True, ftpset=True,surveydone=True,
|
||||
gdproptindate=timezone.now(),
|
||||
rowerplan='coach',clubsize=3)
|
||||
|
||||
self.r3 = Rower.objects.create(user=self.u3,
|
||||
birthdate=faker.profile()['birthdate'],
|
||||
gdproptin=True, ftpset=True,surveydone=True,
|
||||
gdproptindate=timezone.now(),
|
||||
rowerplan='basic')
|
||||
|
||||
self.c = Client()
|
||||
|
||||
self.password2 = faker.word()
|
||||
self.u2.set_password(self.password2)
|
||||
self.u2.save()
|
||||
|
||||
self.password3 = faker.word()
|
||||
self.u3.set_password(self.password3)
|
||||
self.u3.save()
|
||||
|
||||
self.team = TeamFactory(manager=self.u2)
|
||||
|
||||
# all are team members
|
||||
add_member(self.team.id, self.r)
|
||||
add_member(self.team.id, self.r2)
|
||||
add_member(self.team.id, self.r3)
|
||||
|
||||
self.user_workouts = WorkoutFactory.create_batch(5, user=self.r)
|
||||
for w in self.user_workouts:
|
||||
if w.id <= 2:
|
||||
w.workoutsource = 'strava'
|
||||
w.privacy = 'hidden'
|
||||
elif w.id == 3: # user can change privacy but cannot change workoutsource
|
||||
w.workoutsource = 'strava'
|
||||
w.privacy = 'visible'
|
||||
else:
|
||||
w.workoutsource = 'concept2'
|
||||
w.privacy = 'visible'
|
||||
w.team.add(self.team)
|
||||
w.csvfilename = get_random_file(filename='rowers/tests/testdata/thyro.csv')['filename']
|
||||
w.save()
|
||||
|
||||
# r2 coaches r
|
||||
add_coach(self.r2, self.r)
|
||||
|
||||
self.factory = APIRequestFactory()
|
||||
|
||||
def tearDown(self):
|
||||
for workout in self.user_workouts:
|
||||
try:
|
||||
os.remove(workout.csvfilename)
|
||||
except (OSError, FileNotFoundError, IOError):
|
||||
pass
|
||||
|
||||
# Test if workout with workoutsource strava and privacy hidden can be seen by coach
|
||||
def test_privacy_coach(self):
|
||||
login = self.c.login(username=self.u2.username, password=self.password2)
|
||||
self.assertTrue(login)
|
||||
|
||||
w = self.user_workouts[0]
|
||||
url = reverse('workout_view',kwargs={'id':encoder.encode_hex(w.id)})
|
||||
response = self.c.get(url)
|
||||
self.assertEqual(response.status_code,403)
|
||||
|
||||
# Same test as above but for 'workout_edit_view'
|
||||
def test_privacy_coach_edit(self):
|
||||
login = self.c.login(username=self.u2.username, password=self.password2)
|
||||
self.assertTrue(login)
|
||||
|
||||
w = self.user_workouts[0]
|
||||
url = reverse('workout_edit_view',kwargs={'id':encoder.encode_hex(w.id)})
|
||||
response = self.c.get(url)
|
||||
self.assertEqual(response.status_code,403)
|
||||
|
||||
# Test if workout with workoutsource strava and privacy hidden can be seen by team member
|
||||
def test_privacy_member(self):
|
||||
login = self.c.login(username=self.u3.username, password=self.password3)
|
||||
self.assertTrue(login)
|
||||
|
||||
w = self.user_workouts[0]
|
||||
url = reverse('workout_view',kwargs={'id':encoder.encode_hex(w.id)})
|
||||
response = self.c.get(url)
|
||||
self.assertEqual(response.status_code,403)
|
||||
|
||||
# Same test as above but for 'workout_edit_view'
|
||||
def test_privacy_member_edit(self):
|
||||
login = self.c.login(username=self.u3.username, password=self.password3)
|
||||
self.assertTrue(login)
|
||||
|
||||
w = self.user_workouts[0]
|
||||
url = reverse('workout_edit_view',kwargs={'id':encoder.encode_hex(w.id)})
|
||||
response = self.c.get(url)
|
||||
self.assertEqual(response.status_code,403)
|
||||
|
||||
# same test as above but with user r and the response code should be 200
|
||||
def test_privacy_owner(self):
|
||||
login = self.c.login(username=self.u.username, password=self.password)
|
||||
self.assertTrue(login)
|
||||
|
||||
w = self.user_workouts[0]
|
||||
url = reverse('workout_view',kwargs={'id':encoder.encode_hex(w.id)})
|
||||
response = self.c.get(url)
|
||||
self.assertEqual(response.status_code,200)
|
||||
|
||||
# same test as above but for 'workout_edit_view'
|
||||
def test_privacy_owner_edit(self):
|
||||
login = self.c.login(username=self.u.username, password=self.password)
|
||||
self.assertTrue(login)
|
||||
|
||||
w = self.user_workouts[0]
|
||||
url = reverse('workout_edit_view',kwargs={'id':encoder.encode_hex(w.id)})
|
||||
response = self.c.get(url)
|
||||
self.assertEqual(response.status_code,200)
|
||||
|
||||
|
||||
|
||||
# test if list_workouts returns all workouts for user r
|
||||
def test_list_workouts(self):
|
||||
login = self.c.login(username=self.u.username, password=self.password)
|
||||
self.assertTrue(login)
|
||||
|
||||
url = reverse('workouts_view')
|
||||
response = self.c.get(url)
|
||||
self.assertEqual(response.status_code,200)
|
||||
|
||||
# the response.content is html, so we need to parse it
|
||||
soup = BeautifulSoup(response.content, 'html.parser')
|
||||
# the workouts look like <a href="/rowers/workout/{id}/...">...</a> and there should be 5 unique ids
|
||||
# the id is a hex string
|
||||
workouts = set([a['href'].split('/')[3] for a in soup.find_all('a') if a['href'].startswith('/rowers/workout/')])
|
||||
|
||||
# throw out "c2import", "nkimport", "stravaimport", "concept2import", "sporttracksimport" from the set
|
||||
workouts = set([w for w in workouts if w not in [
|
||||
'upload', 'addmanual', 'c2import', 'polarimport', 'rp3import', 'nkimport', 'stravaimport', 'concept2import', 'sporttracksimport',
|
||||
'intervalsimport']])
|
||||
|
||||
self.assertEqual(len(workouts),5)
|
||||
|
||||
|
||||
# same test as above but list_workouts with team id = self.team.id
|
||||
def test_list_workouts_team(self):
|
||||
login = self.c.login(username=self.u.username, password=self.password)
|
||||
self.assertTrue(login)
|
||||
|
||||
url = reverse('workouts_view',kwargs={'teamid':self.team.id})
|
||||
response = self.c.get(url)
|
||||
self.assertEqual(response.status_code,200)
|
||||
|
||||
# the response.content is html, so we need to parse it
|
||||
soup = BeautifulSoup(response.content, 'html.parser')
|
||||
# the workouts look like <a href="/rowers/workout/{id}/...">...</a> and there should be 5 unique ids
|
||||
# the id is a hex string
|
||||
workouts = set([a['href'].split('/')[3] for a in soup.find_all('a') if a['href'].startswith('/rowers/workout/')])
|
||||
|
||||
# throw out "c2import", "nkimport", "stravaimport", "concept2import", "sporttracksimport" from the set
|
||||
workouts = set([w for w in workouts if w not in [
|
||||
'upload', 'addmanual', 'c2import', 'polarimport', 'rp3import', 'nkimport', 'stravaimport', 'concept2import', 'sporttracksimport',
|
||||
'intervalsimport']])
|
||||
|
||||
self.assertEqual(len(workouts),2)
|
||||
|
||||
# same test as the previous one but with self.r2 and the number of workouts found should 0
|
||||
def test_list_workouts_team_coach(self):
|
||||
login = self.c.login(username=self.u2.username, password=self.password2)
|
||||
self.assertTrue(login)
|
||||
|
||||
url = reverse('workouts_view',kwargs={'teamid':self.team.id})
|
||||
response = self.c.get(url)
|
||||
self.assertEqual(response.status_code,200)
|
||||
|
||||
# the response.content is html, so we need to parse it
|
||||
soup = BeautifulSoup(response.content, 'html.parser')
|
||||
# the workouts look like <a href="/rowers/workout/{id}/...">...</a> and there should be 5 unique ids
|
||||
# the id is a hex string
|
||||
workouts = set([a['href'].split('/')[3] for a in soup.find_all('a') if a['href'].startswith('/rowers/workout/')])
|
||||
|
||||
# throw out "c2import", "nkimport", "stravaimport", "concept2import", "sporttracksimport" from the set
|
||||
workouts = set([w for w in workouts if w not in [
|
||||
'upload', 'addmanual', 'c2import', 'polarimport', 'rp3import', 'nkimport', 'stravaimport', 'concept2import', 'sporttracksimport',
|
||||
'intervalsimport']])
|
||||
|
||||
self.assertEqual(len(workouts),2)
|
||||
|
||||
# same test as above but with without the teamid kwarg but with a rowerid=self.r.id
|
||||
def test_list_workouts_team_coach2(self):
|
||||
login = self.c.login(username=self.u2.username, password=self.password2)
|
||||
self.assertTrue(login)
|
||||
|
||||
url = reverse('workouts_view',kwargs={'rowerid':self.r.id})
|
||||
response = self.c.get(url)
|
||||
self.assertEqual(response.status_code,200)
|
||||
|
||||
# the response.content is html, so we need to parse it
|
||||
soup = BeautifulSoup(response.content, 'html.parser')
|
||||
# the workouts look like <a href="/rowers/workout/{id}/...">...</a> and there should be 5 unique ids
|
||||
# the id is a hex string
|
||||
workouts = set([a['href'].split('/')[3] for a in soup.find_all('a') if a['href'].startswith('/rowers/workout/')])
|
||||
|
||||
# throw out "c2import", "nkimport", "stravaimport", "concept2import", "sporttracksimport" from the set
|
||||
workouts = set([w for w in workouts if w not in [
|
||||
'upload', 'addmanual', 'c2import', 'polarimport', 'rp3import', 'nkimport', 'stravaimport', 'concept2import', 'sporttracksimport',
|
||||
'intervalsimport']])
|
||||
|
||||
self.assertEqual(len(workouts),2)
|
||||
|
||||
# same test as the previous one but with self.r3 and the number of workouts found should 0
|
||||
def test_list_workouts_team_member(self):
|
||||
login = self.c.login(username=self.u3.username, password=self.password3)
|
||||
self.assertTrue(login)
|
||||
|
||||
url = reverse('workouts_view',kwargs={'teamid':self.team.id})
|
||||
response = self.c.get(url)
|
||||
self.assertEqual(response.status_code,200)
|
||||
|
||||
# the response.content is html, so we need to parse it
|
||||
soup = BeautifulSoup(response.content, 'html.parser')
|
||||
# the workouts look like <a href="/rowers/workout/{id}/...">...</a> and there should be 5 unique ids
|
||||
# the id is a hex string
|
||||
workouts = set([a['href'].split('/')[3] for a in soup.find_all('a') if a['href'].startswith('/rowers/workout/')])
|
||||
|
||||
# throw out "c2import", "nkimport", "stravaimport", "concept2import", "sporttracksimport" from the set
|
||||
workouts = set([w for w in workouts if w not in [
|
||||
'upload', 'addmanual', 'c2import', 'polarimport', 'rp3import', 'nkimport', 'stravaimport', 'concept2import', 'sporttracksimport',
|
||||
'intervalsimport']])
|
||||
|
||||
self.assertEqual(len(workouts),2)
|
||||
|
||||
# now test strava import and test if the created workout has workoutsource strava and privacy hidden
|
||||
@patch('rowers.utils.requests.get', side_effect=mocked_requests)
|
||||
@patch('rowers.integrations.strava.requests.post', side_effect=mocked_requests)
|
||||
@patch('rowers.dataprep.read_data')
|
||||
def test_stravaimport(self, mock_get, mock_post, mocked_read_data):
|
||||
login = self.c.login(username=self.u.username, password=self.password)
|
||||
self.assertTrue(login)
|
||||
|
||||
# remove all self.workouts
|
||||
Workout.objects.filter(user=self.r).delete()
|
||||
|
||||
# create a workout using dataprep.new_workout_from_file with workoutsource = strava
|
||||
result = get_random_file(filename='rowers/tests/testdata/thyro.csv')
|
||||
workout_id, message, filename = dataprep.new_workout_from_file(self.r, result['filename'],
|
||||
workoutsource='strava', makeprivate=True)
|
||||
|
||||
# check if the workout was created
|
||||
ws = Workout.objects.filter(user=self.r)
|
||||
self.assertEqual(len(ws),1)
|
||||
w = ws[0]
|
||||
self.assertEqual(w.workoutsource,'strava')
|
||||
self.assertEqual(w.privacy,'hidden')
|
||||
|
||||
# same as test above but makeprivate = False
|
||||
@patch('rowers.utils.requests.get', side_effect=mocked_requests)
|
||||
@patch('rowers.integrations.strava.requests.post', side_effect=mocked_requests)
|
||||
@patch('rowers.dataprep.read_data')
|
||||
def test_stravaimport_public(self, mock_get, mock_post, mocked_read_data):
|
||||
login = self.c.login(username=self.u.username, password=self.password)
|
||||
self.assertTrue(login)
|
||||
|
||||
# remove all self.workouts
|
||||
Workout.objects.filter(user=self.r).delete()
|
||||
|
||||
# create a workout using dataprep.new_workout_from_file with workoutsource = strava
|
||||
result = get_random_file(filename='rowers/tests/testdata/thyro.csv')
|
||||
workout_id, message, filename = dataprep.new_workout_from_file(self.r, result['filename'],
|
||||
workoutsource='strava', makeprivate=False)
|
||||
|
||||
# check if the workout was created
|
||||
ws = Workout.objects.filter(user=self.r)
|
||||
self.assertEqual(len(ws),1)
|
||||
w = ws[0]
|
||||
self.assertEqual(w.workoutsource,'strava')
|
||||
self.assertEqual(w.privacy,'hidden')
|
||||
|
||||
|
||||
# test ownapi with stravaid = '122'
|
||||
def test_ownapi(self):
|
||||
# remove all self.workouts
|
||||
Workout.objects.filter(user=self.r).delete()
|
||||
|
||||
result = get_random_file(filename='rowers/tests/testdata/thyro.csv')
|
||||
uploadoptions = {
|
||||
'workouttype': 'water',
|
||||
'boattype': '1x',
|
||||
'notes': 'A test file upload',
|
||||
'stravaid': '122',
|
||||
'secret': UPLOAD_SERVICE_SECRET,
|
||||
'user': self.u.id,
|
||||
'file': result['filename'],
|
||||
}
|
||||
url = reverse('workout_upload_api')
|
||||
response = self.c.post(url, uploadoptions)
|
||||
self.assertEqual(response.status_code,200)
|
||||
|
||||
# check if the workout was created
|
||||
ws = Workout.objects.filter(user=self.r)
|
||||
self.assertEqual(len(ws),1)
|
||||
w = ws[0]
|
||||
self.assertEqual(w.workoutsource,'strava')
|
||||
self.assertEqual(w.privacy,'hidden')
|
||||
|
||||
|
||||
# test some analysis, should only use the workouts with workoutsource != strava
|
||||
#@patch('rowers.dataprep.read_data', side_effect=mocked_read_data)
|
||||
#def test_workouts_analysis(self, mocked_read_data):
|
||||
def test_workouts_analysis(self):
|
||||
login = self.c.login(username=self.u.username, password=self.password)
|
||||
self.assertTrue(login)
|
||||
|
||||
url = '/rowers/history/'
|
||||
response = self.c.get(url)
|
||||
self.assertEqual(response.status_code,200)
|
||||
|
||||
url = '/rowers/history/data/'
|
||||
response = self.c.get(url)
|
||||
self.assertEqual(response.status_code,200)
|
||||
# response.json() has a key "script" with a javascript script
|
||||
# check if this is correct
|
||||
self.assertTrue('script' in response.json())
|
||||
|
||||
# now check histogram
|
||||
startdate = (self.user_workouts[0].startdatetime-datetime.timedelta(days=3)).date()
|
||||
enddate = (self.user_workouts[0].startdatetime+datetime.timedelta(days=3)).date()
|
||||
|
||||
# make sure the dates are not naive
|
||||
try:
|
||||
startdate = pytz.utc.localize(startdate)
|
||||
except (ValueError, AttributeError):
|
||||
pass
|
||||
try:
|
||||
enddate = pytz.utc.localize(enddate)
|
||||
except (ValueError, AttributeError):
|
||||
pass
|
||||
|
||||
form_data = {
|
||||
'function':'histo',
|
||||
'xparam':'hr',
|
||||
'plotfield':'spm',
|
||||
'yparam':'pace',
|
||||
'groupby':'spm',
|
||||
'palette':'monochrome_blue',
|
||||
'xaxis':'time',
|
||||
'yaxis1':'power',
|
||||
'yaxis2':'hr',
|
||||
'startdate':startdate,
|
||||
'enddate':enddate,
|
||||
'plottype':'scatter',
|
||||
'spmmin':15,
|
||||
'spmmax':55,
|
||||
'workmin':0,
|
||||
'workmax':1500,
|
||||
'includereststrokes':False,
|
||||
'modality':'all',
|
||||
'waterboattype':['1x','2x','4x'],
|
||||
'userid':self.u.id,
|
||||
'workouts':[w.id for w in Workout.objects.filter(user=self.r)],
|
||||
}
|
||||
|
||||
form = AnalysisChoiceForm(form_data)
|
||||
optionsform = AnalysisOptionsForm(form_data)
|
||||
dateform = DateRangeForm(form_data)
|
||||
|
||||
result = form.is_valid()
|
||||
if not result:
|
||||
print(form.errors)
|
||||
|
||||
self.assertTrue(form.is_valid())
|
||||
self.assertTrue(optionsform.is_valid())
|
||||
self.assertTrue(dateform.is_valid())
|
||||
|
||||
response = self.c.post('/rowers/user-analysis-select/',form_data)
|
||||
|
||||
self.assertEqual(response.status_code,200)
|
||||
|
||||
# count number of workouts by counting the number of occurences of '<label for="id_workouts_xx">' in response.content where xx is a number
|
||||
# print all lines of response.content that contain '<label for="id_workouts_'
|
||||
#print([line for line in response.content.decode('utf-8').split('\n') if '<label for="id_workouts_' in line])
|
||||
#print(form_data['workouts'])
|
||||
#self.assertEqual(response.content.count(b'<label for="id_workouts_'),2) <-- if we forbid the user to use strava workouts
|
||||
self.assertEqual(response.content.count(b'<label for="id_workouts_'),5)
|
||||
|
||||
# get data from histodata function
|
||||
ws = Workout.objects.filter(user=self.r)
|
||||
|
||||
script, div = histodata(ws,form_data)
|
||||
# script has a line starting with 'data = [ ... ]'
|
||||
# we need to get that line
|
||||
data = [line for line in script.split('\n') if line.startswith('data = [')][0]
|
||||
# the line should be a list of float values
|
||||
self.assertTrue(data.startswith('data = ['))
|
||||
self.assertTrue(data.endswith(']'))
|
||||
# count the number of commas between the brackets
|
||||
#self.assertEqual(data.count(','),2062) <-- if we forbid the user to use strava workouts
|
||||
self.assertEqual(data.count(','),5155)
|
||||
|
||||
|
||||
class OwnApi(TestCase):
|
||||
def setUp(self):
|
||||
self.u = UserFactory()
|
||||
@@ -36,9 +478,7 @@ class OwnApi(TestCase):
|
||||
birthdate=faker.profile()['birthdate'],
|
||||
gdproptin=True, ftpset=True,surveydone=True,
|
||||
gdproptindate=timezone.now(),
|
||||
rowerplan='coach',subscription_id=1)
|
||||
|
||||
|
||||
rowerplan='pro',subscription_id=1)
|
||||
self.c = Client()
|
||||
self.user_workouts = WorkoutFactory.create_batch(5, user=self.r)
|
||||
self.factory = RequestFactory()
|
||||
|
||||
@@ -106,10 +106,26 @@ class ChallengesTest(TestCase):
|
||||
workouttype = 'water',
|
||||
)
|
||||
|
||||
|
||||
self.wthyro.startdatetime = arrow.get(nu).datetime
|
||||
self.wthyro.date = nu.date()
|
||||
self.wthyro.save()
|
||||
|
||||
result = get_random_file(filename='rowers/tests/testdata/thyro.csv')
|
||||
self.w_strava = WorkoutFactory(user=self.r,
|
||||
csvfilename=result['filename'],
|
||||
starttime=result['starttime'],
|
||||
startdatetime=result['startdatetime'],
|
||||
duration=result['duration'],
|
||||
distance=result['totaldist'],
|
||||
workouttype = 'water',
|
||||
workoutsource = 'strava',
|
||||
privacy = 'hidden',
|
||||
)
|
||||
self.w_strava.startdatetime = arrow.get(nu).datetime
|
||||
self.w_strava.date = nu.date()
|
||||
self.w_strava.save()
|
||||
|
||||
result = get_random_file(filename='rowers/tests/testdata/thyro.csv')
|
||||
self.wthyro2 = WorkoutFactory(user=self.r2,
|
||||
csvfilename=result['filename'],
|
||||
@@ -591,6 +607,78 @@ class ChallengesTest(TestCase):
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# repeat previous test for self.w_strava, but the response status of virtualevent_submit_result_view should be 403 and len(records) should be 0
|
||||
@patch('django.contrib.gis.geoip2.GeoIP2.city', side_effect=mocked_requests)
|
||||
def test_fastestrace_view_strava(self, mock_get):
|
||||
login = self.c.login(username=self.u.username, password=self.password)
|
||||
self.assertTrue(login)
|
||||
|
||||
race = self.FastestRace
|
||||
|
||||
if self.r.birthdate:
|
||||
age = calculate_age(self.r.birthdate)
|
||||
else:
|
||||
age = 25
|
||||
|
||||
# look at event
|
||||
url = reverse('virtualevent_view',kwargs={'id':race.id})
|
||||
response = self.c.get(url)
|
||||
self.assertEqual(response.status_code,200)
|
||||
|
||||
# register
|
||||
url = reverse('virtualevent_register_view',kwargs={'id':race.id})
|
||||
response = self.c.get(url)
|
||||
self.assertEqual(response.status_code,200)
|
||||
|
||||
|
||||
form_data = {
|
||||
'teamname': 'ApeTeam',
|
||||
'boatclass': 'water',
|
||||
'boattype': '1x',
|
||||
'weightcategory': 'hwt',
|
||||
'adaptiveclass': 'None',
|
||||
'age': age,
|
||||
'mix': False,
|
||||
'acceptsocialmedia': True,
|
||||
}
|
||||
form = VirtualRaceResultForm(form_data)
|
||||
self.assertTrue(form.is_valid())
|
||||
|
||||
|
||||
response = self.c.post(url,form_data,follow=True)
|
||||
expected_url = reverse('virtualevent_view',kwargs={'id':race.id})
|
||||
self.assertRedirects(response, expected_url=expected_url,
|
||||
status_code=302,target_status_code=200)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# submit workout
|
||||
url = reverse('virtualevent_submit_result_view',kwargs={'id':race.id,'workoutid':self.w_strava.id})
|
||||
response = self.c.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# response.content should have a form with only one instance of <label for="id_workouts_0">
|
||||
self.assertEqual(response.content.count(b'<label for="id_workouts_0">'),1)
|
||||
|
||||
|
||||
|
||||
records = IndoorVirtualRaceResult.objects.filter(userid=self.u.id)
|
||||
self.assertEqual(len(records),1)
|
||||
|
||||
record = records[0]
|
||||
|
||||
|
||||
form_data = {
|
||||
'workouts':[self.w_strava.id],
|
||||
'record':record.id,
|
||||
}
|
||||
|
||||
response = self.c.post(url,form_data,follow=True)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# in response.content, there should be a p with class errormessage and the text "Error in form"
|
||||
self.assertTrue(b'Error in form' in response.content)
|
||||
|
||||
@patch('django.contrib.gis.geoip2.GeoIP2.city', side_effect=mocked_requests)
|
||||
def test_virtualevents_view(self, mock_get):
|
||||
|
||||
Vendored
BIN
Binary file not shown.
+81
-24
@@ -130,19 +130,49 @@ def make_plot(r, w, f1, f2, plottype, title, imagename='', plotnr=0):
|
||||
|
||||
|
||||
def do_sync(w, options, quick=False):
|
||||
do_strava_export = w.user.strava_auto_export
|
||||
try:
|
||||
do_strava_export = options['upload_to_Strava'] or do_strava_export
|
||||
except KeyError:
|
||||
pass
|
||||
do_strava_export = False
|
||||
if w.user.strava_auto_export is True:
|
||||
do_strava_export = True
|
||||
else:
|
||||
try:
|
||||
do_strava_export = options['upload_to_Strava'] or do_strava_export
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
try:
|
||||
if options['stravaid'] != 0 and options['stravaid'] != '': # pragma: no cover
|
||||
w.uploadedtostrava = options['stravaid']
|
||||
# upload_to_strava = False
|
||||
do_strava_export = False
|
||||
w.workoutsource = 'strava'
|
||||
w.privacy = 'hidden'
|
||||
w.save()
|
||||
record = create_or_update_syncrecord(w.user, w, stravaid=options['stravaid'])
|
||||
# strava, we shall not sync to other sites -> return
|
||||
return 1
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
do_icu_export = False
|
||||
if w.user.intervals_auto_export is True:
|
||||
do_icu_export = True
|
||||
if w.workoutsource == 'strava':
|
||||
do_icu_export = False
|
||||
else:
|
||||
try:
|
||||
do_icu_export = options['upload_to_Intervals']
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
#dologging("uploads.log", "do_icu_export: {do_icu_export}".format(do_icu_export=do_icu_export))
|
||||
|
||||
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
|
||||
|
||||
@@ -177,11 +207,16 @@ def do_sync(w, options, quick=False):
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
do_c2_export = w.user.c2_auto_export
|
||||
try:
|
||||
do_c2_export = options['upload_to_C2'] or do_c2_export
|
||||
except KeyError:
|
||||
pass
|
||||
do_c2_export = False
|
||||
if w.user.c2_auto_export is True:
|
||||
do_c2_export = True
|
||||
if w.workoutsource == 'strava':
|
||||
do_c2_export = False
|
||||
else:
|
||||
try:
|
||||
do_c2_export = options['upload_to_C2'] or do_c2_export
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
try:
|
||||
if options['c2id'] != 0 and options['c2id'] != '': # pragma: no cover
|
||||
@@ -218,28 +253,27 @@ def do_sync(w, options, quick=False):
|
||||
dologging('c2_log.log','Error C2')
|
||||
pass
|
||||
|
||||
if do_strava_export: # pragma: no cover
|
||||
strava_integration = StravaIntegration(w.user.user)
|
||||
if do_icu_export:
|
||||
intervals_integration = IntervalsIntegration(w.user.user)
|
||||
try:
|
||||
id = strava_integration.workout_export(w)
|
||||
id = intervals_integration.workout_export(w)
|
||||
dologging(
|
||||
'strava_export_log.log',
|
||||
'intervals.icu.log',
|
||||
'exporting workout {id} as {type}'.format(
|
||||
id=w.id,
|
||||
type=w.workouttype,
|
||||
)
|
||||
)
|
||||
except NoTokenError: # pragma: no cover
|
||||
except NoTokenError:
|
||||
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))
|
||||
dologging('intervals.icu.log','NoTokenError')
|
||||
message = "Please connect to Intervals.icu first"
|
||||
except Exception as e:
|
||||
dologging(
|
||||
'intervals.icu.log',
|
||||
e
|
||||
)
|
||||
|
||||
|
||||
do_st_export = w.user.sporttracks_auto_export
|
||||
|
||||
@@ -252,6 +286,8 @@ def do_sync(w, options, quick=False):
|
||||
try: # pragma: no cover
|
||||
upload_to_st = options['upload_to_SportTracks'] or do_st_export
|
||||
do_st_export = upload_to_st
|
||||
if w.workoutsource == 'strava':
|
||||
do_st_export = False
|
||||
except KeyError:
|
||||
upload_to_st = False
|
||||
|
||||
@@ -274,6 +310,8 @@ def do_sync(w, options, quick=False):
|
||||
do_tp_export = w.user.trainingpeaks_auto_export
|
||||
try:
|
||||
upload_to_tp = options['upload_to_TrainingPeaks'] or do_tp_export
|
||||
if w.workoutsource == 'strava':
|
||||
do_tp_export = False
|
||||
do_tp_export = upload_to_tp
|
||||
except KeyError:
|
||||
upload_to_st = False
|
||||
@@ -291,4 +329,23 @@ def do_sync(w, options, quick=False):
|
||||
dologging('tp_export.log','No Token Error')
|
||||
return 0
|
||||
|
||||
# we do Strava last.
|
||||
if do_strava_export: # pragma: no cover
|
||||
strava_integration = StravaIntegration(w.user.user)
|
||||
try:
|
||||
id = strava_integration.workout_export(w)
|
||||
dologging(
|
||||
'strava_export_log.log',
|
||||
'exporting workout {id} as {type}'.format(
|
||||
id=w.id,
|
||||
type=w.workouttype,
|
||||
)
|
||||
)
|
||||
except NoTokenError: # pragma: no cover
|
||||
id = 0
|
||||
message = "Please connect to Strava first"
|
||||
except Exception as e:
|
||||
dologging('stravalog.log', e)
|
||||
|
||||
|
||||
return 1
|
||||
|
||||
+3
-1
@@ -80,7 +80,8 @@ class WorkoutViewSet(viewsets.ModelViewSet):
|
||||
def get_queryset(self): # pragma: no cover
|
||||
try:
|
||||
r = Rower.objects.get(user=self.request.user)
|
||||
return Workout.objects.filter(user=r).order_by("-date", "-starttime")
|
||||
#return Workout.objects.filter(user=r).order_by("-date", "-starttime")
|
||||
return Workout.objects.filter(user=r).exclude(workoutsource='strava').order_by("-date", "-starttime")
|
||||
except TypeError:
|
||||
return []
|
||||
|
||||
@@ -662,6 +663,7 @@ urlpatterns = [
|
||||
re_path(r'^me/messages/$', views.user_messages, name='user_messages'),
|
||||
re_path(r'^me/messages/delete/$', views.user_messages_delete_all, name='user_messages_delete_all'),
|
||||
re_path(r'^me/messages/(?P<id>\d+)/markread/$', views.user_message_markread, name='user_message_markread'),
|
||||
re_path(r'^me/messages/(?P<id>\d+)/delete/$', views.user_message_delete, name='user_message_delete'),
|
||||
re_path(r'^me/messages/user/(?P<userid>\d+)/$', views.user_messages, name='user_messages'),
|
||||
re_path(r'^me/delete/$', views.remove_user, name='remove_user'),
|
||||
re_path(r'^survey/$', views.survey, name='survey'),
|
||||
|
||||
@@ -48,6 +48,9 @@ def analysis_new(request,
|
||||
firstworkout = get_workout(id)
|
||||
if not is_workout_team(request.user, firstworkout): # pragma: no cover
|
||||
raise PermissionDenied("You are not allowed to use this workout")
|
||||
#if workout_is_strava(firstworkout):
|
||||
# messages.error(request, "You cannot use Strava workouts for analysis")
|
||||
# raise PermissionDenied("You cannot use Strava workouts for analysis")
|
||||
firstworkoutquery = Workout.objects.filter(id=encoder.decode_hex(id))
|
||||
|
||||
try:
|
||||
@@ -199,14 +202,14 @@ def analysis_new(request,
|
||||
startdatetime__lte=enddate,
|
||||
workouttype__in=modalities,
|
||||
rankingpiece__in=rankingtypes,
|
||||
)
|
||||
)#.exclude(workoutsource='strava')
|
||||
elif theteam is not None and theteam.viewing == 'coachonly': # pragma: no cover
|
||||
workouts = Workout.objects.filter(team=theteam, user=r,
|
||||
startdatetime__gte=startdate,
|
||||
startdatetime__lte=enddate,
|
||||
workouttype__in=modalities,
|
||||
rankingpiece__in=rankingtypes,
|
||||
)
|
||||
)#.exclude(workoutsource='strava')
|
||||
elif thesession is not None:
|
||||
workouts = get_workouts_session(r, thesession)
|
||||
else:
|
||||
@@ -218,6 +221,7 @@ def analysis_new(request,
|
||||
)
|
||||
if firstworkout:
|
||||
workouts = firstworkoutquery | workouts
|
||||
|
||||
workouts = workouts.order_by(
|
||||
"-date", "-starttime"
|
||||
).exclude(boattype__in=negtypes)
|
||||
@@ -253,7 +257,7 @@ def analysis_new(request,
|
||||
else:
|
||||
selectedworkouts = Workout.objects.filter(id__in=ids)
|
||||
|
||||
form.fields["workouts"].queryset = workouts | selectedworkouts
|
||||
form.fields["workouts"].queryset = (workouts | selectedworkouts)#.exclude(workoutsource='strava')
|
||||
|
||||
optionsform = AnalysisOptionsForm(initial={
|
||||
'modality': modality,
|
||||
@@ -363,6 +367,10 @@ def trendflexdata(workouts, options, userid=0):
|
||||
|
||||
savedata = options.get('savedata',False)
|
||||
|
||||
#try:
|
||||
# workouts = workouts.exclude(workoutsource='strava')
|
||||
#except AttributeError: # pragma: no cover
|
||||
# workouts = [w for w in workouts if w.workoutsource != 'strava']
|
||||
|
||||
fieldlist, fielddict = dataprep.getstatsfields()
|
||||
fieldlist = [xparam, yparam, groupby,
|
||||
@@ -566,6 +574,11 @@ def flexalldata(workouts, options):
|
||||
trendline = options['trendline']
|
||||
promember = True
|
||||
|
||||
#try:
|
||||
# workouts = workouts.exclude(workoutsource='strava')
|
||||
#except AttributeError: # pragma: no cover
|
||||
# workouts = [w for w in workouts if w.workoutsource != 'strava']
|
||||
|
||||
workstrokesonly = not includereststrokes
|
||||
|
||||
userid = options['userid']
|
||||
@@ -612,6 +625,12 @@ def histodata(workouts, options):
|
||||
workmax = options['workmax']
|
||||
userid = options['userid']
|
||||
|
||||
#try:
|
||||
# workouts = workouts.exclude(workoutsource='strava')
|
||||
#except AttributeError: # pragma: no cover
|
||||
# workouts = [w for w in workouts if w.workoutsource != 'strava']
|
||||
|
||||
|
||||
if userid == 0: # pragma: no cover
|
||||
extratitle = ''
|
||||
else:
|
||||
@@ -645,7 +664,8 @@ def cpdata(workouts, options):
|
||||
|
||||
u = User.objects.get(id=userid)
|
||||
r = u.rower
|
||||
|
||||
|
||||
|
||||
delta, cpvalue, avgpower, workoutnames, urls = dataprep.fetchcp_new(
|
||||
r, workouts)
|
||||
|
||||
@@ -798,6 +818,11 @@ def cpdata(workouts, options):
|
||||
|
||||
|
||||
def statsdata(workouts, options):
|
||||
#try:
|
||||
# workouts = workouts.exclude(workoutsource='strava')
|
||||
#except AttributeError: # pragma: no cover
|
||||
# workouts = [w for w in workouts if w.workoutsource != 'strava']
|
||||
|
||||
includereststrokes = options['includereststrokes']
|
||||
ids = options['ids']
|
||||
|
||||
@@ -872,12 +897,17 @@ def statsdata(workouts, options):
|
||||
|
||||
|
||||
def comparisondata(workouts, options):
|
||||
#try:
|
||||
# workouts = workouts.exclude(workoutsource='strava')
|
||||
#except AttributeError: # pragma: no cover
|
||||
# workouts = [w for w in workouts if w.workoutsource != 'strava']
|
||||
|
||||
includereststrokes = options['includereststrokes']
|
||||
xparam = options['xaxis']
|
||||
yparam1 = options['yaxis1']
|
||||
plottype = options['plottype']
|
||||
promember = True
|
||||
|
||||
|
||||
workstrokesonly = not includereststrokes
|
||||
|
||||
ids = [w.id for w in workouts]
|
||||
@@ -915,6 +945,10 @@ def comparisondata(workouts, options):
|
||||
|
||||
|
||||
def boxplotdata(workouts, options):
|
||||
#try:
|
||||
# workouts = workouts.exclude(workoutsource='strava')
|
||||
#except AttributeError:
|
||||
# workouts = [w for w in workouts if w.workoutsource != 'strava']
|
||||
|
||||
includereststrokes = options['includereststrokes']
|
||||
spmmin = options['spmmin']
|
||||
@@ -926,7 +960,7 @@ def boxplotdata(workouts, options):
|
||||
plotfield = options['plotfield']
|
||||
|
||||
workstrokesonly = not includereststrokes
|
||||
|
||||
|
||||
datemapping = {
|
||||
w.id: w.date for w in workouts
|
||||
}
|
||||
@@ -1020,11 +1054,15 @@ def analysis_view_data(request, userid=0):
|
||||
|
||||
for id in ids:
|
||||
try:
|
||||
workouts.append(Workout.objects.get(id=id))
|
||||
w = Workout.objects.get(id=id)
|
||||
#if w.workoutsource != 'strava':
|
||||
# workouts.append(w)
|
||||
workouts.append(w)
|
||||
except Workout.DoesNotExist: # pragma: no cover
|
||||
pass
|
||||
|
||||
|
||||
|
||||
if function == 'boxplot':
|
||||
script, div = boxplotdata(workouts, options)
|
||||
elif function == 'trendflex': # pragma: no cover
|
||||
@@ -1069,7 +1107,7 @@ def create_marker_workouts_view(request, userid=0,
|
||||
workouts = Workout.objects.filter(user=theuser.rower, date__gte=startdate,
|
||||
date__lte=enddate,
|
||||
workouttype__in=mytypes.rowtypes,
|
||||
duplicate=False).order_by('date')
|
||||
duplicate=False).order_by('date')#.exclude(workoutsource='strava')
|
||||
|
||||
for workout in workouts:
|
||||
_ = dataprep.check_marker(workout)
|
||||
@@ -1113,7 +1151,7 @@ def goldmedalscores_view(request, userid=0,
|
||||
theuser, startdate=startdate, enddate=enddate,
|
||||
)
|
||||
|
||||
bestworkouts = Workout.objects.filter(id__in=ids).order_by('-date')
|
||||
bestworkouts = Workout.objects.filter(id__in=ids).order_by('-date')#.exclude(workoutsource='strava')
|
||||
|
||||
breadcrumbs = [
|
||||
{
|
||||
@@ -1311,7 +1349,7 @@ def performancemanager_view(request, userid=0, mode='rower',
|
||||
user = therower, date__gte=startdate-datetime.timedelta(days=90),
|
||||
date__lte=enddate,
|
||||
duplicate=False,
|
||||
rankingpiece=True, workouttype__in=mytypes.rowtypes).order_by('date')
|
||||
rankingpiece=True, workouttype__in=mytypes.rowtypes).order_by('date')#.exclude(workoutsource='strava')
|
||||
|
||||
ids = [w.id for w in markerworkouts]
|
||||
form = PerformanceManagerForm(initial={
|
||||
@@ -1323,7 +1361,7 @@ def performancemanager_view(request, userid=0, mode='rower',
|
||||
|
||||
ids = pd.Series(ids, dtype='int').dropna().values
|
||||
|
||||
bestworkouts = Workout.objects.filter(id__in=ids).order_by('-date')
|
||||
bestworkouts = Workout.objects.filter(id__in=ids).order_by('-date')#.exclude(workoutsource='strava')
|
||||
|
||||
breadcrumbs = [
|
||||
{
|
||||
@@ -2276,6 +2314,8 @@ def history_view_data(request, userid=0):
|
||||
ddf = ddf.with_columns(pl.col("time").diff().clip(lower_bound=0).alias("deltat"))
|
||||
except KeyError: # pragma: no cover
|
||||
pass
|
||||
except ColumnNotFoundError:
|
||||
pass
|
||||
|
||||
ddf = dataprep.clean_df_stats_pl(ddf, workstrokesonly=False,
|
||||
ignoreadvanced=True)
|
||||
@@ -2288,6 +2328,8 @@ def history_view_data(request, userid=0):
|
||||
ddict['hrmax'] = int(ddf['hr'].max())
|
||||
except (KeyError, ValueError, AttributeError, ColumnNotFoundError): # pragma: no cover
|
||||
ddict['hrmax'] = 0
|
||||
except ColumnNotFoundError:
|
||||
ddict['hrmax'] = 0
|
||||
|
||||
ddict['powermean'] = int(wavg(ddf, 'power', 'deltat'))
|
||||
try:
|
||||
|
||||
@@ -575,19 +575,40 @@ def strokedata_fit(request):
|
||||
return JsonResponse({
|
||||
"status": "error",
|
||||
"message": f"An error occurred while saving the FIT file: {str(e)}"
|
||||
}, status=500)
|
||||
}, status=400)
|
||||
|
||||
try:
|
||||
# Parse the FIT file
|
||||
row = FP(fit_filename)
|
||||
try:
|
||||
row = FP(fit_filename)
|
||||
except ValueError as e:
|
||||
return JsonResponse({
|
||||
"status": "error",
|
||||
"message": f"An error occurred while parsing the FIT file: {str(e)}"
|
||||
}, status=422)
|
||||
|
||||
rowdata = rowingdata(df=row.df)
|
||||
duration = totaltime_sec_to_string(rowdata.duration)
|
||||
title = "ActiveSpeed water"
|
||||
|
||||
duration = totaltime_sec_to_string(rowdata.duration)
|
||||
distance = rowdata.df[" Horizontal (meters)"].iloc[-1]
|
||||
title = ""
|
||||
try:
|
||||
startdatetime = rowdata.rowdatetime
|
||||
startdate = startdatetime.date()
|
||||
partofday = part_of_day(startdatetime.hour)
|
||||
title = '{partofday} water'.format(partofday=partofday)
|
||||
except Exception as e:
|
||||
dologging('apilog.log','FIT error to get time')
|
||||
dologging('apilog.log',e)
|
||||
_ = myqueue(queuehigh, handle_sendemail_unrecognized, fit_filename, "fit parser")
|
||||
return HttpResponse(status=422)
|
||||
|
||||
w = Workout.objects.create(user=request.user.rower,
|
||||
duration=duration,
|
||||
name=title,)
|
||||
distance=distance,
|
||||
name=title,
|
||||
date=startdate,
|
||||
workouttype='water',)
|
||||
|
||||
uploadoptions = {
|
||||
'secret': UPLOAD_SERVICE_SECRET,
|
||||
@@ -598,7 +619,7 @@ def strokedata_fit(request):
|
||||
'title': title,
|
||||
'rpe': 0,
|
||||
'notes': '',
|
||||
'workoutid': w.id,
|
||||
'id': w.id,
|
||||
'offline': False,
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -3397,12 +3397,12 @@ def virtualevent_submit_result_view(request, id=0, workoutid=0):
|
||||
startdatetime__gte=startdatetime,
|
||||
startdatetime__lte=enddatetime,
|
||||
distance__gte=race.approximate_distance,
|
||||
).order_by("-date", "-startdatetime", "id")
|
||||
).order_by("-date", "-startdatetime", "id").exclude(workoutsource='strava')
|
||||
|
||||
if not ws: # pragma: no cover
|
||||
messages.info(
|
||||
request,
|
||||
'You have no workouts executed during the race window. Please upload a result or enter it manually.'
|
||||
'You have no eligible workouts executed during the race window. Please upload a result or enter it manually.'
|
||||
)
|
||||
|
||||
url = reverse('virtualevent_view',
|
||||
@@ -3436,6 +3436,7 @@ def virtualevent_submit_result_view(request, id=0, workoutid=0):
|
||||
splitsecond = 0
|
||||
recordid = w_form.cleaned_data['record']
|
||||
else:
|
||||
messages.error(request,"Error in form")
|
||||
selectedworkout = None
|
||||
|
||||
if selectedworkout is not None:
|
||||
@@ -3518,7 +3519,12 @@ def virtualevent_submit_result_view(request, id=0, workoutid=0):
|
||||
|
||||
else:
|
||||
if workoutid:
|
||||
workoutdata['initial'] = encoder.decode_hex(workoutid)
|
||||
try:
|
||||
w = Workout.objects.get(id=workoutid)
|
||||
if w.workoutsource != 'strava':
|
||||
workoutdata['initial'] = encoder.decode_hex(workoutid)
|
||||
except Workout.DoesNotExist:
|
||||
pass
|
||||
w_form = WorkoutRaceSelectForm(workoutdata, entries)
|
||||
|
||||
breadcrumbs = [
|
||||
|
||||
@@ -28,6 +28,7 @@ from rest_framework.response import Response
|
||||
from rq.job import Job
|
||||
from rules.contrib.views import permission_required, objectgetter
|
||||
from django.core.cache import cache
|
||||
from django.db import models
|
||||
from django.utils.crypto import get_random_string
|
||||
from rq.registry import StartedJobRegistry
|
||||
from rq.exceptions import NoSuchJobError
|
||||
@@ -81,7 +82,8 @@ from rowers.rower_rules import (
|
||||
can_add_workout_member, can_plan_user, is_paid_coach,
|
||||
can_start_trial, can_start_plantrial, can_start_coachtrial,
|
||||
can_plan, is_workout_team,
|
||||
is_promember,user_is_basic, is_coachtrial, is_coach
|
||||
is_promember,user_is_basic, is_coachtrial, is_coach,
|
||||
workout_is_strava
|
||||
)
|
||||
|
||||
from django.shortcuts import render
|
||||
@@ -179,7 +181,13 @@ from rowers.models import ( RowerPowerForm, RowerHRZonesForm, SimpleRowerPowerFo
|
||||
IndoorVirtualRaceForm, PlannedSessionCommentForm, Alert,
|
||||
Condition, StaticChartRowerForm, FollowerForm,
|
||||
VirtualRaceAthleteForm, InstantPlanForm, DataRowerForm,
|
||||
StepEditorForm, iDokladToken )
|
||||
StepEditorForm, iDokladToken,
|
||||
RowerExportFormStrava, RowerExportFormPolar,
|
||||
RowerExportFormSportTracks, RowerExportFormTrainingPeaks,
|
||||
RowerExportFormConcept2, RowerExportFormGarmin,
|
||||
RowerExportFormIntervals, RowerExportFormRP3,
|
||||
RowerExportFormNK, RowerPrivateImportForm,
|
||||
)
|
||||
from rowers.models import (
|
||||
FavoriteForm, BaseFavoriteFormSet, SiteAnnouncement, BasePlannedSessionFormSet,
|
||||
get_course_timezone, BaseConditionFormSet,
|
||||
|
||||
@@ -279,7 +279,6 @@ def user_message_delete(request,id=0): # pragma: no cover
|
||||
messages.error(request,'Could not find this message')
|
||||
url = reverse('user_messages')
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
|
||||
if msg.receiver == request.user.rower:
|
||||
msg.delete()
|
||||
@@ -458,19 +457,45 @@ def rower_exportsettings_view(request, userid=0):
|
||||
'polar_auto_import': 'polartoken',
|
||||
'c2_auto_export': 'c2token',
|
||||
'c2_auto_import': 'c2token',
|
||||
'runkeeper_auto_export': 'runkeepertoken',
|
||||
'sporttracks_auto_export': 'sporttrackstoken',
|
||||
'strava_auto_export': 'stravatoken',
|
||||
'strava_auto_import': 'stravatoken',
|
||||
'strava_auto_delete': 'stravatoken',
|
||||
'trainingpeaks_auto_export': 'tptoken',
|
||||
'rp3_auto_import': 'rp3token',
|
||||
'nk_auto_import': 'nktoken'
|
||||
'nk_auto_import': 'nktoken',
|
||||
'intervals_auto_export': 'intervals_token',
|
||||
'intervals_resample_to_1s': 'intervals_token',
|
||||
}
|
||||
r = getrequestrowercoachee(request, userid=userid)
|
||||
|
||||
forms = {
|
||||
'polar': RowerExportFormPolar(instance=r),
|
||||
'c2': RowerExportFormConcept2(instance=r),
|
||||
'sporttracks': RowerExportFormSportTracks(instance=r),
|
||||
'strava': RowerExportFormStrava(instance=r),
|
||||
'trainingpeaks': RowerExportFormTrainingPeaks(instance=r),
|
||||
'rp3': RowerExportFormRP3(instance=r),
|
||||
'intervals': RowerExportFormIntervals(instance=r),
|
||||
'nk': RowerExportFormNK(instance=r),
|
||||
'garmin': RowerExportFormGarmin(instance=r),
|
||||
'imports_are_private': RowerPrivateImportForm(instance=r)
|
||||
}
|
||||
|
||||
if request.method == 'POST':
|
||||
form = RowerExportForm(request.POST)
|
||||
forms = {
|
||||
'polar': RowerExportFormPolar(request.POST, instance=r),
|
||||
'c2': RowerExportFormConcept2(request.POST, instance=r),
|
||||
'sporttracks': RowerExportFormSportTracks(request.POST, instance=r),
|
||||
'strava': RowerExportFormStrava(request.POST, instance=r),
|
||||
'trainingpeaks': RowerExportFormTrainingPeaks(request.POST, instance=r),
|
||||
'rp3': RowerExportFormRP3(request.POST, instance=r),
|
||||
'intervals': RowerExportFormIntervals(request.POST, instance=r),
|
||||
'nk': RowerExportFormNK(request.POST, instance=r),
|
||||
'garmin': RowerExportFormGarmin(request.POST, instance=r),
|
||||
'imports_are_private': RowerPrivateImportForm(request.POST, instance=r),
|
||||
}
|
||||
if form.is_valid():
|
||||
cd = form.cleaned_data
|
||||
if r.rowerplan == 'basic': # pragma: no cover
|
||||
@@ -529,6 +554,7 @@ def rower_exportsettings_view(request, userid=0):
|
||||
|
||||
return render(request, 'rower_exportsettings.html',
|
||||
{'form': form,
|
||||
'forms': forms,
|
||||
'rower': r,
|
||||
'breadcrumbs': breadcrumbs,
|
||||
'grants': grants,
|
||||
@@ -569,11 +595,13 @@ def rower_edit_view(request, rowerid=0, userid=0, message=""):
|
||||
if request.method == 'POST':
|
||||
accountform = AccountRowerForm(request.POST, instance=r)
|
||||
userform = UserForm(request.POST, instance=r.user)
|
||||
privateform = RowerPrivateImportForm(request.POST, instance=r)
|
||||
|
||||
if accountform.is_valid() and userform.is_valid():
|
||||
if accountform.is_valid() and userform.is_valid() and privateform.is_valid():
|
||||
# process
|
||||
cd = accountform.cleaned_data
|
||||
ucd = userform.cleaned_data
|
||||
pcd = privateform.cleaned_data
|
||||
|
||||
first_name = ucd['first_name']
|
||||
last_name = ucd['last_name']
|
||||
@@ -609,6 +637,7 @@ def rower_edit_view(request, rowerid=0, userid=0, message=""):
|
||||
resetbounce = True
|
||||
|
||||
emailalternatives = cd['emailalternatives']
|
||||
imports_are_private = pcd['imports_are_private']
|
||||
|
||||
u.save()
|
||||
r.defaulttimezone = defaulttimezone
|
||||
@@ -620,6 +649,7 @@ def rower_edit_view(request, rowerid=0, userid=0, message=""):
|
||||
r.defaultlandingpage = defaultlandingpage
|
||||
r.showfavoritechartnotes = showfavoritechartnotes
|
||||
r.share_course_results = share_course_results
|
||||
r.imports_are_private = imports_are_private
|
||||
r.sex = sex
|
||||
r.birthdate = birthdate
|
||||
r.autojoin = autojoin
|
||||
@@ -634,11 +664,13 @@ def rower_edit_view(request, rowerid=0, userid=0, message=""):
|
||||
|
||||
accountform = AccountRowerForm(instance=r)
|
||||
userform = UserForm(instance=u)
|
||||
privateform = RowerPrivateImportForm(instance=r)
|
||||
successmessage = 'Account Information changed'
|
||||
messages.info(request, successmessage)
|
||||
else:
|
||||
accountform = AccountRowerForm(instance=r)
|
||||
userform = UserForm(instance=r.user)
|
||||
privateform = RowerPrivateImportForm(instance=r)
|
||||
|
||||
grants = AccessToken.objects.filter(user=request.user)
|
||||
try:
|
||||
@@ -654,6 +686,7 @@ def rower_edit_view(request, rowerid=0, userid=0, message=""):
|
||||
'grants': grants,
|
||||
'userform': userform,
|
||||
'accountform': accountform,
|
||||
'privateform': privateform,
|
||||
'rower': r,
|
||||
'apikey': apikey.key,
|
||||
})
|
||||
|
||||
@@ -687,7 +687,7 @@ def addmanual_view(request, raceid=0):
|
||||
empowerside = form.cleaned_data.get('empowerside','port')
|
||||
|
||||
if private: # pragma: no cover
|
||||
privacy = 'private'
|
||||
privacy = 'hidden'
|
||||
else:
|
||||
privacy = 'visible'
|
||||
|
||||
@@ -2204,25 +2204,25 @@ def workouts_view(request, message='', successmessage='',
|
||||
team=theteam,
|
||||
startdatetime__gte=startdate,
|
||||
startdatetime__lte=enddate,
|
||||
privacy='visible').order_by("-date", "-starttime")
|
||||
privacy='visible').order_by("-date", "-starttime").exclude(workoutsource='strava')
|
||||
g_workouts = Workout.objects.filter(
|
||||
team=theteam,
|
||||
startdatetime__gte=activity_startdate,
|
||||
startdatetime__lte=activity_enddate,
|
||||
duplicate=False,
|
||||
privacy='visible').order_by("-date", "-starttime")
|
||||
privacy='visible').order_by("-date", "-starttime").exclude(workoutsource='strava')
|
||||
elif theteam.viewing == 'coachonly': # pragma: no cover
|
||||
workouts = Workout.objects.filter(
|
||||
team=theteam, user=r,
|
||||
startdatetime__gte=startdate,
|
||||
startdatetime__lte=enddate,
|
||||
privacy='visible').order_by("-startdatetime")
|
||||
privacy='visible').order_by("-startdatetime").exclude(workoutsource='strava')
|
||||
g_workouts = Workout.objects.filter(
|
||||
team=theteam, user=r,
|
||||
startdatetime__gte=activity_startdate,
|
||||
startdatetime__lte=activity_enddate,
|
||||
duplicate=False,
|
||||
privacy='visible').order_by("-startdatetime")
|
||||
privacy='visible').order_by("-startdatetime").exclude(workoutsource='strava')
|
||||
|
||||
elif request.user != r.user:
|
||||
theteam = None
|
||||
@@ -2230,13 +2230,13 @@ def workouts_view(request, message='', successmessage='',
|
||||
user=r,
|
||||
startdatetime__gte=startdate,
|
||||
startdatetime__lte=enddate,
|
||||
privacy='visible').order_by("-date", "-starttime")
|
||||
privacy='visible').order_by("-date", "-starttime").exclude(workoutsource='strava')
|
||||
g_workouts = Workout.objects.filter(
|
||||
user=r,
|
||||
startdatetime__gte=activity_startdate,
|
||||
startdatetime__lte=activity_enddate,
|
||||
duplicate=False,
|
||||
privacy='visible').order_by("-startdatetime")
|
||||
privacy='visible').order_by("-startdatetime").exclude(workoutsource='strava')
|
||||
else:
|
||||
theteam = None
|
||||
workouts = Workout.objects.filter(
|
||||
@@ -2252,7 +2252,7 @@ def workouts_view(request, message='', successmessage='',
|
||||
if g_workouts.count() == 0:
|
||||
g_workouts = Workout.objects.filter(
|
||||
user=r,
|
||||
startdatetime__gte=timezone.now()-timedelta(days=15)).order_by("-startdatetime")
|
||||
startdatetime__gte=timezone.now()-timedelta(days=15)).order_by("-startdatetime").exclude(workoutsource='strava')
|
||||
g_enddate = timezone.now()
|
||||
g_startdate = (timezone.now()-timedelta(days=15))
|
||||
|
||||
@@ -2266,7 +2266,8 @@ def workouts_view(request, message='', successmessage='',
|
||||
reduce(operator.and_,
|
||||
(Q(name__icontains=q) for q in query_list)) |
|
||||
reduce(operator.and_,
|
||||
(Q(notes__icontains=q) for q in query_list))
|
||||
(Q(notes__icontains=q) for q in query_list)),
|
||||
exclude_strava=False,
|
||||
)
|
||||
searchform = SearchForm(initial={'q': query})
|
||||
else:
|
||||
@@ -4269,7 +4270,7 @@ def workout_flexchart_stacked_view(request, *args, **kwargs):
|
||||
def workout_unsubscribe_view(request, id=0):
|
||||
w = get_workout(id)
|
||||
|
||||
if w.privacy == 'private' and w.user.user != request.user: # pragma: no cover
|
||||
if w.privacy == 'hidden' and w.user.user != request.user: # pragma: no cover
|
||||
return HttpResponseForbidden("Permission error")
|
||||
|
||||
comments = WorkoutComment.objects.filter(workout=w,
|
||||
@@ -4299,7 +4300,7 @@ def workout_unsubscribe_view(request, id=0):
|
||||
def workout_comment_view(request, id=0):
|
||||
w = get_workout(id)
|
||||
|
||||
if w.privacy == 'private' and w.user.user != request.user: # pragma: no cover
|
||||
if w.privacy == 'hidden' and w.user.user != request.user: # pragma: no cover
|
||||
return HttpResponseForbidden("Permission error")
|
||||
|
||||
comments = WorkoutComment.objects.filter(workout=w).order_by("created")
|
||||
@@ -4489,7 +4490,7 @@ def workout_edit_view(request, id=0, message="", successmessage=""):
|
||||
|
||||
|
||||
if private:
|
||||
privacy = 'private'
|
||||
privacy = 'hidden'
|
||||
else: # pragma: no cover
|
||||
privacy = 'visible'
|
||||
|
||||
@@ -4699,6 +4700,7 @@ def workout_map_view(request, id=0):
|
||||
u = w.user.user
|
||||
r = getrower(u)
|
||||
rowdata = rdata(csvfile=f1)
|
||||
|
||||
hascoordinates = 1
|
||||
if rowdata != 0:
|
||||
try:
|
||||
@@ -4933,7 +4935,7 @@ def workout_upload_api(request):
|
||||
|
||||
# only allow local host
|
||||
hostt = request.get_host().split(':')
|
||||
if hostt[0] not in ['localhost', '127.0.0.1', 'dev.rowsandall.com', 'rowsandall.com']:
|
||||
if hostt[0] not in ['localhost', '127.0.0.1', 'dev.rowsandall.com', 'rowsandall.com','testserver']:
|
||||
message = {'status': 'false',
|
||||
'message': 'permission denied for host '+hostt[0]}
|
||||
return JSONResponse(status=403, data=message)
|
||||
@@ -4986,6 +4988,7 @@ def workout_upload_api(request):
|
||||
boatname = post_data.get('boatName','')
|
||||
portStarboard = post_data.get('portStarboard', 1)
|
||||
empowerside = 'port'
|
||||
stravaid = post_data.get('stravaid','')
|
||||
if portStarboard == 1:
|
||||
empowerside = 'starboard'
|
||||
|
||||
@@ -5194,6 +5197,8 @@ def workout_upload_view(request,
|
||||
is_ajax = False
|
||||
|
||||
r = getrower(request.user)
|
||||
if r.imports_are_private:
|
||||
uploadoptions['makeprivate'] = True
|
||||
if r.rowerplan == 'freecoach': # pragma: no cover
|
||||
url = reverse('team_workout_upload_view')
|
||||
return HttpResponseRedirect(url)
|
||||
@@ -5247,6 +5252,7 @@ def workout_upload_view(request,
|
||||
upload_to_strava = uploadoptions.get('upload_to_Strava', False)
|
||||
upload_to_st = uploadoptions.get('upload_to_SportTracks', False)
|
||||
upload_to_tp = uploadoptions.get('upload_to_TrainingPeaks', False)
|
||||
upload_to_intervals = uploadoptions.get('upload_to_Intervals', False)
|
||||
|
||||
response = {}
|
||||
if request.method == 'POST':
|
||||
@@ -5296,6 +5302,7 @@ def workout_upload_view(request,
|
||||
upload_to_strava = optionsform.cleaned_data['upload_to_Strava']
|
||||
upload_to_st = optionsform.cleaned_data['upload_to_SportTracks']
|
||||
upload_to_tp = optionsform.cleaned_data['upload_to_TrainingPeaks']
|
||||
upload_to_intervals = optionsform.cleaned_data['upload_to_Intervals']
|
||||
makeprivate = optionsform.cleaned_data['makeprivate']
|
||||
landingpage = optionsform.cleaned_data['landingpage']
|
||||
raceid = optionsform.cleaned_data['raceid']
|
||||
@@ -5313,6 +5320,7 @@ def workout_upload_view(request,
|
||||
'upload_to_Strava': upload_to_strava,
|
||||
'upload_to_SportTracks': upload_to_st,
|
||||
'upload_to_TrainingPeaks': upload_to_tp,
|
||||
'upload_to_Intervals': upload_to_intervals,
|
||||
'landingpage': landingpage,
|
||||
'boattype': boattype,
|
||||
'rpe': rpe,
|
||||
@@ -5447,6 +5455,14 @@ def workout_upload_view(request,
|
||||
message = "Please connect to TrainingPeaks first"
|
||||
messages.error(request, message)
|
||||
|
||||
if (upload_to_intervals):
|
||||
intervals_integration = IntervalsIntegration(request.user)
|
||||
try:
|
||||
id = intervals_integration.workout_export(w)
|
||||
except NoTokenError:
|
||||
message = "Please connect to Intervals.icu first"
|
||||
messages.error(request, message)
|
||||
|
||||
if int(registrationid) < 0: # pragma: no cover
|
||||
race = VirtualRace.objects.get(id=-int(registrationid))
|
||||
if race.sessiontype == 'race':
|
||||
@@ -5598,17 +5614,6 @@ def workout_upload_view(request,
|
||||
return response
|
||||
else:
|
||||
if not is_ajax:
|
||||
if r.c2_auto_export and ispromember(r.user): # pragma: no cover
|
||||
uploadoptions['upload_to_C2'] = True
|
||||
|
||||
if r.strava_auto_export and ispromember(r.user): # pragma: no cover
|
||||
uploadoptions['upload_to_Strava'] = True
|
||||
|
||||
if r.sporttracks_auto_export and ispromember(r.user): # pragma: no cover
|
||||
uploadoptions['upload_to_SportTracks'] = True
|
||||
|
||||
if r.trainingpeaks_auto_export and ispromember(r.user): # pragma: no cover
|
||||
uploadoptions['upload_to_TrainingPeaks'] = True
|
||||
|
||||
form = DocumentsForm(initial=docformoptions)
|
||||
optionsform = UploadOptionsForm(initial=uploadoptions,
|
||||
|
||||
@@ -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']
|
||||
@@ -463,7 +478,8 @@ OAUTH2_PROVIDER = {
|
||||
"https",
|
||||
"rowingcoachexport",
|
||||
"com.performancephones.crewnerd",
|
||||
"pocketcox"],
|
||||
"pocketcox",
|
||||
"app"],
|
||||
'ACCESS_TOKEN_MODEL': 'oauth2_provider.AccessToken',
|
||||
'APPLICATION_MODEL': 'oauth2_provider.Application',
|
||||
'REFRESH_TOKEN_MODEL': 'oauth2_provider.RefreshToken',
|
||||
|
||||
@@ -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),
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 3.7 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
Reference in New Issue
Block a user