feat: Simplify content groups v2 response JSON (#37976)
This commit is contained in:
@@ -46,10 +46,10 @@ paths:
|
||||
get:
|
||||
tags:
|
||||
- Content Groups
|
||||
summary: List content group configurations
|
||||
summary: List content groups
|
||||
description: |
|
||||
Returns all content group configurations (scheme='cohort') for a course.
|
||||
If no content group exists, an empty one is automatically created.
|
||||
Returns the content groups for a course along with the configuration ID
|
||||
and a link to Studio for managing content groups.
|
||||
operationId: listGroupConfigurations
|
||||
produces:
|
||||
- application/json
|
||||
@@ -153,20 +153,15 @@ definitions:
|
||||
ContentGroupsListResponse:
|
||||
type: object
|
||||
properties:
|
||||
all_group_configurations:
|
||||
id:
|
||||
type: integer
|
||||
nullable: true
|
||||
description: ID of the content group configuration (null if none exists)
|
||||
groups:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/ContentGroupConfiguration'
|
||||
description: List of content group configurations
|
||||
should_show_enrollment_track:
|
||||
type: boolean
|
||||
description: Whether enrollment track groups should be displayed
|
||||
should_show_experiment_groups:
|
||||
type: boolean
|
||||
description: Whether experiment groups should be displayed
|
||||
group_configuration_url:
|
||||
$ref: '#/definitions/Group'
|
||||
description: Flat list of content groups for the course
|
||||
studio_content_groups_link:
|
||||
type: string
|
||||
description: Base URL for accessing individual configurations
|
||||
course_outline_url:
|
||||
type: string
|
||||
description: URL to the course outline
|
||||
description: Full URL to Studio's content group configuration page
|
||||
|
||||
@@ -36,10 +36,15 @@ class ContentGroupConfigurationSerializer(serializers.Serializer):
|
||||
class ContentGroupsListResponseSerializer(serializers.Serializer):
|
||||
"""
|
||||
Response serializer for listing all content groups.
|
||||
|
||||
Returns the content group configuration ID, a flat list of content groups,
|
||||
and a link to Studio where instructors can manage content groups.
|
||||
"""
|
||||
all_group_configurations = ContentGroupConfigurationSerializer(many=True)
|
||||
should_show_enrollment_track = serializers.BooleanField()
|
||||
should_show_experiment_groups = serializers.BooleanField()
|
||||
context_course = serializers.JSONField(required=False, allow_null=True)
|
||||
group_configuration_url = serializers.CharField()
|
||||
course_outline_url = serializers.CharField()
|
||||
id = serializers.IntegerField(
|
||||
allow_null=True,
|
||||
help_text="ID of the content group configuration (null if none exists)"
|
||||
)
|
||||
groups = GroupSerializer(many=True)
|
||||
studio_content_groups_link = serializers.CharField(
|
||||
help_text="Full URL to Studio's content group configuration page"
|
||||
)
|
||||
|
||||
@@ -3,15 +3,18 @@ Tests for Content Groups REST API v2.
|
||||
"""
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.test import override_settings
|
||||
from rest_framework import status
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from xmodule.partitions.partitions import Group, UserPartition
|
||||
from common.djangoapps.student.tests.factories import UserFactory
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
from openedx.core.djangoapps.course_groups.constants import COHORT_SCHEME
|
||||
from openedx.core.djangolib.testing.utils import skip_unless_lms
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
from xmodule.partitions.partitions import Group, UserPartition
|
||||
|
||||
TEST_STUDIO_BASE_URL = "https://studio.example.com"
|
||||
|
||||
|
||||
@skip_unless_lms
|
||||
@@ -28,10 +31,16 @@ class GroupConfigurationsListViewTestCase(ModuleStoreTestCase):
|
||||
self.api_client.force_authenticate(user=self.user)
|
||||
|
||||
def _get_url(self, course_id=None):
|
||||
"""Helper to get the list URL"""
|
||||
"""Helper to get the API URL"""
|
||||
course_id = course_id or str(self.course.id)
|
||||
return f'/api/cohorts/v2/courses/{course_id}/group_configurations'
|
||||
|
||||
def _get_expected_studio_url(self, course_id=None):
|
||||
"""Helper to get the expected Studio URL"""
|
||||
course_id = course_id or str(self.course.id)
|
||||
return f'{TEST_STUDIO_BASE_URL}/course/{course_id}/group_configurations'
|
||||
|
||||
@override_settings(MFE_CONFIG={"STUDIO_BASE_URL": TEST_STUDIO_BASE_URL})
|
||||
@patch('lms.djangoapps.instructor.permissions.InstructorPermission.has_permission')
|
||||
def test_list_content_groups_returns_json(self, mock_perm):
|
||||
"""Verify endpoint returns JSON with correct structure"""
|
||||
@@ -57,18 +66,26 @@ class GroupConfigurationsListViewTestCase(ModuleStoreTestCase):
|
||||
self.assertEqual(response['Content-Type'], 'application/json')
|
||||
|
||||
data = response.json()
|
||||
self.assertIn('all_group_configurations', data)
|
||||
self.assertIn('should_show_enrollment_track', data)
|
||||
self.assertIn('should_show_experiment_groups', data)
|
||||
self.assertIn('id', data)
|
||||
self.assertIn('groups', data)
|
||||
self.assertIn('studio_content_groups_link', data)
|
||||
|
||||
configs = data['all_group_configurations']
|
||||
self.assertEqual(len(configs), 1)
|
||||
self.assertEqual(configs[0]['scheme'], COHORT_SCHEME)
|
||||
self.assertEqual(len(configs[0]['groups']), 2)
|
||||
# Verify partition ID is returned
|
||||
self.assertEqual(data['id'], 50)
|
||||
|
||||
# Verify groups
|
||||
groups = data['groups']
|
||||
self.assertEqual(len(groups), 2)
|
||||
self.assertEqual(groups[0]['name'], 'Content Group A')
|
||||
self.assertEqual(groups[1]['name'], 'Content Group B')
|
||||
|
||||
# Verify full Studio URL
|
||||
expected_studio_url = self._get_expected_studio_url()
|
||||
self.assertEqual(data['studio_content_groups_link'], expected_studio_url)
|
||||
|
||||
@patch('lms.djangoapps.instructor.permissions.InstructorPermission.has_permission')
|
||||
def test_list_content_groups_filters_non_cohort_partitions(self, mock_perm):
|
||||
"""Verify only cohort-scheme partitions are returned"""
|
||||
"""Verify only groups from cohort-scheme partitions are returned"""
|
||||
mock_perm.return_value = True
|
||||
|
||||
self.course.user_partitions = [
|
||||
@@ -92,25 +109,32 @@ class GroupConfigurationsListViewTestCase(ModuleStoreTestCase):
|
||||
response = self.api_client.get(self._get_url())
|
||||
|
||||
data = response.json()
|
||||
configs = data['all_group_configurations']
|
||||
|
||||
self.assertEqual(len(configs), 1)
|
||||
self.assertEqual(configs[0]['id'], 50)
|
||||
self.assertEqual(configs[0]['scheme'], COHORT_SCHEME)
|
||||
# Verify cohort partition ID is returned
|
||||
self.assertEqual(data['id'], 50)
|
||||
|
||||
# Only groups from cohort partition should be returned
|
||||
groups = data['groups']
|
||||
self.assertEqual(len(groups), 1)
|
||||
self.assertEqual(groups[0]['name'], 'Group A')
|
||||
|
||||
@override_settings(MFE_CONFIG={"STUDIO_BASE_URL": TEST_STUDIO_BASE_URL})
|
||||
@patch('lms.djangoapps.instructor.permissions.InstructorPermission.has_permission')
|
||||
def test_list_auto_creates_empty_content_group_if_none_exists(self, mock_perm):
|
||||
"""Verify empty content group is auto-created when none exists"""
|
||||
def test_list_returns_empty_groups_when_none_exist(self, mock_perm):
|
||||
"""Verify empty groups array and null id when no content groups exist"""
|
||||
mock_perm.return_value = True
|
||||
|
||||
response = self.api_client.get(self._get_url())
|
||||
|
||||
data = response.json()
|
||||
configs = data['all_group_configurations']
|
||||
|
||||
self.assertEqual(len(configs), 1)
|
||||
self.assertEqual(configs[0]['scheme'], COHORT_SCHEME)
|
||||
self.assertEqual(len(configs[0]['groups']), 0)
|
||||
# ID should be null when no partition exists
|
||||
self.assertIsNone(data['id'])
|
||||
self.assertEqual(len(data['groups']), 0)
|
||||
|
||||
# Verify full Studio URL
|
||||
expected_studio_url = self._get_expected_studio_url()
|
||||
self.assertEqual(data['studio_content_groups_link'], expected_studio_url)
|
||||
|
||||
def test_list_requires_authentication(self):
|
||||
"""Verify endpoint requires authentication"""
|
||||
|
||||
@@ -2,29 +2,25 @@
|
||||
REST API views for content group configurations.
|
||||
"""
|
||||
import edx_api_doc_tools as apidocs
|
||||
from common.djangoapps.util.db import MYSQL_MAX_INT, generate_int_id
|
||||
from django.conf import settings
|
||||
from opaque_keys import InvalidKeyError
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from rest_framework import status
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
from xmodule.partitions.partitions import MINIMUM_UNUSED_PARTITION_ID, UserPartition
|
||||
|
||||
from lms.djangoapps.instructor import permissions
|
||||
from openedx.core.djangoapps.course_groups.constants import (
|
||||
COHORT_SCHEME,
|
||||
CONTENT_GROUP_CONFIGURATION_DESCRIPTION,
|
||||
CONTENT_GROUP_CONFIGURATION_NAME,
|
||||
)
|
||||
from openedx.core.djangoapps.course_groups.constants import COHORT_SCHEME
|
||||
from openedx.core.djangoapps.course_groups.partition_scheme import get_cohorted_user_partition
|
||||
from openedx.core.djangoapps.course_groups.rest_api.serializers import (
|
||||
ContentGroupConfigurationSerializer,
|
||||
ContentGroupsListResponseSerializer,
|
||||
ContentGroupsListResponseSerializer
|
||||
)
|
||||
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
|
||||
from openedx.core.lib.api.view_utils import DeveloperErrorViewMixin
|
||||
from openedx.core.lib.courses import get_course_by_id
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
|
||||
|
||||
class GroupConfigurationsListView(DeveloperErrorViewMixin, APIView):
|
||||
@@ -72,26 +68,26 @@ class GroupConfigurationsListView(DeveloperErrorViewMixin, APIView):
|
||||
|
||||
content_group_partition = get_cohorted_user_partition(course)
|
||||
|
||||
if content_group_partition is None:
|
||||
used_ids = {p.id for p in course.user_partitions}
|
||||
content_group_partition = UserPartition(
|
||||
id=generate_int_id(MINIMUM_UNUSED_PARTITION_ID, MYSQL_MAX_INT, used_ids),
|
||||
name=str(CONTENT_GROUP_CONFIGURATION_NAME),
|
||||
description=str(CONTENT_GROUP_CONFIGURATION_DESCRIPTION),
|
||||
groups=[],
|
||||
scheme_id=COHORT_SCHEME
|
||||
)
|
||||
# Extract partition ID and groups, or None/empty list if no partition exists
|
||||
if content_group_partition is not None:
|
||||
partition_id = content_group_partition.id
|
||||
groups = [group.to_json() for group in content_group_partition.groups]
|
||||
else:
|
||||
partition_id = None
|
||||
groups = []
|
||||
|
||||
context = {
|
||||
"all_group_configurations": [content_group_partition.to_json()],
|
||||
"should_show_enrollment_track": False,
|
||||
"should_show_experiment_groups": True,
|
||||
"context_course": None,
|
||||
"group_configuration_url": f"/api/cohorts/v2/courses/{course_id}/group_configurations",
|
||||
"course_outline_url": f"/api/contentstore/v1/courses/{course_id}",
|
||||
# Build full Studio URL for content group configuration
|
||||
mfe_config = configuration_helpers.get_value("MFE_CONFIG", settings.MFE_CONFIG)
|
||||
studio_base_url = mfe_config.get("STUDIO_BASE_URL", "")
|
||||
studio_content_groups_link = f"{studio_base_url}/course/{course_id}/group_configurations"
|
||||
|
||||
response_data = {
|
||||
"id": partition_id,
|
||||
"groups": groups,
|
||||
"studio_content_groups_link": studio_content_groups_link,
|
||||
}
|
||||
|
||||
serializer = ContentGroupsListResponseSerializer(context)
|
||||
serializer = ContentGroupsListResponseSerializer(response_data)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user