feat: Add API endpoint to manage course waffle flags (#35622)
Co-authored-by: Sagirov Eugeniy <evhenyj.sahyrov@raccoongang.com>
This commit is contained in:
@@ -6,6 +6,7 @@ from .course_details import CourseDetailsSerializer
|
||||
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
|
||||
|
||||
@@ -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)
|
||||
@@ -17,6 +17,7 @@ from .views import (
|
||||
CourseRerunView,
|
||||
CourseSettingsView,
|
||||
CourseVideosView,
|
||||
CourseWaffleFlagsView,
|
||||
HomePageView,
|
||||
HomePageCoursesView,
|
||||
HomePageLibrariesView,
|
||||
@@ -131,6 +132,11 @@ urlpatterns = [
|
||||
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
|
||||
|
||||
@@ -5,6 +5,7 @@ from .certificates import CourseCertificatesView
|
||||
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
|
||||
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user