diff --git a/rowers/dataprep.py b/rowers/dataprep.py index 827a9717..a824f571 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 @@ -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 diff --git a/rowers/forms.py b/rowers/forms.py index 0ebc5fb3..506f900f 100644 --- a/rowers/forms.py +++ b/rowers/forms.py @@ -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') diff --git a/rowers/integrations/intervals.py b/rowers/integrations/intervals.py index b6498821..32e191b0 100644 --- a/rowers/integrations/intervals.py +++ b/rowers/integrations/intervals.py @@ -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 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/management/commands/setstravaprivate.py b/rowers/management/commands/setstravaprivate.py new file mode 100644 index 00000000..c90c8f3f --- /dev/null +++ b/rowers/management/commands/setstravaprivate.py @@ -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.')) diff --git a/rowers/models.py b/rowers/models.py index 15a66474..51e90d7a 100644 --- a/rowers/models.py +++ b/rowers/models.py @@ -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): diff --git a/rowers/plannedsessions.py b/rowers/plannedsessions.py index b326b0b2..e9298d77 100644 --- a/rowers/plannedsessions.py +++ b/rowers/plannedsessions.py @@ -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 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 @@

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

    diff --git a/rowers/templates/rower_exportsettings.html b/rowers/templates/rower_exportsettings.html index b6aae035..35c5b974 100644 --- a/rowers/templates/rower_exportsettings.html +++ b/rowers/templates/rower_exportsettings.html @@ -7,8 +7,10 @@ {% block main %}

    Import and Export Settings for {{ rower.user.first_name }} {{ rower.user.last_name }}

    +
    + {% csrf_token %}
    -

    Click on one of the icons below to connect to the service of your - choice or to renew the authorization.

    -

    connect with strava

    -

    connect with Concept2

    -

    connect with NK Logbook

    -

    connect with SportTracks

    -

    connect with Polar

    -

    connect with Polar

    - -

    connect with Garmin

    -

    connect with RP3

    -

    connect with Rojabo

    -

    connect with intervals.icu

    + + {% endblock %} diff --git a/rowers/tests/test_api.py b/rowers/tests/test_api.py index 74dff25d..14f0429a 100644 --- a/rowers/tests/test_api.py +++ b/rowers/tests/test_api.py @@ -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 ... 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 ... 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 ... 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 ... 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 ... 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 '