From 33f4fc2513220ad44c1e678c857b54091ee0026e Mon Sep 17 00:00:00 2001 From: Sam Date: Mon, 23 Dec 2024 16:17:22 +0100 Subject: [PATCH] API & Client - add analysis visibility level --- ...0bec0a410_add_analysis_visibility_level.py | 64 ++ fittrackee/tests/test_visibility_levels.py | 44 +- fittrackee/tests/users/test_auth_api.py | 53 +- .../tests/users/test_users_export_data.py | 6 + fittrackee/tests/users/test_users_model.py | 7 + .../tests/workouts/test_timeline_api.py | 1 + .../test_workouts_api_0_get_workout.py | 298 +++++++--- .../workouts/test_workouts_api_1_post.py | 140 ++++- .../workouts/test_workouts_api_2_patch.py | 156 ++++- .../tests/workouts/test_workouts_model.py | 551 +++++++++++++++--- fittrackee/users/auth.py | 22 +- fittrackee/users/models.py | 6 + fittrackee/visibility_levels.py | 17 +- fittrackee/workouts/models.py | 59 +- fittrackee/workouts/utils/workouts.py | 41 +- fittrackee/workouts/workouts.py | 45 +- .../User/ProfileDisplay/UserPreferences.vue | 4 + .../ProfileEdition/UserPreferencesEdition.vue | 46 +- .../WorkoutDetail/WorkoutVisibility.vue | 19 +- .../Workout/WorkoutDetail/index.vue | 8 +- .../src/components/Workout/WorkoutEdition.vue | 60 +- .../src/locales/en/visibility_levels.json | 3 +- .../src/locales/fr/visibility_levels.json | 3 +- .../src/store/modules/workouts/actions.ts | 4 +- fittrackee_client/src/types/user.ts | 2 + fittrackee_client/src/types/workouts.ts | 5 + .../src/utils/visibility_levels.ts | 31 +- .../src/views/workouts/Workout.vue | 3 +- .../unit/utils/visibility_levels.spec.ts | 16 +- 29 files changed, 1433 insertions(+), 281 deletions(-) create mode 100644 fittrackee/migrations/versions/44_8a80bec0a410_add_analysis_visibility_level.py diff --git a/fittrackee/migrations/versions/44_8a80bec0a410_add_analysis_visibility_level.py b/fittrackee/migrations/versions/44_8a80bec0a410_add_analysis_visibility_level.py new file mode 100644 index 000000000..258302bb0 --- /dev/null +++ b/fittrackee/migrations/versions/44_8a80bec0a410_add_analysis_visibility_level.py @@ -0,0 +1,64 @@ +"""add analysis visibility level + +Revision ID: 8a80bec0a410 +Revises: 70f12f8c0218 +Create Date: 2024-12-23 10:16:23.026455 + +""" + +import sqlalchemy as sa +from alembic import op +from sqlalchemy.dialects import postgresql + + +# revision identifiers, used by Alembic. +revision = '8a80bec0a410' +down_revision = '70f12f8c0218' +branch_labels = None +depends_on = None + +visibility_levels = postgresql.ENUM( + 'PUBLIC', + 'FOLLOWERS_AND_REMOTE', # for a next version, not used for now + 'FOLLOWERS', + 'PRIVATE', + name='visibility_levels', +) + + +def upgrade(): + visibility_levels.create(op.get_bind()) + with op.batch_alter_table('users', schema=None) as batch_op: + batch_op.add_column( + sa.Column( + 'analysis_visibility', + visibility_levels, + server_default='PRIVATE', + nullable=True, + ) + ) + op.execute("UPDATE users SET analysis_visibility = 'PRIVATE';") + op.alter_column('users', 'analysis_visibility', nullable=False) + + with op.batch_alter_table('workouts', schema=None) as batch_op: + batch_op.add_column( + sa.Column( + 'analysis_visibility', + visibility_levels, + server_default='PRIVATE', + nullable=True, + ) + ) + op.execute("UPDATE workouts SET analysis_visibility = 'PRIVATE';") + op.alter_column('workouts', 'analysis_visibility', nullable=False) + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('workouts', schema=None) as batch_op: + batch_op.drop_column('analysis_visibility') + + with op.batch_alter_table('users', schema=None) as batch_op: + batch_op.drop_column('analysis_visibility') + + # ### end Alembic commands ### diff --git a/fittrackee/tests/test_visibility_levels.py b/fittrackee/tests/test_visibility_levels.py index 1632f9776..cdf401c8e 100644 --- a/fittrackee/tests/test_visibility_levels.py +++ b/fittrackee/tests/test_visibility_levels.py @@ -1,57 +1,69 @@ import pytest -from fittrackee.visibility_levels import VisibilityLevel, get_map_visibility +from fittrackee.visibility_levels import ( + VisibilityLevel, + get_calculated_visibility, +) class TestMapVisibility: @pytest.mark.parametrize( - 'input_map_visibility', + 'input_visibility', [ VisibilityLevel.PUBLIC, VisibilityLevel.FOLLOWERS, VisibilityLevel.PRIVATE, ], ) - def test_it_returns_map_visibility_when_workout_visibility_is_public( + def test_it_returns_visibility_when_parent_visibility_is_public( self, - input_map_visibility: VisibilityLevel, + input_visibility: VisibilityLevel, ) -> None: assert ( - get_map_visibility(input_map_visibility, VisibilityLevel.PUBLIC) - == input_map_visibility + get_calculated_visibility( + visibility=input_visibility, + parent_visibility=VisibilityLevel.PUBLIC, + ) + == input_visibility ) @pytest.mark.parametrize( - 'input_map_visibility, expected_map_visibility', + 'input_visibility, expected_visibility', [ (VisibilityLevel.PUBLIC, VisibilityLevel.FOLLOWERS), (VisibilityLevel.FOLLOWERS, VisibilityLevel.FOLLOWERS), (VisibilityLevel.PRIVATE, VisibilityLevel.PRIVATE), ], ) - def test_it_returns_map_visibility_when_workout_visibility_is_followers_only( # noqa + def test_it_returns_map_visibility_when_analysis_visibility_is_followers_only( # noqa self, - input_map_visibility: VisibilityLevel, - expected_map_visibility: VisibilityLevel, + input_visibility: VisibilityLevel, + expected_visibility: VisibilityLevel, ) -> None: assert ( - get_map_visibility(input_map_visibility, VisibilityLevel.FOLLOWERS) - == expected_map_visibility + get_calculated_visibility( + visibility=input_visibility, + parent_visibility=VisibilityLevel.FOLLOWERS, + ) + == expected_visibility ) @pytest.mark.parametrize( - 'input_map_visibility', + 'input_visibility', [ VisibilityLevel.PUBLIC, VisibilityLevel.FOLLOWERS, VisibilityLevel.PRIVATE, ], ) - def test_it_returns_map_visibility_when_workout_visibility_is_private( + def test_it_returns_visibility_when_parent_visibility_is_private( self, - input_map_visibility: VisibilityLevel, + input_visibility: VisibilityLevel, ) -> None: assert ( - get_map_visibility(input_map_visibility, VisibilityLevel.PRIVATE) + get_calculated_visibility( + visibility=input_visibility, + parent_visibility=VisibilityLevel.PRIVATE, + ) == VisibilityLevel.PRIVATE ) diff --git a/fittrackee/tests/users/test_auth_api.py b/fittrackee/tests/users/test_auth_api.py index 69429e4d9..03f96e450 100644 --- a/fittrackee/tests/users/test_auth_api.py +++ b/fittrackee/tests/users/test_auth_api.py @@ -1598,7 +1598,8 @@ def test_it_updates_user_preferences( use_dark_mode=True, use_raw_gpx_speed=True, date_format='yyyy-MM-dd', - map_visibility='followers_only', + map_visibility='private', + analysis_visibility='followers_only', workouts_visibility='public', manually_approves_followers=False, hide_profile_in_users_directory=False, @@ -1624,10 +1625,36 @@ def test_it_updates_user_preferences( assert data['data']['hide_profile_in_users_directory'] is False @pytest.mark.parametrize( - 'input_map_visibility,input_workout_visibility', + 'input_map_visibility,input_analysis_visibility,input_workout_visibility,expected_map_visibility,expected_analysis_visibility', [ - (VisibilityLevel.FOLLOWERS, VisibilityLevel.PRIVATE), - (VisibilityLevel.PUBLIC, VisibilityLevel.FOLLOWERS), + ( + VisibilityLevel.FOLLOWERS, + VisibilityLevel.PRIVATE, + VisibilityLevel.PRIVATE, + VisibilityLevel.PRIVATE, + VisibilityLevel.PRIVATE, + ), + ( + VisibilityLevel.FOLLOWERS, + VisibilityLevel.PRIVATE, + VisibilityLevel.PRIVATE, + VisibilityLevel.PRIVATE, + VisibilityLevel.PRIVATE, + ), + ( + VisibilityLevel.PRIVATE, + VisibilityLevel.FOLLOWERS, + VisibilityLevel.PUBLIC, + VisibilityLevel.PRIVATE, + VisibilityLevel.FOLLOWERS, + ), + ( + VisibilityLevel.PUBLIC, + VisibilityLevel.PUBLIC, + VisibilityLevel.PUBLIC, + VisibilityLevel.PUBLIC, + VisibilityLevel.PUBLIC, + ), ], ) def test_it_updates_user_preferences_with_valid_map_visibility( @@ -1635,7 +1662,10 @@ def test_it_updates_user_preferences_with_valid_map_visibility( app: Flask, user_1: User, input_map_visibility: VisibilityLevel, + input_analysis_visibility: VisibilityLevel, input_workout_visibility: VisibilityLevel, + expected_map_visibility: VisibilityLevel, + expected_analysis_visibility: VisibilityLevel, ) -> None: client, auth_token = self.get_test_client_and_auth_token( app, user_1.email @@ -1652,13 +1682,14 @@ def test_it_updates_user_preferences_with_valid_map_visibility( imperial_units=True, display_ascent=True, date_format='MM/dd/yyyy', - map_visibility=input_map_visibility.value, start_elevation_at_zero=False, use_raw_gpx_speed=False, - workouts_visibility=input_workout_visibility.value, manually_approves_followers=True, hide_profile_in_users_directory=True, use_dark_mode=None, + map_visibility=input_map_visibility.value, + analysis_visibility=input_analysis_visibility.value, + workouts_visibility=input_workout_visibility.value, ) ), headers=dict(Authorization=f'Bearer {auth_token}'), @@ -1666,7 +1697,11 @@ def test_it_updates_user_preferences_with_valid_map_visibility( assert response.status_code == 200 data = json.loads(response.data.decode()) - assert data['data']['map_visibility'] == input_workout_visibility.value + assert data['data']['map_visibility'] == expected_map_visibility.value + assert ( + data['data']['analysis_visibility'] + == expected_analysis_visibility.value + ) assert ( data['data']['workouts_visibility'] == input_workout_visibility.value @@ -1686,6 +1721,7 @@ def test_it_updates_user_preferences_when_user_is_suspended( content_type='application/json', data=json.dumps( dict( + analysis_visibility=VisibilityLevel.PUBLIC.value, timezone='America/New_York', weekm=True, language='fr', @@ -1707,6 +1743,9 @@ def test_it_updates_user_preferences_when_user_is_suspended( assert response.status_code == 200 data = json.loads(response.data.decode()) assert data['data']['map_visibility'] == VisibilityLevel.PUBLIC.value + assert ( + data['data']['analysis_visibility'] == VisibilityLevel.PUBLIC.value + ) assert ( data['data']['workouts_visibility'] == VisibilityLevel.PUBLIC.value ) diff --git a/fittrackee/tests/users/test_users_export_data.py b/fittrackee/tests/users/test_users_export_data.py index 026349ef1..a1f7e92c2 100644 --- a/fittrackee/tests/users/test_users_export_data.py +++ b/fittrackee/tests/users/test_users_export_data.py @@ -87,6 +87,9 @@ def test_it_returns_data_for_workout_without_gpx( 'description': None, 'liked': workout_cycling_user_1.liked_by(user_1), 'likes_count': workout_cycling_user_1.likes.count(), + 'analysis_visibility': ( + workout_cycling_user_1.calculated_analysis_visibility.value + ), 'map_visibility': ( workout_cycling_user_1.calculated_map_visibility.value ), @@ -140,6 +143,9 @@ def test_it_returns_data_for_workout_with_gpx( 'description': None, 'liked': workout.liked_by(user_1), 'likes_count': workout.likes.count(), + 'analysis_visibility': ( + workout.calculated_analysis_visibility.value + ), 'map_visibility': workout.calculated_map_visibility.value, 'workout_visibility': workout.workout_visibility.value, } diff --git a/fittrackee/tests/users/test_users_model.py b/fittrackee/tests/users/test_users_model.py index 090c5a174..4d7d49ca4 100644 --- a/fittrackee/tests/users/test_users_model.py +++ b/fittrackee/tests/users/test_users_model.py @@ -172,6 +172,10 @@ def test_it_returns_user_preferences( serialized_user['workouts_visibility'] == user_1.workouts_visibility ) + assert ( + serialized_user['analysis_visibility'] + == user_1.analysis_visibility + ) assert serialized_user['map_visibility'] == user_1.map_visibility assert ( serialized_user['manually_approves_followers'] @@ -299,6 +303,7 @@ def test_it_does_return_user_preferences( assert 'use_raw_gpx_speed' not in serialized_user assert 'use_dark_mode' not in serialized_user assert 'workouts_visibility' not in serialized_user + assert 'analysis_visibility' not in serialized_user assert 'map_visibility' not in serialized_user assert 'manually_approves_followers' not in serialized_user assert 'hide_profile_in_users_directory' not in serialized_user @@ -383,6 +388,7 @@ def test_it_does_return_user_preferences( assert 'use_raw_gpx_speed' not in serialized_user assert 'use_dark_mode' not in serialized_user assert 'workouts_visibility' not in serialized_user + assert 'analysis_visibility' not in serialized_user assert 'map_visibility' not in serialized_user assert 'manually_approves_followers' not in serialized_user assert 'hide_profile_in_users_directory' not in serialized_user @@ -460,6 +466,7 @@ def test_it_does_return_user_preferences( assert 'timezone' not in serialized_user assert 'weekm' not in serialized_user assert 'workouts_visibility' not in serialized_user + assert 'analysis_visibility' not in serialized_user assert 'map_visibility' not in serialized_user assert 'manually_approves_followers' not in serialized_user assert 'hide_profile_in_users_directory' not in serialized_user diff --git a/fittrackee/tests/workouts/test_timeline_api.py b/fittrackee/tests/workouts/test_timeline_api.py index b0a63f144..c6e92a1c8 100644 --- a/fittrackee/tests/workouts/test_timeline_api.py +++ b/fittrackee/tests/workouts/test_timeline_api.py @@ -386,6 +386,7 @@ def test_it_returns_followed_user_workout_map_when_visibility_allows_it( ) -> None: user_2.approves_follow_request_from(user_1) workout_cycling_user_2.workout_visibility = input_visibility + workout_cycling_user_2.analysis_visibility = input_visibility workout_cycling_user_2.map_visibility = input_visibility map_id = self.random_string() workout_cycling_user_2.map_id = map_id diff --git a/fittrackee/tests/workouts/test_workouts_api_0_get_workout.py b/fittrackee/tests/workouts/test_workouts_api_0_get_workout.py index 2111a7ff7..8d626e696 100644 --- a/fittrackee/tests/workouts/test_workouts_api_0_get_workout.py +++ b/fittrackee/tests/workouts/test_workouts_api_0_get_workout.py @@ -18,25 +18,32 @@ class GetWorkoutGpxAsFollowerMixin: @staticmethod - def init_test_data( + def init_test_data_for_follower( workout: Workout, + *, + analysis_visibility: VisibilityLevel = VisibilityLevel.FOLLOWERS, map_visibility: VisibilityLevel, follower: User, followed: User, ) -> None: workout.gpx = 'file.gpx' - workout.workout_visibility = VisibilityLevel.FOLLOWERS + workout.workout_visibility = analysis_visibility + workout.analysis_visibility = analysis_visibility workout.map_visibility = map_visibility followed.approves_follow_request_from(follower) class GetWorkoutGpxPublicVisibilityMixin: @staticmethod - def init_test_data( - workout: Workout, map_visibility: VisibilityLevel + def init_test_data_for_public_workout( + workout: Workout, + *, + analysis_visibility: VisibilityLevel = VisibilityLevel.PUBLIC, + map_visibility: VisibilityLevel, ) -> None: workout.gpx = 'file.gpx' - workout.workout_visibility = VisibilityLevel.PUBLIC + workout.workout_visibility = analysis_visibility + workout.analysis_visibility = VisibilityLevel.PUBLIC workout.map_visibility = map_visibility @@ -437,6 +444,10 @@ def test_it_returns_another_user_workout_when_visibility_is_public( data['data']['workouts'][0]['map_visibility'] == VisibilityLevel.PRIVATE.value ) + assert ( + data['data']['workouts'][0]['analysis_visibility'] + == VisibilityLevel.PRIVATE.value + ) assert ( data['data']['workouts'][0]['workout_visibility'] == VisibilityLevel.PUBLIC.value @@ -550,6 +561,10 @@ def test_it_returns_a_user_workout_when_visibility_is_public( data['data']['workouts'][0]['map_visibility'] == VisibilityLevel.PRIVATE.value ) + assert ( + data['data']['workouts'][0]['analysis_visibility'] + == VisibilityLevel.PRIVATE.value + ) assert ( data['data']['workouts'][0]['workout_visibility'] == VisibilityLevel.PUBLIC.value @@ -651,8 +666,11 @@ def test_it_returns_404_when_map_visibility_is_private( workout_cycling_user_2: Workout, follow_request_from_user_1_to_user_2: FollowRequest, ) -> None: - self.init_test_data( - workout_cycling_user_2, VisibilityLevel.PRIVATE, user_1, user_2 + self.init_test_data_for_follower( + workout_cycling_user_2, + map_visibility=VisibilityLevel.PRIVATE, + follower=user_1, + followed=user_2, ) client, auth_token = self.get_test_client_and_auth_token( app, user_1.email @@ -692,8 +710,11 @@ def test_it_returns_followed_user_workout_gpx( workout_cycling_user_2: Workout, follow_request_from_user_1_to_user_2: FollowRequest, ) -> None: - self.init_test_data( - workout_cycling_user_2, input_map_visibility, user_1, user_2 + self.init_test_data_for_follower( + workout_cycling_user_2, + map_visibility=input_map_visibility, + follower=user_1, + followed=user_2, ) gpx_content = self.random_string() client, auth_token = self.get_test_client_and_auth_token( @@ -723,8 +744,11 @@ def test_it_returns_error_when_user_is_suspended( workout_cycling_user_2: Workout, follow_request_from_user_1_to_user_2: FollowRequest, ) -> None: - self.init_test_data( - workout_cycling_user_2, VisibilityLevel.FOLLOWERS, user_1, user_2 + self.init_test_data_for_follower( + workout_cycling_user_2, + map_visibility=VisibilityLevel.FOLLOWERS, + follower=user_1, + followed=user_2, ) gpx_content = self.random_string() user_1.suspended_at = datetime.utcnow() @@ -784,7 +808,9 @@ def test_it_returns_404_when_map_visibility_is_not_public( sport_1_cycling: Sport, workout_cycling_user_2: Workout, ) -> None: - self.init_test_data(workout_cycling_user_2, input_map_visibility) + self.init_test_data_for_public_workout( + workout_cycling_user_2, map_visibility=input_map_visibility + ) client, auth_token = self.get_test_client_and_auth_token( app, user_1.email ) @@ -813,7 +839,9 @@ def test_it_returns_gpx_when_map_visibility_is_public( sport_1_cycling: Sport, workout_cycling_user_2: Workout, ) -> None: - self.init_test_data(workout_cycling_user_2, VisibilityLevel.PUBLIC) + self.init_test_data_for_public_workout( + workout_cycling_user_2, map_visibility=VisibilityLevel.PUBLIC + ) gpx_content = self.random_string() client, auth_token = self.get_test_client_and_auth_token( app, user_1.email @@ -841,7 +869,9 @@ def test_it_returns_error_when_user_is_suspended( sport_1_cycling: Sport, workout_cycling_user_2: Workout, ) -> None: - self.init_test_data(workout_cycling_user_2, VisibilityLevel.PUBLIC) + self.init_test_data_for_public_workout( + workout_cycling_user_2, map_visibility=VisibilityLevel.PUBLIC + ) gpx_content = self.random_string() user_1.suspended_at = datetime.utcnow() db.session.commit() @@ -895,7 +925,9 @@ def test_it_returns_404_when_map_visibility_is_not_public( sport_1_cycling: Sport, workout_cycling_user_1: Workout, ) -> None: - self.init_test_data(workout_cycling_user_1, input_map_visibility) + self.init_test_data_for_public_workout( + workout_cycling_user_1, map_visibility=input_map_visibility + ) client = app.test_client() with patch( 'builtins.open', @@ -921,7 +953,9 @@ def test_it_returns_gpx_when_map_visibility_is_public( workout_cycling_user_1: Workout, ) -> None: gpx_content = self.random_string() - self.init_test_data(workout_cycling_user_1, VisibilityLevel.PUBLIC) + self.init_test_data_for_public_workout( + workout_cycling_user_1, map_visibility=VisibilityLevel.PUBLIC + ) client = app.test_client() with patch( 'builtins.open', new_callable=mock_open, read_data=gpx_content @@ -938,11 +972,11 @@ def test_it_returns_gpx_when_map_visibility_is_public( assert data['data']['gpx'] == gpx_content -class GetGetWorkoutChartDataTestCase(WorkoutApiTestCaseMixin): +class GetWorkoutChartDataTestCase(WorkoutApiTestCaseMixin): route = '/api/workouts/{workout_uuid}/chart_data' -class TestGetWorkoutChartDataAsWorkoutOwner(GetGetWorkoutChartDataTestCase): +class TestGetWorkoutChartDataAsWorkoutOwner(GetWorkoutChartDataTestCase): def test_it_returns_404_if_workout_have_no_chart_data( self, app: Flask, @@ -1040,9 +1074,9 @@ def test_it_returns_error_when_user_is_suspended( class TestGetWorkoutChartDataAsFollower( - GetGetWorkoutChartDataTestCase, GetWorkoutGpxAsFollowerMixin + GetWorkoutChartDataTestCase, GetWorkoutGpxAsFollowerMixin ): - def test_it_returns_404_when_map_visibility_is_private( + def test_it_returns_404_when_analysis_visibility_is_private( self, app: Flask, user_1: User, @@ -1051,8 +1085,12 @@ def test_it_returns_404_when_map_visibility_is_private( workout_cycling_user_2: Workout, follow_request_from_user_1_to_user_2: FollowRequest, ) -> None: - self.init_test_data( - workout_cycling_user_2, VisibilityLevel.PRIVATE, user_1, user_2 + self.init_test_data_for_follower( + workout_cycling_user_2, + analysis_visibility=VisibilityLevel.PRIVATE, + map_visibility=VisibilityLevel.PRIVATE, + follower=user_1, + followed=user_2, ) client, auth_token = self.get_test_client_and_auth_token( app, user_1.email @@ -1069,16 +1107,16 @@ def test_it_returns_404_when_map_visibility_is_private( ) @pytest.mark.parametrize( - 'input_desc,input_map_visibility', + 'input_desc,input_analysis_visibility', [ - ('map visibility: followers_only', VisibilityLevel.FOLLOWERS), - ('map visibility: public', VisibilityLevel.PUBLIC), + ('analysis visibility: followers_only', VisibilityLevel.FOLLOWERS), + ('analysis visibility: public', VisibilityLevel.PUBLIC), ], ) - def test_it_returns_chart_data_for_followed_user_workout( + def test_it_returns_chart_data_when_visibile_to_follower( self, input_desc: str, - input_map_visibility: VisibilityLevel, + input_analysis_visibility: VisibilityLevel, app: Flask, user_1: User, user_2: User, @@ -1086,8 +1124,12 @@ def test_it_returns_chart_data_for_followed_user_workout( workout_cycling_user_2: Workout, follow_request_from_user_1_to_user_2: FollowRequest, ) -> None: - self.init_test_data( - workout_cycling_user_2, input_map_visibility, user_1, user_2 + self.init_test_data_for_follower( + workout_cycling_user_2, + analysis_visibility=input_analysis_visibility, + map_visibility=VisibilityLevel.PRIVATE, + follower=user_1, + followed=user_2, ) client, auth_token = self.get_test_client_and_auth_token( app, user_1.email @@ -1123,8 +1165,12 @@ def test_it_returns_error_when_user_is_suspended( follow_request_from_user_1_to_user_2: FollowRequest, ) -> None: workout_cycling_user_2.gpx = 'file.gpx' - self.init_test_data( - workout_cycling_user_2, VisibilityLevel.FOLLOWERS, user_1, user_2 + self.init_test_data_for_follower( + workout_cycling_user_2, + analysis_visibility=VisibilityLevel.FOLLOWERS, + map_visibility=VisibilityLevel.PRIVATE, + follower=user_1, + followed=user_2, ) user_1.suspended_at = datetime.utcnow() db.session.commit() @@ -1140,7 +1186,7 @@ def test_it_returns_error_when_user_is_suspended( class TestGetWorkoutChartDataAsUser( - GetGetWorkoutChartDataTestCase, GetWorkoutGpxPublicVisibilityMixin + GetWorkoutChartDataTestCase, GetWorkoutGpxPublicVisibilityMixin ): def test_it_returns_404_if_workout_does_not_exist( self, app: Flask, user_1: User @@ -1161,23 +1207,27 @@ def test_it_returns_404_if_workout_does_not_exist( assert data['data']['chart_data'] == '' @pytest.mark.parametrize( - 'input_desc,input_map_visibility', + 'input_desc,input_analysis_visibility', [ - ('map visibility: private', VisibilityLevel.PRIVATE), - ('map visibility: followers_only', VisibilityLevel.FOLLOWERS), + ('analysis visibility: private', VisibilityLevel.PRIVATE), + ('analysis visibility: followers_only', VisibilityLevel.FOLLOWERS), ], ) - def test_it_returns_404_when_map_visibility_is_not_public( + def test_it_returns_404_when_analysis_visibility_is_not_public( self, input_desc: str, - input_map_visibility: VisibilityLevel, + input_analysis_visibility: VisibilityLevel, app: Flask, user_1: User, user_2: User, sport_1_cycling: Sport, workout_cycling_user_2: Workout, ) -> None: - self.init_test_data(workout_cycling_user_2, input_map_visibility) + self.init_test_data_for_public_workout( + workout_cycling_user_2, + analysis_visibility=input_analysis_visibility, + map_visibility=VisibilityLevel.PRIVATE, + ) client, auth_token = self.get_test_client_and_auth_token( app, user_1.email ) @@ -1192,7 +1242,7 @@ def test_it_returns_404_when_map_visibility_is_not_public( f'workout not found (id: {workout_cycling_user_2.short_id})', ) - def test_it_returns_chart_data_when_map_visibility_is_public( + def test_it_returns_chart_data_when_analysis_visibility_is_public( self, app: Flask, user_1: User, @@ -1200,7 +1250,11 @@ def test_it_returns_chart_data_when_map_visibility_is_public( sport_1_cycling: Sport, workout_cycling_user_2: Workout, ) -> None: - self.init_test_data(workout_cycling_user_2, VisibilityLevel.PUBLIC) + self.init_test_data_for_public_workout( + workout_cycling_user_2, + analysis_visibility=VisibilityLevel.PUBLIC, + map_visibility=VisibilityLevel.PRIVATE, + ) client, auth_token = self.get_test_client_and_auth_token( app, user_1.email ) @@ -1233,7 +1287,11 @@ def test_it_returns_error_when_user_is_suspended( sport_1_cycling: Sport, workout_cycling_user_2: Workout, ) -> None: - self.init_test_data(workout_cycling_user_2, VisibilityLevel.PUBLIC) + self.init_test_data_for_public_workout( + workout_cycling_user_2, + analysis_visibility=VisibilityLevel.PUBLIC, + map_visibility=VisibilityLevel.PRIVATE, + ) client, auth_token = self.get_test_client_and_auth_token( app, user_1.email ) @@ -1250,7 +1308,7 @@ def test_it_returns_error_when_user_is_suspended( class TestGetWorkoutChartDataAsUnauthenticatedUser( - GetGetWorkoutChartDataTestCase, GetWorkoutGpxPublicVisibilityMixin + GetWorkoutChartDataTestCase, GetWorkoutGpxPublicVisibilityMixin ): def test_it_returns_404_if_workout_does_not_exist( self, app: Flask @@ -1268,22 +1326,26 @@ def test_it_returns_404_if_workout_does_not_exist( assert data['data']['chart_data'] == '' @pytest.mark.parametrize( - 'input_desc,input_map_visibility', + 'input_desc,input_analysis_visibility', [ - ('map visibility: private', VisibilityLevel.PRIVATE), - ('map visibility: followers_only', VisibilityLevel.FOLLOWERS), + ('analysis visibility: private', VisibilityLevel.PRIVATE), + ('analysis visibility: followers_only', VisibilityLevel.FOLLOWERS), ], ) def test_it_returns_404_when_map_visibility_is_not_public( self, input_desc: str, - input_map_visibility: VisibilityLevel, + input_analysis_visibility: VisibilityLevel, app: Flask, user_1: User, sport_1_cycling: Sport, workout_cycling_user_1: Workout, ) -> None: - self.init_test_data(workout_cycling_user_1, input_map_visibility) + self.init_test_data_for_public_workout( + workout_cycling_user_1, + analysis_visibility=input_analysis_visibility, + map_visibility=VisibilityLevel.PRIVATE, + ) client = app.test_client() response = client.get( @@ -1303,7 +1365,11 @@ def test_it_returns_chart_data_when_map_visibility_is_public( workout_cycling_user_1: Workout, ) -> None: chart_data: List = [] - self.init_test_data(workout_cycling_user_1, VisibilityLevel.PUBLIC) + self.init_test_data_for_public_workout( + workout_cycling_user_1, + analysis_visibility=VisibilityLevel.PUBLIC, + map_visibility=VisibilityLevel.PRIVATE, + ) client = app.test_client() with ( patch('builtins.open', new_callable=mock_open), @@ -1450,8 +1516,11 @@ def test_it_returns_404_when_map_visibility_is_private( follow_request_from_user_2_to_user_1: FollowRequest, gpx_file_with_segments: str, ) -> None: - self.init_test_data( - workout_cycling_user_1, VisibilityLevel.PRIVATE, user_2, user_1 + self.init_test_data_for_follower( + workout_cycling_user_1, + map_visibility=VisibilityLevel.PRIVATE, + follower=user_2, + followed=user_1, ) client, auth_token = self.get_test_client_and_auth_token( app, user_2.email @@ -1494,8 +1563,11 @@ def test_it_returns_segment_gpx_for_followed_user_workout( follow_request_from_user_2_to_user_1: FollowRequest, gpx_file_with_segments: str, ) -> None: - self.init_test_data( - workout_cycling_user_1, input_map_visibility, user_2, user_1 + self.init_test_data_for_follower( + workout_cycling_user_1, + map_visibility=input_map_visibility, + follower=user_2, + followed=user_1, ) client, auth_token = self.get_test_client_and_auth_token( app, user_2.email @@ -1528,8 +1600,11 @@ def test_it_returns_error_when_user_is_suspended( follow_request_from_user_2_to_user_1: FollowRequest, gpx_file_with_segments: str, ) -> None: - self.init_test_data( - workout_cycling_user_1, VisibilityLevel.FOLLOWERS, user_2, user_1 + self.init_test_data_for_follower( + workout_cycling_user_1, + map_visibility=VisibilityLevel.FOLLOWERS, + follower=user_2, + followed=user_1, ) user_2.suspended_at = datetime.utcnow() db.session.commit() @@ -1592,7 +1667,9 @@ def test_it_returns_404_when_map_visibility_is_not_public( workout_cycling_user_1_segment: WorkoutSegment, gpx_file_with_segments: str, ) -> None: - self.init_test_data(workout_cycling_user_1, input_map_visibility) + self.init_test_data_for_public_workout( + workout_cycling_user_1, map_visibility=input_map_visibility + ) client, auth_token = self.get_test_client_and_auth_token( app, user_2.email ) @@ -1623,7 +1700,9 @@ def test_it_returns_segment_gpx_when_map_visibility_is_public( workout_cycling_user_1_segment: WorkoutSegment, gpx_file_with_segments: str, ) -> None: - self.init_test_data(workout_cycling_user_1, VisibilityLevel.PUBLIC) + self.init_test_data_for_public_workout( + workout_cycling_user_1, map_visibility=VisibilityLevel.PUBLIC + ) client, auth_token = self.get_test_client_and_auth_token( app, user_2.email ) @@ -1654,7 +1733,9 @@ def test_it_returns_error_when_user_is_suspended( workout_cycling_user_1_segment: WorkoutSegment, gpx_file_with_segments: str, ) -> None: - self.init_test_data(workout_cycling_user_1, VisibilityLevel.PUBLIC) + self.init_test_data_for_public_workout( + workout_cycling_user_1, map_visibility=VisibilityLevel.PUBLIC + ) user_2.suspended_at = datetime.utcnow() db.session.commit() client, auth_token = self.get_test_client_and_auth_token( @@ -1707,7 +1788,9 @@ def test_it_returns_404_when_map_visibility_is_not_public( workout_cycling_user_1_segment: WorkoutSegment, gpx_file_with_segments: str, ) -> None: - self.init_test_data(workout_cycling_user_1, input_map_visibility) + self.init_test_data_for_public_workout( + workout_cycling_user_1, map_visibility=input_map_visibility + ) client = app.test_client() with patch( 'builtins.open', @@ -1735,7 +1818,9 @@ def test_it_returns_segment_gpx_when_map_visibility_is_public( workout_cycling_user_1_segment: WorkoutSegment, gpx_file_with_segments: str, ) -> None: - self.init_test_data(workout_cycling_user_1, VisibilityLevel.PUBLIC) + self.init_test_data_for_public_workout( + workout_cycling_user_1, map_visibility=VisibilityLevel.PUBLIC + ) client = app.test_client() with patch( 'builtins.open', @@ -1862,7 +1947,7 @@ def test_it_returns_error_when_user_is_suspended( class TestGetWorkoutSegmentChartDataAsFollower( GetWorkoutSegmentChartDataTestCase, GetWorkoutGpxAsFollowerMixin ): - def test_it_returns_404_when_map_visibility_is_private( + def test_it_returns_404_when_analysis_visibility_is_private( self, app: Flask, user_1: User, @@ -1872,8 +1957,12 @@ def test_it_returns_404_when_map_visibility_is_private( workout_cycling_user_1_segment: WorkoutSegment, follow_request_from_user_2_to_user_1: FollowRequest, ) -> None: - self.init_test_data( - workout_cycling_user_1, VisibilityLevel.PRIVATE, user_2, user_1 + self.init_test_data_for_follower( + workout_cycling_user_1, + analysis_visibility=VisibilityLevel.PRIVATE, + map_visibility=VisibilityLevel.PRIVATE, + follower=user_2, + followed=user_1, ) client, auth_token = self.get_test_client_and_auth_token( app, user_2.email @@ -1892,16 +1981,16 @@ def test_it_returns_404_when_map_visibility_is_private( ) @pytest.mark.parametrize( - 'input_desc,input_map_visibility', + 'input_desc,input_analysis_visibility', [ - ('map visibility: followers_only', VisibilityLevel.FOLLOWERS), - ('map visibility: public', VisibilityLevel.PUBLIC), + ('analysis visibility: followers_only', VisibilityLevel.FOLLOWERS), + ('analysis visibility: public', VisibilityLevel.PUBLIC), ], ) def test_it_returns_chart_data_for_follower_user_workout( self, input_desc: str, - input_map_visibility: VisibilityLevel, + input_analysis_visibility: VisibilityLevel, app: Flask, user_1: User, user_2: User, @@ -1910,8 +1999,12 @@ def test_it_returns_chart_data_for_follower_user_workout( workout_cycling_user_1_segment: WorkoutSegment, follow_request_from_user_2_to_user_1: FollowRequest, ) -> None: - self.init_test_data( - workout_cycling_user_1, input_map_visibility, user_2, user_1 + self.init_test_data_for_follower( + workout_cycling_user_1, + analysis_visibility=input_analysis_visibility, + map_visibility=VisibilityLevel.PRIVATE, + follower=user_2, + followed=user_1, ) client, auth_token = self.get_test_client_and_auth_token( app, user_2.email @@ -1948,8 +2041,12 @@ def test_it_returns_error_when_user_is_suspended( follow_request_from_user_2_to_user_1: FollowRequest, ) -> None: workout_cycling_user_1.gpx = 'file.gpx' - self.init_test_data( - workout_cycling_user_1, VisibilityLevel.FOLLOWERS, user_2, user_1 + self.init_test_data_for_follower( + workout_cycling_user_1, + analysis_visibility=VisibilityLevel.FOLLOWERS, + map_visibility=VisibilityLevel.PRIVATE, + follower=user_2, + followed=user_1, ) user_2.suspended_at = datetime.utcnow() db.session.commit() @@ -1997,16 +2094,16 @@ def test_it_returns_404_if_workout_does_not_exist( assert data['data']['chart_data'] == '' @pytest.mark.parametrize( - 'input_desc,input_map_visibility', + 'input_desc,input_analysis_visibility', [ - ('map visibility: private', VisibilityLevel.PRIVATE), - ('map visibility: followers_only', VisibilityLevel.FOLLOWERS), + ('analysis visibility: private', VisibilityLevel.PRIVATE), + ('analysis visibility: followers_only', VisibilityLevel.FOLLOWERS), ], ) - def test_it_returns_404_when_map_visibility_is_not_public( + def test_it_returns_404_when_analysis_visibility_is_not_public( self, input_desc: str, - input_map_visibility: VisibilityLevel, + input_analysis_visibility: VisibilityLevel, app: Flask, user_1: User, user_2: User, @@ -2014,7 +2111,11 @@ def test_it_returns_404_when_map_visibility_is_not_public( workout_cycling_user_1: Workout, workout_cycling_user_1_segment: WorkoutSegment, ) -> None: - self.init_test_data(workout_cycling_user_1, input_map_visibility) + self.init_test_data_for_public_workout( + workout_cycling_user_1, + analysis_visibility=input_analysis_visibility, + map_visibility=VisibilityLevel.PRIVATE, + ) client, auth_token = self.get_test_client_and_auth_token( app, user_2.email ) @@ -2031,7 +2132,7 @@ def test_it_returns_404_when_map_visibility_is_not_public( f'workout not found (id: {workout_cycling_user_1.short_id})', ) - def test_it_returns_chart_data_when_map_visibility_is_public( + def test_it_returns_chart_data_when_analysis_visibility_is_public( self, app: Flask, user_1: User, @@ -2041,7 +2142,11 @@ def test_it_returns_chart_data_when_map_visibility_is_public( workout_cycling_user_1_segment: WorkoutSegment, ) -> None: chart_data: List = [] - self.init_test_data(workout_cycling_user_1, VisibilityLevel.PUBLIC) + self.init_test_data_for_public_workout( + workout_cycling_user_1, + analysis_visibility=VisibilityLevel.PUBLIC, + map_visibility=VisibilityLevel.PRIVATE, + ) client, auth_token = self.get_test_client_and_auth_token( app, user_2.email ) @@ -2075,7 +2180,11 @@ def test_it_returns_error_when_user_is_suspended( workout_cycling_user_1_segment: WorkoutSegment, ) -> None: chart_data: List = [] - self.init_test_data(workout_cycling_user_1, VisibilityLevel.PUBLIC) + self.init_test_data_for_public_workout( + workout_cycling_user_1, + analysis_visibility=VisibilityLevel.PUBLIC, + map_visibility=VisibilityLevel.PRIVATE, + ) workout_cycling_user_1.gpx = 'file.gpx' user_2.suspended_at = datetime.utcnow() db.session.commit() @@ -2118,23 +2227,27 @@ def test_it_returns_404_if_workout_does_not_exist( assert data['data']['chart_data'] == '' @pytest.mark.parametrize( - 'input_desc,input_map_visibility', + 'input_desc,input_analysis_visibility', [ - ('map visibility: private', VisibilityLevel.PRIVATE), - ('map visibility: followers_only', VisibilityLevel.FOLLOWERS), + ('analysis visibility: private', VisibilityLevel.PRIVATE), + ('analysis visibility: followers_only', VisibilityLevel.FOLLOWERS), ], ) def test_it_returns_404_when_map_visibility_is_not_public( self, input_desc: str, - input_map_visibility: VisibilityLevel, + input_analysis_visibility: VisibilityLevel, app: Flask, user_1: User, sport_1_cycling: Sport, workout_cycling_user_1: Workout, workout_cycling_user_1_segment: WorkoutSegment, ) -> None: - self.init_test_data(workout_cycling_user_1, input_map_visibility) + self.init_test_data_for_public_workout( + workout_cycling_user_1, + analysis_visibility=input_analysis_visibility, + map_visibility=VisibilityLevel.PRIVATE, + ) client = app.test_client() response = client.get( @@ -2148,7 +2261,7 @@ def test_it_returns_404_when_map_visibility_is_not_public( f'workout not found (id: {workout_cycling_user_1.short_id})', ) - def test_it_returns_chart_data_when_map_visibility_is_public( + def test_it_returns_chart_data_when_analysis_visibility_is_public( self, app: Flask, user_1: User, @@ -2157,7 +2270,11 @@ def test_it_returns_chart_data_when_map_visibility_is_public( workout_cycling_user_1_segment: WorkoutSegment, ) -> None: chart_data: List = [] - self.init_test_data(workout_cycling_user_1, VisibilityLevel.PUBLIC) + self.init_test_data_for_public_workout( + workout_cycling_user_1, + analysis_visibility=VisibilityLevel.PUBLIC, + map_visibility=VisibilityLevel.PRIVATE, + ) client = app.test_client() with ( patch('builtins.open', new_callable=mock_open), @@ -2396,6 +2513,7 @@ def test_it_returns_404_for_followed_user_workout( user_2.approves_follow_request_from(user_1) workout_cycling_user_2.gpx = 'file.gpx' workout_cycling_user_2.workout_visibility = VisibilityLevel.PUBLIC + workout_cycling_user_2.analysis_visibility = VisibilityLevel.PUBLIC workout_cycling_user_2.map_visibility = VisibilityLevel.PUBLIC client, auth_token = self.get_test_client_and_auth_token( app, user_1.email @@ -2442,7 +2560,9 @@ def test_it_returns_404_for_another_user_workout( sport_1_cycling: Sport, workout_cycling_user_2: Workout, ) -> None: - self.init_test_data(workout_cycling_user_2, VisibilityLevel.PUBLIC) + self.init_test_data_for_public_workout( + workout_cycling_user_2, map_visibility=VisibilityLevel.PUBLIC + ) client, auth_token = self.get_test_client_and_auth_token( app, user_1.email ) @@ -2468,7 +2588,9 @@ def test_it_returns_404( sport_1_cycling: Sport, workout_cycling_user_1: Workout, ) -> None: - self.init_test_data(workout_cycling_user_1, VisibilityLevel.PUBLIC) + self.init_test_data_for_public_workout( + workout_cycling_user_1, map_visibility=VisibilityLevel.PUBLIC + ) client = app.test_client() response = client.get( diff --git a/fittrackee/tests/workouts/test_workouts_api_1_post.py b/fittrackee/tests/workouts/test_workouts_api_1_post.py index 486cc16dd..5c7847c16 100644 --- a/fittrackee/tests/workouts/test_workouts_api_1_post.py +++ b/fittrackee/tests/workouts/test_workouts_api_1_post.py @@ -1261,6 +1261,7 @@ def test_workout_is_created_with_user_privacy_parameters_when_no_provided( input_visibility: VisibilityLevel, ) -> None: user_1.map_visibility = input_visibility + user_1.analysis_visibility = input_visibility user_1.workouts_visibility = input_visibility client, auth_token = self.get_test_client_and_auth_token( app, user_1.email @@ -1286,16 +1287,33 @@ def test_workout_is_created_with_user_privacy_parameters_when_no_provided( data['data']['workouts'][0]['map_visibility'] == user_1.map_visibility.value ) + assert ( + data['data']['workouts'][0]['analysis_visibility'] + == user_1.analysis_visibility.value + ) assert ( data['data']['workouts'][0]['workout_visibility'] == user_1.workouts_visibility.value ) @pytest.mark.parametrize( - 'input_map_visibility,input_workout_visibility', + 'input_map_visibility,input_analysis_visibility,input_workout_visibility', [ - (VisibilityLevel.FOLLOWERS, VisibilityLevel.PUBLIC), - (VisibilityLevel.PRIVATE, VisibilityLevel.FOLLOWERS), + ( + VisibilityLevel.FOLLOWERS, + VisibilityLevel.PUBLIC, + VisibilityLevel.PUBLIC, + ), + ( + VisibilityLevel.PRIVATE, + VisibilityLevel.FOLLOWERS, + VisibilityLevel.FOLLOWERS, + ), + ( + VisibilityLevel.PRIVATE, + VisibilityLevel.FOLLOWERS, + VisibilityLevel.PUBLIC, + ), ], ) def test_workout_is_created_with_provided_privacy_parameters( @@ -1305,6 +1323,7 @@ def test_workout_is_created_with_provided_privacy_parameters( sport_1_cycling: Sport, gpx_file: str, input_map_visibility: VisibilityLevel, + input_analysis_visibility: VisibilityLevel, input_workout_visibility: VisibilityLevel, ) -> None: client, auth_token = self.get_test_client_and_auth_token( @@ -1318,6 +1337,8 @@ def test_workout_is_created_with_provided_privacy_parameters( data=( f'{{"sport_id": 1, "map_visibility": ' f'"{input_map_visibility.value}", ' + f'"analysis_visibility": ' + f'"{input_analysis_visibility.value}", ' f'"workout_visibility": ' f'"{input_workout_visibility.value}"}}' ), @@ -1336,16 +1357,41 @@ def test_workout_is_created_with_provided_privacy_parameters( data['data']['workouts'][0]['map_visibility'] == input_map_visibility.value ) + assert ( + data['data']['workouts'][0]['analysis_visibility'] + == input_analysis_visibility.value + ) assert ( data['data']['workouts'][0]['workout_visibility'] == input_workout_visibility.value ) @pytest.mark.parametrize( - 'input_map_visibility,input_workout_visibility', + 'input_map_visibility,input_analysis_visibility,' + 'input_workout_visibility,expected_map_visibility,' + 'expected_analysis_visibility', [ - (VisibilityLevel.FOLLOWERS, VisibilityLevel.PRIVATE), - (VisibilityLevel.PUBLIC, VisibilityLevel.FOLLOWERS), + ( + VisibilityLevel.FOLLOWERS, + VisibilityLevel.PRIVATE, + VisibilityLevel.PUBLIC, + VisibilityLevel.PRIVATE, + VisibilityLevel.PRIVATE, + ), + ( + VisibilityLevel.PUBLIC, + VisibilityLevel.FOLLOWERS, + VisibilityLevel.FOLLOWERS, + VisibilityLevel.FOLLOWERS, + VisibilityLevel.FOLLOWERS, + ), + ( + VisibilityLevel.PUBLIC, + VisibilityLevel.FOLLOWERS, + VisibilityLevel.PRIVATE, + VisibilityLevel.PRIVATE, + VisibilityLevel.PRIVATE, + ), ], ) def test_workout_is_created_with_valid_privacy_parameters_when_provided( @@ -1355,7 +1401,10 @@ def test_workout_is_created_with_valid_privacy_parameters_when_provided( sport_1_cycling: Sport, gpx_file: str, input_map_visibility: VisibilityLevel, + input_analysis_visibility: VisibilityLevel, input_workout_visibility: VisibilityLevel, + expected_map_visibility: VisibilityLevel, + expected_analysis_visibility: VisibilityLevel, ) -> None: """ when workout visibility is stricter, map visibility is initialised @@ -1372,6 +1421,8 @@ def test_workout_is_created_with_valid_privacy_parameters_when_provided( data=( f'{{"sport_id": 1, "map_visibility": ' f'"{input_map_visibility.value}", ' + f'"analysis_visibility": ' + f'"{input_analysis_visibility.value}", ' f'"workout_visibility": ' f'"{input_workout_visibility.value}"}}' ), @@ -1388,7 +1439,11 @@ def test_workout_is_created_with_valid_privacy_parameters_when_provided( assert len(data['data']['workouts']) == 1 assert ( data['data']['workouts'][0]['map_visibility'] - == input_workout_visibility.value + == expected_map_visibility.value + ) + assert ( + data['data']['workouts'][0]['analysis_visibility'] + == expected_analysis_visibility.value ) assert ( data['data']['workouts'][0]['workout_visibility'] @@ -2665,6 +2720,7 @@ def test_workout_is_created_with_user_privacy_parameters_when_no_provided( input_visibility: VisibilityLevel, ) -> None: user_1.map_visibility = input_visibility + user_1.analysis_visibility = input_visibility user_1.workouts_visibility = input_visibility client, auth_token = self.get_test_client_and_auth_token( app, user_1.email @@ -2690,7 +2746,11 @@ def test_workout_is_created_with_user_privacy_parameters_when_no_provided( assert len(data['data']['workouts']) == 1 assert ( data['data']['workouts'][0]['map_visibility'] - == user_1.map_visibility.value + == VisibilityLevel.PRIVATE.value + ) + assert ( + data['data']['workouts'][0]['analysis_visibility'] + == VisibilityLevel.PRIVATE.value ) assert ( data['data']['workouts'][0]['workout_visibility'] @@ -2736,6 +2796,14 @@ def test_workout_is_created_with_provided_privacy_parameters( data = json.loads(response.data.decode()) assert 'created' in data['status'] assert len(data['data']['workouts']) == 1 + assert ( + data['data']['workouts'][0]['map_visibility'] + == VisibilityLevel.PRIVATE.value + ) + assert ( + data['data']['workouts'][0]['analysis_visibility'] + == VisibilityLevel.PRIVATE.value + ) assert ( data['data']['workouts'][0]['workout_visibility'] == input_workout_visibility.value @@ -2984,6 +3052,7 @@ def test_workouts_are_created_with_user_privacy_parameters_when_no_provided( # ) -> None: file_path = os.path.join(app.root_path, 'tests/files/gpx_test.zip') user_1.map_visibility = input_visibility + user_1.analysis_visibility = input_visibility user_1.workouts_visibility = input_visibility client, auth_token = self.get_test_client_and_auth_token( app, user_1.email @@ -3010,16 +3079,28 @@ def test_workouts_are_created_with_user_privacy_parameters_when_no_provided( # data['data']['workouts'][n]['map_visibility'] == user_1.map_visibility.value ) + assert ( + data['data']['workouts'][n]['analysis_visibility'] + == user_1.analysis_visibility.value + ) assert ( data['data']['workouts'][n]['workout_visibility'] == user_1.workouts_visibility.value ) @pytest.mark.parametrize( - 'input_map_visibility,input_workout_visibility', + 'input_map_visibility,input_analysis_visibility,input_workout_visibility', [ - (VisibilityLevel.FOLLOWERS, VisibilityLevel.PUBLIC), - (VisibilityLevel.PRIVATE, VisibilityLevel.FOLLOWERS), + ( + VisibilityLevel.FOLLOWERS, + VisibilityLevel.PUBLIC, + VisibilityLevel.PUBLIC, + ), + ( + VisibilityLevel.PRIVATE, + VisibilityLevel.PRIVATE, + VisibilityLevel.FOLLOWERS, + ), ], ) def test_workouts_are_created_with_provided_privacy_parameters( @@ -3028,6 +3109,7 @@ def test_workouts_are_created_with_provided_privacy_parameters( user_1: User, sport_1_cycling: Sport, input_map_visibility: VisibilityLevel, + input_analysis_visibility: VisibilityLevel, input_workout_visibility: VisibilityLevel, ) -> None: file_path = os.path.join(app.root_path, 'tests/files/gpx_test.zip') @@ -3043,6 +3125,8 @@ def test_workouts_are_created_with_provided_privacy_parameters( data=( f'{{"sport_id": 1, "map_visibility": ' f'"{input_map_visibility.value}", ' + f'"analysis_visibility": ' + f'"{input_analysis_visibility.value}", ' f'"workout_visibility": ' f'"{input_workout_visibility.value}"}}' ), @@ -3062,6 +3146,10 @@ def test_workouts_are_created_with_provided_privacy_parameters( data['data']['workouts'][n]['map_visibility'] == input_map_visibility.value ) + assert ( + data['data']['workouts'][n]['analysis_visibility'] + == input_analysis_visibility.value + ) assert ( data['data']['workouts'][n]['workout_visibility'] == input_workout_visibility.value @@ -3199,10 +3287,20 @@ def test_it_adds_a_workout_with_default_sport_equipments_when_no_equipment_ids_p ] @pytest.mark.parametrize( - 'input_map_visibility,input_workout_visibility', + 'input_map_visibility,input_analysis_visibility,input_workout_visibility,expected_visibility', [ - (VisibilityLevel.FOLLOWERS, VisibilityLevel.PRIVATE), - (VisibilityLevel.PUBLIC, VisibilityLevel.FOLLOWERS), + ( + VisibilityLevel.FOLLOWERS, + VisibilityLevel.PRIVATE, + VisibilityLevel.PUBLIC, + VisibilityLevel.PRIVATE, + ), + ( + VisibilityLevel.PRIVATE, + VisibilityLevel.FOLLOWERS, + VisibilityLevel.PRIVATE, + VisibilityLevel.PRIVATE, + ), ], ) def test_workouts_are_created_with_valid_privacy_parameters_when_provided( @@ -3211,11 +3309,15 @@ def test_workouts_are_created_with_valid_privacy_parameters_when_provided( user_1: User, sport_1_cycling: Sport, input_map_visibility: VisibilityLevel, + input_analysis_visibility: VisibilityLevel, input_workout_visibility: VisibilityLevel, + expected_visibility: VisibilityLevel, ) -> None: """ - when workout visibility is stricter, map visibility is initialised + when workout visibility is stricter, analysis visibility is initialised with workout visibility value + when analysis visibility is stricter, map visibility is initialised + with analysis visibility value """ file_path = os.path.join(app.root_path, 'tests/files/gpx_test.zip') client, auth_token = self.get_test_client_and_auth_token( @@ -3230,6 +3332,8 @@ def test_workouts_are_created_with_valid_privacy_parameters_when_provided( data=( f'{{"sport_id": 1, "map_visibility": ' f'"{input_map_visibility.value}", ' + f'"analysis_visibility": ' + f'"{input_analysis_visibility.value}", ' f'"workout_visibility": ' f'"{input_workout_visibility.value}"}}' ), @@ -3247,7 +3351,11 @@ def test_workouts_are_created_with_valid_privacy_parameters_when_provided( for n in range(3): assert ( data['data']['workouts'][n]['map_visibility'] - == input_workout_visibility.value + == expected_visibility.value + ) + assert ( + data['data']['workouts'][n]['analysis_visibility'] + == expected_visibility.value ) assert ( data['data']['workouts'][n]['workout_visibility'] diff --git a/fittrackee/tests/workouts/test_workouts_api_2_patch.py b/fittrackee/tests/workouts/test_workouts_api_2_patch.py index fd014cc19..595242537 100644 --- a/fittrackee/tests/workouts/test_workouts_api_2_patch.py +++ b/fittrackee/tests/workouts/test_workouts_api_2_patch.py @@ -1680,6 +1680,14 @@ def test_it_updates_workout_visibility_for_workout_without_gpx( data['data']['workouts'][0]['workout_visibility'] == input_workout_visibility.value ) + assert ( + data['data']['workouts'][0]['analysis_visibility'] + == VisibilityLevel.PRIVATE.value + ) + assert ( + data['data']['workouts'][0]['map_visibility'] + == VisibilityLevel.PRIVATE.value + ) @pytest.mark.parametrize( 'input_description,input_map_visibility', @@ -1718,6 +1726,49 @@ def test_it_does_not_update_map_visibility_for_workout_without_gpx( == workout_cycling_user_1.map_visibility.value ) + @pytest.mark.parametrize( + 'input_description,input_analysis_visibility', + [ + ('private', VisibilityLevel.PRIVATE), + ('followers_only', VisibilityLevel.FOLLOWERS), + ('public', VisibilityLevel.PUBLIC), + ], + ) + def test_it_does_not_update_analysis_visibility_for_workout_without_gpx( + self, + app: Flask, + user_1: User, + sport_1_cycling: Sport, + workout_cycling_user_1: Workout, + input_description: str, + input_analysis_visibility: VisibilityLevel, + ) -> None: + client, auth_token = self.get_test_client_and_auth_token( + app, user_1.email + ) + + response = client.patch( + f'/api/workouts/{workout_cycling_user_1.short_id}', + content_type='application/json', + data=json.dumps( + dict(analysis_visibility=input_analysis_visibility.value) + ), + headers=dict(Authorization=f'Bearer {auth_token}'), + ) + + assert response.status_code == 200 + data = json.loads(response.data.decode()) + assert 'success' in data['status'] + assert len(data['data']['workouts']) == 1 + assert ( + data['data']['workouts'][0]['analysis_visibility'] + == VisibilityLevel.PRIVATE.value + ) + assert ( + data['data']['workouts'][0]['map_visibility'] + == VisibilityLevel.PRIVATE.value + ) + @pytest.mark.parametrize( 'input_description,input_workout_visibility', [ @@ -1758,6 +1809,95 @@ def test_it_updates_workout_visibility_for_workout_with_gpx( == input_workout_visibility.value ) + @pytest.mark.parametrize( + 'input_description,input_analysis_visibility', + [ + ('private', VisibilityLevel.PRIVATE), + ('followers_only', VisibilityLevel.FOLLOWERS), + ('public', VisibilityLevel.PUBLIC), + ], + ) + def test_it_updates_analysis_visibility_for_workout_with_gpx( + self, + app: Flask, + user_1: User, + sport_1_cycling: Sport, + workout_cycling_user_1: Workout, + input_description: str, + input_analysis_visibility: VisibilityLevel, + ) -> None: + workout_cycling_user_1.workout_visibility = VisibilityLevel.PUBLIC + workout_cycling_user_1.gpx = 'file.gpx' + client, auth_token = self.get_test_client_and_auth_token( + app, user_1.email + ) + + response = client.patch( + f'/api/workouts/{workout_cycling_user_1.short_id}', + content_type='application/json', + data=json.dumps( + dict(analysis_visibility=input_analysis_visibility.value) + ), + headers=dict(Authorization=f'Bearer {auth_token}'), + ) + + assert response.status_code == 200 + data = json.loads(response.data.decode()) + assert 'success' in data['status'] + assert len(data['data']['workouts']) == 1 + assert ( + data['data']['workouts'][0]['analysis_visibility'] + == input_analysis_visibility.value + ) + assert ( + workout_cycling_user_1.analysis_visibility.value + == input_analysis_visibility.value + ) + + @pytest.mark.parametrize( + 'input_analysis_visibility,input_workout_visibility', + [ + (VisibilityLevel.FOLLOWERS, VisibilityLevel.PRIVATE), + (VisibilityLevel.PUBLIC, VisibilityLevel.FOLLOWERS), + ], + ) + def test_it_updates_analysis_visibility_with_valid_value_for_workout_with_gpx( # noqa + self, + app: Flask, + user_1: User, + sport_1_cycling: Sport, + workout_cycling_user_1: Workout, + input_analysis_visibility: VisibilityLevel, + input_workout_visibility: VisibilityLevel, + ) -> None: + workout_cycling_user_1.workout_visibility = input_workout_visibility + workout_cycling_user_1.gpx = 'file.gpx' + client, auth_token = self.get_test_client_and_auth_token( + app, user_1.email + ) + + response = client.patch( + f'/api/workouts/{workout_cycling_user_1.short_id}', + content_type='application/json', + data=json.dumps( + dict(analysis_visibility=input_analysis_visibility.value) + ), + headers=dict(Authorization=f'Bearer {auth_token}'), + ) + + assert response.status_code == 200 + data = json.loads(response.data.decode()) + assert 'success' in data['status'] + assert len(data['data']['workouts']) == 1 + assert ( + data['data']['workouts'][0]['analysis_visibility'] + == input_workout_visibility.value + ) + assert ( + workout_cycling_user_1.analysis_visibility.value + == input_workout_visibility.value + ) + @pytest.mark.parametrize( 'input_description,input_map_visibility', [ @@ -1775,7 +1915,8 @@ def test_it_updates_map_visibility_for_workout_with_gpx( input_description: str, input_map_visibility: VisibilityLevel, ) -> None: - workout_cycling_user_1.workout_visibility = input_map_visibility + workout_cycling_user_1.workout_visibility = VisibilityLevel.PUBLIC + workout_cycling_user_1.analysis_visibility = VisibilityLevel.PUBLIC workout_cycling_user_1.gpx = 'file.gpx' client, auth_token = self.get_test_client_and_auth_token( app, user_1.email @@ -1802,22 +1943,23 @@ def test_it_updates_map_visibility_for_workout_with_gpx( ) @pytest.mark.parametrize( - 'input_map_visibility,input_workout_visibility', + 'input_map_visibility,input_analysis_visibility', [ (VisibilityLevel.FOLLOWERS, VisibilityLevel.PRIVATE), (VisibilityLevel.PUBLIC, VisibilityLevel.FOLLOWERS), ], ) - def test_it_updates_valid_map_visibility_for_workout_with_gpx( + def test_it_updates_map_visibility_with_valid_value_for_workout_with_gpx( self, app: Flask, user_1: User, sport_1_cycling: Sport, workout_cycling_user_1: Workout, input_map_visibility: VisibilityLevel, - input_workout_visibility: VisibilityLevel, + input_analysis_visibility: VisibilityLevel, ) -> None: - workout_cycling_user_1.workout_visibility = input_workout_visibility + workout_cycling_user_1.workout_visibility = VisibilityLevel.PUBLIC + workout_cycling_user_1.analysis_visibility = input_analysis_visibility workout_cycling_user_1.gpx = 'file.gpx' client, auth_token = self.get_test_client_and_auth_token( app, user_1.email @@ -1836,9 +1978,9 @@ def test_it_updates_valid_map_visibility_for_workout_with_gpx( assert len(data['data']['workouts']) == 1 assert ( data['data']['workouts'][0]['map_visibility'] - == input_workout_visibility.value + == input_analysis_visibility.value ) assert ( workout_cycling_user_1.map_visibility.value - == input_workout_visibility.value + == input_analysis_visibility.value ) diff --git a/fittrackee/tests/workouts/test_workouts_model.py b/fittrackee/tests/workouts/test_workouts_model.py index a189f2207..70dfe9efc 100644 --- a/fittrackee/tests/workouts/test_workouts_model.py +++ b/fittrackee/tests/workouts/test_workouts_model.py @@ -11,7 +11,12 @@ from fittrackee.utils import encode_uuid from fittrackee.visibility_levels import VisibilityLevel from fittrackee.workouts.exceptions import WorkoutForbiddenException -from fittrackee.workouts.models import Sport, Workout, WorkoutLike +from fittrackee.workouts.models import ( + Sport, + Workout, + WorkoutLike, + WorkoutSegment, +) from ..mixins import ReportMixin from ..utils import random_string @@ -21,17 +26,25 @@ @pytest.mark.disable_autouse_update_records_patch class WorkoutModelTestCase(ReportMixin): @staticmethod - def update_workout( + def update_workout_with_gpx_data( workout: Workout, map_id: Optional[str] = None, gpx_path: Optional[str] = None, bounds: Optional[List[float]] = None, + ascent: Optional[int] = 26, + descent: Optional[int] = 12, + max_alt: Optional[int] = 260, + min_alt: Optional[int] = 236, ) -> Workout: workout.map_id = map_id workout.map = random_string() if map_id is None else map_id workout.gpx = random_string() if gpx_path is None else gpx_path workout.bounds = [1.0, 2.0, 3.0, 4.0] if bounds is None else bounds workout.pauses = timedelta(minutes=15) + workout.ascent = ascent + workout.descent = descent + workout.max_alt = max_alt + workout.min_alt = min_alt return workout @@ -115,6 +128,7 @@ def test_it_serializes_workout_without_gpx( serialized_workout = workout.serialize(user=user_1, light=False) assert serialized_workout == { + 'analysis_visibility': workout.analysis_visibility.value, 'ascent': None, 'ave_speed': float(workout.ave_speed), 'bounds': [], @@ -149,6 +163,7 @@ def test_it_serializes_workout_without_gpx( 'weather_start': None, 'workout_date': workout.workout_date, 'workout_visibility': workout.workout_visibility.value, + 'with_analysis': False, 'with_gpx': False, } @@ -166,6 +181,7 @@ def test_it_serializes_workout_without_gpx_and_with_ascent_and_descent( serialized_workout = workout.serialize(user=user_1, light=False) assert serialized_workout == { + 'analysis_visibility': workout.analysis_visibility.value, 'ascent': float(workout.ascent), 'ave_speed': float(workout.ave_speed), 'bounds': [], @@ -200,6 +216,7 @@ def test_it_serializes_workout_without_gpx_and_with_ascent_and_descent( 'weather_start': None, 'workout_date': workout.workout_date, 'workout_visibility': workout.workout_visibility.value, + 'with_analysis': False, 'with_gpx': False, } @@ -211,13 +228,12 @@ def test_it_serializes_workout_with_gpx( workout_cycling_user_1: Workout, workout_cycling_user_1_segment: Workout, ) -> None: - workout = self.update_workout(workout_cycling_user_1) - workout.ascent = 0 - workout.descent = 10 + workout = self.update_workout_with_gpx_data(workout_cycling_user_1) serialized_workout = workout.serialize(user=user_1, light=False) assert serialized_workout == { + 'analysis_visibility': workout.analysis_visibility.value, 'ascent': float(workout.ascent), 'ave_speed': float(workout.ave_speed), 'bounds': workout.bounds, @@ -232,9 +248,9 @@ def test_it_serializes_workout_with_gpx( 'likes_count': 0, 'map': None, 'map_visibility': workout.map_visibility.value, - 'max_alt': None, + 'max_alt': float(workout.max_alt), 'max_speed': float(workout.max_speed), - 'min_alt': None, + 'min_alt': float(workout.min_alt), 'modification_date': workout.modification_date, 'moving': str(workout.moving), 'next_workout': None, @@ -252,11 +268,12 @@ def test_it_serializes_workout_with_gpx( 'weather_start': None, 'workout_date': workout.workout_date, 'workout_visibility': workout.workout_visibility.value, + 'with_analysis': True, 'with_gpx': True, } @pytest.mark.parametrize( - 'input_map_visibility,input_workout_visibility,' + 'input_map_visibility,input_analysis_visibility,' 'expected_map_visibility', [ ( @@ -309,15 +326,16 @@ def test_it_serializes_workout_with_gpx( def test_workout_visibility_overrides_map_visibility_when_stricter( self, input_map_visibility: VisibilityLevel, - input_workout_visibility: VisibilityLevel, + input_analysis_visibility: VisibilityLevel, expected_map_visibility: VisibilityLevel, app: Flask, sport_1_cycling: Sport, user_1: User, workout_cycling_user_1: Workout, ) -> None: + workout_cycling_user_1.workout_visibility = VisibilityLevel.PUBLIC + workout_cycling_user_1.analysis_visibility = input_analysis_visibility workout_cycling_user_1.map_visibility = input_map_visibility - workout_cycling_user_1.workout_visibility = input_workout_visibility assert ( workout_cycling_user_1.calculated_map_visibility @@ -360,6 +378,9 @@ def test_it_serializes_suspended_workout( ) assert serialized_workout == { + 'analysis_visibility': ( + workout_cycling_user_1.analysis_visibility.value + ), 'ascent': None, 'ave_speed': float(workout_cycling_user_1.ave_speed), 'bounds': [], @@ -399,6 +420,7 @@ def test_it_serializes_suspended_workout( 'workout_visibility': ( workout_cycling_user_1.workout_visibility.value ), + 'with_analysis': False, 'with_gpx': False, } @@ -414,6 +436,9 @@ def test_it_serializes_minimal_workout( ) assert serialized_workout == { + 'analysis_visibility': ( + workout_cycling_user_1.analysis_visibility.value + ), 'ascent': None, 'ave_speed': float(workout_cycling_user_1.ave_speed), 'bounds': [], @@ -449,6 +474,7 @@ def test_it_serializes_minimal_workout( 'workout_visibility': ( workout_cycling_user_1.workout_visibility.value ), + 'with_analysis': False, 'with_gpx': False, } @@ -459,15 +485,18 @@ def test_it_serializes_minimal_workout_with_gpx( user_1: User, workout_cycling_user_1: Workout, ) -> None: - workout = self.update_workout(workout_cycling_user_1) - workout.ascent = 0 - workout.descent = 10 + workout_cycling_user_1 = self.update_workout_with_gpx_data( + workout_cycling_user_1 + ) serialized_workout = workout_cycling_user_1.serialize( user=user_1, light=True ) assert serialized_workout == { + 'analysis_visibility': ( + workout_cycling_user_1.analysis_visibility.value + ), 'ascent': float(workout_cycling_user_1.ascent), 'ave_speed': float(workout_cycling_user_1.ave_speed), 'bounds': [], @@ -481,9 +510,9 @@ def test_it_serializes_minimal_workout_with_gpx( 'likes_count': 0, 'map': None, 'map_visibility': workout_cycling_user_1.map_visibility.value, - 'max_alt': None, + 'max_alt': float(workout_cycling_user_1.max_alt), 'max_speed': float(workout_cycling_user_1.max_speed), - 'min_alt': None, + 'min_alt': float(workout_cycling_user_1.min_alt), 'modification_date': None, 'moving': str(workout_cycling_user_1.moving), 'next_workout': None, @@ -503,6 +532,7 @@ def test_it_serializes_minimal_workout_with_gpx( 'workout_visibility': ( workout_cycling_user_1.workout_visibility.value ), + 'with_analysis': True, 'with_gpx': True, } @@ -524,6 +554,9 @@ def test_it_serializes_minimal_suspended_workout( ) assert serialized_workout == { + 'analysis_visibility': ( + workout_cycling_user_1.analysis_visibility.value + ), 'ascent': None, 'ave_speed': float(workout_cycling_user_1.ave_speed), 'bounds': [], @@ -562,6 +595,7 @@ def test_it_serializes_minimal_suspended_workout( 'workout_visibility': ( workout_cycling_user_1.workout_visibility.value ), + 'with_analysis': False, 'with_gpx': False, } @@ -571,7 +605,7 @@ def test_workout_segment_model( sport_1_cycling: Sport, user_1: User, workout_cycling_user_1: Workout, - workout_cycling_user_1_segment: Workout, + workout_cycling_user_1_segment: WorkoutSegment, ) -> None: assert ( f' None: workout_cycling_user_1.workout_visibility = input_workout_visibility - workout_cycling_user_1.map_visibility = input_map_visibility + workout_cycling_user_1.analysis_visibility = input_analysis_visibility add_follower(user_1, user_2) - workout = self.update_workout( + workout = self.update_workout_with_gpx_data( workout_cycling_user_1, map_id=random_string() ) serialized_workout = workout.serialize(user=user_2, light=False) - assert serialized_workout['map'] == workout.map - assert serialized_workout['bounds'] == workout.bounds - assert serialized_workout['with_gpx'] is True - assert serialized_workout['map_visibility'] == input_map_visibility + assert ( + serialized_workout['analysis_visibility'] + == input_analysis_visibility + ) + assert serialized_workout['ascent'] == float(workout.ascent) + assert serialized_workout['bounds'] == [] + assert serialized_workout['descent'] == float(workout.descent) + assert serialized_workout['map'] is None + assert serialized_workout['map_visibility'] == VisibilityLevel.PRIVATE + assert serialized_workout['max_alt'] == float(workout.max_alt) + assert serialized_workout['min_alt'] == float(workout.min_alt) + assert serialized_workout['segments'] == [ + workout_cycling_user_1_segment.serialize() + ] + assert serialized_workout['with_analysis'] is True + assert serialized_workout['with_gpx'] is False assert ( serialized_workout['workout_visibility'] == input_workout_visibility ) - assert serialized_workout['segments'] == [] @pytest.mark.parametrize( - 'input_map_visibility,input_workout_visibility', + 'input_analysis_visibility,input_workout_visibility', [ ( VisibilityLevel.PRIVATE, @@ -912,32 +958,152 @@ def test_serializer_returns_map_related_data( ), ], ) - def test_serializer_does_not_return_map_related_data( + def test_serializer_does_not_return_analysis_related_data( self, - input_map_visibility: VisibilityLevel, + input_analysis_visibility: VisibilityLevel, input_workout_visibility: VisibilityLevel, app: Flask, sport_1_cycling: Sport, user_1: User, user_2: User, workout_cycling_user_1: Workout, + workout_cycling_user_1_segment: WorkoutSegment, ) -> None: workout_cycling_user_1.workout_visibility = input_workout_visibility - workout_cycling_user_1.map_visibility = input_map_visibility + workout_cycling_user_1.analysis_visibility = input_analysis_visibility add_follower(user_1, user_2) - workout = self.update_workout(workout_cycling_user_1) + workout = self.update_workout_with_gpx_data(workout_cycling_user_1) serialized_workout = workout.serialize(user=user_2, light=False) - assert serialized_workout['map'] is None + assert ( + serialized_workout['analysis_visibility'] + == input_analysis_visibility + ) + assert serialized_workout['ascent'] == float(workout.ascent) assert serialized_workout['bounds'] == [] + assert serialized_workout['descent'] == float(workout.descent) + assert serialized_workout['map'] is None + assert serialized_workout['map_visibility'] == VisibilityLevel.PRIVATE + assert serialized_workout['max_alt'] is None + assert serialized_workout['min_alt'] is None + assert serialized_workout['segments'] == [] + assert serialized_workout['with_analysis'] is False assert serialized_workout['with_gpx'] is False - assert serialized_workout['map_visibility'] == input_map_visibility assert ( serialized_workout['workout_visibility'] == input_workout_visibility ) - assert serialized_workout['segments'] == [] + + @pytest.mark.parametrize( + 'input_map_visibility,input_analysis_visibility', + [ + ( + VisibilityLevel.FOLLOWERS, + VisibilityLevel.FOLLOWERS, + ), + ( + VisibilityLevel.FOLLOWERS, + VisibilityLevel.PUBLIC, + ), + ( + VisibilityLevel.PUBLIC, + VisibilityLevel.PUBLIC, + ), + ], + ) + def test_serializer_returns_map_related_data( + self, + input_map_visibility: VisibilityLevel, + input_analysis_visibility: VisibilityLevel, + app: Flask, + sport_1_cycling: Sport, + user_1: User, + user_2: User, + workout_cycling_user_1: Workout, + workout_cycling_user_1_segment: WorkoutSegment, + ) -> None: + workout_cycling_user_1.workout_visibility = VisibilityLevel.PUBLIC + workout_cycling_user_1.analysis_visibility = input_analysis_visibility + workout_cycling_user_1.map_visibility = input_map_visibility + add_follower(user_1, user_2) + workout = self.update_workout_with_gpx_data( + workout_cycling_user_1, map_id=random_string() + ) + + serialized_workout = workout.serialize(user=user_2, light=False) + + assert ( + serialized_workout['analysis_visibility'] + == input_analysis_visibility + ) + assert serialized_workout['ascent'] == float(workout.ascent) + assert serialized_workout['bounds'] == workout.bounds + assert serialized_workout['descent'] == float(workout.descent) + assert serialized_workout['map'] == workout.map + assert serialized_workout['map_visibility'] == input_map_visibility + assert serialized_workout['max_alt'] == float(workout.max_alt) + assert serialized_workout['min_alt'] == float(workout.min_alt) + assert serialized_workout['segments'] == [ + workout_cycling_user_1_segment.serialize() + ] + assert serialized_workout['with_analysis'] is True + assert serialized_workout['with_gpx'] is True + assert ( + serialized_workout['workout_visibility'] == VisibilityLevel.PUBLIC + ) + + @pytest.mark.parametrize( + 'input_map_visibility,input_analysis_visibility', + [ + ( + VisibilityLevel.PRIVATE, + VisibilityLevel.FOLLOWERS, + ), + ( + VisibilityLevel.PRIVATE, + VisibilityLevel.PUBLIC, + ), + ], + ) + def test_serializer_does_not_return_map_related_data( + self, + input_map_visibility: VisibilityLevel, + input_analysis_visibility: VisibilityLevel, + app: Flask, + sport_1_cycling: Sport, + user_1: User, + user_2: User, + workout_cycling_user_1: Workout, + workout_cycling_user_1_segment: WorkoutSegment, + ) -> None: + workout_cycling_user_1.workout_visibility = VisibilityLevel.PUBLIC + workout_cycling_user_1.analysis_visibility = input_analysis_visibility + workout_cycling_user_1.map_visibility = input_map_visibility + add_follower(user_1, user_2) + workout = self.update_workout_with_gpx_data(workout_cycling_user_1) + + serialized_workout = workout.serialize(user=user_2, light=False) + + assert ( + serialized_workout['analysis_visibility'] + == input_analysis_visibility + ) + assert serialized_workout['ascent'] == float(workout.ascent) + assert serialized_workout['bounds'] == [] + assert serialized_workout['descent'] == float(workout.descent) + assert serialized_workout['map'] is None + assert serialized_workout['map_visibility'] == input_map_visibility + assert serialized_workout['max_alt'] == float(workout.max_alt) + assert serialized_workout['min_alt'] == float(workout.min_alt) + assert serialized_workout['segments'] == [ + workout_cycling_user_1_segment.serialize() + ] + assert serialized_workout['with_analysis'] is True + assert serialized_workout['with_gpx'] is False + assert ( + serialized_workout['workout_visibility'] == VisibilityLevel.PUBLIC + ) def test_serializer_does_not_return_next_workout( self, @@ -1078,6 +1244,7 @@ def test_serialize_returns_suspended_workout_when_user_commented_workout( 'user': user_1.serialize(), 'weather_end': None, 'weather_start': None, + 'with_analysis': False, 'with_gpx': False, 'workout_date': workout_cycling_user_1.workout_date, 'workout_visibility': ( @@ -1126,6 +1293,9 @@ def test_it_serializes_minimal_workout( ) assert serialized_workout == { + 'analysis_visibility': ( + workout_cycling_user_1.analysis_visibility.value + ), 'ascent': None, 'ave_speed': float(workout_cycling_user_1.ave_speed), 'bounds': [], @@ -1160,6 +1330,7 @@ def test_it_serializes_minimal_workout( 'workout_visibility': ( workout_cycling_user_1.workout_visibility.value ), + 'with_analysis': False, 'with_gpx': False, } @@ -1204,6 +1375,95 @@ def test_serializer_does_not_return_notes( assert serialized_workout['notes'] is None + def test_serializer_returns_analysis_related_data_when_visibility_is_public( # noqa + self, + app: Flask, + sport_1_cycling: Sport, + user_1: User, + user_2: User, + workout_cycling_user_1: Workout, + workout_cycling_user_1_segment: WorkoutSegment, + ) -> None: + workout_cycling_user_1.workout_visibility = VisibilityLevel.PUBLIC + workout_cycling_user_1.analysis_visibility = VisibilityLevel.PUBLIC + workout_cycling_user_1.map_visibility = VisibilityLevel.PRIVATE + workout = self.update_workout_with_gpx_data( + workout_cycling_user_1, map_id=random_string() + ) + + serialized_workout = workout.serialize(user=user_2, light=False) + + assert ( + serialized_workout['analysis_visibility'] == VisibilityLevel.PUBLIC + ) + assert serialized_workout['ascent'] == float(workout.ascent) + assert serialized_workout['bounds'] == [] + assert serialized_workout['descent'] == float(workout.descent) + assert serialized_workout['map'] is None + assert serialized_workout['map_visibility'] == VisibilityLevel.PRIVATE + assert serialized_workout['max_alt'] == float(workout.max_alt) + assert serialized_workout['min_alt'] == float(workout.min_alt) + assert serialized_workout['segments'] == [ + workout_cycling_user_1_segment.serialize() + ] + assert serialized_workout['with_analysis'] is True + assert serialized_workout['with_gpx'] is False + assert ( + serialized_workout['workout_visibility'] == VisibilityLevel.PUBLIC + ) + assert serialized_workout['segments'] == [ + workout_cycling_user_1_segment.serialize() + ] + + @pytest.mark.parametrize( + 'input_analysis_visibility,input_workout_visibility', + [ + ( + VisibilityLevel.PRIVATE, + VisibilityLevel.PUBLIC, + ), + ( + VisibilityLevel.FOLLOWERS, + VisibilityLevel.PUBLIC, + ), + ], + ) + def test_serializer_does_not_return_analysis_related_data( + self, + input_analysis_visibility: VisibilityLevel, + input_workout_visibility: VisibilityLevel, + app: Flask, + sport_1_cycling: Sport, + user_1: User, + user_2: User, + workout_cycling_user_1: Workout, + workout_cycling_user_1_segment: WorkoutSegment, + ) -> None: + workout_cycling_user_1.workout_visibility = input_workout_visibility + workout_cycling_user_1.analysis_visibility = input_analysis_visibility + workout = self.update_workout_with_gpx_data(workout_cycling_user_1) + + serialized_workout = workout.serialize(user=user_2, light=False) + + assert ( + serialized_workout['analysis_visibility'] + == VisibilityLevel.PRIVATE + ) + assert serialized_workout['ascent'] == float(workout.ascent) + assert serialized_workout['bounds'] == [] + assert serialized_workout['descent'] == float(workout.descent) + assert serialized_workout['map'] is None + assert serialized_workout['map_visibility'] == VisibilityLevel.PRIVATE + assert serialized_workout['max_alt'] is None + assert serialized_workout['min_alt'] is None + assert serialized_workout['segments'] == [] + assert serialized_workout['with_analysis'] is False + assert serialized_workout['with_gpx'] is False + assert ( + serialized_workout['workout_visibility'] + == input_workout_visibility + ) + def test_serializer_returns_map_related_data_when_visibility_is_public( self, app: Flask, @@ -1213,24 +1473,30 @@ def test_serializer_returns_map_related_data_when_visibility_is_public( workout_cycling_user_1: Workout, ) -> None: workout_cycling_user_1.workout_visibility = VisibilityLevel.PUBLIC + workout_cycling_user_1.analysis_visibility = VisibilityLevel.PUBLIC workout_cycling_user_1.map_visibility = VisibilityLevel.PUBLIC - workout = self.update_workout( + workout = self.update_workout_with_gpx_data( workout_cycling_user_1, map_id=random_string() ) serialized_workout = workout.serialize(user=user_2, light=False) - assert serialized_workout['map'] == workout.map + assert serialized_workout['ascent'] == float(workout.ascent) assert serialized_workout['bounds'] == workout.bounds - assert serialized_workout['with_gpx'] is True + assert serialized_workout['descent'] == float(workout.descent) + assert serialized_workout['map'] == workout.map assert serialized_workout['map_visibility'] == VisibilityLevel.PUBLIC + assert serialized_workout['max_alt'] == float(workout.max_alt) + assert serialized_workout['min_alt'] == float(workout.min_alt) + assert serialized_workout['segments'] == [] + assert serialized_workout['with_analysis'] is True + assert serialized_workout['with_gpx'] is True assert ( serialized_workout['workout_visibility'] == VisibilityLevel.PUBLIC ) - assert serialized_workout['segments'] == [] @pytest.mark.parametrize( - 'input_map_visibility,input_workout_visibility', + 'input_map_visibility,input_analysis_visibility', [ ( VisibilityLevel.PRIVATE, @@ -1245,28 +1511,37 @@ def test_serializer_returns_map_related_data_when_visibility_is_public( def test_serializer_does_not_return_map_related_data( self, input_map_visibility: VisibilityLevel, - input_workout_visibility: VisibilityLevel, + input_analysis_visibility: VisibilityLevel, app: Flask, sport_1_cycling: Sport, user_1: User, user_2: User, workout_cycling_user_1: Workout, ) -> None: - workout_cycling_user_1.workout_visibility = input_workout_visibility + workout_cycling_user_1.workout_visibility = VisibilityLevel.PUBLIC + workout_cycling_user_1.analysis_visibility = input_analysis_visibility workout_cycling_user_1.map_visibility = input_map_visibility - workout = self.update_workout(workout_cycling_user_1) + workout = self.update_workout_with_gpx_data(workout_cycling_user_1) serialized_workout = workout.serialize(user=user_2, light=False) - assert serialized_workout['map'] is None + assert ( + serialized_workout['analysis_visibility'] + == input_analysis_visibility + ) + assert serialized_workout['ascent'] == float(workout.ascent) assert serialized_workout['bounds'] == [] - assert serialized_workout['with_gpx'] is False + assert serialized_workout['descent'] == float(workout.descent) + assert serialized_workout['map'] is None assert serialized_workout['map_visibility'] == VisibilityLevel.PRIVATE + assert serialized_workout['max_alt'] == float(workout.max_alt) + assert serialized_workout['min_alt'] == float(workout.min_alt) + assert serialized_workout['segments'] == [] + assert serialized_workout['with_analysis'] is True + assert serialized_workout['with_gpx'] is False assert ( - serialized_workout['workout_visibility'] - == input_workout_visibility + serialized_workout['workout_visibility'] == VisibilityLevel.PUBLIC ) - assert serialized_workout['segments'] == [] def test_serializer_does_not_return_next_workout( self, @@ -1397,6 +1672,7 @@ def test_serialize_returns_suspended_workout_when_user_commented_workout( 'user': user_1.serialize(), 'weather_end': None, 'weather_start': None, + 'with_analysis': False, 'with_gpx': False, 'workout_date': workout_cycling_user_1.workout_date, 'workout_visibility': workout_cycling_user_1.workout_visibility, @@ -1441,6 +1717,9 @@ def test_it_serializes_minimal_workout( ) assert serialized_workout == { + 'analysis_visibility': ( + workout_cycling_user_1.analysis_visibility.value + ), 'ascent': None, 'ave_speed': float(workout_cycling_user_1.ave_speed), 'bounds': [], @@ -1475,6 +1754,7 @@ def test_it_serializes_minimal_workout( 'workout_visibility': ( workout_cycling_user_1.workout_visibility.value ), + 'with_analysis': False, 'with_gpx': False, } @@ -1517,32 +1797,127 @@ def test_serializer_does_not_return_notes( assert serialized_workout['notes'] is None + def test_serializer_returns_analysis_related_data_when_visibility_is_public( # noqa + self, + app: Flask, + sport_1_cycling: Sport, + user_1: User, + workout_cycling_user_1: Workout, + workout_cycling_user_1_segment: WorkoutSegment, + ) -> None: + workout_cycling_user_1.workout_visibility = VisibilityLevel.PUBLIC + workout_cycling_user_1.analysis_visibility = VisibilityLevel.PUBLIC + workout = self.update_workout_with_gpx_data( + workout_cycling_user_1, map_id=random_string() + ) + + serialized_workout = workout.serialize(light=False) + + assert serialized_workout['ascent'] == float(workout.ascent) + assert serialized_workout['bounds'] == [] + assert serialized_workout['descent'] == float(workout.descent) + assert serialized_workout['map'] is None + assert serialized_workout['map_visibility'] == VisibilityLevel.PRIVATE + assert serialized_workout['max_alt'] == float(workout.max_alt) + assert serialized_workout['min_alt'] == float(workout.min_alt) + assert serialized_workout['segments'] == [ + workout_cycling_user_1_segment.serialize() + ] + assert serialized_workout['with_analysis'] is True + assert serialized_workout['with_gpx'] is False + assert ( + serialized_workout['analysis_visibility'] == VisibilityLevel.PUBLIC + ) + assert ( + serialized_workout['workout_visibility'] == VisibilityLevel.PUBLIC + ) + + @pytest.mark.parametrize( + 'input_analysis_visibility,input_workout_visibility', + [ + ( + VisibilityLevel.PRIVATE, + VisibilityLevel.PUBLIC, + ), + ( + VisibilityLevel.FOLLOWERS, + VisibilityLevel.PUBLIC, + ), + ], + ) + def test_serializer_does_not_return_analysis_related_data( + self, + input_analysis_visibility: VisibilityLevel, + input_workout_visibility: VisibilityLevel, + app: Flask, + sport_1_cycling: Sport, + user_1: User, + workout_cycling_user_1: Workout, + workout_cycling_user_1_segment: WorkoutSegment, + ) -> None: + workout_cycling_user_1.workout_visibility = input_workout_visibility + workout_cycling_user_1.analysis_visibility = input_analysis_visibility + workout = self.update_workout_with_gpx_data(workout_cycling_user_1) + + serialized_workout = workout.serialize(light=False) + + assert serialized_workout['ascent'] == float(workout.ascent) + assert serialized_workout['bounds'] == [] + assert serialized_workout['descent'] == float(workout.descent) + assert serialized_workout['map'] is None + assert serialized_workout['map_visibility'] == VisibilityLevel.PRIVATE + assert serialized_workout['max_alt'] is None + assert serialized_workout['min_alt'] is None + assert serialized_workout['segments'] == [] + assert serialized_workout['with_analysis'] is False + assert serialized_workout['with_gpx'] is False + assert ( + serialized_workout['analysis_visibility'] + == VisibilityLevel.PRIVATE + ) + assert ( + serialized_workout['workout_visibility'] + == input_workout_visibility + ) + def test_serializer_returns_map_related_data_when_visibility_is_public( self, app: Flask, sport_1_cycling: Sport, user_1: User, workout_cycling_user_1: Workout, + workout_cycling_user_1_segment: WorkoutSegment, ) -> None: workout_cycling_user_1.workout_visibility = VisibilityLevel.PUBLIC + workout_cycling_user_1.analysis_visibility = VisibilityLevel.PUBLIC workout_cycling_user_1.map_visibility = VisibilityLevel.PUBLIC - workout = self.update_workout( + workout = self.update_workout_with_gpx_data( workout_cycling_user_1, map_id=random_string() ) serialized_workout = workout.serialize(light=False) + assert serialized_workout['ascent'] == float(workout.ascent) + assert serialized_workout['descent'] == float(workout.descent) assert serialized_workout['map'] == workout.map + assert serialized_workout['max_alt'] == float(workout.max_alt) + assert serialized_workout['min_alt'] == float(workout.min_alt) assert serialized_workout['bounds'] == workout.bounds + assert serialized_workout['with_analysis'] is True assert serialized_workout['with_gpx'] is True assert serialized_workout['map_visibility'] == VisibilityLevel.PUBLIC + assert ( + serialized_workout['analysis_visibility'] == VisibilityLevel.PUBLIC + ) assert ( serialized_workout['workout_visibility'] == VisibilityLevel.PUBLIC ) - assert serialized_workout['segments'] == [] + assert serialized_workout['segments'] == [ + workout_cycling_user_1_segment.serialize() + ] @pytest.mark.parametrize( - 'input_map_visibility,input_workout_visibility', + 'input_map_visibility,input_analysis_visibility', [ ( VisibilityLevel.PRIVATE, @@ -1557,27 +1932,39 @@ def test_serializer_returns_map_related_data_when_visibility_is_public( def test_serializer_does_not_return_map_related_data( self, input_map_visibility: VisibilityLevel, - input_workout_visibility: VisibilityLevel, + input_analysis_visibility: VisibilityLevel, app: Flask, sport_1_cycling: Sport, user_1: User, workout_cycling_user_1: Workout, + workout_cycling_user_1_segment: WorkoutSegment, ) -> None: - workout_cycling_user_1.workout_visibility = input_workout_visibility + workout_cycling_user_1.workout_visibility = VisibilityLevel.PUBLIC + workout_cycling_user_1.analysis_visibility = input_analysis_visibility workout_cycling_user_1.map_visibility = input_map_visibility - workout = self.update_workout(workout_cycling_user_1) + workout = self.update_workout_with_gpx_data(workout_cycling_user_1) - serialized_workout = workout.serialize() + serialized_workout = workout.serialize(light=False) - assert serialized_workout['map'] is None + assert serialized_workout['ascent'] == float(workout.ascent) assert serialized_workout['bounds'] == [] - assert serialized_workout['with_gpx'] is False + assert serialized_workout['descent'] == float(workout.descent) + assert serialized_workout['map'] is None assert serialized_workout['map_visibility'] == VisibilityLevel.PRIVATE + assert serialized_workout['max_alt'] == float(workout.max_alt) + assert serialized_workout['min_alt'] == float(workout.min_alt) + assert serialized_workout['segments'] == [ + workout_cycling_user_1_segment.serialize() + ] + assert serialized_workout['with_analysis'] is True + assert serialized_workout['with_gpx'] is False assert ( - serialized_workout['workout_visibility'] - == input_workout_visibility + serialized_workout['analysis_visibility'] + == input_analysis_visibility + ) + assert ( + serialized_workout['workout_visibility'] == VisibilityLevel.PUBLIC ) - assert serialized_workout['segments'] == [] def test_serializer_does_not_return_next_workout( self, @@ -1699,6 +2086,9 @@ def test_it_serializes_minimal_workout( serialized_workout = workout_cycling_user_1.serialize(light=True) assert serialized_workout == { + 'analysis_visibility': ( + workout_cycling_user_1.analysis_visibility.value + ), 'ascent': None, 'ave_speed': float(workout_cycling_user_1.ave_speed), 'bounds': [], @@ -1733,6 +2123,7 @@ def test_it_serializes_minimal_workout( 'workout_visibility': ( workout_cycling_user_1.workout_visibility.value ), + 'with_analysis': False, 'with_gpx': False, } @@ -1786,6 +2177,9 @@ def test_it_returns_workout_when_report_flag_is_true( ) assert serialized_workout == { + 'analysis_visibility': ( + workout_cycling_user_2.analysis_visibility.value + ), 'ascent': None, 'ave_speed': float(workout_cycling_user_2.ave_speed), 'bounds': [], @@ -1822,6 +2216,7 @@ def test_it_returns_workout_when_report_flag_is_true( 'workout_visibility': ( workout_cycling_user_2.workout_visibility.value ), + 'with_analysis': False, 'with_gpx': False, } @@ -1843,9 +2238,10 @@ def test_it_returns_workout_with_map_when_report_flag_is_true( workout_cycling_user_2: Workout, ) -> None: workout_cycling_user_2.map_visibility = input_workout_visibility + workout_cycling_user_2.analysis_visibility = input_workout_visibility workout_cycling_user_2.workout_visibility = input_workout_visibility map_id = random_string() - workout_cycling_user_2 = self.update_workout( + workout_cycling_user_2 = self.update_workout_with_gpx_data( workout_cycling_user_2, map_id=map_id ) @@ -1854,11 +2250,14 @@ def test_it_returns_workout_with_map_when_report_flag_is_true( ) assert serialized_workout == { - 'ascent': None, + 'analysis_visibility': ( + workout_cycling_user_2.analysis_visibility.value + ), + 'ascent': float(workout_cycling_user_2.ascent), 'ave_speed': float(workout_cycling_user_2.ave_speed), 'bounds': workout_cycling_user_2.bounds, 'creation_date': workout_cycling_user_2.creation_date, - 'descent': None, + 'descent': float(workout_cycling_user_2.descent), 'description': None, 'distance': float(workout_cycling_user_2.distance), 'duration': str(workout_cycling_user_2.duration), @@ -1868,9 +2267,9 @@ def test_it_returns_workout_with_map_when_report_flag_is_true( 'likes_count': 0, 'map': map_id, 'map_visibility': workout_cycling_user_2.map_visibility.value, - 'max_alt': None, + 'max_alt': float(workout_cycling_user_2.max_alt), 'max_speed': float(workout_cycling_user_2.max_speed), - 'min_alt': None, + 'min_alt': float(workout_cycling_user_2.min_alt), 'modification_date': workout_cycling_user_2.modification_date, 'moving': str(workout_cycling_user_2.moving), 'next_workout': None, @@ -1890,6 +2289,7 @@ def test_it_returns_workout_with_map_when_report_flag_is_true( 'workout_visibility': ( workout_cycling_user_2.workout_visibility.value ), + 'with_analysis': True, 'with_gpx': True, } @@ -1969,6 +2369,9 @@ def test_it_serializes_minimal_workout( ) assert serialized_workout == { + 'analysis_visibility': ( + workout_cycling_user_2.analysis_visibility.value + ), 'ascent': None, 'ave_speed': float(workout_cycling_user_2.ave_speed), 'bounds': [], @@ -2004,6 +2407,7 @@ def test_it_serializes_minimal_workout( 'workout_visibility': ( workout_cycling_user_2.workout_visibility.value ), + 'with_analysis': False, 'with_gpx': False, } @@ -2037,6 +2441,9 @@ def test_it_returns_workout_when_report_flag_is_true( ) assert serialized_workout == { + 'analysis_visibility': ( + workout_cycling_user_2.analysis_visibility.value + ), 'ascent': None, 'ave_speed': float(workout_cycling_user_2.ave_speed), 'bounds': [], @@ -2073,6 +2480,7 @@ def test_it_returns_workout_when_report_flag_is_true( 'workout_visibility': ( workout_cycling_user_2.workout_visibility.value ), + 'with_analysis': False, 'with_gpx': False, } @@ -2085,9 +2493,10 @@ def test_it_returns_workout_with_map_when_report_flag_is_true( workout_cycling_user_2: Workout, ) -> None: workout_cycling_user_2.map_visibility = VisibilityLevel.FOLLOWERS + workout_cycling_user_2.analysis_visibility = VisibilityLevel.FOLLOWERS workout_cycling_user_2.workout_visibility = VisibilityLevel.FOLLOWERS map_id = random_string() - workout_cycling_user_2 = self.update_workout( + workout_cycling_user_2 = self.update_workout_with_gpx_data( workout_cycling_user_2, map_id=map_id ) @@ -2096,11 +2505,14 @@ def test_it_returns_workout_with_map_when_report_flag_is_true( ) assert serialized_workout == { - 'ascent': None, + 'analysis_visibility': ( + workout_cycling_user_2.analysis_visibility.value + ), + 'ascent': float(workout_cycling_user_2.ascent), 'ave_speed': float(workout_cycling_user_2.ave_speed), 'bounds': workout_cycling_user_2.bounds, 'creation_date': workout_cycling_user_2.creation_date, - 'descent': None, + 'descent': float(workout_cycling_user_2.descent), 'description': None, 'distance': float(workout_cycling_user_2.distance), 'duration': str(workout_cycling_user_2.duration), @@ -2110,9 +2522,9 @@ def test_it_returns_workout_with_map_when_report_flag_is_true( 'likes_count': 0, 'map': map_id, 'map_visibility': workout_cycling_user_2.map_visibility.value, - 'max_alt': None, + 'max_alt': float(workout_cycling_user_2.max_alt), 'max_speed': float(workout_cycling_user_2.max_speed), - 'min_alt': None, + 'min_alt': float(workout_cycling_user_2.min_alt), 'modification_date': workout_cycling_user_2.modification_date, 'moving': str(workout_cycling_user_2.moving), 'next_workout': None, @@ -2132,5 +2544,6 @@ def test_it_returns_workout_with_map_when_report_flag_is_true( 'workout_visibility': ( workout_cycling_user_2.workout_visibility.value ), + 'with_analysis': True, 'with_gpx': True, } diff --git a/fittrackee/users/auth.py b/fittrackee/users/auth.py index fe188220f..d2bf72189 100644 --- a/fittrackee/users/auth.py +++ b/fittrackee/users/auth.py @@ -46,7 +46,10 @@ ) from fittrackee.users.users_service import UserManagerService from fittrackee.utils import decode_short_id, get_readable_duration -from fittrackee.visibility_levels import VisibilityLevel, get_map_visibility +from fittrackee.visibility_levels import ( + VisibilityLevel, + get_calculated_visibility, +) from fittrackee.workouts.models import Sport from ..reports.models import ReportAction, ReportActionAppeal @@ -319,6 +322,7 @@ def get_authenticated_user_profile( "data": { "accepted_privacy_policy": true, "admin": false, + "analysis_visibility": "private", "bio": null, "birth_date": null, "created_at": "Sun, 14 Jul 2019 14:09:58 GMT", @@ -448,6 +452,7 @@ def edit_user(auth_user: User) -> Union[Dict, HttpResponse]: "data": { "accepted_privacy_policy": true, "admin": false, + "analysis_visibility": "private", "bio": null, "birth_date": null, "created_at": "Sun, 14 Jul 2019 14:09:58 GMT", @@ -629,6 +634,7 @@ def update_user_account(auth_user: User) -> Union[Dict, HttpResponse]: "data": { "accepted_privacy_policy": true, "admin": false, + "analysis_visibility": "private", "bio": null, "birth_date": null, "created_at": "Sun, 14 Jul 2019 14:09:58 GMT", @@ -864,6 +870,7 @@ def edit_user_preferences(auth_user: User) -> Union[Dict, HttpResponse]: "data": { "accepted_privacy_policy": true, "admin": false, + "analysis_visibility": "private", "bio": null, "birth_date": null, "created_at": "Sun, 14 Jul 2019 14:09:58 GMT", @@ -952,6 +959,8 @@ def edit_user_preferences(auth_user: User) -> Union[Dict, HttpResponse]: "status": "success" } + : Union[Dict, HttpResponse]: # get post data post_data = request.get_json() user_mandatory_data = { + 'analysis_visibility', 'date_format', 'display_ascent', 'hide_profile_in_users_directory', @@ -1013,6 +1023,7 @@ def edit_user_preferences(auth_user: User) -> Union[Dict, HttpResponse]: timezone = post_data.get('timezone') weekm = post_data.get('weekm') map_visibility = post_data.get('map_visibility') + analysis_visibility = post_data.get('analysis_visibility') workouts_visibility = post_data.get('workouts_visibility') manually_approves_followers = post_data.get('manually_approves_followers') hide_profile_in_users_directory = post_data.get( @@ -1030,8 +1041,13 @@ def edit_user_preferences(auth_user: User) -> Union[Dict, HttpResponse]: auth_user.use_raw_gpx_speed = use_raw_gpx_speed auth_user.weekm = weekm auth_user.workouts_visibility = VisibilityLevel(workouts_visibility) - auth_user.map_visibility = get_map_visibility( - VisibilityLevel(map_visibility), auth_user.workouts_visibility + auth_user.analysis_visibility = get_calculated_visibility( + visibility=VisibilityLevel(analysis_visibility), + parent_visibility=auth_user.workouts_visibility, + ) + auth_user.map_visibility = get_calculated_visibility( + visibility=VisibilityLevel(map_visibility), + parent_visibility=auth_user.analysis_visibility, ) auth_user.manually_approves_followers = manually_approves_followers auth_user.hide_profile_in_users_directory = ( diff --git a/fittrackee/users/models.py b/fittrackee/users/models.py index dc4db45f9..f13e5eec3 100644 --- a/fittrackee/users/models.py +++ b/fittrackee/users/models.py @@ -273,6 +273,11 @@ class User(BaseModel): nullable=False, default=UserRole.USER.value, ) + analysis_visibility = db.Column( + Enum(VisibilityLevel, name='visibility_levels'), + server_default='PRIVATE', + nullable=False, + ) workouts = db.relationship( 'Workout', @@ -787,6 +792,7 @@ def serialize( 'use_raw_gpx_speed': self.use_raw_gpx_speed, 'weekm': self.weekm, 'map_visibility': self.map_visibility.value, + 'analysis_visibility': self.analysis_visibility.value, 'workouts_visibility': self.workouts_visibility.value, 'manually_approves_followers': ( self.manually_approves_followers diff --git a/fittrackee/visibility_levels.py b/fittrackee/visibility_levels.py index 4a91fe469..ade285bce 100644 --- a/fittrackee/visibility_levels.py +++ b/fittrackee/visibility_levels.py @@ -13,16 +13,17 @@ class VisibilityLevel(str, Enum): # to make enum serializable PRIVATE = 'private' # in case of comments, for mentioned users only -def get_map_visibility( - map_visibility: VisibilityLevel, workout_visibility: VisibilityLevel +def get_calculated_visibility( + *, visibility: VisibilityLevel, parent_visibility: VisibilityLevel ) -> VisibilityLevel: - # workout visibility overrides map visibility, when stricter - if workout_visibility == VisibilityLevel.PRIVATE or ( - workout_visibility == VisibilityLevel.FOLLOWERS - and map_visibility == VisibilityLevel.PUBLIC + # - workout visibility overrides analysis visibility, when stricter, + # - analysis visibility overrides map visibility, when stricter. + if parent_visibility == VisibilityLevel.PRIVATE or ( + parent_visibility == VisibilityLevel.FOLLOWERS + and visibility == VisibilityLevel.PUBLIC ): - return workout_visibility - return map_visibility + return parent_visibility + return visibility def can_view( diff --git a/fittrackee/workouts/models.py b/fittrackee/workouts/models.py index ec6e5701c..860faf8be 100644 --- a/fittrackee/workouts/models.py +++ b/fittrackee/workouts/models.py @@ -21,7 +21,7 @@ from fittrackee.visibility_levels import ( VisibilityLevel, can_view, - get_map_visibility, + get_calculated_visibility, ) from .exceptions import WorkoutForbiddenException @@ -275,6 +275,11 @@ class Workout(BaseModel): nullable=False, ) suspended_at = db.Column(db.DateTime, nullable=True) + analysis_visibility = db.Column( + Enum(VisibilityLevel, name='visibility_levels'), + server_default='PRIVATE', + nullable=False, + ) segments = db.relationship( 'WorkoutSegment', @@ -327,9 +332,19 @@ def __init__( def short_id(self) -> str: return encode_uuid(self.uuid) + @property + def calculated_analysis_visibility(self) -> VisibilityLevel: + return get_calculated_visibility( + visibility=self.analysis_visibility, + parent_visibility=self.workout_visibility, + ) + @property def calculated_map_visibility(self) -> VisibilityLevel: - return get_map_visibility(self.map_visibility, self.workout_visibility) + return get_calculated_visibility( + visibility=self.map_visibility, + parent_visibility=self.analysis_visibility, + ) def liked_by(self, user: 'User') -> bool: return user in self.likes.all() @@ -354,6 +369,7 @@ def get_workout_data( self, user: Optional['User'], *, + can_see_analysis_data: Optional[bool] = None, can_see_map_data: Optional[bool] = None, for_report: bool = False, additional_data: bool = False, @@ -363,7 +379,8 @@ def get_workout_data( """ Used by Workout serializer and data export - - can_see_map_data: if user can see map related data + - can_see_analysis_data: if user can see charts + - can_see_map_data: if user can see map - for_report: privacy levels are overridden on report - additional_data is False when: - workout is not suspended @@ -379,6 +396,13 @@ def get_workout_data( for_report = ( for_report and user is not None and user.has_moderator_rights ) + if can_see_analysis_data is None: + can_see_analysis_data = can_view( + self, + "calculated_analysis_visibility", + user=user, + for_report=for_report, + ) if can_see_map_data is None: can_see_map_data = can_view( self, @@ -418,10 +442,23 @@ def get_workout_data( 'max_speed': ( None if self.max_speed is None else float(self.max_speed) ), - 'min_alt': None if self.min_alt is None else float(self.min_alt), - 'max_alt': None if self.max_alt is None else float(self.max_alt), + 'min_alt': ( + float(self.min_alt) + if self.min_alt is not None and can_see_analysis_data + else None + ), + 'max_alt': ( + float(self.max_alt) + if self.max_alt is not None and can_see_analysis_data + else None + ), 'descent': None if self.descent is None else float(self.descent), 'ascent': None if self.ascent is None else float(self.ascent), + 'analysis_visibility': ( + self.calculated_analysis_visibility.value + if can_see_analysis_data + else VisibilityLevel.PRIVATE + ), 'map_visibility': ( self.calculated_map_visibility.value if can_see_map_data @@ -451,7 +488,7 @@ def get_workout_data( ), 'segments': ( [segment.serialize() for segment in self.segments] - if can_see_map_data + if can_see_analysis_data else [] ), 'weather_start': self.weather_start, @@ -484,6 +521,12 @@ def serialize( self, "workout_visibility", user=user, for_report=for_report ): raise WorkoutForbiddenException() + can_see_analysis_data = can_view( + self, + "calculated_analysis_visibility", + user=user, + for_report=for_report, + ) can_see_map_data = can_view( self, "calculated_map_visibility", user=user, for_report=for_report ) @@ -493,6 +536,7 @@ def serialize( workout = self.get_workout_data( user, + can_see_analysis_data=can_see_analysis_data, can_see_map_data=can_see_map_data, for_report=for_report, additional_data=additional_data, @@ -508,6 +552,9 @@ def serialize( workout["with_gpx"] = ( self.gpx is not None and can_see_map_data and additional_data ) + workout["with_analysis"] = ( + self.gpx is not None and can_see_analysis_data and additional_data + ) workout["suspended"] = is_workout_suspended workout["user"] = self.user.serialize() diff --git a/fittrackee/workouts/utils/workouts.py b/fittrackee/workouts/utils/workouts.py index 1c7444e00..d593f4b36 100644 --- a/fittrackee/workouts/utils/workouts.py +++ b/fittrackee/workouts/utils/workouts.py @@ -18,7 +18,7 @@ from fittrackee.visibility_levels import ( VisibilityLevel, can_view, - get_map_visibility, + get_calculated_visibility, ) from ..constants import WORKOUT_DATE_FORMAT @@ -176,11 +176,27 @@ def create_workout( new_workout.workout_visibility = VisibilityLevel( workout_data.get('workout_visibility', user.workouts_visibility.value) ) - new_workout.map_visibility = get_map_visibility( - VisibilityLevel( - workout_data.get('map_visibility', user.map_visibility.value) - ), - new_workout.workout_visibility, + new_workout.analysis_visibility = ( + get_calculated_visibility( + visibility=VisibilityLevel( + workout_data.get( + 'analysis_visibility', user.analysis_visibility.value + ) + ), + parent_visibility=new_workout.workout_visibility, + ) + if gpx_data + else VisibilityLevel.PRIVATE + ) + new_workout.map_visibility = ( + get_calculated_visibility( + visibility=VisibilityLevel( + workout_data.get('map_visibility', user.map_visibility.value) + ), + parent_visibility=new_workout.analysis_visibility, + ) + if gpx_data + else VisibilityLevel.PRIVATE ) if title is not None and title != '': @@ -308,12 +324,21 @@ def edit_workout( workout.descent = workout_data.get('descent') else: + if workout_data.get('analysis_visibility') is not None: + analysis_visibility = VisibilityLevel( + workout_data.get('analysis_visibility') + ) + workout.analysis_visibility = get_calculated_visibility( + visibility=analysis_visibility, + parent_visibility=workout.workout_visibility, + ) if workout_data.get('map_visibility') is not None: map_visibility = VisibilityLevel( workout_data.get('map_visibility') ) - workout.map_visibility = get_map_visibility( - map_visibility, workout.workout_visibility + workout.map_visibility = get_calculated_visibility( + visibility=map_visibility, + parent_visibility=workout.analysis_visibility, ) return workout diff --git a/fittrackee/workouts/workouts.py b/fittrackee/workouts/workouts.py index 47ca3c008..7da0d43cc 100644 --- a/fittrackee/workouts/workouts.py +++ b/fittrackee/workouts/workouts.py @@ -104,6 +104,7 @@ def get_workouts(auth_user: User) -> Union[Dict, HttpResponse]: "data": { "workouts": [ { + "analysis_visibility": "private", "ascent": null, "ave_speed": 10.0, "bounds": [], @@ -191,6 +192,7 @@ def get_workouts(auth_user: User) -> Union[Dict, HttpResponse]: }, "weather_end": null, "weather_start": null, + "with_analysis": false, "with_gpx": false, "workout_date": "Mon, 01 Jan 2018 00:00:00 GMT", "workout_visibility": "private" @@ -437,6 +439,7 @@ def get_workout( "data": { "workouts": [ { + "analysis_visibility": "private", "ascent": null, "ave_speed": 16, "bounds": [], @@ -478,6 +481,7 @@ def get_workout( }, "weather_end": null, "weather_start": null, + "with_analysis": false, "with_gpx": false, "workout_date": "Sun, 07 Jul 2019 07:00:00 GMT", "workout_visibility": "private" @@ -539,7 +543,13 @@ def get_workout_data( if not workout: return not_found_response - if not can_view(workout, 'calculated_map_visibility', auth_user): + if not can_view( + workout, + 'calculated_analysis_visibility' + if data_type == 'chart_data' + else 'calculated_map_visibility', + auth_user, + ): return not_found_response if not workout.gpx or workout.gpx == '': @@ -1028,6 +1038,7 @@ def post_workout(auth_user: User) -> Union[Tuple[Dict, int], HttpResponse]: "data": { "workouts": [ { + "analysis_visibility": "private", "ascent": 435.621, "ave_speed": 13.14, "bounds": [ @@ -1135,6 +1146,7 @@ def post_workout(auth_user: User) -> Union[Tuple[Dict, int], HttpResponse]: }, "weather_end": null, "weather_start": null, + "with_analysis": false, "with_gpx": true, "workout_date": "Tue, 26 Apr 2016 14:42:30 GMT", "workout_visibility": "private" @@ -1145,10 +1157,12 @@ def post_workout(auth_user: User) -> Union[Tuple[Dict, int], HttpResponse]: } :form file: gpx file (allowed extensions: .gpx, .zip) - :form data: sport id, equipment id, description, title and notes, + :form data: sport id, equipment id, description, title, notes, visibility + for workout, analysis and map for example: ``{"sport_id": 1, "notes": "", "title": "", "description": "", - "equipment_ids": []}``. + "analysis_visibility": "private", "map_visibility": "private", + "workout_visibility": "private", "equipment_ids": []}``. Double quotes in notes, description and title must be escaped. The maximum length is 500 characters for notes, 10000 characters for @@ -1162,7 +1176,9 @@ def post_workout(auth_user: User) -> Union[Tuple[Dict, int], HttpResponse]: If not provided and default equipment exists for sport, default equipment will be associated. - Notes, description, title and equipment ids are not mandatory. + Notes, description, title, equipment ids and visibility + for workout, analysis and map are not mandatory. + Visibility levels default to user preferences. :reqheader Authorization: OAuth 2.0 Bearer Token @@ -1290,6 +1306,7 @@ def post_workout_no_gpx( "data": { "workouts": [ { + "analysis_visibility": "private", "ascent": null, "ave_speed": 10.0, "bounds": [], @@ -1369,6 +1386,7 @@ def post_workout_no_gpx( "uuid": "kjxavSTUrJvoAh2wvCeGEF" "weather_end": null, "weather_start": null, + "with_analysis": false, "with_gpx": false, "workout_date": "Mon, 01 Jan 2018 00:00:00 GMT", "workout_visibility": "private" @@ -1378,6 +1396,9 @@ def post_workout_no_gpx( "status": "success" } + : {{ $t(`visibility_levels.LEVELS.${user.workouts_visibility}`) }} +
{{ $t('visibility_levels.ANALYSIS_VISIBILITY') }}:
+
+ {{ $t(`visibility_levels.LEVELS.${user.analysis_visibility}`) }} +
{{ $t('visibility_levels.MAP_VISIBILITY') }}:
{{ $t(`visibility_levels.LEVELS.${user.map_visibility}`) }} diff --git a/fittrackee_client/src/components/User/ProfileEdition/UserPreferencesEdition.vue b/fittrackee_client/src/components/User/ProfileEdition/UserPreferencesEdition.vue index bfa0eb7aa..1cbb2d878 100644 --- a/fittrackee_client/src/components/User/ProfileEdition/UserPreferencesEdition.vue +++ b/fittrackee_client/src/components/User/ProfileEdition/UserPreferencesEdition.vue @@ -237,7 +237,7 @@ id="workouts_visibility" v-model="userForm.workouts_visibility" :disabled="authUserLoading" - @change="updateMapVisibility" + @change="updateAnalysisAndMapVisibility" >