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
273 changes: 160 additions & 113 deletions lms/djangoapps/instructor/views/api_v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import logging

import edx_api_doc_tools as apidocs
from common.djangoapps.util.db import MYSQL_MAX_INT, generate_int_id
from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey, UsageKey
from rest_framework import status
Expand All @@ -21,19 +22,31 @@
from django.utils.html import strip_tags
from django.utils.translation import gettext as _
from common.djangoapps.util.json_request import JsonResponseBadRequest
from openedx.core.djangoapps.course_groups.constants import (
CONTENT_GROUP_CONFIGURATION_NAME,
CONTENT_GROUP_CONFIGURATION_DESCRIPTION,
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
)
from xmodule.modulestore.exceptions import ItemNotFoundError
from xmodule.partitions.partitions import MINIMUM_UNUSED_PARTITION_ID, UserPartition

from lms.djangoapps.courseware.tabs import get_course_tab_list
from lms.djangoapps.instructor import permissions
from lms.djangoapps.instructor.ora import get_open_response_assessment_list, get_ora_summary
from lms.djangoapps.instructor.views.api import _display_unit, get_student_from_identifier
from lms.djangoapps.instructor.views.instructor_task_helpers import extract_task_features
from lms.djangoapps.instructor_task import api as task_api
from lms.djangoapps.instructor.ora import get_open_response_assessment_list, get_ora_summary
from openedx.core.lib.api.view_utils import DeveloperErrorViewMixin
from openedx.core.lib.courses import get_course_by_id
from .serializers_v2 import (
InstructorTaskListSerializer,
CourseInformationSerializerV2,
BlockDueDateSerializerV2,
CourseInformationSerializerV2,
InstructorTaskListSerializer,
ORASerializer,
ORASummarySerializer,
)
Expand Down Expand Up @@ -77,76 +90,6 @@ def get(self, request, course_id):
"""
Retrieve comprehensive course information including metadata, enrollment statistics,
dashboard configuration, and user permissions.

**Use Cases**

Retrieve comprehensive course metadata including enrollment counts, dashboard configuration,
permissions, and navigation sections.

**Example Requests**

GET /api/instructor/v2/courses/{course_id}

**Response Values**

{
"course_id": "course-v1:edX+DemoX+Demo_Course",
"display_name": "Demonstration Course",
"org": "edX",
"course_number": "DemoX",
"enrollment_start": "2013-02-05T00:00:00Z",
"enrollment_end": null,
"start": "2013-02-05T05:00:00Z",
"end": "2024-12-31T23:59:59Z",
"pacing": "instructor",
"has_started": true,
"has_ended": false,
"total_enrollment": 150,
"enrollment_counts": {
"total": 150,
"audit": 100,
"verified": 40,
"honor": 10
},
"num_sections": 12,
"grade_cutoffs": "A is 0.9, B is 0.8, C is 0.7, D is 0.6",
"course_errors": [],
"studio_url": "https://studio.example.com/course/course-v1:edX+DemoX+2024",
"permissions": {
"admin": false,
"instructor": true,
"finance_admin": false,
"sales_admin": false,
"staff": true,
"forum_admin": true,
"data_researcher": false
},
"tabs": [
{
"tab_id": "courseware",
"title": "Course",
"url": "INSTRUCTOR_MICROFRONTEND_URL/courses/course-v1:edX+DemoX+2024/courseware"
},
{
"tab_id": "progress",
"title": "Progress",
"url": "INSTRUCTOR_MICROFRONTEND_URL/courses/course-v1:edX+DemoX+2024/progress"
},
],
"disable_buttons": false,
"analytics_dashboard_message": "To gain insights into student enrollment and participation..."
}

**Parameters**

course_key: Course key for the course.

**Returns**

* 200: OK - Returns course metadata
* 401: Unauthorized - User is not authenticated
* 403: Forbidden - User lacks instructor permissions
* 404: Not Found - Course does not exist
"""
course_key = CourseKey.from_string(course_id)
course = get_course_by_id(course_key)
Expand All @@ -168,46 +111,6 @@ class InstructorTaskListView(DeveloperErrorViewMixin, APIView):
**Use Cases**

List instructor tasks for a course.

**Example Requests**

GET /api/instructor/v2/courses/{course_key}/instructor_tasks
GET /api/instructor/v2/courses/{course_key}/instructor_tasks?problem_location_str=block-v1:...
GET /api/instructor/v2/courses/{course_key}/instructor_tasks?
problem_location_str=block-v1:...&unique_student_identifier=student@example.com

