feat: Simplify content groups v2 response JSON (#37976)

This commit is contained in:
brianjbuck-wgu
2026-02-19 10:43:14 -07:00
committed by GitHub
parent 76018183d4
commit 8b3c3fd52f
4 changed files with 91 additions and 71 deletions

View File

@@ -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

View File

@@ -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"
)

View File

@@ -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"""

View File

@@ -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)