From f73027626370bcf1f4916ead2f431867059b6cdb Mon Sep 17 00:00:00 2001 From: Sagirov Evgeniy <34642612+UvgenGen@users.noreply.github.com> Date: Mon, 4 Nov 2024 20:13:37 +0200 Subject: [PATCH] feat: Add API endpoint to manage course waffle flags (#35622) Co-authored-by: Sagirov Eugeniy --- .../rest_api/v1/serializers/__init__.py | 1 + .../v1/serializers/course_waffle_flags.py | 146 ++++++++++++++++++ .../contentstore/rest_api/v1/urls.py | 6 + .../rest_api/v1/views/__init__.py | 1 + .../rest_api/v1/views/course_waffle_flags.py | 73 +++++++++ .../views/tests/test_course_waffle_flags.py | 112 ++++++++++++++ 6 files changed, 339 insertions(+) create mode 100644 cms/djangoapps/contentstore/rest_api/v1/serializers/course_waffle_flags.py create mode 100644 cms/djangoapps/contentstore/rest_api/v1/views/course_waffle_flags.py create mode 100644 cms/djangoapps/contentstore/rest_api/v1/views/tests/test_course_waffle_flags.py diff --git a/cms/djangoapps/contentstore/rest_api/v1/serializers/__init__.py b/cms/djangoapps/contentstore/rest_api/v1/serializers/__init__.py index 6fe829ce0e3a..616204ef59c7 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/serializers/__init__.py +++ b/cms/djangoapps/contentstore/rest_api/v1/serializers/__init__.py @@ -6,6 +6,7 @@ from .course_index import CourseIndexSerializer from .course_rerun import CourseRerunSerializer from .course_team import CourseTeamSerializer +from .course_waffle_flags import CourseWaffleFlagsSerializer from .grading import CourseGradingModelSerializer, CourseGradingSerializer from .group_configurations import CourseGroupConfigurationsSerializer from .home import StudioHomeSerializer, CourseHomeTabSerializer, LibraryTabSerializer diff --git a/cms/djangoapps/contentstore/rest_api/v1/serializers/course_waffle_flags.py b/cms/djangoapps/contentstore/rest_api/v1/serializers/course_waffle_flags.py new file mode 100644 index 000000000000..b37254234663 --- /dev/null +++ b/cms/djangoapps/contentstore/rest_api/v1/serializers/course_waffle_flags.py @@ -0,0 +1,146 @@ +""" +API Serializers for course waffle flags +""" + +from rest_framework import serializers + +from cms.djangoapps.contentstore import toggles + + +class CourseWaffleFlagsSerializer(serializers.Serializer): + """ + Serializer for course waffle flags + """ + use_new_home_page = serializers.SerializerMethodField() + use_new_custom_pages = serializers.SerializerMethodField() + use_new_schedule_details_page = serializers.SerializerMethodField() + use_new_advanced_settings_page = serializers.SerializerMethodField() + use_new_grading_page = serializers.SerializerMethodField() + use_new_updates_page = serializers.SerializerMethodField() + use_new_import_page = serializers.SerializerMethodField() + use_new_export_page = serializers.SerializerMethodField() + use_new_files_uploads_page = serializers.SerializerMethodField() + use_new_video_uploads_page = serializers.SerializerMethodField() + use_new_course_outline_page = serializers.SerializerMethodField() + use_new_unit_page = serializers.SerializerMethodField() + use_new_course_team_page = serializers.SerializerMethodField() + use_new_certificates_page = serializers.SerializerMethodField() + use_new_textbooks_page = serializers.SerializerMethodField() + use_new_group_configurations_page = serializers.SerializerMethodField() + + def get_course_key(self): + """ + Retrieve the course_key from the context + """ + return self.context.get("course_key") + + def get_use_new_home_page(self, obj): + """ + Method to get the use_new_home_page switch + """ + return toggles.use_new_home_page() + + def get_use_new_custom_pages(self, obj): + """ + Method to get the use_new_custom_pages switch + """ + course_key = self.get_course_key() + return toggles.use_new_custom_pages(course_key) + + def get_use_new_schedule_details_page(self, obj): + """ + Method to get the use_new_schedule_details_page switch + """ + course_key = self.get_course_key() + return toggles.use_new_schedule_details_page(course_key) + + def get_use_new_advanced_settings_page(self, obj): + """ + Method to get the use_new_advanced_settings_page switch + """ + course_key = self.get_course_key() + return toggles.use_new_advanced_settings_page(course_key) + + def get_use_new_grading_page(self, obj): + """ + Method to get the use_new_grading_page switch + """ + course_key = self.get_course_key() + return toggles.use_new_grading_page(course_key) + + def get_use_new_updates_page(self, obj): + """ + Method to get the use_new_updates_page switch + """ + course_key = self.get_course_key() + return toggles.use_new_updates_page(course_key) + + def get_use_new_import_page(self, obj): + """ + Method to get the use_new_import_page switch + """ + course_key = self.get_course_key() + return toggles.use_new_import_page(course_key) + + def get_use_new_export_page(self, obj): + """ + Method to get the use_new_export_page switch + """ + course_key = self.get_course_key() + return toggles.use_new_export_page(course_key) + + def get_use_new_files_uploads_page(self, obj): + """ + Method to get the use_new_files_uploads_page switch + """ + course_key = self.get_course_key() + return toggles.use_new_files_uploads_page(course_key) + + def get_use_new_video_uploads_page(self, obj): + """ + Method to get the use_new_video_uploads_page switch + """ + course_key = self.get_course_key() + return toggles.use_new_video_uploads_page(course_key) + + def get_use_new_course_outline_page(self, obj): + """ + Method to get the use_new_course_outline_page switch + """ + course_key = self.get_course_key() + return toggles.use_new_course_outline_page(course_key) + + def get_use_new_unit_page(self, obj): + """ + Method to get the use_new_unit_page switch + """ + course_key = self.get_course_key() + return toggles.use_new_unit_page(course_key) + + def get_use_new_course_team_page(self, obj): + """ + Method to get the use_new_course_team_page switch + """ + course_key = self.get_course_key() + return toggles.use_new_course_team_page(course_key) + + def get_use_new_certificates_page(self, obj): + """ + Method to get the use_new_certificates_page switch + """ + course_key = self.get_course_key() + return toggles.use_new_certificates_page(course_key) + + def get_use_new_textbooks_page(self, obj): + """ + Method to get the use_new_textbooks_page switch + """ + course_key = self.get_course_key() + return toggles.use_new_textbooks_page(course_key) + + def get_use_new_group_configurations_page(self, obj): + """ + Method to get the use_new_group_configurations_page switch + """ + course_key = self.get_course_key() + return toggles.use_new_group_configurations_page(course_key) diff --git a/cms/djangoapps/contentstore/rest_api/v1/urls.py b/cms/djangoapps/contentstore/rest_api/v1/urls.py index e2afe48c96a4..349218679709 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/urls.py +++ b/cms/djangoapps/contentstore/rest_api/v1/urls.py @@ -17,6 +17,7 @@ CourseRerunView, CourseSettingsView, CourseVideosView, + CourseWaffleFlagsView, HomePageView, HomePageCoursesView, HomePageLibrariesView, @@ -131,6 +132,11 @@ VerticalContainerView.as_view(), name="container_vertical" ), + re_path( + fr'^course_waffle_flags(?:/{COURSE_ID_PATTERN})?$', + CourseWaffleFlagsView.as_view(), + name="course_waffle_flags" + ), # Authoring API # Do not use under v1 yet (Nov. 23). The Authoring API is still experimental and the v0 versions should be used diff --git a/cms/djangoapps/contentstore/rest_api/v1/views/__init__.py b/cms/djangoapps/contentstore/rest_api/v1/views/__init__.py index dfba1e63f72f..89d8d56eaa11 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/views/__init__.py +++ b/cms/djangoapps/contentstore/rest_api/v1/views/__init__.py @@ -5,6 +5,7 @@ from .course_details import CourseDetailsView from .course_index import CourseIndexView from .course_rerun import CourseRerunView +from .course_waffle_flags import CourseWaffleFlagsView from .course_team import CourseTeamView from .grading import CourseGradingView from .group_configurations import CourseGroupConfigurationsView diff --git a/cms/djangoapps/contentstore/rest_api/v1/views/course_waffle_flags.py b/cms/djangoapps/contentstore/rest_api/v1/views/course_waffle_flags.py new file mode 100644 index 000000000000..ba96ff2b4a6c --- /dev/null +++ b/cms/djangoapps/contentstore/rest_api/v1/views/course_waffle_flags.py @@ -0,0 +1,73 @@ +""" API Views for course waffle flags """ + +from opaque_keys.edx.keys import CourseKey +from rest_framework.decorators import APIView +from rest_framework.response import Response + +from openedx.core.lib.api.view_utils import view_auth_classes + +from ..serializers import CourseWaffleFlagsSerializer + + +@view_auth_classes(is_authenticated=True) +class CourseWaffleFlagsView(APIView): + """ + API view to retrieve course waffle flag settings for a specific course. + + This view provides a GET endpoint that returns the status of various waffle + flags for a given course. It requires the user to be authenticated. + """ + + def get(self, request, course_id=None): + """ + Retrieve the waffle flag settings for the specified course. + + Args: + request (HttpRequest): The HTTP request object. + course_id (str, optional): The ID of the course for which to retrieve + the waffle flag settings. If not provided, + defaults to None. + + Returns: + Response: A JSON response containing the status of various waffle flags + for the specified course. + + **Example Request** + + GET /api/contentstore/v1/course_waffle_flags + GET /api/contentstore/v1/course_waffle_flags/course-v1:test+test+test + + **Response Values** + + A JSON response containing the status of various waffle flags + for the specified course. + + **Example Response** + + ```json + { + "use_new_home_page": true, + "use_new_custom_pages": true, + "use_new_schedule_details_page": true, + "use_new_advanced_settings_page": true, + "use_new_grading_page": true, + "use_new_updates_page": true, + "use_new_import_page": true, + "use_new_export_page": true, + "use_new_files_uploads_page": true, + "use_new_video_uploads_page": false, + "use_new_course_outline_page": true, + "use_new_unit_page": false, + "use_new_course_team_page": true, + "use_new_certificates_page": true, + "use_new_textbooks_page": true, + "use_new_group_configurations_page": true + } + ``` + """ + course_key = CourseKey.from_string(course_id) if course_id else None + serializer = CourseWaffleFlagsSerializer( + context={"course_key": course_key}, data={} + ) + serializer.is_valid(raise_exception=True) + return Response(serializer.data) diff --git a/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_course_waffle_flags.py b/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_course_waffle_flags.py new file mode 100644 index 000000000000..0a713fb81cd1 --- /dev/null +++ b/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_course_waffle_flags.py @@ -0,0 +1,112 @@ +""" +Unit tests for the course waffle flags view +""" + +from django.contrib.auth import get_user_model +from django.urls import reverse +from rest_framework import status + +from cms.djangoapps.contentstore.tests.utils import CourseTestCase +from openedx.core.djangoapps.waffle_utils.models import WaffleFlagCourseOverrideModel + +User = get_user_model() + + +class CourseWaffleFlagsViewTest(CourseTestCase): + """ + Tests for the CourseWaffleFlagsView endpoint, which returns waffle flag states + for a specific course or globally if no course ID is provided. + """ + + course_waffle_flags = [ + "use_new_custom_pages", + "use_new_schedule_details_page", + "use_new_advanced_settings_page", + "use_new_grading_page", + "use_new_updates_page", + "use_new_import_page", + "use_new_export_page", + "use_new_files_uploads_page", + "use_new_video_uploads_page", + "use_new_course_outline_page", + "use_new_unit_page", + "use_new_course_team_page", + "use_new_certificates_page", + "use_new_textbooks_page", + "use_new_group_configurations_page", + ] + + def setUp(self): + """ + Set up test data and state before each test method. + + This method initializes the endpoint URL and creates a set of waffle flags + for the test course, setting each flag's value to `True`. + """ + super().setUp() + self.url = reverse("cms.djangoapps.contentstore:v1:course_waffle_flags") + self.create_waffle_flags(self.course_waffle_flags) + + def create_waffle_flags(self, flags, enabled=True): + """ + Helper method to create waffle flag entries in the database for the test course. + + Args: + flags (list): A list of flag names to set up. + enabled (bool): The value to set for each flag's enabled state. + """ + for flag in flags: + WaffleFlagCourseOverrideModel.objects.create( + waffle_flag=f"contentstore.new_studio_mfe.{flag}", + course_id=self.course.id, + enabled=enabled, + ) + + def expected_response(self, enabled=False): + """ + Generate an expected response dictionary based on the enabled flag. + + Args: + enabled (bool): State to assign to each waffle flag in the response. + + Returns: + dict: A dictionary with each flag set to the value of `enabled`. + """ + return {flag: enabled for flag in self.course_waffle_flags} + + def test_get_course_waffle_flags_with_course_id(self): + """ + Test that waffle flags for a specific course are correctly returned when + a valid course ID is provided. + + Expected Behavior: + - The response should return HTTP 200 status. + - Each flag returned should be `True` as set up in the `setUp` method. + """ + course_url = reverse( + "cms.djangoapps.contentstore:v1:course_waffle_flags", + kwargs={"course_id": self.course.id}, + ) + + expected_response = self.expected_response(enabled=True) + expected_response["use_new_home_page"] = False + + response = self.client.get(course_url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertDictEqual(expected_response, response.data) + + def test_get_course_waffle_flags_without_course_id(self): + """ + Test that the default waffle flag states are returned when no course ID is provided. + + Expected Behavior: + - The response should return HTTP 200 status. + - Each flag returned should default to `False`, representing the global + default state for each flag. + """ + expected_response = self.expected_response(enabled=False) + expected_response["use_new_home_page"] = False + + response = self.client.get(self.url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertDictEqual(expected_response, response.data)