Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 8 additions & 10 deletions cms/djangoapps/contentstore/course_group_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,21 @@
from cms.djangoapps.contentstore.utils import reverse_usage_url
from common.djangoapps.util.db import MYSQL_MAX_INT, generate_int_id
from lms.lib.utils import get_parent_unit
# Re-exported for backward compatibility - other modules import these from here
from openedx.core.djangoapps.course_groups.constants import ( # pylint: disable=unused-import
COHORT_SCHEME,
CONTENT_GROUP_CONFIGURATION_DESCRIPTION,
CONTENT_GROUP_CONFIGURATION_NAME,
ENROLLMENT_SCHEME,
RANDOM_SCHEME,
)
from openedx.core.djangoapps.course_groups.partition_scheme import get_cohorted_user_partition
from xmodule.partitions.partitions import MINIMUM_UNUSED_PARTITION_ID, ReadOnlyUserPartitionError, UserPartition # lint-amnesty, pylint: disable=wrong-import-order
from xmodule.partitions.partitions_service import get_all_partitions_for_course # lint-amnesty, pylint: disable=wrong-import-order
from xmodule.split_test_block import get_split_user_partitions # lint-amnesty, pylint: disable=wrong-import-order

MINIMUM_GROUP_ID = MINIMUM_UNUSED_PARTITION_ID

RANDOM_SCHEME = "random"
COHORT_SCHEME = "cohort"
ENROLLMENT_SCHEME = "enrollment_track"

CONTENT_GROUP_CONFIGURATION_DESCRIPTION = _(
'The groups in this configuration can be mapped to cohorts in the Instructor Dashboard.'
)

CONTENT_GROUP_CONFIGURATION_NAME = _('Content Groups')

log = logging.getLogger(__name__)


Expand Down
98 changes: 98 additions & 0 deletions lms/djangoapps/instructor/views/serializers_v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -475,3 +475,101 @@ class ORASummarySerializer(serializers.Serializer):
waiting = serializers.IntegerField()
staff = serializers.IntegerField()
final_grade_received = serializers.IntegerField()


class GroupSerializer(serializers.Serializer):
"""
Serializer for a single group within a content group configuration.

Groups represent cohorts that can be assigned different course content.
"""

id = serializers.IntegerField(
help_text="Unique identifier for this group within the configuration"
)
name = serializers.CharField(
max_length=255,
help_text="Human-readable name of the group"
)
version = serializers.IntegerField(
help_text="Group version number (always 1 for current Group format)"
)
usage = serializers.ListField(
child=serializers.DictField(),
required=False,
default=list,
help_text="List of course units using this group for content restriction"
)


class ContentGroupConfigurationSerializer(serializers.Serializer):
"""
Serializer for a content group configuration (UserPartition with scheme='cohort').

Content groups enable course creators to assign different course content
to different learner cohorts.
"""

id = serializers.IntegerField(
help_text="Unique identifier for this content group configuration"
)
name = serializers.CharField(
max_length=255,
help_text="Human-readable name of the configuration"
)
scheme = serializers.CharField(
help_text="Partition scheme (always 'cohort' for content groups)"
)
description = serializers.CharField(
allow_blank=True,
help_text="Detailed description of how this group is used"
)
parameters = serializers.DictField(
help_text="Additional partition parameters (usually empty for cohort scheme)"
)
groups = GroupSerializer(
many=True,
help_text="List of groups (cohorts) in this configuration"
)
active = serializers.BooleanField(
help_text="Whether this configuration is active"
)
version = serializers.IntegerField(
help_text="Configuration version number (always 3 for current UserPartition format)"
)
is_read_only = serializers.BooleanField(
required=False,
default=False,
help_text="Whether this configuration is read-only (system-managed)"
)


class ContentGroupsListResponseSerializer(serializers.Serializer):
"""
Response serializer for listing all content groups.

Returns content group configurations along with context about whether
to show enrollment tracks and experiment groups.
"""

all_group_configurations = ContentGroupConfigurationSerializer(
many=True,
help_text="List of content group configurations (only scheme='cohort' partitions)"
)
should_show_enrollment_track = serializers.BooleanField(
help_text="Whether enrollment track groups should be displayed"
)
should_show_experiment_groups = serializers.BooleanField(
help_text="Whether experiment groups should be displayed"
)
context_course = serializers.JSONField(
required=False,
allow_null=True,
help_text="Course context object (null in API responses)"
)
group_configuration_url = serializers.CharField(
help_text="Base URL for accessing individual group configurations"
)
course_outline_url = serializers.CharField(
help_text="URL to the course outline page"
)
13 changes: 13 additions & 0 deletions openedx/core/djangoapps/course_groups/constants.py
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given that there is a djangoapp specific to course_groups, does it make sense to put the related API views there as well? Is there a reason we shouldn't do that?

Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
"""
Constants for course groups.
"""
from django.utils.translation import gettext_lazy as _

