Private
Public Access
1
0

Merge branch 'develop' into feature/icu_sessions

This commit is contained in:
2024-12-15 16:08:30 +01:00
23 changed files with 1081 additions and 163 deletions

View File

@@ -217,6 +217,9 @@ def workout_goldmedalstandard(workout, reset=False):
def check_marker(workout):
r = workout.user
if workout.workoutsource == 'strava':
return None
gmstandard, gmseconds = workout_goldmedalstandard(workout)
if gmseconds < 60:
return None
@@ -369,8 +372,20 @@ def workout_summary_to_df(
return df
def resample(id, r, parent, overwrite='copy'):
def resample(id, r, parent, overwrite=False):
data, row = getrowdata_db(id=id)
rowdata = rrdata(csvfile=parent.csvfilename).df
# drop all columns except ' latitude' and ' longitude' and 'TimeStamp (sec)' from rowdata
allowedcolumns = [' latitude', ' longitude', 'TimeStamp (sec)']
rowdata = rowdata.filter(allowedcolumns)
rowdata.rename(columns={'TimeStamp (sec)': 'time'}, inplace=True)
rowdata['time'] = (rowdata['time']-rowdata.loc[0,'time'])*1000.
rowdata.set_index('time', inplace=True)
data.set_index('time', inplace=True)
rowdata_interpolated = rowdata.reindex(data.index.union(rowdata.index)).interpolate('index')
data = data.merge(rowdata_interpolated, left_index=True, right_index=True, how='left')
data = data.reset_index()
messages = []
# resample
@@ -393,7 +408,7 @@ def resample(id, r, parent, overwrite='copy'):
data['pace'] = data['pace'] / 1000.
data['time'] = data['time'] / 1000.
if overwrite == 'overwrite':
if overwrite == True:
# remove CP data
try:
cpfile = 'media/cpdata_{id}.parquet.gz'.format(id=parent.id)
@@ -1304,8 +1319,11 @@ def save_workout_database(f2, r, dosmooth=True, workouttype='rower',
if makeprivate: # pragma: no cover
privacy = 'hidden'
else:
elif workoutsource != 'strava':
privacy = 'visible'
else:
privacy = 'hidden'
# checking for inf values

View File

@@ -67,13 +67,12 @@ class FlexibleDecimalField(forms.DecimalField):
class ResampleForm(forms.Form):
resamplechoices = (
('overwrite', 'Overwrite Workout'),
('copy', 'Create a Duplicate Workout')
)
# add resamplechoice field, the result is a True or False boolean, labels are "overwrite" and "create copy"
resamplechoice = forms.ChoiceField(
initial='copy', choices=resamplechoices, label='Copy behavior')
required=True,
choices=((True, 'overwrite'), (False, 'create copy')),
label='Resample choice',
widget=forms.RadioSelect)
class TrainingZonesForm(forms.Form):
@@ -582,6 +581,11 @@ class UploadOptionsForm(forms.Form):
races = VirtualRace.objects.filter(
registration_closure__gt=timezone.now())
# set upload_to_X based on r.X_auto_export
for field in ['C2', 'Strava', 'SportTracks', 'TrainingPeaks', 'Intervals']:
if getattr(r, field.lower()+'_auto_export') and r.rowerplan in ['pro', 'plan','coach']:
self.fields['upload_to_'+field].initial = True
registrations = IndoorVirtualRaceResult.objects.filter(
race__in=races,
userid=r.id)
@@ -665,6 +669,10 @@ class TeamUploadOptionsForm(forms.Form):
upload_to_TrainingPeaks = forms.BooleanField(initial=False,
required=False,
label='Export to TrainingPeaks')
upload_to_Intervals = forms.BooleanField(initial=False,
required=False,
label='Export to TrainingPeaks')
# do_physics = forms.BooleanField(initial=False,required=False,label='Power Estimate (OTW)')
makeprivate = forms.BooleanField(initial=False, required=False,
label='Make Workout Private')

View File

@@ -17,6 +17,7 @@ import os
from uuid import uuid4
from django.utils import timezone
from datetime import timedelta
import rowers.dataprep as dataprep
from rowsandall_app.settings import (
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:
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:
row = rowingdata(csvfile=filename)
except IOError: # pragma: no cover
@@ -128,6 +138,8 @@ class IntervalsIntegration(SyncIntegration):
except TypeError:
newnotes = 'from'+w.workoutsource+' via rowsandall.com'
if w.user.intervals_resample_to_1s and w_resampled:
w_resampled.delete()
row.exporttotcx(tcxfilename, notes=newnotes, sport=mytypes.intervalsmapping[w.workouttype])
if dozip:
gzfilename = tcxfilename + '.gz'
@@ -230,25 +242,29 @@ class IntervalsIntegration(SyncIntegration):
workouts = []
for item in data:
i = item['id']
r = item['type']
d = item['distance']
ttot = seconds_to_duration(item['moving_time'])
s = item['start_date']
s2 = ''
c = item['name']
if i in known_interval_ids:
nnn = ''
else:
nnn = 'NEW'
try:
i = item['id']
r = item['type']
d = item['distance']
ttot = seconds_to_duration(item['moving_time'])
s = item['start_date']
s2 = ''
c = item['name']
if i in known_interval_ids:
nnn = ''
else:
nnn = 'NEW'
keys = ['id','distance','duration','starttime',
'rowtype','source','name','new']
keys = ['id','distance','duration','starttime',
'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

View File

@@ -214,7 +214,7 @@ class StravaIntegration(SyncIntegration):
def get_workout(self, id, *args, **kwargs) -> int:
try:
_ = self.open()
except NoTokenError("Strava error"):
except NoTokenError:
return 0
record = create_or_update_syncrecord(self.rower, None, stravaid=id)

View File

@@ -550,7 +550,7 @@ def goldmedalscorechart(user, startdate=None, enddate=None):
workouts = Workout.objects.filter(user=user.rower, date__gte=startdate,
date__lte=enddate,
workouttype__in=mytypes.rowtypes,
duplicate=False).order_by('date')
duplicate=False).order_by('date').exclude(workoutsource='strava')
markerworkouts = workouts.filter(rankingpiece=True)
outids = [w.id for w in markerworkouts]

View 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.'))

View File

@@ -1174,6 +1174,7 @@ class Rower(models.Model):
c2_auto_import = models.BooleanField(default=False)
intervals_auto_export = models.BooleanField(default=False)
intervals_auto_import = models.BooleanField(default=False)
intervals_resample_to_1s = models.BooleanField(default=False, verbose_name='Resample to 1s on export')
sporttrackstoken = models.CharField(
default='', max_length=200, blank=True, null=True)
sporttrackstokenexpirydate = models.DateTimeField(blank=True, null=True)
@@ -1240,7 +1241,7 @@ class Rower(models.Model):
strava_auto_export = models.BooleanField(default=False)
strava_auto_import = models.BooleanField(default=False)
strava_auto_delete = models.BooleanField(default=False)
strava_auto_delete = models.BooleanField(default=True)
intervals_token = models.CharField(
default='', max_length=200, blank=True, null=True)
@@ -1440,9 +1441,26 @@ parchoicesy1 = list(sorted(favchartlabelsy1.items(), key=lambda x: x[1]))
parchoicesy2 = list(sorted(favchartlabelsy2.items(), key=lambda x: x[1]))
parchoicesx = list(sorted(favchartlabelsx.items(), key=lambda x: x[1]))
# special filter for workouts to exclude strava workouts by default
class WorkoutQuerySet(models.QuerySet):
def filter(self, *args, exclude_strava=True, **kwargs):
queryset = super().filter(*args, **kwargs)
if exclude_strava:
queryset = queryset.exclude(workoutsource='strava')
return queryset
def get(self, *args, **kwargs):
queryset = self
return super().get(*args, **kwargs)
class WorkoutManager(models.Manager):
def get_queryset(self):
return WorkoutQuerySet(self.model, using=self._db)
# Saving a chart as a favorite chart
class FavoriteChart(models.Model):
workouttypechoices = [
('ote', 'Erg/SkiErg'),
@@ -3740,6 +3758,9 @@ class Workout(models.Model):
default=False, verbose_name='Duplicate Workout')
impeller = models.BooleanField(default=False, verbose_name='Impeller')
# attach the WorkoutManager
#objects = WorkoutManager()
def url(self):
str = '/rowers/workout/{id}/'.format(
id=encoder.encode_hex(self.id)
@@ -4578,7 +4599,78 @@ class RowerExportForm(ModelForm):
'rp3_auto_import',
'intervals_auto_import',
'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
class SimpleRowerPowerForm(ModelForm):

View File

@@ -1605,13 +1605,28 @@ def add_workout_fastestrace(ws, race, r, recordid=0, doregister=False):
enddatetime
)
# from ws, remove any w where w.workoutsource = 'strava'. For each removal add an error "strava workout not permitted" to the errors list and if there are no workouts left, return 0, comments, errors, 0
ws2 = []
for w in ws:
if w.workoutsource != 'strava':
ws2.append(w)
else:
errors.append('Strava workouts are not permitted')
ws = ws2
if len(ws) == 0:
return result, comments, errors, 0
ids = [w.id for w in ws]
ids = list(set(ids))
if len(ids) > 1 and race.sessiontype in ['test', 'coursetest', 'race', 'indoorrace', 'fastest_time', 'fastest_distance']: # pragma: no cover
errors.append('For tests, you can only attach one workout')
return result, comments, errors, 0
if r.birthdate:
age = calculate_age(r.birthdate)
else: # pragma: no cover
@@ -1767,6 +1782,19 @@ def add_workout_indoorrace(ws, race, r, recordid=0, doregister=False):
enddatetime
)
# from ws, remove any w where w.workoutsource = 'strava'. For each removal add an error "strava workout not permitted" to the errors list and if there are no workouts left, return 0, comments, errors, 0
ws2 = []
for w in ws:
if w.workoutsource != 'strava':
ws2.append(w)
else:
errors.append('Strava workouts are not permitted')
ws = ws2
if len(ws) == 0:
return result, comments, errors, 0
# check if all sessions have same date
dates = [w.date for w in ws]
if (not all(d == dates[0] for d in dates)) and race.sessiontype not in ['challenge', 'cycletarget']: # pragma: no cover
@@ -1914,6 +1942,19 @@ def add_workout_race(ws, race, r, splitsecond=0, recordid=0, doregister=False):
enddatetime
)
# from ws, remove any w where w.workoutsource = 'strava'. For each removal add an error "strava workout not permitted" to the errors list and if there are no workouts left, return 0, comments, errors, 0
ws2 = []
for w in ws:
if w.workoutsource != 'strava':
ws2.append(w)
else:
errors.append('Strava workouts are not permitted')
ws = ws2
if len(ws) == 0:
return result, comments, errors, 0
# check if all sessions have same date
dates = [w.date for w in ws]
if (not all(d == dates[0] for d in dates)) and race.sessiontype not in ['challenge', 'cycletarget']: # pragma: no cover