**Response Values**

{
"tasks": [
{
"task_id": "2519ff31-22d9-4a62-91e2-55495895b355",
"task_type": "grade_problems",
"task_state": "PROGRESS",
"status": "Incomplete",
"created": "2019-01-15T18:00:15.902470+00:00",
"task_input": "{}",
"task_output": null,
"duration_sec": "unknown",
"task_message": "No status information available",
"requester": "staff"
}
]
}

**Parameters**

course_key: Course key for the course.
problem_location_str (optional): Filter tasks to a specific problem location.
unique_student_identifier (optional): Filter tasks to specific student (must be used with problem_location_str).

**Returns**

* 200: OK - Returns list of instructor tasks
* 400: Bad Request - Invalid parameters
* 401: Unauthorized - User is not authenticated
* 403: Forbidden - User lacks instructor permissions
* 404: Not Found - Course does not exist
"""

permission_classes = (IsAuthenticated, permissions.InstructorPermission)
Expand Down Expand Up @@ -444,3 +347,147 @@ def get(self, request, *args, **kwargs):

serializer = self.get_serializer(items)
return Response(serializer.data)


class GroupConfigurationsListView(DeveloperErrorViewMixin, APIView):
"""
API view for listing content group configurations.
"""
permission_classes = (IsAuthenticated, permissions.InstructorPermission)
permission_name = permissions.VIEW_DASHBOARD

@apidocs.schema(
parameters=[
apidocs.string_parameter(
"course_id",
apidocs.ParameterLocation.PATH,
description="The course key (e.g., course-v1:org+course+run)",
),
],
responses={
200: "Successfully retrieved content groups",
400: "Invalid course key",
401: "Authentication required",
403: "User does not have permission to access this course",
404: "Course not found",
},
)
def get(self, request, course_id):
"""
List all content groups for a course.

Returns all content group configurations (scheme='cohort') along with
context about whether to show enrollment tracks and experiment groups.

If no content group exists, an empty content group partition is automatically created.
"""
try:
course_key = CourseKey.from_string(course_id)
except InvalidKeyError:
return Response(
{"error": f"Invalid course key: {course_id}"},
status=status.HTTP_400_BAD_REQUEST
)

try:
course = get_course_by_id(course_key)
except ItemNotFoundError:
return Response(
{"error": f"Course not found: {course_id}"},
status=status.HTTP_404_NOT_FOUND
)

# Get or create content group partition
content_group_partition = get_cohorted_user_partition(course)

if content_group_partition is None:
# Auto-create empty content group if none exists
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
)

# Build response context
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/instructor/v2/courses/{course_id}/group_configurations",
"course_outline_url": f"/api/contentstore/v1/courses/{course_id}",
}

# Serialize and return
serializer = ContentGroupsListResponseSerializer(context)
return Response(serializer.data, status=status.HTTP_200_OK)


class GroupConfigurationDetailView(DeveloperErrorViewMixin, APIView):
"""
API view for retrieving a specific content group configuration.
"""
permission_classes = (IsAuthenticated, permissions.InstructorPermission)
permission_name = permissions.VIEW_DASHBOARD

@apidocs.schema(
parameters=[
apidocs.string_parameter(
"course_id",
apidocs.ParameterLocation.PATH,
description="The course key",
),
apidocs.path_parameter(
"configuration_id",
int,
description="The ID of the content group configuration",
),
],
responses={
200: "Content group configuration details",
400: "Invalid course key",
401: "Authentication required",
403: "User does not have permission to access this course",
404: "Content group configuration not found",
},
)
def get(self, request, course_id, configuration_id):
"""
Retrieve a specific content group configuration.
"""
try:
course_key = CourseKey.from_string(course_id)
except InvalidKeyError:
return Response(
{"error": f"Invalid course key: {course_id}"},
status=status.HTTP_400_BAD_REQUEST
)

try:
course = get_course_by_id(course_key)
except ItemNotFoundError:
return Response(
{"error": f"Course not found: {course_id}"},
status=status.HTTP_404_NOT_FOUND
)

# Find the configuration
partition = None
for p in course.user_partitions:
if p.id == int(configuration_id) and p.scheme.name == COHORT_SCHEME:
partition = p
break

if not partition:
return Response(
{"error": f"Content group configuration {configuration_id} not found"},
status=status.HTTP_404_NOT_FOUND
)

# Serialize and return
response_data = partition.to_json()
serializer = ContentGroupConfigurationSerializer(response_data)
return Response(serializer.data, status=status.HTTP_200_OK)
Loading
Loading