COHORT_SCHEME = 'cohort'
RANDOM_SCHEME = 'random'
ENROLLMENT_SCHEME = 'enrollment_track'

CONTENT_GROUP_CONFIGURATION_NAME = _('Content Groups')
CONTENT_GROUP_CONFIGURATION_DESCRIPTION = _(
'Use this group configuration to control access to content.'
)
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
swagger: '2.0'
info:
title: Content Groups API v2
version: 2.0.0
description: |
REST API for managing content group configurations.

Content groups allow course authors to restrict access to specific
course content based on cohort membership.

host: courses.example.com
basePath: /
schemes:
- https

securityDefinitions:
JWTAuth:
type: apiKey
in: header
name: Authorization
description: JWT token authentication.

security:
- JWTAuth: []

tags:
- name: Content Groups
description: Content group configuration management

parameters:
CourseId:
name: course_id
in: path
required: true
type: string
description: The course key (e.g., course-v1:org+course+run)
ConfigurationId:
name: configuration_id
in: path
required: true
type: integer
description: The ID of the content group configuration

paths:
/api/cohorts/v2/courses/{course_id}/group_configurations:
get:
tags:
- Content Groups
summary: List content group configurations
description: |
Returns all content group configurations (scheme='cohort') for a course.
If no content group exists, an empty one is automatically created.
operationId: listGroupConfigurations
produces:
- application/json
parameters:
- $ref: '#/parameters/CourseId'
responses:
200:
description: Content groups retrieved successfully
schema:
$ref: '#/definitions/ContentGroupsListResponse'
400:
description: Invalid course key
401:
description: Authentication required
403:
description: User lacks instructor permission
404:
description: Course not found

/api/cohorts/v2/courses/{course_id}/group_configurations/{configuration_id}:
get:
tags:
- Content Groups
summary: Get content group configuration details
description: |
Retrieve a specific content group configuration by ID.
Only returns configurations with scheme='cohort'.
operationId: getGroupConfiguration
produces:
- application/json
parameters:
- $ref: '#/parameters/CourseId'
- $ref: '#/parameters/ConfigurationId'
responses:
200:
description: Configuration retrieved successfully
schema:
$ref: '#/definitions/ContentGroupConfiguration'
400:
description: Invalid course key
401:
description: Authentication required
403:
description: User lacks instructor permission
404:
description: Configuration not found

definitions:
Group:
type: object
properties:
id:
type: integer
description: Unique identifier for the group
name:
type: string
description: Display name of the group
version:
type: integer
description: Version number of the group
usage:
type: array
items:
type: object
description: List of content blocks using this group

ContentGroupConfiguration:
type: object
properties:
id:
type: integer
description: Unique identifier for the configuration
name:
type: string
description: Display name (typically "Content Groups")
scheme:
type: string
enum: [cohort]
description: Partition scheme type
description:
type: string
description: Human-readable description
parameters:
type: object
description: Additional configuration parameters
groups:
type: array
items:
$ref: '#/definitions/Group'
description: List of groups in this configuration
active:
type: boolean
description: Whether this configuration is active
version:
type: integer
description: Version number of the configuration
read_only:
type: boolean
description: Whether this configuration is system-managed

ContentGroupsListResponse:
type: object
properties:
all_group_configurations:
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:
type: string
description: Base URL for accessing individual configurations
course_outline_url:
type: string
description: URL to the course outline
45 changes: 45 additions & 0 deletions openedx/core/djangoapps/course_groups/rest_api/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
"""
Serializers for content group configurations REST API.
"""
from rest_framework import serializers


class GroupSerializer(serializers.Serializer):
"""
Serializer for a single group within a content group configuration.
"""
id = serializers.IntegerField()
name = serializers.CharField(max_length=255)
version = serializers.IntegerField()
usage = serializers.ListField(
child=serializers.DictField(),
required=False,
default=list
)


class ContentGroupConfigurationSerializer(serializers.Serializer):
"""
Serializer for a content group configuration (UserPartition with scheme='cohort').
"""
id = serializers.IntegerField()
name = serializers.CharField(max_length=255)
scheme = serializers.CharField()
description = serializers.CharField(allow_blank=True)
parameters = serializers.DictField()
groups = GroupSerializer(many=True)
active = serializers.BooleanField()
version = serializers.IntegerField()
is_read_only = serializers.BooleanField(required=False, default=False)


class ContentGroupsListResponseSerializer(serializers.Serializer):
"""
Response serializer for listing all 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()
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"""
Tests for Content Groups REST API v2.
"""
Loading