Skip to content

Commit 8955f91

Browse files
feat: Add Content Groups V2 JSON REST API
Implements a pure JSON REST API for fetching content group configurations, replacing the legacy HTML+JSON hybrid endpoint with a RESTful interface.
1 parent 2c53232 commit 8955f91

5 files changed

Lines changed: 521 additions & 3 deletions

File tree

cms/djangoapps/contentstore/rest_api/v2/serializers/__init__.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,20 @@
66
PublishableEntityLinksSummarySerializer,
77
PublishableEntityLinkSerializer
88
)
9+
from cms.djangoapps.contentstore.rest_api.v2.serializers.group_configurations import (
10+
ContentGroupConfigurationSerializer,
11+
ContentGroupsListResponseSerializer,
12+
GroupSerializer,
13+
)
914
from cms.djangoapps.contentstore.rest_api.v2.serializers.home import CourseHomeTabSerializerV2
1015

1116
__all__ = [
12-
'CourseHomeTabSerializerV2',
1317
'ComponentLinksSerializer',
14-
'PublishableEntityLinkSerializer',
1518
'ContainerLinksSerializer',
19+
'ContentGroupConfigurationSerializer',
20+
'ContentGroupsListResponseSerializer',
21+
'CourseHomeTabSerializerV2',
22+
'GroupSerializer',
23+
'PublishableEntityLinkSerializer',
1624
'PublishableEntityLinksSummarySerializer',
1725
]
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
"""
2+
API Serializers for Content Groups (Group Configurations) V2 API.
3+
"""
4+
from rest_framework import serializers
5+
from openedx.core.lib.api.serializers import CourseKeyField
6+
7+
8+
class GroupSerializer(serializers.Serializer):
9+
"""
10+
Serializer for a single group within a content group configuration.
11+
12+
Groups represent cohorts that can be assigned different course content.
13+
"""
14+
15+
id = serializers.IntegerField(
16+
help_text="Unique identifier for this group within the configuration"
17+
)
18+
name = serializers.CharField(
19+
max_length=255,
20+
help_text="Human-readable name of the group"
21+
)
22+
version = serializers.IntegerField(
23+
help_text="Group version number (always 1 for current Group format)"
24+
)
25+
usage = serializers.ListField(
26+
child=serializers.DictField(),
27+
required=False,
28+
default=list,
29+
help_text="List of course units using this group for content restriction"
30+
)
31+
32+
33+
class ContentGroupConfigurationSerializer(serializers.Serializer):
34+
"""
35+
Serializer for a content group configuration (UserPartition with scheme='cohort').
36+
37+
Content groups enable course creators to assign different course content
38+
to different learner cohorts.
39+
"""
40+
41+
id = serializers.IntegerField(
42+
help_text="Unique identifier for this content group configuration"
43+
)
44+
name = serializers.CharField(
45+
max_length=255,
46+
help_text="Human-readable name of the configuration"
47+
)
48+
scheme = serializers.CharField(
49+
help_text="Partition scheme (always 'cohort' for content groups)"
50+
)
51+
description = serializers.CharField(
52+
allow_blank=True,
53+
help_text="Detailed description of how this group is used"
54+
)
55+
parameters = serializers.DictField(
56+
help_text="Additional partition parameters (usually empty for cohort scheme)"
57+
)
58+
groups = GroupSerializer(
59+
many=True,
60+
help_text="List of groups (cohorts) in this configuration"
61+
)
62+
active = serializers.BooleanField(
63+
help_text="Whether this configuration is active"
64+
)
65+
version = serializers.IntegerField(
66+
help_text="Configuration version number (always 3 for current UserPartition format)"
67+
)
68+
read_only = serializers.BooleanField(
69+
required=False,
70+
default=False,
71+
help_text="Whether this configuration is read-only (system-managed)"
72+
)
73+
74+
75+
class ContentGroupsListResponseSerializer(serializers.Serializer):
76+
"""
77+
Response serializer for listing all content groups.
78+
79+
Returns content group configurations along with context about whether
80+
to show enrollment tracks and experiment groups.
81+
"""
82+
83+
all_group_configurations = ContentGroupConfigurationSerializer(
84+
many=True,
85+
help_text="List of content group configurations (only scheme='cohort' partitions)"
86+
)
87+
should_show_enrollment_track = serializers.BooleanField(
88+
help_text="Whether enrollment track groups should be displayed"
89+
)
90+
should_show_experiment_groups = serializers.BooleanField(
91+
help_text="Whether experiment groups should be displayed"
92+
)
93+
context_course = serializers.JSONField(
94+
required=False,
95+
allow_null=True,
96+
help_text="Course context object (null in API responses)"
97+
)
98+
group_configuration_url = serializers.CharField(
99+
help_text="Base URL for accessing individual group configurations"
100+
)
101+
course_outline_url = serializers.CharField(
102+
help_text="URL to the course outline page"
103+
)

