diff --git a/rowers/dataprep.py b/rowers/dataprep.py
index 28bf84be..95fe1fc1 100644
--- a/rowers/dataprep.py
+++ b/rowers/dataprep.py
@@ -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
diff --git a/rowers/integrations/strava.py b/rowers/integrations/strava.py
index 8df2cb31..8c3bb595 100644
--- a/rowers/integrations/strava.py
+++ b/rowers/integrations/strava.py
@@ -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)
diff --git a/rowers/interactiveplots.py b/rowers/interactiveplots.py
index 4e4d4c74..2f0d2a5d 100644
--- a/rowers/interactiveplots.py
+++ b/rowers/interactiveplots.py
@@ -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]
diff --git a/rowers/models.py b/rowers/models.py
index 047d8d0a..8f50cff9 100644
--- a/rowers/models.py
+++ b/rowers/models.py
@@ -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)
diff --git a/rowers/plannedsessions.py b/rowers/plannedsessions.py
index d9e6add5..1918d750 100644
--- a/rowers/plannedsessions.py
+++ b/rowers/plannedsessions.py
@@ -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
diff --git a/rowers/rower_rules.py b/rowers/rower_rules.py
index c8a14c14..68529023 100644
--- a/rowers/rower_rules.py
+++ b/rowers/rower_rules.py
@@ -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
diff --git a/rowers/templates/developers.html b/rowers/templates/developers.html
index ff0f01d0..3d966f82 100644
--- a/rowers/templates/developers.html
+++ b/rowers/templates/developers.html
@@ -8,7 +8,7 @@
-
On this page, a work in progress, I will collect useful information
+
On this page, I will collect useful information
for developers of rowing data apps and hardware.
I presume you have an app (smartphone app, dedicated hardware, web site)
@@ -61,11 +61,11 @@
Using the REST API
-
We are building a REST API which will allow you to post and
+
We have a REST API which will allow you to post and
receive stroke
data from the site directly.
-
The REST API is a work in progress. We are open to improvement
+
We are open to improvement
suggestions (provided they don't break existing apps). Please send
email to info@rowsandall.com
with questions and/or suggestions. We
@@ -84,7 +84,6 @@
Disadvantages
-
The API is not stable and not fully tested yet.
You need to register your app with us. We can revoke your
permissions if you misuse them.
The user user must grant permissions to your app.
@@ -114,7 +113,7 @@
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
Authentication
@@ -728,11 +727,11 @@
peakdriveforce: Peak handle force (lbs)
lapidx: Lap identifier
hr: Heart rate (beats per minute)
-
wash: Wash as defined per Empower oarlock (degrees)
-
catch: Catch angle per Empower oarlock (degrees)
-
finish: Finish angle per Empower oarlock (degrees)
-
peakforceangle: Peak Force Angle per Empower oarlock (degrees)
-
slip: Slip as defined per Empower oarlock (degrees)
+
wash: Wash as defined for your smart power measuring oarlock (degrees)
+
catch: Catch angle for your smart power measuring oarlock (degrees)
+
finish: Finish angle for your smart power measuring oarlock (degrees)
+
peakforceangle: Peak Force Angle for your smart power measuring oarlock (degrees)
+
slip: Slip as defined for your smart power measuring oarlock (degrees)
diff --git a/rowers/templates/rower_exportsettings.html b/rowers/templates/rower_exportsettings.html
index 13418d15..7056a2c7 100644
--- a/rowers/templates/rower_exportsettings.html
+++ b/rowers/templates/rower_exportsettings.html
@@ -162,7 +162,14 @@
a workout on Strava. If you want Deletions to propagate to Rowsandall, tick the Strava Auto Delete
check box.
-
+ {% if rower.stravatoken and rower.stravatoken != '' %}
+
+ You are connected to Strava. 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.
+
+ {% endif %}
{% if grants %}
diff --git a/rowers/tests/test_api.py b/rowers/tests/test_api.py
index 74dff25d..c8608415 100644
--- a/rowers/tests/test_api.py
+++ b/rowers/tests/test_api.py
@@ -22,12 +22,447 @@ 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 ... 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']])
+
+ 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 ... 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']])
+
+ 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 ... 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']])
+
+ 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 ... 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']])
+
+ 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 ... 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']])
+
+ 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 '