Merge branch 'develop' into feature/icu_sessions
This commit is contained in:
@@ -217,6 +217,9 @@ def workout_goldmedalstandard(workout, reset=False):
|
|||||||
|
|
||||||
def check_marker(workout):
|
def check_marker(workout):
|
||||||
r = workout.user
|
r = workout.user
|
||||||
|
if workout.workoutsource == 'strava':
|
||||||
|
return None
|
||||||
|
|
||||||
gmstandard, gmseconds = workout_goldmedalstandard(workout)
|
gmstandard, gmseconds = workout_goldmedalstandard(workout)
|
||||||
if gmseconds < 60:
|
if gmseconds < 60:
|
||||||
return None
|
return None
|
||||||
@@ -369,8 +372,20 @@ def workout_summary_to_df(
|
|||||||
return df
|
return df
|
||||||
|
|
||||||
|
|
||||||
def resample(id, r, parent, overwrite='copy'):
|
def resample(id, r, parent, overwrite=False):
|
||||||
data, row = getrowdata_db(id=id)
|
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 = []
|
messages = []
|
||||||
|
|
||||||
# resample
|
# resample
|
||||||
@@ -393,7 +408,7 @@ def resample(id, r, parent, overwrite='copy'):
|
|||||||
data['pace'] = data['pace'] / 1000.
|
data['pace'] = data['pace'] / 1000.
|
||||||
data['time'] = data['time'] / 1000.
|
data['time'] = data['time'] / 1000.
|
||||||
|
|
||||||
if overwrite == 'overwrite':
|
if overwrite == True:
|
||||||
# remove CP data
|
# remove CP data
|
||||||
try:
|
try:
|
||||||
cpfile = 'media/cpdata_{id}.parquet.gz'.format(id=parent.id)
|
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
|
if makeprivate: # pragma: no cover
|
||||||
privacy = 'hidden'
|
privacy = 'hidden'
|
||||||
else:
|
elif workoutsource != 'strava':
|
||||||
privacy = 'visible'
|
privacy = 'visible'
|
||||||
|
else:
|
||||||
|
privacy = 'hidden'
|
||||||
|
|
||||||
|
|
||||||
# checking for inf values
|
# checking for inf values
|
||||||
|
|
||||||
|
|||||||
@@ -67,13 +67,12 @@ class FlexibleDecimalField(forms.DecimalField):
|
|||||||
|
|
||||||
|
|
||||||
class ResampleForm(forms.Form):
|
class ResampleForm(forms.Form):
|
||||||
resamplechoices = (
|
# add resamplechoice field, the result is a True or False boolean, labels are "overwrite" and "create copy"
|
||||||
('overwrite', 'Overwrite Workout'),
|
|
||||||
('copy', 'Create a Duplicate Workout')
|
|
||||||
)
|
|
||||||
|
|
||||||
resamplechoice = forms.ChoiceField(
|
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):
|
class TrainingZonesForm(forms.Form):
|
||||||
@@ -582,6 +581,11 @@ class UploadOptionsForm(forms.Form):
|
|||||||
races = VirtualRace.objects.filter(
|
races = VirtualRace.objects.filter(
|
||||||
registration_closure__gt=timezone.now())
|
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(
|
registrations = IndoorVirtualRaceResult.objects.filter(
|
||||||
race__in=races,
|
race__in=races,
|
||||||
userid=r.id)
|
userid=r.id)
|
||||||
@@ -665,6 +669,10 @@ class TeamUploadOptionsForm(forms.Form):
|
|||||||
upload_to_TrainingPeaks = forms.BooleanField(initial=False,
|
upload_to_TrainingPeaks = forms.BooleanField(initial=False,
|
||||||
required=False,
|
required=False,
|
||||||
label='Export to TrainingPeaks')
|
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)')
|
# do_physics = forms.BooleanField(initial=False,required=False,label='Power Estimate (OTW)')
|
||||||
makeprivate = forms.BooleanField(initial=False, required=False,
|
makeprivate = forms.BooleanField(initial=False, required=False,
|
||||||
label='Make Workout Private')
|
label='Make Workout Private')
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import os
|
|||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
import rowers.dataprep as dataprep
|
||||||
|
|
||||||
from rowsandall_app.settings import (
|
from rowsandall_app.settings import (
|
||||||
INTERVALS_CLIENT_ID, INTERVALS_REDIRECT_URI, INTERVALS_CLIENT_SECRET, SITE_URL
|
INTERVALS_CLIENT_ID, INTERVALS_REDIRECT_URI, INTERVALS_CLIENT_SECRET, SITE_URL
|
||||||
@@ -101,7 +102,16 @@ class IntervalsIntegration(SyncIntegration):
|
|||||||
|
|
||||||
def createworkoutdata(self, w, *args, **kwargs) -> str:
|
def createworkoutdata(self, w, *args, **kwargs) -> str:
|
||||||
dozip = kwargs.get('dozip', True)
|
dozip = kwargs.get('dozip', True)
|
||||||
filename = w.csvfilename
|
# 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:
|
try:
|
||||||
row = rowingdata(csvfile=filename)
|
row = rowingdata(csvfile=filename)
|
||||||
except IOError: # pragma: no cover
|
except IOError: # pragma: no cover
|
||||||
@@ -128,6 +138,8 @@ class IntervalsIntegration(SyncIntegration):
|
|||||||
except TypeError:
|
except TypeError:
|
||||||
newnotes = 'from'+w.workoutsource+' via rowsandall.com'
|
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])
|
row.exporttotcx(tcxfilename, notes=newnotes, sport=mytypes.intervalsmapping[w.workouttype])
|
||||||
if dozip:
|
if dozip:
|
||||||
gzfilename = tcxfilename + '.gz'
|
gzfilename = tcxfilename + '.gz'
|
||||||
@@ -230,25 +242,29 @@ class IntervalsIntegration(SyncIntegration):
|
|||||||
workouts = []
|
workouts = []
|
||||||
|
|
||||||
for item in data:
|
for item in data:
|
||||||
i = item['id']
|
try:
|
||||||
r = item['type']
|
i = item['id']
|
||||||
d = item['distance']
|
r = item['type']
|
||||||
ttot = seconds_to_duration(item['moving_time'])
|
d = item['distance']
|
||||||
s = item['start_date']
|
ttot = seconds_to_duration(item['moving_time'])
|
||||||
s2 = ''
|
s = item['start_date']
|
||||||
c = item['name']
|
s2 = ''
|
||||||
if i in known_interval_ids:
|
c = item['name']
|
||||||
nnn = ''
|
if i in known_interval_ids:
|
||||||
else:
|
nnn = ''
|
||||||
nnn = 'NEW'
|
else:
|
||||||
|
nnn = 'NEW'
|
||||||
|
|
||||||
keys = ['id','distance','duration','starttime',
|
keys = ['id','distance','duration','starttime',
|
||||||
'rowtype','source','name','new']
|
'rowtype','source','name','new']
|
||||||
|
|
||||||
values = [i, d, ttot, s, r, s2, c, nnn]
|
values = [i, d, ttot, s, r, s2, c, nnn]
|
||||||
|
|
||||||
|
ress = dict(zip(keys, values))
|
||||||
|
workouts.append(ress)
|
||||||
|
except KeyError:
|
||||||
|
dologging('intervals.icu.log', item)
|
||||||
|
|
||||||
ress = dict(zip(keys, values))
|
|
||||||
workouts.append(ress)
|
|
||||||
|
|
||||||
return workouts
|
return workouts
|
||||||
|
|
||||||
|
|||||||
@@ -214,7 +214,7 @@ class StravaIntegration(SyncIntegration):
|
|||||||
def get_workout(self, id, *args, **kwargs) -> int:
|
def get_workout(self, id, *args, **kwargs) -> int:
|
||||||
try:
|
try:
|
||||||
_ = self.open()
|
_ = self.open()
|
||||||
except NoTokenError("Strava error"):
|
except NoTokenError:
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
record = create_or_update_syncrecord(self.rower, None, stravaid=id)
|
record = create_or_update_syncrecord(self.rower, None, stravaid=id)
|
||||||
|
|||||||
@@ -550,7 +550,7 @@ def goldmedalscorechart(user, startdate=None, enddate=None):
|
|||||||
workouts = Workout.objects.filter(user=user.rower, date__gte=startdate,
|
workouts = Workout.objects.filter(user=user.rower, date__gte=startdate,
|
||||||
date__lte=enddate,
|
date__lte=enddate,
|
||||||
workouttype__in=mytypes.rowtypes,
|
workouttype__in=mytypes.rowtypes,
|
||||||
duplicate=False).order_by('date')
|
duplicate=False).order_by('date').exclude(workoutsource='strava')
|
||||||
|
|
||||||
markerworkouts = workouts.filter(rankingpiece=True)
|
markerworkouts = workouts.filter(rankingpiece=True)
|
||||||
outids = [w.id for w in markerworkouts]
|
outids = [w.id for w in markerworkouts]
|
||||||
|
|||||||
38
rowers/management/commands/setstravaprivate.py
Normal file
38
rowers/management/commands/setstravaprivate.py
Normal file
@@ -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.'))
|
||||||
@@ -1174,6 +1174,7 @@ class Rower(models.Model):
|
|||||||
c2_auto_import = models.BooleanField(default=False)
|
c2_auto_import = models.BooleanField(default=False)
|
||||||
intervals_auto_export = models.BooleanField(default=False)
|
intervals_auto_export = models.BooleanField(default=False)
|
||||||
intervals_auto_import = 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(
|
sporttrackstoken = models.CharField(
|
||||||
default='', max_length=200, blank=True, null=True)
|
default='', max_length=200, blank=True, null=True)
|
||||||
sporttrackstokenexpirydate = models.DateTimeField(blank=True, null=True)
|
sporttrackstokenexpirydate = models.DateTimeField(blank=True, null=True)
|
||||||
@@ -1240,7 +1241,7 @@ class Rower(models.Model):
|
|||||||
|
|
||||||
strava_auto_export = models.BooleanField(default=False)
|
strava_auto_export = models.BooleanField(default=False)
|
||||||
strava_auto_import = models.BooleanField(default=False)
|
strava_auto_import = models.BooleanField(default=False)
|
||||||
strava_auto_delete = models.BooleanField(default=False)
|
strava_auto_delete = models.BooleanField(default=True)
|
||||||
|
|
||||||
intervals_token = models.CharField(
|
intervals_token = models.CharField(
|
||||||
default='', max_length=200, blank=True, null=True)
|
default='', max_length=200, blank=True, null=True)
|
||||||
@@ -1440,9 +1441,26 @@ parchoicesy1 = list(sorted(favchartlabelsy1.items(), key=lambda x: x[1]))
|
|||||||
parchoicesy2 = list(sorted(favchartlabelsy2.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]))
|
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
|
# Saving a chart as a favorite chart
|
||||||
|
|
||||||
|
|
||||||
class FavoriteChart(models.Model):
|
class FavoriteChart(models.Model):
|
||||||
workouttypechoices = [
|
workouttypechoices = [
|
||||||
('ote', 'Erg/SkiErg'),
|
('ote', 'Erg/SkiErg'),
|
||||||
@@ -3740,6 +3758,9 @@ class Workout(models.Model):
|
|||||||
default=False, verbose_name='Duplicate Workout')
|
default=False, verbose_name='Duplicate Workout')
|
||||||
impeller = models.BooleanField(default=False, verbose_name='Impeller')
|
impeller = models.BooleanField(default=False, verbose_name='Impeller')
|
||||||
|
|
||||||
|
# attach the WorkoutManager
|
||||||
|
#objects = WorkoutManager()
|
||||||
|
|
||||||
def url(self):
|
def url(self):
|
||||||
str = '/rowers/workout/{id}/'.format(
|
str = '/rowers/workout/{id}/'.format(
|
||||||
id=encoder.encode_hex(self.id)
|
id=encoder.encode_hex(self.id)
|
||||||
@@ -4578,7 +4599,78 @@ class RowerExportForm(ModelForm):
|
|||||||
'rp3_auto_import',
|
'rp3_auto_import',
|
||||||
'intervals_auto_import',
|
'intervals_auto_import',
|
||||||
'intervals_auto_export',
|
'intervals_auto_export',
|
||||||
|
'intervals_resample_to_1s'
|
||||||
]
|
]
|
||||||
|
|
||||||
|
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
|
# Simple form to set rower's Functional Threshold Power
|
||||||
class SimpleRowerPowerForm(ModelForm):
|
class SimpleRowerPowerForm(ModelForm):
|
||||||
|
|||||||
@@ -1605,13 +1605,28 @@ def add_workout_fastestrace(ws, race, r, recordid=0, doregister=False):
|
|||||||
enddatetime
|
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 = [w.id for w in ws]
|
||||||
ids = list(set(ids))
|
ids = list(set(ids))
|
||||||
|
|
||||||
|
|
||||||
if len(ids) > 1 and race.sessiontype in ['test', 'coursetest', 'race', 'indoorrace', 'fastest_time', 'fastest_distance']: # pragma: no cover
|
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')
|
errors.append('For tests, you can only attach one workout')
|
||||||
return result, comments, errors, 0
|
return result, comments, errors, 0
|
||||||
|
|
||||||
|
|
||||||
if r.birthdate:
|
if r.birthdate:
|
||||||
age = calculate_age(r.birthdate)
|
age = calculate_age(r.birthdate)
|
||||||
else: # pragma: no cover
|
else: # pragma: no cover
|
||||||
@@ -1767,6 +1782,19 @@ def add_workout_indoorrace(ws, race, r, recordid=0, doregister=False):
|
|||||||
enddatetime
|
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
|
# check if all sessions have same date
|
||||||
dates = [w.date for w in ws]
|
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
|
if (not all(d == dates[0] for d in dates)) and race.sessiontype not in ['challenge', 'cycletarget']: # pragma: no cover
|
||||||
@@ -1914,6 +1942,19 @@ def add_workout_race(ws, race, r, splitsecond=0, recordid=0, doregister=False):
|
|||||||
enddatetime
|
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
|
# check if all sessions have same date
|
||||||
dates = [w.date for w in ws]
|
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
|
if (not all(d == dates[0] for d in dates)) and race.sessiontype not in ['challenge', 'cycletarget']: # pragma: no cover
|
||||||
|
|||||||
@@ -451,6 +451,11 @@ def is_workout_user(user, workout):
|
|||||||
except AttributeError: # pragma: no cover
|
except AttributeError: # pragma: no cover
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
if workout.privacy == 'hidden':
|
||||||
|
return user == workout.user.user
|
||||||
|
if workout.workoutsource == 'strava':
|
||||||
|
return user == workout.user.user
|
||||||
|
|
||||||
if workout.user == r:
|
if workout.user == r:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -458,6 +463,9 @@ def is_workout_user(user, workout):
|
|||||||
|
|
||||||
# check if user is in same team as owner of 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
|
@rules.predicate
|
||||||
def is_workout_team(user, workout):
|
def is_workout_team(user, workout):
|
||||||
@@ -469,6 +477,11 @@ def is_workout_team(user, workout):
|
|||||||
except AttributeError: # pragma: no cover
|
except AttributeError: # pragma: no cover
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
if workout.privacy == 'hidden':
|
||||||
|
return user == workout.user.user
|
||||||
|
if workout.workoutsource == 'strava':
|
||||||
|
return user == workout.user.user
|
||||||
|
|
||||||
if workout.user == r:
|
if workout.user == r:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -479,7 +492,9 @@ def is_workout_team(user, workout):
|
|||||||
|
|
||||||
@rules.predicate
|
@rules.predicate
|
||||||
def can_view_workout(user, workout):
|
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
|
return True
|
||||||
if user.is_anonymous: # pragma: no cover
|
if user.is_anonymous: # pragma: no cover
|
||||||
return False
|
return False
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<ul class="main-content">
|
<ul class="main-content">
|
||||||
<li class="grid_4">
|
<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>
|
for developers of rowing data apps and hardware.</p>
|
||||||
|
|
||||||
<p>I presume you have an app (smartphone app, dedicated hardware, web site)
|
<p>I presume you have an app (smartphone app, dedicated hardware, web site)
|
||||||
@@ -61,11 +61,11 @@
|
|||||||
</ul></p>
|
</ul></p>
|
||||||
<h2>Using the REST API</h2>
|
<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
|
receive stroke
|
||||||
data from the site directly.</p>
|
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
|
suggestions (provided they don't break existing apps). Please send
|
||||||
email to <a href="mailto:info@rowsandall.com">info@rowsandall.com</a>
|
email to <a href="mailto:info@rowsandall.com">info@rowsandall.com</a>
|
||||||
with questions and/or suggestions. We
|
with questions and/or suggestions. We
|
||||||
@@ -84,7 +84,6 @@
|
|||||||
|
|
||||||
<li>Disadvantages
|
<li>Disadvantages
|
||||||
<p><ul class="contentli">
|
<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
|
<li>You need to register your app with us. We can revoke your
|
||||||
permissions if you misuse them.</li>
|
permissions if you misuse them.</li>
|
||||||
<li>The user user must grant permissions to your app.</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.
|
<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>
|
need to register an app, please send email to info@rowsandall.com</p>
|
||||||
|
|
||||||
<h3>Authentication</h3>
|
<h3>Authentication</h3>
|
||||||
@@ -728,11 +727,11 @@
|
|||||||
<li><b>peakdriveforce</b>: Peak handle force (lbs)</li>
|
<li><b>peakdriveforce</b>: Peak handle force (lbs)</li>
|
||||||
<li><b>lapidx</b>: Lap identifier</li>
|
<li><b>lapidx</b>: Lap identifier</li>
|
||||||
<li><b>hr</b>: Heart rate (beats per minute)</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>wash</b>: Wash as defined for your smart power measuring oarlock (degrees)</li>
|
||||||
<li><b>catch</b>: Catch angle per Empower oarlock (degrees)</li>
|
<li><b>catch</b>: Catch angle for your smart power measuring oarlock (degrees)</li>
|
||||||
<li><b>finish</b>: Finish angle per Empower oarlock (degrees)</li>
|
<li><b>finish</b>: Finish angle for your smart power measuring oarlock (degrees)</li>
|
||||||
<li><b>peakforceangle</b>: Peak Force Angle per Empower 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 per Empower oarlock (degrees)</li>
|
<li><b>slip</b>: Slip as defined for your smart power measuring oarlock (degrees)</li>
|
||||||
|
|
||||||
</ul>
|
</ul>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -7,8 +7,10 @@
|
|||||||
{% block main %}
|
{% block main %}
|
||||||
<h1>Import and Export Settings for {{ rower.user.first_name }} {{ rower.user.last_name }}</h1>
|
<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">
|
<ul class="main-content">
|
||||||
<li class="grid_2">
|
<li class="grid_4">
|
||||||
<p>You are currently connected to:
|
<p>You are currently connected to:
|
||||||
{% if rower.c2token is not None and rower.c2token != '' %}
|
{% if rower.c2token is not None and rower.c2token != '' %}
|
||||||
Concept2 Logbook,
|
Concept2 Logbook,
|
||||||
@@ -41,44 +43,141 @@
|
|||||||
Intervals.icu
|
Intervals.icu
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</p>
|
</p>
|
||||||
|
<p>
|
||||||
{% if form.errors %}
|
Click on the icons to establish the connection or to renew the authorization.
|
||||||
<p style="color: red;">
|
</p>
|
||||||
Please correct the error{{ form.errors|pluralize }} below.
|
</li>
|
||||||
</p>
|
<li class="grid_4">
|
||||||
{% endif %}
|
<h2>API Key</h2>
|
||||||
<p>
|
<p>{{ apikey }}</p>
|
||||||
<form enctype="multipart/form-data" action="" method="post">
|
<p>
|
||||||
<table>
|
<a href="/rowers/me/regenerateapikey/">Regenerate</a>
|
||||||
{{ form.as_table }}
|
</p>
|
||||||
</table>
|
<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>
|
||||||
{% csrf_token %}
|
</li>
|
||||||
<input type="submit" value="Save">
|
|
||||||
</form>
|
{% if form.errors %}
|
||||||
</p>
|
<li class="rounder">
|
||||||
{% if rower.garmintoken and rower.garmintoken != '' %}
|
<p style="color: red;">
|
||||||
<p>
|
Please correct the error{{ form.errors|pluralize }} below.
|
||||||
<em>You are connected to Garmin.</em> Switching off Garmin Connect sync is on the
|
</p>
|
||||||
<a href="https://connect.garmin.com/modern/settings/accountInformation">Account settings</a>
|
</li>
|
||||||
page. Look for the "Rowsandall" app.
|
{% endif %}
|
||||||
</p>
|
<li class="rounder">
|
||||||
{% endif %}
|
<h2>NK</h2>
|
||||||
<p>
|
<table>
|
||||||
Garmin Connnect has no manual sync, so connecting your account to your Garmin account will
|
{{ forms.nk.as_table }}
|
||||||
automatically auto-sync workouts from Garmin to Rowsandall (but not in the other direction). If you
|
<input type="submit" value="Save">
|
||||||
want to export our structured workout sessions to your Garmin device, you have to set the "Garmin Activity"
|
</table>
|
||||||
to a activity type that is supported by your watch. Not all watches support "Custom" activities, so
|
<p><a href="/rowers/me/nkauthorize/"><img src="/static/img/NKLiNKLogbook.png" alt="connect with NK Logbook" width="120"></a></p>
|
||||||
you may have to set your activity to Run or Ride while rowing.
|
</li>
|
||||||
</p>
|
<li class="rounder">
|
||||||
<p>
|
<h2>Concept2</h2>
|
||||||
Strava Auto Import also imports activity changes on Strava to Rowsandall, except when you delete
|
<table>
|
||||||
a workout on Strava. If you want Deletions to propagate to Rowsandall, tick the Strava Auto Delete
|
{{ forms.c2.as_table }}
|
||||||
check box.
|
<input type="submit" value="Save">
|
||||||
</p>
|
</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>
|
||||||
<li class="grid_2">
|
|
||||||
{% if grants %}
|
{% if grants %}
|
||||||
|
<li class="rounder">
|
||||||
<h2>Applications</h2>
|
<h2>Applications</h2>
|
||||||
|
<p>
|
||||||
|
These applications have access to your Rowsandall data.
|
||||||
|
</p>
|
||||||
<table width="100%">
|
<table width="100%">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -99,35 +198,12 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
<li>
|
||||||
{% endif %}
|
{% 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>
|
</ul>
|
||||||
<p>Click on one of the icons below to connect to the service of your
|
</form>
|
||||||
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>
|
|
||||||
<p><a href="/rowers/me/intervalsauthorize"><img src="/static/img/intervals_icu.png"
|
|
||||||
alt="connect with intervals.icu" height="30"></a></p>
|
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|||||||
@@ -22,12 +22,454 @@ from rowers.opaque import encoder
|
|||||||
|
|
||||||
from rest_framework.test import APIRequestFactory, force_authenticate
|
from rest_framework.test import APIRequestFactory, force_authenticate
|
||||||
|
|
||||||
|
UPLOAD_SERVICE_URL = '/rowers/workout/api/upload/'
|
||||||
|
UPLOAD_SERVICE_SECRET = "FoYezZWLSyfAVimumpHEeYsJjsNCerxV"
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
|
||||||
|
# import BeautifulSoup
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
|
||||||
from rowers.ownapistuff import *
|
from rowers.ownapistuff import *
|
||||||
from rowers.views.apiviews import *
|
from rowers.views.apiviews import *
|
||||||
from rowers.models import APIKey
|
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):
|
class OwnApi(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.u = UserFactory()
|
self.u = UserFactory()
|
||||||
@@ -36,9 +478,7 @@ class OwnApi(TestCase):
|
|||||||
birthdate=faker.profile()['birthdate'],
|
birthdate=faker.profile()['birthdate'],
|
||||||
gdproptin=True, ftpset=True,surveydone=True,
|
gdproptin=True, ftpset=True,surveydone=True,
|
||||||
gdproptindate=timezone.now(),
|
gdproptindate=timezone.now(),
|
||||||
rowerplan='coach',subscription_id=1)
|
rowerplan='pro',subscription_id=1)
|
||||||
|
|
||||||
|
|
||||||
self.c = Client()
|
self.c = Client()
|
||||||
self.user_workouts = WorkoutFactory.create_batch(5, user=self.r)
|
self.user_workouts = WorkoutFactory.create_batch(5, user=self.r)
|
||||||
self.factory = RequestFactory()
|
self.factory = RequestFactory()
|
||||||
|
|||||||
@@ -106,10 +106,26 @@ class ChallengesTest(TestCase):
|
|||||||
workouttype = 'water',
|
workouttype = 'water',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
self.wthyro.startdatetime = arrow.get(nu).datetime
|
self.wthyro.startdatetime = arrow.get(nu).datetime
|
||||||
self.wthyro.date = nu.date()
|
self.wthyro.date = nu.date()
|
||||||
self.wthyro.save()
|
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')
|
result = get_random_file(filename='rowers/tests/testdata/thyro.csv')
|
||||||
self.wthyro2 = WorkoutFactory(user=self.r2,
|
self.wthyro2 = WorkoutFactory(user=self.r2,
|
||||||
csvfilename=result['filename'],
|
csvfilename=result['filename'],
|
||||||
@@ -591,6 +607,78 @@ class ChallengesTest(TestCase):
|
|||||||
|
|
||||||
self.assertEqual(response.status_code, 200)
|
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)
|
@patch('django.contrib.gis.geoip2.GeoIP2.city', side_effect=mocked_requests)
|
||||||
def test_virtualevents_view(self, mock_get):
|
def test_virtualevents_view(self, mock_get):
|
||||||
|
|||||||
BIN
rowers/tests/testdata/testdata.tcx.gz
vendored
BIN
rowers/tests/testdata/testdata.tcx.gz
vendored
Binary file not shown.
BIN
rowers/tests/testdata/thyro.csv.gz
vendored
Normal file
BIN
rowers/tests/testdata/thyro.csv.gz
vendored
Normal file
Binary file not shown.
@@ -144,14 +144,20 @@ def do_sync(w, options, quick=False):
|
|||||||
w.uploadedtostrava = options['stravaid']
|
w.uploadedtostrava = options['stravaid']
|
||||||
# upload_to_strava = False
|
# upload_to_strava = False
|
||||||
do_strava_export = False
|
do_strava_export = False
|
||||||
|
w.workoutsource = 'strava'
|
||||||
|
w.privacy = 'hidden'
|
||||||
w.save()
|
w.save()
|
||||||
record = create_or_update_syncrecord(w.user, w, stravaid=options['stravaid'])
|
record = create_or_update_syncrecord(w.user, w, stravaid=options['stravaid'])
|
||||||
|
# strava, we shall not sync to other sites -> return
|
||||||
|
return 1
|
||||||
except KeyError:
|
except KeyError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
do_icu_export = False
|
do_icu_export = False
|
||||||
if w.user.intervals_auto_export is True:
|
if w.user.intervals_auto_export is True:
|
||||||
do_icu_export = True
|
do_icu_export = True
|
||||||
|
if w.workoutsource == 'strava':
|
||||||
|
do_icu_export = False
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
do_icu_export = options['upload_to_Intervals']
|
do_icu_export = options['upload_to_Intervals']
|
||||||
@@ -204,6 +210,8 @@ def do_sync(w, options, quick=False):
|
|||||||
do_c2_export = False
|
do_c2_export = False
|
||||||
if w.user.c2_auto_export is True:
|
if w.user.c2_auto_export is True:
|
||||||
do_c2_export = True
|
do_c2_export = True
|
||||||
|
if w.workoutsource == 'strava':
|
||||||
|
do_c2_export = False
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
do_c2_export = options['upload_to_C2'] or do_c2_export
|
do_c2_export = options['upload_to_C2'] or do_c2_export
|
||||||
@@ -245,23 +253,6 @@ def do_sync(w, options, quick=False):
|
|||||||
dologging('c2_log.log','Error C2')
|
dologging('c2_log.log','Error C2')
|
||||||
pass
|
pass
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
if do_icu_export:
|
if do_icu_export:
|
||||||
intervals_integration = IntervalsIntegration(w.user.user)
|
intervals_integration = IntervalsIntegration(w.user.user)
|
||||||
try:
|
try:
|
||||||
@@ -295,6 +286,8 @@ def do_sync(w, options, quick=False):
|
|||||||
try: # pragma: no cover
|
try: # pragma: no cover
|
||||||
upload_to_st = options['upload_to_SportTracks'] or do_st_export
|
upload_to_st = options['upload_to_SportTracks'] or do_st_export
|
||||||
do_st_export = upload_to_st
|
do_st_export = upload_to_st
|
||||||
|
if w.workoutsource == 'strava':
|
||||||
|
do_st_export = False
|
||||||
except KeyError:
|
except KeyError:
|
||||||
upload_to_st = False
|
upload_to_st = False
|
||||||
|
|
||||||
@@ -317,6 +310,8 @@ def do_sync(w, options, quick=False):
|
|||||||
do_tp_export = w.user.trainingpeaks_auto_export
|
do_tp_export = w.user.trainingpeaks_auto_export
|
||||||
try:
|
try:
|
||||||
upload_to_tp = options['upload_to_TrainingPeaks'] or do_tp_export
|
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
|
do_tp_export = upload_to_tp
|
||||||
except KeyError:
|
except KeyError:
|
||||||
upload_to_st = False
|
upload_to_st = False
|
||||||
@@ -334,4 +329,23 @@ def do_sync(w, options, quick=False):
|
|||||||
dologging('tp_export.log','No Token Error')
|
dologging('tp_export.log','No Token Error')
|
||||||
return 0
|
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
|
return 1
|
||||||
|
|||||||
@@ -664,6 +664,7 @@ urlpatterns = [
|
|||||||
re_path(r'^me/messages/$', views.user_messages, name='user_messages'),
|
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/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+)/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/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'^me/delete/$', views.remove_user, name='remove_user'),
|
||||||
re_path(r'^survey/$', views.survey, name='survey'),
|
re_path(r'^survey/$', views.survey, name='survey'),
|
||||||
|
|||||||
@@ -48,6 +48,9 @@ def analysis_new(request,
|
|||||||
firstworkout = get_workout(id)
|
firstworkout = get_workout(id)
|
||||||
if not is_workout_team(request.user, firstworkout): # pragma: no cover
|
if not is_workout_team(request.user, firstworkout): # pragma: no cover
|
||||||
raise PermissionDenied("You are not allowed to use this workout")
|
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))
|
firstworkoutquery = Workout.objects.filter(id=encoder.decode_hex(id))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -199,14 +202,14 @@ def analysis_new(request,
|
|||||||
startdatetime__lte=enddate,
|
startdatetime__lte=enddate,
|
||||||
workouttype__in=modalities,
|
workouttype__in=modalities,
|
||||||
rankingpiece__in=rankingtypes,
|
rankingpiece__in=rankingtypes,
|
||||||
)
|
)#.exclude(workoutsource='strava')
|
||||||
elif theteam is not None and theteam.viewing == 'coachonly': # pragma: no cover
|
elif theteam is not None and theteam.viewing == 'coachonly': # pragma: no cover
|
||||||
workouts = Workout.objects.filter(team=theteam, user=r,
|
workouts = Workout.objects.filter(team=theteam, user=r,
|
||||||
startdatetime__gte=startdate,
|
startdatetime__gte=startdate,
|
||||||
startdatetime__lte=enddate,
|
startdatetime__lte=enddate,
|
||||||
workouttype__in=modalities,
|
workouttype__in=modalities,
|
||||||
rankingpiece__in=rankingtypes,
|
rankingpiece__in=rankingtypes,
|
||||||
)
|
)#.exclude(workoutsource='strava')
|
||||||
elif thesession is not None:
|
elif thesession is not None:
|
||||||
workouts = get_workouts_session(r, thesession)
|
workouts = get_workouts_session(r, thesession)
|
||||||
else:
|
else:
|
||||||
@@ -218,6 +221,7 @@ def analysis_new(request,
|
|||||||
)
|
)
|
||||||
if firstworkout:
|
if firstworkout:
|
||||||
workouts = firstworkoutquery | workouts
|
workouts = firstworkoutquery | workouts
|
||||||
|
|
||||||
workouts = workouts.order_by(
|
workouts = workouts.order_by(
|
||||||
"-date", "-starttime"
|
"-date", "-starttime"
|
||||||
).exclude(boattype__in=negtypes)
|
).exclude(boattype__in=negtypes)
|
||||||
@@ -253,7 +257,7 @@ def analysis_new(request,
|
|||||||
else:
|
else:
|
||||||
selectedworkouts = Workout.objects.filter(id__in=ids)
|
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={
|
optionsform = AnalysisOptionsForm(initial={
|
||||||
'modality': modality,
|
'modality': modality,
|
||||||
@@ -363,6 +367,10 @@ def trendflexdata(workouts, options, userid=0):
|
|||||||
|
|
||||||
savedata = options.get('savedata',False)
|
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, fielddict = dataprep.getstatsfields()
|
||||||
fieldlist = [xparam, yparam, groupby,
|
fieldlist = [xparam, yparam, groupby,
|
||||||
@@ -566,6 +574,11 @@ def flexalldata(workouts, options):
|
|||||||
trendline = options['trendline']
|
trendline = options['trendline']
|
||||||
promember = True
|
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
|
workstrokesonly = not includereststrokes
|
||||||
|
|
||||||
userid = options['userid']
|
userid = options['userid']
|
||||||
@@ -612,6 +625,12 @@ def histodata(workouts, options):
|
|||||||
workmax = options['workmax']
|
workmax = options['workmax']
|
||||||
userid = options['userid']
|
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
|
if userid == 0: # pragma: no cover
|
||||||
extratitle = ''
|
extratitle = ''
|
||||||
else:
|
else:
|
||||||
@@ -645,7 +664,8 @@ def cpdata(workouts, options):
|
|||||||
|
|
||||||
u = User.objects.get(id=userid)
|
u = User.objects.get(id=userid)
|
||||||
r = u.rower
|
r = u.rower
|
||||||
|
|
||||||
|
|
||||||
delta, cpvalue, avgpower, workoutnames, urls = dataprep.fetchcp_new(
|
delta, cpvalue, avgpower, workoutnames, urls = dataprep.fetchcp_new(
|
||||||
r, workouts)
|
r, workouts)
|
||||||
|
|
||||||
@@ -798,6 +818,11 @@ def cpdata(workouts, options):
|
|||||||
|
|
||||||
|
|
||||||
def statsdata(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']
|
includereststrokes = options['includereststrokes']
|
||||||
ids = options['ids']
|
ids = options['ids']
|
||||||
|
|
||||||
@@ -872,12 +897,17 @@ def statsdata(workouts, options):
|
|||||||
|
|
||||||
|
|
||||||
def comparisondata(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']
|
includereststrokes = options['includereststrokes']
|
||||||
xparam = options['xaxis']
|
xparam = options['xaxis']
|
||||||
yparam1 = options['yaxis1']
|
yparam1 = options['yaxis1']
|
||||||
plottype = options['plottype']
|
plottype = options['plottype']
|
||||||
promember = True
|
promember = True
|
||||||
|
|
||||||
workstrokesonly = not includereststrokes
|
workstrokesonly = not includereststrokes
|
||||||
|
|
||||||
ids = [w.id for w in workouts]
|
ids = [w.id for w in workouts]
|
||||||
@@ -915,6 +945,10 @@ def comparisondata(workouts, options):
|
|||||||
|
|
||||||
|
|
||||||
def boxplotdata(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']
|
includereststrokes = options['includereststrokes']
|
||||||
spmmin = options['spmmin']
|
spmmin = options['spmmin']
|
||||||
@@ -926,7 +960,7 @@ def boxplotdata(workouts, options):
|
|||||||
plotfield = options['plotfield']
|
plotfield = options['plotfield']
|
||||||
|
|
||||||
workstrokesonly = not includereststrokes
|
workstrokesonly = not includereststrokes
|
||||||
|
|
||||||
datemapping = {
|
datemapping = {
|
||||||
w.id: w.date for w in workouts
|
w.id: w.date for w in workouts
|
||||||
}
|
}
|
||||||
@@ -1020,11 +1054,15 @@ def analysis_view_data(request, userid=0):
|
|||||||
|
|
||||||
for id in ids:
|
for id in ids:
|
||||||
try:
|
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
|
except Workout.DoesNotExist: # pragma: no cover
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if function == 'boxplot':
|
if function == 'boxplot':
|
||||||
script, div = boxplotdata(workouts, options)
|
script, div = boxplotdata(workouts, options)
|
||||||
elif function == 'trendflex': # pragma: no cover
|
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,
|
workouts = Workout.objects.filter(user=theuser.rower, date__gte=startdate,
|
||||||
date__lte=enddate,
|
date__lte=enddate,
|
||||||
workouttype__in=mytypes.rowtypes,
|
workouttype__in=mytypes.rowtypes,
|
||||||
duplicate=False).order_by('date')
|
duplicate=False).order_by('date')#.exclude(workoutsource='strava')
|
||||||
|
|
||||||
for workout in workouts:
|
for workout in workouts:
|
||||||
_ = dataprep.check_marker(workout)
|
_ = dataprep.check_marker(workout)
|
||||||
@@ -1113,7 +1151,7 @@ def goldmedalscores_view(request, userid=0,
|
|||||||
theuser, startdate=startdate, enddate=enddate,
|
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 = [
|
breadcrumbs = [
|
||||||
{
|
{
|
||||||
@@ -1311,7 +1349,7 @@ def performancemanager_view(request, userid=0, mode='rower',
|
|||||||
user = therower, date__gte=startdate-datetime.timedelta(days=90),
|
user = therower, date__gte=startdate-datetime.timedelta(days=90),
|
||||||
date__lte=enddate,
|
date__lte=enddate,
|
||||||
duplicate=False,
|
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]
|
ids = [w.id for w in markerworkouts]
|
||||||
form = PerformanceManagerForm(initial={
|
form = PerformanceManagerForm(initial={
|
||||||
@@ -1323,7 +1361,7 @@ def performancemanager_view(request, userid=0, mode='rower',
|
|||||||
|
|
||||||
ids = pd.Series(ids, dtype='int').dropna().values
|
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 = [
|
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"))
|
ddf = ddf.with_columns(pl.col("time").diff().clip(lower_bound=0).alias("deltat"))
|
||||||
except KeyError: # pragma: no cover
|
except KeyError: # pragma: no cover
|
||||||
pass
|
pass
|
||||||
|
except ColumnNotFoundError:
|
||||||
|
pass
|
||||||
|
|
||||||
ddf = dataprep.clean_df_stats_pl(ddf, workstrokesonly=False,
|
ddf = dataprep.clean_df_stats_pl(ddf, workstrokesonly=False,
|
||||||
ignoreadvanced=True)
|
ignoreadvanced=True)
|
||||||
@@ -2288,6 +2328,8 @@ def history_view_data(request, userid=0):
|
|||||||
ddict['hrmax'] = int(ddf['hr'].max())
|
ddict['hrmax'] = int(ddf['hr'].max())
|
||||||
except (KeyError, ValueError, AttributeError, ColumnNotFoundError): # pragma: no cover
|
except (KeyError, ValueError, AttributeError, ColumnNotFoundError): # pragma: no cover
|
||||||
ddict['hrmax'] = 0
|
ddict['hrmax'] = 0
|
||||||
|
except ColumnNotFoundError:
|
||||||
|
ddict['hrmax'] = 0
|
||||||
|
|
||||||
ddict['powermean'] = int(wavg(ddf, 'power', 'deltat'))
|
ddict['powermean'] = int(wavg(ddf, 'power', 'deltat'))
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -3397,12 +3397,12 @@ def virtualevent_submit_result_view(request, id=0, workoutid=0):
|
|||||||
startdatetime__gte=startdatetime,
|
startdatetime__gte=startdatetime,
|
||||||
startdatetime__lte=enddatetime,
|
startdatetime__lte=enddatetime,
|
||||||
distance__gte=race.approximate_distance,
|
distance__gte=race.approximate_distance,
|
||||||
).order_by("-date", "-startdatetime", "id")
|
).order_by("-date", "-startdatetime", "id").exclude(workoutsource='strava')
|
||||||
|
|
||||||
if not ws: # pragma: no cover
|
if not ws: # pragma: no cover
|
||||||
messages.info(
|
messages.info(
|
||||||
request,
|
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',
|
url = reverse('virtualevent_view',
|
||||||
@@ -3436,6 +3436,7 @@ def virtualevent_submit_result_view(request, id=0, workoutid=0):
|
|||||||
splitsecond = 0
|
splitsecond = 0
|
||||||
recordid = w_form.cleaned_data['record']
|
recordid = w_form.cleaned_data['record']
|
||||||
else:
|
else:
|
||||||
|
messages.error(request,"Error in form")
|
||||||
selectedworkout = None
|
selectedworkout = None
|
||||||
|
|
||||||
if selectedworkout is not None:
|
if selectedworkout is not None:
|
||||||
@@ -3518,7 +3519,12 @@ def virtualevent_submit_result_view(request, id=0, workoutid=0):
|
|||||||
|
|
||||||
else:
|
else:
|
||||||
if workoutid:
|
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)
|
w_form = WorkoutRaceSelectForm(workoutdata, entries)
|
||||||
|
|
||||||
breadcrumbs = [
|
breadcrumbs = [
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ from rest_framework.response import Response
|
|||||||
from rq.job import Job
|
from rq.job import Job
|
||||||
from rules.contrib.views import permission_required, objectgetter
|
from rules.contrib.views import permission_required, objectgetter
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
|
from django.db import models
|
||||||
from django.utils.crypto import get_random_string
|
from django.utils.crypto import get_random_string
|
||||||
from rq.registry import StartedJobRegistry
|
from rq.registry import StartedJobRegistry
|
||||||
from rq.exceptions import NoSuchJobError
|
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_add_workout_member, can_plan_user, is_paid_coach,
|
||||||
can_start_trial, can_start_plantrial, can_start_coachtrial,
|
can_start_trial, can_start_plantrial, can_start_coachtrial,
|
||||||
can_plan, is_workout_team,
|
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
|
from django.shortcuts import render
|
||||||
@@ -179,7 +181,13 @@ from rowers.models import ( RowerPowerForm, RowerHRZonesForm, SimpleRowerPowerFo
|
|||||||
IndoorVirtualRaceForm, PlannedSessionCommentForm, Alert,
|
IndoorVirtualRaceForm, PlannedSessionCommentForm, Alert,
|
||||||
Condition, StaticChartRowerForm, FollowerForm,
|
Condition, StaticChartRowerForm, FollowerForm,
|
||||||
VirtualRaceAthleteForm, InstantPlanForm, DataRowerForm,
|
VirtualRaceAthleteForm, InstantPlanForm, DataRowerForm,
|
||||||
StepEditorForm, iDokladToken )
|
StepEditorForm, iDokladToken,
|
||||||
|
RowerExportFormStrava, RowerExportFormPolar,
|
||||||
|
RowerExportFormSportTracks, RowerExportFormTrainingPeaks,
|
||||||
|
RowerExportFormConcept2, RowerExportFormGarmin,
|
||||||
|
RowerExportFormIntervals, RowerExportFormRP3,
|
||||||
|
RowerExportFormNK,
|
||||||
|
)
|
||||||
from rowers.models import (
|
from rowers.models import (
|
||||||
FavoriteForm, BaseFavoriteFormSet, SiteAnnouncement, BasePlannedSessionFormSet,
|
FavoriteForm, BaseFavoriteFormSet, SiteAnnouncement, BasePlannedSessionFormSet,
|
||||||
get_course_timezone, BaseConditionFormSet,
|
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')
|
messages.error(request,'Could not find this message')
|
||||||
url = reverse('user_messages')
|
url = reverse('user_messages')
|
||||||
return HttpResponseRedirect(url)
|
return HttpResponseRedirect(url)
|
||||||
|
|
||||||
|
|
||||||
if msg.receiver == request.user.rower:
|
if msg.receiver == request.user.rower:
|
||||||
msg.delete()
|
msg.delete()
|
||||||
@@ -458,19 +457,43 @@ def rower_exportsettings_view(request, userid=0):
|
|||||||
'polar_auto_import': 'polartoken',
|
'polar_auto_import': 'polartoken',
|
||||||
'c2_auto_export': 'c2token',
|
'c2_auto_export': 'c2token',
|
||||||
'c2_auto_import': 'c2token',
|
'c2_auto_import': 'c2token',
|
||||||
'runkeeper_auto_export': 'runkeepertoken',
|
|
||||||
'sporttracks_auto_export': 'sporttrackstoken',
|
'sporttracks_auto_export': 'sporttrackstoken',
|
||||||
'strava_auto_export': 'stravatoken',
|
'strava_auto_export': 'stravatoken',
|
||||||
'strava_auto_import': 'stravatoken',
|
'strava_auto_import': 'stravatoken',
|
||||||
'strava_auto_delete': 'stravatoken',
|
'strava_auto_delete': 'stravatoken',
|
||||||
'trainingpeaks_auto_export': 'tptoken',
|
'trainingpeaks_auto_export': 'tptoken',
|
||||||
'rp3_auto_import': 'rp3token',
|
'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)
|
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),
|
||||||
|
}
|
||||||
|
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
form = RowerExportForm(request.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),
|
||||||
|
}
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
cd = form.cleaned_data
|
cd = form.cleaned_data
|
||||||
if r.rowerplan == 'basic': # pragma: no cover
|
if r.rowerplan == 'basic': # pragma: no cover
|
||||||
@@ -529,6 +552,7 @@ def rower_exportsettings_view(request, userid=0):
|
|||||||
|
|
||||||
return render(request, 'rower_exportsettings.html',
|
return render(request, 'rower_exportsettings.html',
|
||||||
{'form': form,
|
{'form': form,
|
||||||
|
'forms': forms,
|
||||||
'rower': r,
|
'rower': r,
|
||||||
'breadcrumbs': breadcrumbs,
|
'breadcrumbs': breadcrumbs,
|
||||||
'grants': grants,
|
'grants': grants,
|
||||||
|
|||||||
@@ -2204,25 +2204,25 @@ def workouts_view(request, message='', successmessage='',
|
|||||||
team=theteam,
|
team=theteam,
|
||||||
startdatetime__gte=startdate,
|
startdatetime__gte=startdate,
|
||||||
startdatetime__lte=enddate,
|
startdatetime__lte=enddate,
|
||||||
privacy='visible').order_by("-date", "-starttime")
|
privacy='visible').order_by("-date", "-starttime").exclude(workoutsource='strava')
|
||||||
g_workouts = Workout.objects.filter(
|
g_workouts = Workout.objects.filter(
|
||||||
team=theteam,
|
team=theteam,
|
||||||
startdatetime__gte=activity_startdate,
|
startdatetime__gte=activity_startdate,
|
||||||
startdatetime__lte=activity_enddate,
|
startdatetime__lte=activity_enddate,
|
||||||
duplicate=False,
|
duplicate=False,
|
||||||
privacy='visible').order_by("-date", "-starttime")
|
privacy='visible').order_by("-date", "-starttime").exclude(workoutsource='strava')
|
||||||
elif theteam.viewing == 'coachonly': # pragma: no cover
|
elif theteam.viewing == 'coachonly': # pragma: no cover
|
||||||
workouts = Workout.objects.filter(
|
workouts = Workout.objects.filter(
|
||||||
team=theteam, user=r,
|
team=theteam, user=r,
|
||||||
startdatetime__gte=startdate,
|
startdatetime__gte=startdate,
|
||||||
startdatetime__lte=enddate,
|
startdatetime__lte=enddate,
|
||||||
privacy='visible').order_by("-startdatetime")
|
privacy='visible').order_by("-startdatetime").exclude(workoutsource='strava')
|
||||||
g_workouts = Workout.objects.filter(
|
g_workouts = Workout.objects.filter(
|
||||||
team=theteam, user=r,
|
team=theteam, user=r,
|
||||||
startdatetime__gte=activity_startdate,
|
startdatetime__gte=activity_startdate,
|
||||||
startdatetime__lte=activity_enddate,
|
startdatetime__lte=activity_enddate,
|
||||||
duplicate=False,
|
duplicate=False,
|
||||||
privacy='visible').order_by("-startdatetime")
|
privacy='visible').order_by("-startdatetime").exclude(workoutsource='strava')
|
||||||
|
|
||||||
elif request.user != r.user:
|
elif request.user != r.user:
|
||||||
theteam = None
|
theteam = None
|
||||||
@@ -2230,13 +2230,13 @@ def workouts_view(request, message='', successmessage='',
|
|||||||
user=r,
|
user=r,
|
||||||
startdatetime__gte=startdate,
|
startdatetime__gte=startdate,
|
||||||
startdatetime__lte=enddate,
|
startdatetime__lte=enddate,
|
||||||
privacy='visible').order_by("-date", "-starttime")
|
privacy='visible').order_by("-date", "-starttime").exclude(workoutsource='strava')
|
||||||
g_workouts = Workout.objects.filter(
|
g_workouts = Workout.objects.filter(
|
||||||
user=r,
|
user=r,
|
||||||
startdatetime__gte=activity_startdate,
|
startdatetime__gte=activity_startdate,
|
||||||
startdatetime__lte=activity_enddate,
|
startdatetime__lte=activity_enddate,
|
||||||
duplicate=False,
|
duplicate=False,
|
||||||
privacy='visible').order_by("-startdatetime")
|
privacy='visible').order_by("-startdatetime").exclude(workoutsource='strava')
|
||||||
else:
|
else:
|
||||||
theteam = None
|
theteam = None
|
||||||
workouts = Workout.objects.filter(
|
workouts = Workout.objects.filter(
|
||||||
@@ -2252,7 +2252,7 @@ def workouts_view(request, message='', successmessage='',
|
|||||||
if g_workouts.count() == 0:
|
if g_workouts.count() == 0:
|
||||||
g_workouts = Workout.objects.filter(
|
g_workouts = Workout.objects.filter(
|
||||||
user=r,
|
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_enddate = timezone.now()
|
||||||
g_startdate = (timezone.now()-timedelta(days=15))
|
g_startdate = (timezone.now()-timedelta(days=15))
|
||||||
|
|
||||||
@@ -2266,7 +2266,8 @@ def workouts_view(request, message='', successmessage='',
|
|||||||
reduce(operator.and_,
|
reduce(operator.and_,
|
||||||
(Q(name__icontains=q) for q in query_list)) |
|
(Q(name__icontains=q) for q in query_list)) |
|
||||||
reduce(operator.and_,
|
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})
|
searchform = SearchForm(initial={'q': query})
|
||||||
else:
|
else:
|
||||||
@@ -4699,6 +4700,7 @@ def workout_map_view(request, id=0):
|
|||||||
u = w.user.user
|
u = w.user.user
|
||||||
r = getrower(u)
|
r = getrower(u)
|
||||||
rowdata = rdata(csvfile=f1)
|
rowdata = rdata(csvfile=f1)
|
||||||
|
|
||||||
hascoordinates = 1
|
hascoordinates = 1
|
||||||
if rowdata != 0:
|
if rowdata != 0:
|
||||||
try:
|
try:
|
||||||
@@ -4933,7 +4935,7 @@ def workout_upload_api(request):
|
|||||||
|
|
||||||
# only allow local host
|
# only allow local host
|
||||||
hostt = request.get_host().split(':')
|
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 = {'status': 'false',
|
||||||
'message': 'permission denied for host '+hostt[0]}
|
'message': 'permission denied for host '+hostt[0]}
|
||||||
return JSONResponse(status=403, data=message)
|
return JSONResponse(status=403, data=message)
|
||||||
@@ -4986,6 +4988,7 @@ def workout_upload_api(request):
|
|||||||
boatname = post_data.get('boatName','')
|
boatname = post_data.get('boatName','')
|
||||||
portStarboard = post_data.get('portStarboard', 1)
|
portStarboard = post_data.get('portStarboard', 1)
|
||||||
empowerside = 'port'
|
empowerside = 'port'
|
||||||
|
stravaid = post_data.get('stravaid','')
|
||||||
if portStarboard == 1:
|
if portStarboard == 1:
|
||||||
empowerside = 'starboard'
|
empowerside = 'starboard'
|
||||||
|
|
||||||
@@ -5609,17 +5612,6 @@ def workout_upload_view(request,
|
|||||||
return response
|
return response
|
||||||
else:
|
else:
|
||||||
if not is_ajax:
|
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)
|
form = DocumentsForm(initial=docformoptions)
|
||||||
optionsform = UploadOptionsForm(initial=uploadoptions,
|
optionsform = UploadOptionsForm(initial=uploadoptions,
|
||||||
|
|||||||
BIN
static/img/intervals_logo_with_name.png
Normal file
BIN
static/img/intervals_logo_with_name.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
Reference in New Issue
Block a user