diff --git a/lms/djangoapps/user_tours/toggles.py b/lms/djangoapps/user_tours/toggles.py new file mode 100644 index 000000000000..e3bb2dfab53f --- /dev/null +++ b/lms/djangoapps/user_tours/toggles.py @@ -0,0 +1,15 @@ +""" +Toggles for the User Tours Experience. +""" + +from edx_toggles.toggles import WaffleFlag + +# .. toggle_name: user_tours.tours_disabled +# .. toggle_implementation: WaffleFlag +# .. toggle_default: False +# .. toggle_description: This flag disables user tours in LMS. +# .. toggle_warnings: None +# .. toggle_use_cases: opt_out +# .. toggle_creation_date: 2024-02-06 +# .. toggle_target_removal_date: None +USER_TOURS_DISABLED = WaffleFlag('user_tours.tours_disabled', module_name=__name__, log_prefix='user_tours') diff --git a/lms/djangoapps/user_tours/v1/tests/test_views.py b/lms/djangoapps/user_tours/v1/tests/test_views.py index 57aa7ca5dcc6..f78586b78dec 100644 --- a/lms/djangoapps/user_tours/v1/tests/test_views.py +++ b/lms/djangoapps/user_tours/v1/tests/test_views.py @@ -5,11 +5,13 @@ from django.db.models.signals import post_save from django.test import TestCase, override_settings from django.urls import reverse +from edx_toggles.toggles.testutils import override_waffle_flag from rest_framework import status from common.djangoapps.student.tests.factories import UserFactory from lms.djangoapps.user_tours.handlers import init_user_tour from lms.djangoapps.user_tours.models import UserTour, UserDiscussionsTours +from lms.djangoapps.user_tours.toggles import USER_TOURS_DISABLED from openedx.core.djangoapps.oauth_dispatch.jwt import create_jwt_for_user User = get_user_model() @@ -47,6 +49,13 @@ def send_request(self, jwt_user, request_user, method, data=None): elif method == 'PATCH': return self.client.patch(url, data, content_type='application/json', **headers) + @ddt.data('GET', 'PATCH') + @override_waffle_flag(USER_TOURS_DISABLED, active=True) + def test_tours_disabled(self, method): + """ Test that the tours can be turned off with a waffle flag. """ + response = self.send_request(self.staff_user, self.user, method) + assert response.status_code == status.HTTP_403_FORBIDDEN + @ddt.data('GET', 'PATCH') def test_unauthorized_user(self, method): """ Test all endpoints if request does not have jwt auth. """ @@ -188,6 +197,11 @@ def test_get_tours(self): self.assertEqual(response.data[1]['tour_name'], 'not_responded_filter') self.assertTrue(response.data[1]['show_tour']) + # Test that the view can be disabled by a waffle flag. + with override_waffle_flag(USER_TOURS_DISABLED, active=True): + response = self.client.get(self.url, **headers) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + def test_get_tours_unauthenticated(self): """ Test that an unauthenticated user cannot access the discussion tours endpoint. @@ -215,3 +229,8 @@ def test_update_tour(self): # Check that the tour was updated in the database updated_tour = UserDiscussionsTours.objects.get(id=self.tour.id) self.assertEqual(updated_tour.show_tour, False) + + # Test that the view can be disabled by a waffle flag. + with override_waffle_flag(USER_TOURS_DISABLED, active=True): + response = self.client.put(url, updated_data, content_type='application/json', **headers) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) diff --git a/lms/djangoapps/user_tours/v1/views.py b/lms/djangoapps/user_tours/v1/views.py index dca1964b64db..ce4c354e5dc2 100644 --- a/lms/djangoapps/user_tours/v1/views.py +++ b/lms/djangoapps/user_tours/v1/views.py @@ -10,6 +10,7 @@ from rest_framework import status from lms.djangoapps.user_tours.models import UserTour, UserDiscussionsTours +from lms.djangoapps.user_tours.toggles import USER_TOURS_DISABLED from lms.djangoapps.user_tours.v1.serializers import UserTourSerializer, UserDiscussionsToursSerializer from rest_framework.views import APIView @@ -41,9 +42,12 @@ def get(self, request, username): # pylint: disable=arguments-differ 400 if there is a not allowed request (requesting a user you don't have access to) 401 if unauthorized request - 403 if waffle flag is not enabled + 403 if tours are disabled 404 if the UserTour does not exist (shouldn't happen, but safety first) """ + if USER_TOURS_DISABLED.is_enabled(): + return Response(status=status.HTTP_403_FORBIDDEN) + if request.user.username != username and not request.user.is_staff: return Response(status=status.HTTP_400_BAD_REQUEST) @@ -66,8 +70,11 @@ def patch(self, request, username): # pylint: disable=arguments-differ 400 if update was unsuccessful or there was nothing to update 401 if unauthorized request - 403 if waffle flag is not enabled + 403 if tours are disabled """ + if USER_TOURS_DISABLED.is_enabled(): + return Response(status=status.HTTP_403_FORBIDDEN) + if request.user.username != username: return Response(status=status.HTTP_400_BAD_REQUEST) @@ -125,8 +132,11 @@ def get(self, request, tour_id=None): "user": 1 } ] + 403 if the tours are disabled """ + if USER_TOURS_DISABLED.is_enabled(): + return Response(status=status.HTTP_403_FORBIDDEN) try: with transaction.atomic(): tours = UserDiscussionsTours.objects.filter(user=request.user) @@ -158,9 +168,11 @@ def put(self, request, tour_id): Returns: 200: The updated tour, serialized using the UserDiscussionsToursSerializer 404: If the tour does not exist - 403: If the user does not have permission to update the tour + 403: If the user does not have permission to update the tour or the tours are disabled 400: Validation error """ + if USER_TOURS_DISABLED.is_enabled(): + return Response(status=status.HTTP_403_FORBIDDEN) tour = get_object_or_404(UserDiscussionsTours, pk=tour_id) if tour.user != request.user: return Response(status=status.HTTP_403_FORBIDDEN)