View File

@@ -451,6 +451,11 @@ def is_workout_user(user, workout):
except AttributeError: # pragma: no cover
return False
if workout.privacy == 'hidden':
return user == workout.user.user
if workout.workoutsource == 'strava':
return user == workout.user.user
if workout.user == r:
return True
@@ -458,6 +463,9 @@ def is_workout_user(user, workout):
# check if user is in same team as owner of workout
@rules.predicate
def workout_is_strava(workout):
return workout.workoutsource == 'strava'
@rules.predicate
def is_workout_team(user, workout):
@@ -469,6 +477,11 @@ def is_workout_team(user, workout):
except AttributeError: # pragma: no cover
return False
if workout.privacy == 'hidden':
return user == workout.user.user
if workout.workoutsource == 'strava':
return user == workout.user.user
if workout.user == r:
return True
@@ -479,7 +492,9 @@ def is_workout_team(user, workout):
@rules.predicate
def can_view_workout(user, workout):
if workout.privacy != 'private':
if workout.workoutsource == 'strava':
return user == workout.user.user
if workout.privacy not in ('hidden', 'private'):
return True
if user.is_anonymous: # pragma: no cover
return False

View File

@@ -8,7 +8,7 @@
<ul class="main-content">
<li class="grid_4">
<p>On this page, a work in progress, I will collect useful information
<p>On this page, I will collect useful information
for developers of rowing data apps and hardware.</p>
<p>I presume you have an app (smartphone app, dedicated hardware, web site)
@@ -61,11 +61,11 @@
</ul></p>
<h2>Using the REST API</h2>
<p>We are building a REST API which will allow you to post and
<p>We have a REST API which will allow you to post and
receive stroke
data from the site directly.</p>
<p>The REST API is a work in progress. We are open to improvement
<p>We are open to improvement
suggestions (provided they don't break existing apps). Please send
email to <a href="mailto:info@rowsandall.com">info@rowsandall.com</a>
with questions and/or suggestions. We
@@ -84,7 +84,6 @@
<li>Disadvantages
<p><ul class="contentli">
<li>The API is not stable and not fully tested yet.</li>
<li>You need to register your app with us. We can revoke your
permissions if you misuse them.</li>
<li>The user user must grant permissions to your app.</li>
@@ -114,7 +113,7 @@
<p>We have disabled the self service app link for security reasons.
We will replace it with a secure self service app link soon. If you
If you
need to register an app, please send email to info@rowsandall.com</p>
<h3>Authentication</h3>
@@ -728,11 +727,11 @@
<li><b>peakdriveforce</b>: Peak handle force (lbs)</li>
<li><b>lapidx</b>: Lap identifier</li>
<li><b>hr</b>: Heart rate (beats per minute)</li>
<li><b>wash</b>: Wash as defined per Empower oarlock (degrees)</li>
<li><b>catch</b>: Catch angle per Empower oarlock (degrees)</li>
<li><b>finish</b>: Finish angle per Empower oarlock (degrees)</li>
<li><b>peakforceangle</b>: Peak Force Angle per Empower oarlock (degrees)</li>
<li><b>slip</b>: Slip as defined per Empower oarlock (degrees)</li>
<li><b>wash</b>: Wash as defined for your smart power measuring oarlock (degrees)</li>
<li><b>catch</b>: Catch angle for your smart power measuring oarlock (degrees)</li>
<li><b>finish</b>: Finish angle for your smart power measuring oarlock (degrees)</li>
<li><b>peakforceangle</b>: Peak Force Angle for your smart power measuring oarlock (degrees)</li>
<li><b>slip</b>: Slip as defined for your smart power measuring oarlock (degrees)</li>
</ul>
</p>

View File

@@ -7,8 +7,10 @@
{% block main %}
<h1>Import and Export Settings for {{ rower.user.first_name }} {{ rower.user.last_name }}</h1>
<form enctype="multipart/form-data" action="" method="post">
{% csrf_token %}
<ul class="main-content">
<li class="grid_2">
<li class="grid_4">
<p>You are currently connected to:
{% if rower.c2token is not None and rower.c2token != '' %}
Concept2 Logbook,
@@ -41,44 +43,141 @@
Intervals.icu
{% endif %}
</p>
{% if form.errors %}
<p style="color: red;">
Please correct the error{{ form.errors|pluralize }} below.
</p>
{% endif %}
<p>
<form enctype="multipart/form-data" action="" method="post">
<table>
{{ form.as_table }}
</table>
{% csrf_token %}
<input type="submit" value="Save">
</form>
</p>
{% if rower.garmintoken and rower.garmintoken != '' %}
<p>
<em>You are connected to Garmin.</em> Switching off Garmin Connect sync is on the
<a href="https://connect.garmin.com/modern/settings/accountInformation">Account settings</a>
page. Look for the "Rowsandall" app.
</p>
{% endif %}
<p>
Garmin Connnect has no manual sync, so connecting your account to your Garmin account will
automatically auto-sync workouts from Garmin to Rowsandall (but not in the other direction). If you
want to export our structured workout sessions to your Garmin device, you have to set the "Garmin Activity"
to a activity type that is supported by your watch. Not all watches support "Custom" activities, so
you may have to set your activity to Run or Ride while rowing.
</p>
<p>
Strava Auto Import also imports activity changes on Strava to Rowsandall, except when you delete
a workout on Strava. If you want Deletions to propagate to Rowsandall, tick the Strava Auto Delete
check box.
</p>
<p>
Click on the icons to establish the connection or to renew the authorization.
</p>
</li>
<li class="grid_4">
<h2>API Key</h2>
<p>{{ apikey }}</p>
<p>
<a href="/rowers/me/regenerateapikey/">Regenerate</a>
</p>
<p>This API key can be used to access the Rowsandall API. It is used by some third party applications to access your data. Keep it secret.</p>
</li>
{% if form.errors %}
<li class="rounder">
<p style="color: red;">
Please correct the error{{ form.errors|pluralize }} below.
</p>
</li>
{% endif %}
<li class="rounder">
<h2>NK</h2>
<table>
{{ forms.nk.as_table }}
<input type="submit" value="Save">
</table>
<p><a href="/rowers/me/nkauthorize/"><img src="/static/img/NKLiNKLogbook.png" alt="connect with NK Logbook" width="120"></a></p>
</li>
<li class="rounder">
<h2>Concept2</h2>
<table>
{{ forms.c2.as_table }}
<input type="submit" value="Save">
</table>
<p><a href="/rowers/me/c2authorize/"><img src="/static/img/blueC2logo.png" alt="connect with Concept2" width="120"></a></p>
</li>
<li class="rounder">
<h2>RP3</h2>
<table>
{{ forms.rp3.as_table }}
<input type="submit" value="Save">
</table>
<p><a href="/rowers/me/rp3authorize"><img src="/static/img/logo-rp3-full-black.png"
alt="connect with RP3" width="130"></a></p>
</li>
<li class="rounder">
<h2>Rojabo</h2>
<p><a href="/rowers/me/rojaboauthorize"><img src="/static/img/rojabo.png"
alt="connect with Rojabo" width="130"></a></p>
</li>
<li class="rounder">
<h2>Intervals.icu</h2>
<table>
{{ forms.intervals.as_table }}
<input type="submit" value="Save">
</table>
<p><a href="/rowers/me/intervalsauthorize"><img src="/static/img/intervals_logo_with_name.png"
alt="connect with intervals.icu"></a></p>
</li>
<li class="rounder">
<h2>SportTracks</h2>
<table>
{{ forms.sporttracks.as_table }}
<input type="submit" value="Save">
</table>
<p><a href="/rowers/me/sporttracksauthorize/"><img src="/static/img/sporttracks-button.png" alt="connect with SportTracks" width="120"></a></p>
</li>
<li class="rounder">
<h2>TrainingPeaks</h2>
<table>
{{ forms.trainingpeaks.as_table }}
<input type="submit" value="Save">
</table>
<p><a href="/rowers/me/tpauthorize/"><img src="/static/img/TP_logo_horz_2_color.png"
alt="connect with Polar" width="130"></a></p>
</li>
<li class="rounder">
<h2>Polar</h2>
<table>
{{ forms.polar.as_table }}
<input type="submit" value="Save">
</table>
<p><a href="/rowers/me/polarauthorize/"><img src="/static/img/Polar_connectwith_btn_white.png"
alt="connect with Polar" width="130"></a></p>
</li>
<li class="rounder">
<h2>Garmin Connect</h2>
<table>
{{ forms.garmin.as_table }}
<input type="submit" value="Save">
</table>
<p><a href="/rowers/me/garminauthorize"><img src="/static/img/garmin_badge_130.png"
alt="connect with Garmin" width="130"></a></p>
<p>
Garmin Connnect has no manual sync, so connecting your account to your Garmin account will
automatically auto-sync workouts from Garmin to Rowsandall (but not in the other direction). If you
want to export our structured workout sessions to your Garmin device, you have to set the "Garmin Activity"
to a activity type that is supported by your watch. Not all watches support "Custom" activities, so
you may have to set your activity to Run or Ride while rowing.
</p>
{% if rower.garmintoken and rower.garmintoken != '' %}
<p>
<em>You are connected to Garmin.</em> Switching off Garmin Connect sync is on the
<a href="https://connect.garmin.com/modern/settings/accountInformation">Account settings</a>
page. Look for the "Rowsandall" app.
</p>
{% endif %}
</li>
<li class="rounder">
<h2>Strava</h2>
<p><em>Warning: API restrictions!</em></p>
<p><input type="submit" value="Save"></p>
{{ forms.strava.as_p }}
<p><a href="/rowers/me/stravaauthorize/"><img src="/static/img/ConnectWithStrava.png" alt="connect with strava" width="120"></a></p>
<p>
Strava Auto Import also imports activity changes on Strava to Rowsandall, except when you delete
a workout on Strava. If you want Deletions to propagate to Rowsandall, tick the Strava Auto Delete
check box.
</p>
{% if rower.stravatoken and rower.stravatoken != '' %}
<p>
<em>You are connected to Strava.</em> Workouts imported from Strava will not be synced
to other platforms and the data will only be visible to you, not your team members or coaches.
We have to respect the terms and conditions of the Strava API, which do not allow us to sync
data to other platforms or to share the data with others.
</p>
{% endif %}
</li>
<li class="grid_2">
{% if grants %}
<li class="rounder">
<h2>Applications</h2>
<p>
These applications have access to your Rowsandall data.
</p>
<table width="100%">
<thead>
<tr>
@@ -99,35 +198,12 @@
{% endfor %}
</tbody>
</table>
<li>
{% endif %}
<h2>API Key</h2>
<p>{{ apikey }}</p>
<p>
<a href="/rowers/me/regenerateapikey/">Regenerate</a>
</p>
This API key can be used to access the Rowsandall API. It is used by some third party applications to access your data. Keep it secret.
</li>
</ul>
<p>Click on one of the icons below to connect to the service of your
choice or to renew the authorization.</p>
<p><a href="/rowers/me/stravaauthorize/"><img src="/static/img/ConnectWithStrava.png" alt="connect with strava" width="120"></a></p>
<p><a href="/rowers/me/c2authorize/"><img src="/static/img/blueC2logo.png" alt="connect with Concept2" width="120"></a></p>
<p><a href="/rowers/me/nkauthorize/"><img src="/static/img/NKLiNKLogbook.png" alt="connect with NK Logbook" width="120"></a></p>
<p><a href="/rowers/me/sporttracksauthorize/"><img src="/static/img/sporttracks-button.png" alt="connect with SportTracks" width="120"></a></p>
<p><a href="/rowers/me/polarauthorize/"><img src="/static/img/Polar_connectwith_btn_white.png"
alt="connect with Polar" width="130"></a></p>
<p><a href="/rowers/me/tpauthorize/"><img src="/static/img/TP_logo_horz_2_color.png"
alt="connect with Polar" width="130"></a></p>
<p><a href="/rowers/me/garminauthorize"><img src="/static/img/garmin_badge_130.png"
alt="connect with Garmin" width="130"></a></p>
<p><a href="/rowers/me/rp3authorize"><img src="/static/img/logo-rp3-full-black.png"
alt="connect with RP3" width="130"></a></p>
<p><a href="/rowers/me/rojaboauthorize"><img src="/static/img/rojabo.png"
alt="connect with Rojabo" width="130"></a></p>
<p><a href="/rowers/me/intervalsauthorize"><img src="/static/img/intervals_icu.png"
alt="connect with intervals.icu" height="30"></a></p>
</form>
{% endblock %}

View File

@@ -22,12 +22,454 @@ from rowers.opaque import encoder
from rest_framework.test import APIRequestFactory, force_authenticate
UPLOAD_SERVICE_URL = '/rowers/workout/api/upload/'
UPLOAD_SERVICE_SECRET = "FoYezZWLSyfAVimumpHEeYsJjsNCerxV"
import json
# import BeautifulSoup
from bs4 import BeautifulSoup
from rowers.ownapistuff import *
from rowers.views.apiviews import *
from rowers.models import APIKey
from rowers.teams import add_member, add_coach
from rowers.views.analysisviews import histodata
class TeamFactory(factory.DjangoModelFactory):
class Meta:
model = Team
name = factory.LazyAttribute(lambda _: faker.word())
notes = faker.text()
private = 'open'
viewing = 'allmembers'
class StravaPrivacy(TestCase):
def setUp(self):
self.u = UserFactory()
self.u2 = UserFactory()
self.u3 = UserFactory()
self.r = Rower.objects.create(user=self.u,
birthdate=faker.profile()['birthdate'],
gdproptin=True, ftpset=True,surveydone=True,
gdproptindate=timezone.now(),
rowerplan='coach',subscription_id=1)
self.r.stravatoken = '12'
self.r.stravarefreshtoken = '123'
self.r.stravatokenexpirydate = arrow.get(datetime.datetime.now()-datetime.timedelta(days=1)).datetime
self.r.strava_owner_id = 4
self.r.save()
self.c = Client()
self.factory = RequestFactory()
self.password = faker.word()
self.u.set_password(self.password)
self.u.save()
self.factory = APIRequestFactory()
self.r2 = Rower.objects.create(user=self.u2,
birthdate=faker.profile()['birthdate'],
gdproptin=True, ftpset=True,surveydone=True,
gdproptindate=timezone.now(),
rowerplan='coach',clubsize=3)
self.r3 = Rower.objects.create(user=self.u3,
birthdate=faker.profile()['birthdate'],
gdproptin=True, ftpset=True,surveydone=True,
gdproptindate=timezone.now(),
rowerplan='basic')
self.c = Client()
self.password2 = faker.word()
self.u2.set_password(self.password2)
self.u2.save()
self.password3 = faker.word()
self.u3.set_password(self.password3)
self.u3.save()
self.team = TeamFactory(manager=self.u2)
# all are team members
add_member(self.team.id, self.r)
add_member(self.team.id, self.r2)
add_member(self.team.id, self.r3)
self.user_workouts = WorkoutFactory.create_batch(5, user=self.r)
for w in self.user_workouts:
if w.id <= 2:
w.workoutsource = 'strava'
w.privacy = 'hidden'
elif w.id == 3: # user can change privacy but cannot change workoutsource
w.workoutsource = 'strava'
w.privacy = 'visible'
else:
w.workoutsource = 'concept2'
w.privacy = 'visible'
w.team.add(self.team)
w.csvfilename = get_random_file(filename='rowers/tests/testdata/thyro.csv')['filename']
w.save()
# r2 coaches r
add_coach(self.r2, self.r)
self.factory = APIRequestFactory()
def tearDown(self):
for workout in self.user_workouts:
try:
os.remove(workout.csvfilename)
except (OSError, FileNotFoundError, IOError):
pass
# Test if workout with workoutsource strava and privacy hidden can be seen by coach
def test_privacy_coach(self):
login = self.c.login(username=self.u2.username, password=self.password2)
self.assertTrue(login)
w = self.user_workouts[0]
url = reverse('workout_view',kwargs={'id':encoder.encode_hex(w.id)})
response = self.c.get(url)
self.assertEqual(response.status_code,403)
# Same test as above but for 'workout_edit_view'
def test_privacy_coach_edit(self):
login = self.c.login(username=self.u2.username, password=self.password2)
self.assertTrue(login)
w = self.user_workouts[0]
url = reverse('workout_edit_view',kwargs={'id':encoder.encode_hex(w.id)})
response = self.c.get(url)
self.assertEqual(response.status_code,403)
# Test if workout with workoutsource strava and privacy hidden can be seen by team member
def test_privacy_member(self):
login = self.c.login(username=self.u3.username, password=self.password3)
self.assertTrue(login)
w = self.user_workouts[0]
url = reverse('workout_view',kwargs={'id':encoder.encode_hex(w.id)})
response = self.c.get(url)
self.assertEqual(response.status_code,403)
# Same test as above but for 'workout_edit_view'
def test_privacy_member_edit(self):
login = self.c.login(username=self.u3.username, password=self.password3)
self.assertTrue(login)
w = self.user_workouts[0]
url = reverse('workout_edit_view',kwargs={'id':encoder.encode_hex(w.id)})
response = self.c.get(url)
self.assertEqual(response.status_code,403)
# same test as above but with user r and the response code should be 200
def test_privacy_owner(self):
login = self.c.login(username=self.u.username, password=self.password)
self.assertTrue(login)
w = self.user_workouts[0]
url = reverse('workout_view',kwargs={'id':encoder.encode_hex(w.id)})
response = self.c.get(url)
self.assertEqual(response.status_code,200)
# same test as above but for 'workout_edit_view'
def test_privacy_owner_edit(self):
login = self.c.login(username=self.u.username, password=self.password)
self.assertTrue(login)
w = self.user_workouts[0]
url = reverse('workout_edit_view',kwargs={'id':encoder.encode_hex(w.id)})
response = self.c.get(url)
self.assertEqual(response.status_code,200)
# test if list_workouts returns all workouts for user r
def test_list_workouts(self):
login = self.c.login(username=self.u.username, password=self.password)
self.assertTrue(login)
url = reverse('workouts_view')
response = self.c.get(url)
self.assertEqual(response.status_code,200)
# the response.content is html, so we need to parse it
soup = BeautifulSoup(response.content, 'html.parser')
# the workouts look like <a href="/rowers/workout/{id}/...">...</a> and there should be 5 unique ids
# the id is a hex string
workouts = set([a['href'].split('/')[3] for a in soup.find_all('a') if a['href'].startswith('/rowers/workout/')])
# throw out "c2import", "nkimport", "stravaimport", "concept2import", "sporttracksimport" from the set
workouts = set([w for w in workouts if w not in [
'upload', 'addmanual', 'c2import', 'polarimport', 'rp3import', 'nkimport', 'stravaimport', 'concept2import', 'sporttracksimport',
'intervalsimport']])
self.assertEqual(len(workouts),5)
# same test as above but list_workouts with team id = self.team.id
def test_list_workouts_team(self):
login = self.c.login(username=self.u.username, password=self.password)
self.assertTrue(login)
url = reverse('workouts_view',kwargs={'teamid':self.team.id})
response = self.c.get(url)
self.assertEqual(response.status_code,200)
# the response.content is html, so we need to parse it
soup = BeautifulSoup(response.content, 'html.parser')
# the workouts look like <a href="/rowers/workout/{id}/...">...</a> and there should be 5 unique ids
# the id is a hex string
workouts = set([a['href'].split('/')[3] for a in soup.find_all('a') if a['href'].startswith('/rowers/workout/')])
# throw out "c2import", "nkimport", "stravaimport", "concept2import", "sporttracksimport" from the set
workouts = set([w for w in workouts if w not in [
'upload', 'addmanual', 'c2import', 'polarimport', 'rp3import', 'nkimport', 'stravaimport', 'concept2import', 'sporttracksimport',
'intervalsimport']])
self.assertEqual(len(workouts),2)
# same test as the previous one but with self.r2 and the number of workouts found should 0
def test_list_workouts_team_coach(self):
login = self.c.login(username=self.u2.username, password=self.password2)
self.assertTrue(login)
url = reverse('workouts_view',kwargs={'teamid':self.team.id})
response = self.c.get(url)
self.assertEqual(response.status_code,200)
# the response.content is html, so we need to parse it
soup = BeautifulSoup(response.content, 'html.parser')
# the workouts look like <a href="/rowers/workout/{id}/...">...</a> and there should be 5 unique ids
# the id is a hex string
workouts = set([a['href'].split('/')[3] for a in soup.find_all('a') if a['href'].startswith('/rowers/workout/')])
# throw out "c2import", "nkimport", "stravaimport", "concept2import", "sporttracksimport" from the set
workouts = set([w for w in workouts if w not in [
'upload', 'addmanual', 'c2import', 'polarimport', 'rp3import', 'nkimport', 'stravaimport', 'concept2import', 'sporttracksimport',
'intervalsimport']])
self.assertEqual(len(workouts),2)
# same test as above but with without the teamid kwarg but with a rowerid=self.r.id
def test_list_workouts_team_coach2(self):
login = self.c.login(username=self.u2.username, password=self.password2)
self.assertTrue(login)
url = reverse('workouts_view',kwargs={'rowerid':self.r.id})
response = self.c.get(url)
self.assertEqual(response.status_code,200)
# the response.content is html, so we need to parse it
soup = BeautifulSoup(response.content, 'html.parser')
# the workouts look like <a href="/rowers/workout/{id}/...">...</a> and there should be 5 unique ids
# the id is a hex string
workouts = set([a['href'].split('/')[3] for a in soup.find_all('a') if a['href'].startswith('/rowers/workout/')])
# throw out "c2import", "nkimport", "stravaimport", "concept2import", "sporttracksimport" from the set
workouts = set([w for w in workouts if w not in [
'upload', 'addmanual', 'c2import', 'polarimport', 'rp3import', 'nkimport', 'stravaimport', 'concept2import', 'sporttracksimport',
'intervalsimport']])
self.assertEqual(len(workouts),2)
# same test as the previous one but with self.r3 and the number of workouts found should 0
def test_list_workouts_team_member(self):
login = self.c.login(username=self.u3.username, password=self.password3)
self.assertTrue(login)
url = reverse('workouts_view',kwargs={'teamid':self.team.id})
response = self.c.get(url)
self.assertEqual(response.status_code,200)
# the response.content is html, so we need to parse it
soup = BeautifulSoup(response.content, 'html.parser')
# the workouts look like <a href="/rowers/workout/{id}/...">...</a> and there should be 5 unique ids
# the id is a hex string
workouts = set([a['href'].split('/')[3] for a in soup.find_all('a') if a['href'].startswith('/rowers/workout/')])
# throw out "c2import", "nkimport", "stravaimport", "concept2import", "sporttracksimport" from the set
workouts = set([w for w in workouts if w not in [
'upload', 'addmanual', 'c2import', 'polarimport', 'rp3import', 'nkimport', 'stravaimport', 'concept2import', 'sporttracksimport',
'intervalsimport']])
self.assertEqual(len(workouts),2)
# now test strava import and test if the created workout has workoutsource strava and privacy hidden
@patch('rowers.utils.requests.get', side_effect=mocked_requests)
@patch('rowers.integrations.strava.requests.post', side_effect=mocked_requests)
@patch('rowers.dataprep.read_data')
def test_stravaimport(self, mock_get, mock_post, mocked_read_data):
login = self.c.login(username=self.u.username, password=self.password)
self.assertTrue(login)
# remove all self.workouts
Workout.objects.filter(user=self.r).delete()
# create a workout using dataprep.new_workout_from_file with workoutsource = strava
result = get_random_file(filename='rowers/tests/testdata/thyro.csv')
workout_id, message, filename = dataprep.new_workout_from_file(self.r, result['filename'],
workoutsource='strava', makeprivate=True)
# check if the workout was created
ws = Workout.objects.filter(user=self.r)
self.assertEqual(len(ws),1)
w = ws[0]
self.assertEqual(w.workoutsource,'strava')
self.assertEqual(w.privacy,'hidden')
# same as test above but makeprivate = False
@patch('rowers.utils.requests.get', side_effect=mocked_requests)
@patch('rowers.integrations.strava.requests.post', side_effect=mocked_requests)
@patch('rowers.dataprep.read_data')
def test_stravaimport_public(self, mock_get, mock_post, mocked_read_data):
login = self.c.login(username=self.u.username, password=self.password)
self.assertTrue(login)
# remove all self.workouts
Workout.objects.filter(user=self.r).delete()
# create a workout using dataprep.new_workout_from_file with workoutsource = strava
result = get_random_file(filename='rowers/tests/testdata/thyro.csv')
workout_id, message, filename = dataprep.new_workout_from_file(self.r, result['filename'],
workoutsource='strava', makeprivate=False)
# check if the workout was created
ws = Workout.objects.filter(user=self.r)
self.assertEqual(len(ws),1)
w = ws[0]
self.assertEqual(w.workoutsource,'strava')
self.assertEqual(w.privacy,'hidden')
# test ownapi with stravaid = '122'
def test_ownapi(self):
# remove all self.workouts
Workout.objects.filter(user=self.r).delete()
result = get_random_file(filename='rowers/tests/testdata/thyro.csv')
uploadoptions = {
'workouttype': 'water',
'boattype': '1x',
'notes': 'A test file upload',
'stravaid': '122',
'secret': UPLOAD_SERVICE_SECRET,
'user': self.u.id,
'file': result['filename'],
}
url = reverse('workout_upload_api')
response = self.c.post(url, uploadoptions)
self.assertEqual(response.status_code,200)
# check if the workout was created
ws = Workout.objects.filter(user=self.r)
self.assertEqual(len(ws),1)
w = ws[0]
self.assertEqual(w.workoutsource,'strava')
self.assertEqual(w.privacy,'hidden')
# test some analysis, should only use the workouts with workoutsource != strava
#@patch('rowers.dataprep.read_data', side_effect=mocked_read_data)
#def test_workouts_analysis(self, mocked_read_data):
def test_workouts_analysis(self):
login = self.c.login(username=self.u.username, password=self.password)
self.assertTrue(login)
url = '/rowers/history/'
response = self.c.get(url)
self.assertEqual(response.status_code,200)
url = '/rowers/history/data/'
response = self.c.get(url)
self.assertEqual(response.status_code,200)
# response.json() has a key "script" with a javascript script
# check if this is correct
self.assertTrue('script' in response.json())
# now check histogram
startdate = (self.user_workouts[0].startdatetime-datetime.timedelta(days=3)).date()
enddate = (self.user_workouts[0].startdatetime+datetime.timedelta(days=3)).date()
# make sure the dates are not naive
try:
startdate = pytz.utc.localize(startdate)
except (ValueError, AttributeError):
pass
try:
enddate = pytz.utc.localize(enddate)
except (ValueError, AttributeError):
pass
form_data = {
'function':'histo',
'xparam':'hr',
'plotfield':'spm',
'yparam':'pace',
'groupby':'spm',
'palette':'monochrome_blue',
'xaxis':'time',
'yaxis1':'power',
'yaxis2':'hr',
'startdate':startdate,
'enddate':enddate,
'plottype':'scatter',
'spmmin':15,
'spmmax':55,
'workmin':0,
'workmax':1500,
'includereststrokes':False,
'modality':'all',
'waterboattype':['1x','2x','4x'],
'userid':self.u.id,
'workouts':[w.id for w in Workout.objects.filter(user=self.r)],
}
form = AnalysisChoiceForm(form_data)
optionsform = AnalysisOptionsForm(form_data)
dateform = DateRangeForm(form_data)
result = form.is_valid()
if not result:
print(form.errors)
self.assertTrue(form.is_valid())
self.assertTrue(optionsform.is_valid())
self.assertTrue(dateform.is_valid())
response = self.c.post('/rowers/user-analysis-select/',form_data)
self.assertEqual(response.status_code,200)
# count number of workouts by counting the number of occurences of '<label for="id_workouts_xx">' in response.content where xx is a number
# print all lines of response.content that contain '<label for="id_workouts_'
#print([line for line in response.content.decode('utf-8').split('\n') if '<label for="id_workouts_' in line])
#print(form_data['workouts'])
#self.assertEqual(response.content.count(b'<label for="id_workouts_'),2) <-- if we forbid the user to use strava workouts
self.assertEqual(response.content.count(b'<label for="id_workouts_'),5)
# get data from histodata function
ws = Workout.objects.filter(user=self.r)
script, div = histodata(ws,form_data)
# script has a line starting with 'data = [ ... ]'
# we need to get that line
data = [line for line in script.split('\n') if line.startswith('data = [')][0]
# the line should be a list of float values
self.assertTrue(data.startswith('data = ['))
self.assertTrue(data.endswith(']'))
# count the number of commas between the brackets
#self.assertEqual(data.count(','),2062) <-- if we forbid the user to use strava workouts
self.assertEqual(data.count(','),5155)
class OwnApi(TestCase):
def setUp(self):
self.u = UserFactory()
@@ -36,9 +478,7 @@ class OwnApi(TestCase):
birthdate=faker.profile()['birthdate'],
gdproptin=True, ftpset=True,surveydone=True,
gdproptindate=timezone.now(),
rowerplan='coach',subscription_id=1)
rowerplan='pro',subscription_id=1)
self.c = Client()
self.user_workouts = WorkoutFactory.create_batch(5, user=self.r)
self.factory = RequestFactory()

