Skip to content

Commit

Permalink
feat: Add API endpoint to manage course waffle flags (#35622)
Browse files Browse the repository at this point in the history
Co-authored-by: Sagirov Eugeniy <[email protected]>
  • Loading branch information
UvgenGen and Sagirov Eugeniy authored Nov 4, 2024
1 parent d0dbb8d commit f730276
Show file tree
Hide file tree
Showing 6 changed files with 339 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
6 changes: 6 additions & 0 deletions cms/djangoapps/contentstore/rest_api/v1/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
CourseRerunView,
CourseSettingsView,
CourseVideosView,
CourseWaffleFlagsView,
HomePageView,
HomePageCoursesView,
HomePageLibrariesView,
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions cms/djangoapps/contentstore/rest_api/v1/views/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
Original file line number Diff line number Diff line change
@@ -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)

0 comments on commit f730276

Please sign in to comment.