Private
Public Access
1
0

Merge branch 'release/v22.5.0'

This commit is contained in:
2024-12-15 14:15:46 +01:00
18 changed files with 766 additions and 47 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
@@ -1304,8 +1307,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

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

@@ -1241,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)
@@ -1441,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'),
@@ -3723,6 +3740,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)

View File

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

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

@@ -154,15 +154,23 @@
</li>
<li class="rounder">
<h2>Strava</h2>
<p><input type="submit" value="Save"></p>
{{ forms.strava.as_p }}
<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>
{% if grants %}
<li class="rounder">

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
@@ -278,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
@@ -300,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

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

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:
@@ -4933,7 +4934,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 +4987,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'