View File

@@ -106,10 +106,26 @@ class ChallengesTest(TestCase):
workouttype = 'water',
)
self.wthyro.startdatetime = arrow.get(nu).datetime
self.wthyro.date = nu.date()
self.wthyro.save()
result = get_random_file(filename='rowers/tests/testdata/thyro.csv')
self.w_strava = WorkoutFactory(user=self.r,
csvfilename=result['filename'],
starttime=result['starttime'],
startdatetime=result['startdatetime'],
duration=result['duration'],
distance=result['totaldist'],
workouttype = 'water',
workoutsource = 'strava',
privacy = 'hidden',
)
self.w_strava.startdatetime = arrow.get(nu).datetime
self.w_strava.date = nu.date()
self.w_strava.save()
result = get_random_file(filename='rowers/tests/testdata/thyro.csv')
self.wthyro2 = WorkoutFactory(user=self.r2,
csvfilename=result['filename'],
@@ -591,6 +607,78 @@ class ChallengesTest(TestCase):
self.assertEqual(response.status_code, 200)
# repeat previous test for self.w_strava, but the response status of virtualevent_submit_result_view should be 403 and len(records) should be 0
@patch('django.contrib.gis.geoip2.GeoIP2.city', side_effect=mocked_requests)
def test_fastestrace_view_strava(self, mock_get):
login = self.c.login(username=self.u.username, password=self.password)
self.assertTrue(login)
race = self.FastestRace
if self.r.birthdate:
age = calculate_age(self.r.birthdate)
else:
age = 25
# look at event
url = reverse('virtualevent_view',kwargs={'id':race.id})
response = self.c.get(url)
self.assertEqual(response.status_code,200)
# register
url = reverse('virtualevent_register_view',kwargs={'id':race.id})
response = self.c.get(url)
self.assertEqual(response.status_code,200)
form_data = {
'teamname': 'ApeTeam',
'boatclass': 'water',
'boattype': '1x',
'weightcategory': 'hwt',
'adaptiveclass': 'None',
'age': age,
'mix': False,
'acceptsocialmedia': True,
}
form = VirtualRaceResultForm(form_data)
self.assertTrue(form.is_valid())
response = self.c.post(url,form_data,follow=True)
expected_url = reverse('virtualevent_view',kwargs={'id':race.id})
self.assertRedirects(response, expected_url=expected_url,
status_code=302,target_status_code=200)
self.assertEqual(response.status_code, 200)
# submit workout
url = reverse('virtualevent_submit_result_view',kwargs={'id':race.id,'workoutid':self.w_strava.id})
response = self.c.get(url)
self.assertEqual(response.status_code, 200)
# response.content should have a form with only one instance of <label for="id_workouts_0">
self.assertEqual(response.content.count(b'<label for="id_workouts_0">'),1)
records = IndoorVirtualRaceResult.objects.filter(userid=self.u.id)
self.assertEqual(len(records),1)
record = records[0]
form_data = {
'workouts':[self.w_strava.id],
'record':record.id,
}
response = self.c.post(url,form_data,follow=True)
self.assertEqual(response.status_code, 200)
# in response.content, there should be a p with class errormessage and the text "Error in form"
self.assertTrue(b'Error in form' in response.content)
@patch('django.contrib.gis.geoip2.GeoIP2.city', side_effect=mocked_requests)
def test_virtualevents_view(self, mock_get):

Binary file not shown.

BIN
rowers/tests/testdata/thyro.csv.gz vendored Normal file

Binary file not shown.

View File

@@ -144,14 +144,20 @@ def do_sync(w, options, quick=False):
w.uploadedtostrava = options['stravaid']
# upload_to_strava = False
do_strava_export = False
w.workoutsource = 'strava'
w.privacy = 'hidden'
w.save()
record = create_or_update_syncrecord(w.user, w, stravaid=options['stravaid'])
# strava, we shall not sync to other sites -> return
return 1
except KeyError:
pass
do_icu_export = False
if w.user.intervals_auto_export is True:
do_icu_export = True
if w.workoutsource == 'strava':
do_icu_export = False
else:
try:
do_icu_export = options['upload_to_Intervals']
@@ -204,6 +210,8 @@ def do_sync(w, options, quick=False):
do_c2_export = False
if w.user.c2_auto_export is True:
do_c2_export = True
if w.workoutsource == 'strava':
do_c2_export = False
else:
try:
do_c2_export = options['upload_to_C2'] or do_c2_export
@@ -245,23 +253,6 @@ def do_sync(w, options, quick=False):
dologging('c2_log.log','Error C2')
pass
if do_strava_export: # pragma: no cover
strava_integration = StravaIntegration(w.user.user)
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:
intervals_integration = IntervalsIntegration(w.user.user)
try:
@@ -295,6 +286,8 @@ def do_sync(w, options, quick=False):
try: # pragma: no cover
upload_to_st = options['upload_to_SportTracks'] or do_st_export
do_st_export = upload_to_st
if w.workoutsource == 'strava':
do_st_export = False
except KeyError:
upload_to_st = False
@@ -317,6 +310,8 @@ def do_sync(w, options, quick=False):
do_tp_export = w.user.trainingpeaks_auto_export
try:
upload_to_tp = options['upload_to_TrainingPeaks'] or do_tp_export
if w.workoutsource == 'strava':
do_tp_export = False
do_tp_export = upload_to_tp
except KeyError:
upload_to_st = False
@@ -334,4 +329,23 @@ def do_sync(w, options, quick=False):
dologging('tp_export.log','No Token Error')
return 0
# we do Strava last.
if do_strava_export: # pragma: no cover
strava_integration = StravaIntegration(w.user.user)
try:
id = strava_integration.workout_export(w)
dologging(
'strava_export_log.log',
'exporting workout {id} as {type}'.format(
id=w.id,
type=w.workouttype,
)
)
except NoTokenError: # pragma: no cover
id = 0
message = "Please connect to Strava first"
except Exception as e:
dologging('stravalog.log', e)
return 1

View File

@@ -664,6 +664,7 @@ urlpatterns = [
re_path(r'^me/messages/$', views.user_messages, name='user_messages'),
re_path(r'^me/messages/delete/$', views.user_messages_delete_all, name='user_messages_delete_all'),
re_path(r'^me/messages/(?P<id>\d+)/markread/$', views.user_message_markread, name='user_message_markread'),
re_path(r'^me/messages/(?P<id>\d+)/delete/$', views.user_message_delete, name='user_message_delete'),
re_path(r'^me/messages/user/(?P<userid>\d+)/$', views.user_messages, name='user_messages'),
re_path(r'^me/delete/$', views.remove_user, name='remove_user'),
re_path(r'^survey/$', views.survey, name='survey'),

View File

@@ -48,6 +48,9 @@ def analysis_new(request,
firstworkout = get_workout(id)
if not is_workout_team(request.user, firstworkout): # pragma: no cover
raise PermissionDenied("You are not allowed to use this workout")
#if workout_is_strava(firstworkout):
# messages.error(request, "You cannot use Strava workouts for analysis")
# raise PermissionDenied("You cannot use Strava workouts for analysis")
firstworkoutquery = Workout.objects.filter(id=encoder.decode_hex(id))
try:
@@ -199,14 +202,14 @@ def analysis_new(request,
startdatetime__lte=enddate,
workouttype__in=modalities,
rankingpiece__in=rankingtypes,
)
)#.exclude(workoutsource='strava')
elif theteam is not None and theteam.viewing == 'coachonly': # pragma: no cover
workouts = Workout.objects.filter(team=theteam, user=r,
startdatetime__gte=startdate,
startdatetime__lte=enddate,
workouttype__in=modalities,
rankingpiece__in=rankingtypes,
)
)#.exclude(workoutsource='strava')
elif thesession is not None:
workouts = get_workouts_session(r, thesession)
else:
@@ -218,6 +221,7 @@ def analysis_new(request,
)
if firstworkout:
workouts = firstworkoutquery | workouts
workouts = workouts.order_by(
"-date", "-starttime"
).exclude(boattype__in=negtypes)
@@ -253,7 +257,7 @@ def analysis_new(request,
else:
selectedworkouts = Workout.objects.filter(id__in=ids)
form.fields["workouts"].queryset = workouts | selectedworkouts
form.fields["workouts"].queryset = (workouts | selectedworkouts)#.exclude(workoutsource='strava')
optionsform = AnalysisOptionsForm(initial={
'modality': modality,
@@ -363,6 +367,10 @@ def trendflexdata(workouts, options, userid=0):
savedata = options.get('savedata',False)
#try:
# workouts = workouts.exclude(workoutsource='strava')
#except AttributeError: # pragma: no cover
# workouts = [w for w in workouts if w.workoutsource != 'strava']
fieldlist, fielddict = dataprep.getstatsfields()
fieldlist = [xparam, yparam, groupby,
@@ -566,6 +574,11 @@ def flexalldata(workouts, options):
trendline = options['trendline']
promember = True
#try:
# workouts = workouts.exclude(workoutsource='strava')
#except AttributeError: # pragma: no cover
# workouts = [w for w in workouts if w.workoutsource != 'strava']
workstrokesonly = not includereststrokes
userid = options['userid']
@@ -612,6 +625,12 @@ def histodata(workouts, options):
workmax = options['workmax']
userid = options['userid']
#try:
# workouts = workouts.exclude(workoutsource='strava')
#except AttributeError: # pragma: no cover
# workouts = [w for w in workouts if w.workoutsource != 'strava']
if userid == 0: # pragma: no cover
extratitle = ''
else:
@@ -645,7 +664,8 @@ def cpdata(workouts, options):
u = User.objects.get(id=userid)
r = u.rower
delta, cpvalue, avgpower, workoutnames, urls = dataprep.fetchcp_new(
r, workouts)
@@ -798,6 +818,11 @@ def cpdata(workouts, options):
def statsdata(workouts, options):
#try:
# workouts = workouts.exclude(workoutsource='strava')
#except AttributeError: # pragma: no cover
# workouts = [w for w in workouts if w.workoutsource != 'strava']
includereststrokes = options['includereststrokes']
ids = options['ids']
@@ -872,12 +897,17 @@ def statsdata(workouts, options):
def comparisondata(workouts, options):
#try:
# workouts = workouts.exclude(workoutsource='strava')
#except AttributeError: # pragma: no cover
# workouts = [w for w in workouts if w.workoutsource != 'strava']
includereststrokes = options['includereststrokes']
xparam = options['xaxis']
yparam1 = options['yaxis1']
plottype = options['plottype']
promember = True
workstrokesonly = not includereststrokes
ids = [w.id for w in workouts]
@@ -915,6 +945,10 @@ def comparisondata(workouts, options):
def boxplotdata(workouts, options):
#try:
# workouts = workouts.exclude(workoutsource='strava')
#except AttributeError:
# workouts = [w for w in workouts if w.workoutsource != 'strava']
includereststrokes = options['includereststrokes']
spmmin = options['spmmin']
@@ -926,7 +960,7 @@ def boxplotdata(workouts, options):
plotfield = options['plotfield']
workstrokesonly = not includereststrokes
datemapping = {
w.id: w.date for w in workouts
}
@@ -1020,11 +1054,15 @@ def analysis_view_data(request, userid=0):
for id in ids:
try:
workouts.append(Workout.objects.get(id=id))
w = Workout.objects.get(id=id)
#if w.workoutsource != 'strava':
# workouts.append(w)
workouts.append(w)
except Workout.DoesNotExist: # pragma: no cover
pass
if function == 'boxplot':
script, div = boxplotdata(workouts, options)
elif function == 'trendflex': # pragma: no cover
@@ -1069,7 +1107,7 @@ def create_marker_workouts_view(request, userid=0,
workouts = Workout.objects.filter(user=theuser.rower, date__gte=startdate,
date__lte=enddate,
workouttype__in=mytypes.rowtypes,
duplicate=False).order_by('date')
duplicate=False).order_by('date')#.exclude(workoutsource='strava')
for workout in workouts:
_ = dataprep.check_marker(workout)
@@ -1113,7 +1151,7 @@ def goldmedalscores_view(request, userid=0,
theuser, startdate=startdate, enddate=enddate,
)
bestworkouts = Workout.objects.filter(id__in=ids).order_by('-date')
bestworkouts = Workout.objects.filter(id__in=ids).order_by('-date')#.exclude(workoutsource='strava')
breadcrumbs = [
{
@@ -1311,7 +1349,7 @@ def performancemanager_view(request, userid=0, mode='rower',
user = therower, date__gte=startdate-datetime.timedelta(days=90),
date__lte=enddate,
duplicate=False,
rankingpiece=True, workouttype__in=mytypes.rowtypes).order_by('date')
rankingpiece=True, workouttype__in=mytypes.rowtypes).order_by('date')#.exclude(workoutsource='strava')
ids = [w.id for w in markerworkouts]
form = PerformanceManagerForm(initial={
@@ -1323,7 +1361,7 @@ def performancemanager_view(request, userid=0, mode='rower',
ids = pd.Series(ids, dtype='int').dropna().values
bestworkouts = Workout.objects.filter(id__in=ids).order_by('-date')
bestworkouts = Workout.objects.filter(id__in=ids).order_by('-date')#.exclude(workoutsource='strava')
breadcrumbs = [
{
@@ -2276,6 +2314,8 @@ def history_view_data(request, userid=0):
ddf = ddf.with_columns(pl.col("time").diff().clip(lower_bound=0).alias("deltat"))
except KeyError: # pragma: no cover
pass
except ColumnNotFoundError:
pass
ddf = dataprep.clean_df_stats_pl(ddf, workstrokesonly=False,
ignoreadvanced=True)
@@ -2288,6 +2328,8 @@ def history_view_data(request, userid=0):
ddict['hrmax'] = int(ddf['hr'].max())
except (KeyError, ValueError, AttributeError, ColumnNotFoundError): # pragma: no cover
ddict['hrmax'] = 0
except ColumnNotFoundError:
ddict['hrmax'] = 0
ddict['powermean'] = int(wavg(ddf, 'power', 'deltat'))
try:

View File

@@ -3397,12 +3397,12 @@ def virtualevent_submit_result_view(request, id=0, workoutid=0):
startdatetime__gte=startdatetime,
startdatetime__lte=enddatetime,
distance__gte=race.approximate_distance,
).order_by("-date", "-startdatetime", "id")
).order_by("-date", "-startdatetime", "id").exclude(workoutsource='strava')
if not ws: # pragma: no cover
messages.info(
request,
'You have no workouts executed during the race window. Please upload a result or enter it manually.'
'You have no eligible workouts executed during the race window. Please upload a result or enter it manually.'
)
url = reverse('virtualevent_view',
@@ -3436,6 +3436,7 @@ def virtualevent_submit_result_view(request, id=0, workoutid=0):
splitsecond = 0
recordid = w_form.cleaned_data['record']
else:
messages.error(request,"Error in form")
selectedworkout = None
if selectedworkout is not None:
@@ -3518,7 +3519,12 @@ def virtualevent_submit_result_view(request, id=0, workoutid=0):
else:
if workoutid:
workoutdata['initial'] = encoder.decode_hex(workoutid)
try:
w = Workout.objects.get(id=workoutid)
if w.workoutsource != 'strava':
workoutdata['initial'] = encoder.decode_hex(workoutid)
except Workout.DoesNotExist:
pass
w_form = WorkoutRaceSelectForm(workoutdata, entries)
breadcrumbs = [

View File

@@ -28,6 +28,7 @@ from rest_framework.response import Response
from rq.job import Job
from rules.contrib.views import permission_required, objectgetter
from django.core.cache import cache
from django.db import models
from django.utils.crypto import get_random_string
from rq.registry import StartedJobRegistry
from rq.exceptions import NoSuchJobError
@@ -81,7 +82,8 @@ from rowers.rower_rules import (
can_add_workout_member, can_plan_user, is_paid_coach,
can_start_trial, can_start_plantrial, can_start_coachtrial,
can_plan, is_workout_team,
is_promember,user_is_basic, is_coachtrial, is_coach
is_promember,user_is_basic, is_coachtrial, is_coach,
workout_is_strava
)
from django.shortcuts import render
@@ -179,7 +181,13 @@ from rowers.models import ( RowerPowerForm, RowerHRZonesForm, SimpleRowerPowerFo
IndoorVirtualRaceForm, PlannedSessionCommentForm, Alert,
Condition, StaticChartRowerForm, FollowerForm,
VirtualRaceAthleteForm, InstantPlanForm, DataRowerForm,
StepEditorForm, iDokladToken )
StepEditorForm, iDokladToken,
RowerExportFormStrava, RowerExportFormPolar,
RowerExportFormSportTracks, RowerExportFormTrainingPeaks,
RowerExportFormConcept2, RowerExportFormGarmin,
RowerExportFormIntervals, RowerExportFormRP3,
RowerExportFormNK,
)
from rowers.models import (
FavoriteForm, BaseFavoriteFormSet, SiteAnnouncement, BasePlannedSessionFormSet,
get_course_timezone, BaseConditionFormSet,

View File

@@ -279,7 +279,6 @@ def user_message_delete(request,id=0): # pragma: no cover
messages.error(request,'Could not find this message')
url = reverse('user_messages')
return HttpResponseRedirect(url)
if msg.receiver == request.user.rower:
msg.delete()
@@ -458,19 +457,43 @@ def rower_exportsettings_view(request, userid=0):
'polar_auto_import': 'polartoken',
'c2_auto_export': 'c2token',
'c2_auto_import': 'c2token',
'runkeeper_auto_export': 'runkeepertoken',
'sporttracks_auto_export': 'sporttrackstoken',
'strava_auto_export': 'stravatoken',
'strava_auto_import': 'stravatoken',
'strava_auto_delete': 'stravatoken',
'trainingpeaks_auto_export': 'tptoken',
'rp3_auto_import': 'rp3token',
'nk_auto_import': 'nktoken'
'nk_auto_import': 'nktoken',
'intervals_auto_export': 'intervals_token',
'intervals_resample_to_1s': 'intervals_token',
}
r = getrequestrowercoachee(request, userid=userid)
forms = {
'polar': RowerExportFormPolar(instance=r),
'c2': RowerExportFormConcept2(instance=r),
'sporttracks': RowerExportFormSportTracks(instance=r),
'strava': RowerExportFormStrava(instance=r),
'trainingpeaks': RowerExportFormTrainingPeaks(instance=r),
'rp3': RowerExportFormRP3(instance=r),
'intervals': RowerExportFormIntervals(instance=r),
'nk': RowerExportFormNK(instance=r),
'garmin': RowerExportFormGarmin(instance=r),
}
if request.method == 'POST':
form = RowerExportForm(request.POST)
forms = {
'polar': RowerExportFormPolar(request.POST, instance=r),
'c2': RowerExportFormConcept2(request.POST, instance=r),
'sporttracks': RowerExportFormSportTracks(request.POST, instance=r),
'strava': RowerExportFormStrava(request.POST, instance=r),
'trainingpeaks': RowerExportFormTrainingPeaks(request.POST, instance=r),
'rp3': RowerExportFormRP3(request.POST, instance=r),
'intervals': RowerExportFormIntervals(request.POST, instance=r),
'nk': RowerExportFormNK(request.POST, instance=r),
'garmin': RowerExportFormGarmin(request.POST, instance=r),
}
if form.is_valid():
cd = form.cleaned_data
if r.rowerplan == 'basic': # pragma: no cover
@@ -529,6 +552,7 @@ def rower_exportsettings_view(request, userid=0):
return render(request, 'rower_exportsettings.html',
{'form': form,
'forms': forms,
'rower': r,
'breadcrumbs': breadcrumbs,
'grants': grants,

View File

@@ -2204,25 +2204,25 @@ def workouts_view(request, message='', successmessage='',
team=theteam,
startdatetime__gte=startdate,
startdatetime__lte=enddate,
privacy='visible').order_by("-date", "-starttime")
privacy='visible').order_by("-date", "-starttime").exclude(workoutsource='strava')
g_workouts = Workout.objects.filter(
team=theteam,
startdatetime__gte=activity_startdate,
startdatetime__lte=activity_enddate,
duplicate=False,
privacy='visible').order_by("-date", "-starttime")
privacy='visible').order_by("-date", "-starttime").exclude(workoutsource='strava')
elif theteam.viewing == 'coachonly': # pragma: no cover
workouts = Workout.objects.filter(
team=theteam, user=r,
startdatetime__gte=startdate,
startdatetime__lte=enddate,
privacy='visible').order_by("-startdatetime")
privacy='visible').order_by("-startdatetime").exclude(workoutsource='strava')
g_workouts = Workout.objects.filter(
team=theteam, user=r,
startdatetime__gte=activity_startdate,
startdatetime__lte=activity_enddate,
duplicate=False,
privacy='visible').order_by("-startdatetime")
privacy='visible').order_by("-startdatetime").exclude(workoutsource='strava')
elif request.user != r.user:
theteam = None
@@ -2230,13 +2230,13 @@ def workouts_view(request, message='', successmessage='',
user=r,
startdatetime__gte=startdate,
startdatetime__lte=enddate,
privacy='visible').order_by("-date", "-starttime")
privacy='visible').order_by("-date", "-starttime").exclude(workoutsource='strava')
g_workouts = Workout.objects.filter(
user=r,
startdatetime__gte=activity_startdate,
startdatetime__lte=activity_enddate,
duplicate=False,
privacy='visible').order_by("-startdatetime")
privacy='visible').order_by("-startdatetime").exclude(workoutsource='strava')
else:
theteam = None
workouts = Workout.objects.filter(
@@ -2252,7 +2252,7 @@ def workouts_view(request, message='', successmessage='',
if g_workouts.count() == 0:
g_workouts = Workout.objects.filter(
user=r,
startdatetime__gte=timezone.now()-timedelta(days=15)).order_by("-startdatetime")
startdatetime__gte=timezone.now()-timedelta(days=15)).order_by("-startdatetime").exclude(workoutsource='strava')
g_enddate = timezone.now()
g_startdate = (timezone.now()-timedelta(days=15))
@@ -2266,7 +2266,8 @@ def workouts_view(request, message='', successmessage='',
reduce(operator.and_,
(Q(name__icontains=q) for q in query_list)) |
reduce(operator.and_,
(Q(notes__icontains=q) for q in query_list))
(Q(notes__icontains=q) for q in query_list)),
exclude_strava=False,
)
searchform = SearchForm(initial={'q': query})
else:
@@ -4699,6 +4700,7 @@ def workout_map_view(request, id=0):
u = w.user.user
r = getrower(u)
rowdata = rdata(csvfile=f1)
hascoordinates = 1
if rowdata != 0:
try:
@@ -4933,7 +4935,7 @@ def workout_upload_api(request):
# only allow local host
hostt = request.get_host().split(':')
if hostt[0] not in ['localhost', '127.0.0.1', 'dev.rowsandall.com', 'rowsandall.com']:
if hostt[0] not in ['localhost', '127.0.0.1', 'dev.rowsandall.com', 'rowsandall.com','testserver']:
message = {'status': 'false',
'message': 'permission denied for host '+hostt[0]}
return JSONResponse(status=403, data=message)
@@ -4986,6 +4988,7 @@ def workout_upload_api(request):
boatname = post_data.get('boatName','')
portStarboard = post_data.get('portStarboard', 1)
empowerside = 'port'
stravaid = post_data.get('stravaid','')
if portStarboard == 1:
empowerside = 'starboard'
@@ -5609,17 +5612,6 @@ def workout_upload_view(request,
return response
else:
if not is_ajax:
if r.c2_auto_export and ispromember(r.user): # pragma: no cover
uploadoptions['upload_to_C2'] = True
if r.strava_auto_export and ispromember(r.user): # pragma: no cover
uploadoptions['upload_to_Strava'] = True
if r.sporttracks_auto_export and ispromember(r.user): # pragma: no cover
uploadoptions['upload_to_SportTracks'] = True
if r.trainingpeaks_auto_export and ispromember(r.user): # pragma: no cover
uploadoptions['upload_to_TrainingPeaks'] = True
form = DocumentsForm(initial=docformoptions)
optionsform = UploadOptionsForm(initial=uploadoptions,

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB