Skip to content

Commit 365364a

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 365364a

5 files changed

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

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: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
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
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+
120+
# Use existing helper to get context
121+
context = get_group_configurations_context(course, store)
122+
123+
# Filter to only cohort-scheme partitions for v2 API
124+
cohort_configs = [
125+
config for config in context['all_group_configurations']
126+
if config.get('scheme') == COHORT_SCHEME
127+
]
128+
context['all_group_configurations'] = cohort_configs
129+
130+
# Set context_course to None for JSON API (it's only needed for HTML rendering)
131+
context['context_course'] = None
132+
133+
# Serialize and return
134+
serializer = ContentGroupsListResponseSerializer(context)
135+
return Response(serializer.data, status=status.HTTP_200_OK)
136+
137+
138+
@view_auth_classes(is_authenticated=True)
139+
class GroupConfigurationDetailView(APIView):
140+
"""
141+
API view for retrieving a specific content group configuration.
142+
"""
143+
144+
@apidocs.schema(
145+
parameters=[
146+
apidocs.string_parameter(
147+
"course_key_string",
148+
apidocs.ParameterLocation.PATH,
149+
description="The course key",
150+
),
151+
apidocs.path_parameter(
152+
"configuration_id",
153+
int,
154+
description="The ID of the content group configuration",
155+
),
156+
],
157+
responses={
158+
200: ContentGroupConfigurationSerializer,
159+
401: "Authentication required",
160+
403: "User does not have permission to access this course",
161+
404: "Content group configuration not found",
162+
},
163+
)
164+
def get(self, request, course_key_string, configuration_id):
165+
"""
166+
Retrieve a specific content group configuration.
167+
168+
Returns all metadata including groups, partition scheme, and usage information.
169+
"""
170+
try:
171+
course_key = CourseKey.from_string(course_key_string)
172+
except InvalidKeyError as exc:
173+
raise ValidationError(f"Invalid course key: {course_key_string}") from exc
174+
175+
try:
176+
course = get_course_and_check_access(course_key, request.user)
177+
except ItemNotFoundError as exc:
178+
raise NotFound(f"Course not found: {course_key_string}") from exc
179+
180+
# Find the configuration
181+
partition = None
182+
for p in course.user_partitions:
183+
if p.id == int(configuration_id) and p.scheme.name == COHORT_SCHEME:
184+
partition = p
185+
break
186+
187+
if not partition:
188+
raise NotFound(f"Content group configuration {configuration_id} not found")
189+
190+
# Serialize and return
191+
response_data = partition.to_json()
192+
serializer = ContentGroupConfigurationSerializer(response_data)
193+
return Response(serializer.data, status=status.HTTP_200_OK)

0 commit comments

Comments
 (0)