cms/djangoapps/contentstore/rest_api/v2/urls.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from django.conf import settings
44
from django.urls import path, re_path
55

6-
from cms.djangoapps.contentstore.rest_api.v2.views import downstreams, home, utils
6+
from cms.djangoapps.contentstore.rest_api.v2.views import downstreams, group_configurations, home, utils
77

88
app_name = "v2"
99

@@ -13,6 +13,17 @@
1313
home.HomePageCoursesViewV2.as_view(),
1414
name="courses",
1515
),
16+
# Group Configurations (Content Groups) endpoints
17+
re_path(
18+
fr'^courses/{settings.COURSE_KEY_PATTERN}/group_configurations$',
19+
group_configurations.GroupConfigurationsListView.as_view(),
20+
name="group_configurations_list",
21+
),
22+
re_path(
23+
fr'^courses/{settings.COURSE_KEY_PATTERN}/group_configurations/(?P<configuration_id>\d+)$',
24+
group_configurations.GroupConfigurationDetailView.as_view(),
25+
name="group_configurations_detail",
26+
),
1627
re_path(
1728
r'^downstreams/$',
1829
downstreams.DownstreamListView.as_view(),
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
"""
2+
API Views for Content Groups (Group Configurations) V2 API.
3+
4+
Content groups enable course creators to assign different course content to different
5+
learner cohorts. This pure JSON API provides access to content group configurations.
6+
7+
API paths:
8+
9+
/api/contentstore/v2/courses/{course_id}/group_configurations
10+
11+
GET: List all content groups for a course.
12+
200: Successfully retrieved content groups. Returns ContentGroupsListResponse.
13+
401: Authentication required.
14+
403: User does not have permission to access this course.
15+
404: Course not found.
16+
17+
/api/contentstore/v2/courses/{course_id}/group_configurations/{configuration_id}
18+
19+
GET: Retrieve a specific content group configuration.
20+
200: Content group configuration details. Returns ContentGroupConfiguration.
21+
401: Authentication required.
22+
403: User does not have permission to access this course.
23+
404: Content group configuration not found or course not found.
24+
"""
25+
26+
import edx_api_doc_tools as apidocs
27+
import logging
28+
29+
from opaque_keys import InvalidKeyError
30+
from opaque_keys.edx.keys import CourseKey
31+
from rest_framework.exceptions import NotFound, ValidationError, PermissionDenied
32+
from rest_framework.response import Response
33+
from rest_framework.views import APIView
34+
from rest_framework import status
35+
36+
from openedx.core.lib.api.view_utils import view_auth_classes
37+
from xmodule.modulestore.django import modulestore
38+
from xmodule.modulestore.exceptions import ItemNotFoundError
39+
40+
from cms.djangoapps.contentstore.views.course import get_course_and_check_access
41+
from cms.djangoapps.contentstore.utils import get_group_configurations_context
42+
from cms.djangoapps.contentstore.course_group_config import COHORT_SCHEME
43+
from cms.djangoapps.contentstore.rest_api.v2.serializers import (
44+
ContentGroupConfigurationSerializer,
45+
ContentGroupsListResponseSerializer,
46+
)
47+
48+
49+
log = logging.getLogger(__name__)
50+
51+
52+
@view_auth_classes(is_authenticated=True)
53+
class GroupConfigurationsListView(APIView):
54+
"""
55+
API view for listing content group configurations.
56+
57+
**GET Example Response:**
58+
```json
59+
{
60+
"all_group_configurations": [
61+
{
62+
"id": 50,
63+
"name": "Content Groups",
64+
"scheme": "cohort",
65+
"description": "The groups in this configuration can be mapped to cohorts...",
66+
"parameters": {},
67+
"groups": [
68+
{"id": 1, "name": "Content Group A", "version": 1, "usage": []},
69+
{"id": 2, "name": "Content Group B", "version": 1, "usage": []}
70+
],
71+
"active": true,
72+
"version": 3,
73+
"read_only": false
74+
}
75+
],
76+
"should_show_enrollment_track": false,
77+
"should_show_experiment_groups": true,
78+
"group_configuration_url": "/api/contentstore/v2/courses/...",
79+
"course_outline_url": "/api/contentstore/v1/courses/..."
80+
}
81+
```
82+
"""
83+
84+
@apidocs.schema(
85+
parameters=[
86+
apidocs.string_parameter(
87+
"course_key_string",
88+
apidocs.ParameterLocation.PATH,
89+
description="The course key (e.g., course-v1:org+course+run)",
90+
),
91+
],
92+
responses={
93+
200: ContentGroupsListResponseSerializer,
94+
401: "Authentication required",
95+
403: "User does not have permission to access this course",
96+
404: "Course not found",
97+
},
98+
)
99+
def get(self, request, course_key_string):
100+
"""
101+
List all content groups for a course.
102+
103+
Returns all content group configurations (scheme='cohort') along with
104+
context about whether to show enrollment tracks and experiment groups.
105+
106+
If no content group exists, an empty content group partition is automatically created.
107+
"""
108+
try:
109+
course_key = CourseKey.from_string(course_key_string)
110+
except InvalidKeyError as exc:
111+
raise ValidationError(f"Invalid course key: {course_key_string}") from exc
112+
113+
store = modulestore()
114+
115+
try:
116+
course = get_course_and_check_access(course_key, request.user)
117+
except ItemNotFoundError as exc:
118+
raise NotFound(f"Course not found: {course_key_string}") from exc
119+
except PermissionDenied:
120+
raise
121+
122+
# Use existing helper to get context
123+
context = get_group_configurations_context(course, store)
124+
125+
# Filter to only cohort-scheme partitions for v2 API
126+
cohort_configs = [
127+
config for config in context['all_group_configurations']
128+
if config.get('scheme') == COHORT_SCHEME
129+
]
130+
context['all_group_configurations'] = cohort_configs
131+
132+
# Set context_course to None for JSON API (it's only needed for HTML rendering)
133+
context['context_course'] = None
134+
135+
# Serialize and return
136+
serializer = ContentGroupsListResponseSerializer(context)
137+
return Response(serializer.data, status=status.HTTP_200_OK)
138+
139+
140+
@view_auth_classes(is_authenticated=True)
141+
class GroupConfigurationDetailView(APIView):
142+
"""
143+
API view for retrieving a specific content group configuration.
144+
"""
145+
146+
@apidocs.schema(
147+
parameters=[
148+
apidocs.string_parameter(
149+
"course_id",
150+
apidocs.ParameterLocation.PATH,
151+
description="The course key",
152+
),
153+
apidocs.path_parameter(
154+
"configuration_id",
155+
int,
156+
description="The ID of the content group configuration",
157+
),
158+
],
159+
responses={
160+
200: ContentGroupConfigurationSerializer,
161+
401: "Authentication required",
162+
403: "User does not have permission to access this course",
163+
404: "Content group configuration not found",
164+
},
165+
)
166+
def get(self, request, course_key_string, configuration_id):
167+
"""
168+
Retrieve a specific content group configuration.
169+
170+
Returns all metadata including groups, partition scheme, and usage information.
171+
"""
172+
try:
173+
course_key = CourseKey.from_string(course_key_string)
174+
except InvalidKeyError as exc:
175+
raise ValidationError(f"Invalid course key: {course_key_string}") from exc
176+
177+
store = modulestore()
178+
179+
try:
180+
course = get_course_and_check_access(course_key, request.user)
181+
except ItemNotFoundError as exc:
182+
raise NotFound(f"Course not found: {course_key_string}") from exc
183+
except PermissionDenied:
184+
raise
185+
186+
# Find the configuration
187+
partition = None
188+
for p in course.user_partitions:
189+
if p.id == int(configuration_id) and p.scheme.name == COHORT_SCHEME:
190+
partition = p
191+
break
192+
193+
if not partition:
194+
raise NotFound(f"Content group configuration {configuration_id} not found")
195+
196+
# Serialize and return
197+
response_data = partition.to_json()
198+
serializer = ContentGroupConfigurationSerializer(response_data)
199+
return Response(serializer.data, status=status.HTTP_200_OK)

0 commit comments

Comments
 (0)