Skip to content

Commit

Permalink
feat: [AXIMST-64] create API for group configurations
Browse files Browse the repository at this point in the history
  • Loading branch information
ruzniaievdm committed Jan 10, 2024
1 parent d29e2e8 commit 7aae58a
Show file tree
Hide file tree
Showing 8 changed files with 314 additions and 79 deletions.
11 changes: 6 additions & 5 deletions cms/djangoapps/contentstore/rest_api/v1/serializers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,25 @@
"""
from .certificates import CourseCertificatesSerializer
from .course_details import CourseDetailsSerializer
from .course_index import CourseIndexSerializer
from .course_rerun import CourseRerunSerializer
from .course_team import CourseTeamSerializer
from .course_index import CourseIndexSerializer
from .grading import CourseGradingModelSerializer, CourseGradingSerializer
from .group_configurations import CourseGroupConfigurationsSerializer
from .home import CourseHomeSerializer, CourseHomeTabSerializer, LibraryTabSerializer
from .proctoring import (
LimitedProctoredExamSettingsSerializer,
ProctoredExamConfigurationSerializer,
ProctoredExamSettingsSerializer,
ProctoringErrorsSerializer
ProctoringErrorsSerializer,
)
from .settings import CourseSettingsSerializer
from .textbooks import CourseTextbooksSerializer
from .vertical_block import ContainerHandlerSerializer, VerticalContainerSerializer
from .videos import (
CourseVideosSerializer,
VideoUploadSerializer,
VideoDownloadSerializer,
VideoImageSerializer,
VideoUploadSerializer,
VideoUsageSerializer,
VideoDownloadSerializer
)
from .vertical_block import ContainerHandlerSerializer, VerticalContainerSerializer
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
"""
API Serializers for course's settings group configurations.
"""

from rest_framework import serializers


class GroupConfigurationUsageSerializer(serializers.Serializer):
"""
Serializer for representing nested usage inside configuration.
"""

label = serializers.CharField()
url = serializers.CharField()


class GroupConfigurationGroupSerializer(serializers.Serializer):
"""
Serializer for representing nested group inside configuration.
"""

id = serializers.IntegerField()
name = serializers.CharField()
usage = GroupConfigurationUsageSerializer(allow_null=True, many=True)
version = serializers.IntegerField()


class GroupConfigurationItemSerializer(serializers.Serializer):
"""
Serializer for representing group configurations item.
"""

active = serializers.BooleanField()
description = serializers.CharField()
groups = GroupConfigurationGroupSerializer(allow_null=True, many=True)
id = serializers.IntegerField()
name = serializers.CharField()
parameters = serializers.DictField()
read_only = serializers.BooleanField(required=False)
scheme = serializers.CharField()
version = serializers.IntegerField()


class CourseGroupConfigurationsSerializer(serializers.Serializer):
"""
Serializer for representing course's settings group configurations.
"""

all_group_configurations = GroupConfigurationItemSerializer(many=True)
experiment_group_configurations = GroupConfigurationItemSerializer(
allow_null=True, many=True
)
mfe_proctored_exam_settings_url = serializers.CharField(
required=False, allow_null=True, allow_blank=True
)
should_show_enrollment_track = serializers.BooleanField()
should_show_experiment_groups = serializers.BooleanField()
6 changes: 6 additions & 0 deletions cms/djangoapps/contentstore/rest_api/v1/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
CourseTextbooksView,
CourseIndexView,
CourseGradingView,
CourseGroupConfigurationsView,
CourseRerunView,
CourseSettingsView,
CourseVideosView,
Expand Down Expand Up @@ -115,6 +116,11 @@
CourseTextbooksView.as_view(),
name="textbooks"
),
re_path(
fr'^group_configurations/{COURSE_ID_PATTERN}$',
CourseGroupConfigurationsView.as_view(),
name="group_configurations"
),
re_path(
fr'^container_handler/{settings.USAGE_KEY_PATTERN}$',
ContainerHandlerView.as_view(),
Expand Down
13 changes: 5 additions & 8 deletions cms/djangoapps/contentstore/rest_api/v1/views/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,14 @@
from .certificates import CourseCertificatesView
from .course_details import CourseDetailsView
from .course_index import CourseIndexView
from .course_team import CourseTeamView
from .course_rerun import CourseRerunView
from .course_team import CourseTeamView
from .grading import CourseGradingView
from .group_configurations import CourseGroupConfigurationsView
from .help_urls import HelpUrlsView
from .home import HomePageCoursesView, HomePageLibrariesView, HomePageView
from .proctoring import ProctoredExamSettingsView, ProctoringErrorsView
from .home import HomePageView, HomePageCoursesView, HomePageLibrariesView
from .settings import CourseSettingsView
from .textbooks import CourseTextbooksView
from .videos import (
CourseVideosView,
VideoUsageView,
VideoDownloadView
)
from .help_urls import HelpUrlsView
from .vertical_block import ContainerHandlerView, VerticalContainerView
from .videos import CourseVideosView, VideoDownloadView, VideoUsageView
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
""" API Views for course's settings group configurations """

import edx_api_doc_tools as apidocs
from opaque_keys.edx.keys import CourseKey
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.views import APIView

from cms.djangoapps.contentstore.utils import get_group_configurations_context
from cms.djangoapps.contentstore.rest_api.v1.serializers import (
CourseGroupConfigurationsSerializer,
)
from common.djangoapps.student.auth import has_studio_read_access
from openedx.core.lib.api.view_utils import (
DeveloperErrorViewMixin,
verify_course_exists,
view_auth_classes,
)
from xmodule.modulestore.django import modulestore


@view_auth_classes(is_authenticated=True)
class CourseGroupConfigurationsView(DeveloperErrorViewMixin, APIView):
"""
View for course's settings group configurations.
"""

@apidocs.schema(
parameters=[
apidocs.string_parameter(
"course_id", apidocs.ParameterLocation.PATH, description="Course ID"
),
],
responses={
200: CourseGroupConfigurationsSerializer,
401: "The requester is not authenticated.",
403: "The requester cannot access the specified course.",
404: "The requested course does not exist.",
},
)
@verify_course_exists()
def get(self, request: Request, course_id: str):
"""
Get an object containing course's settings group configurations.
**Example Request**
GET /api/contentstore/v1/group_configurations/{course_id}
**Response Values**
If the request is successful, an HTTP 200 "OK" response is returned.
The HTTP 200 response contains a single dict that contains keys that
are the course's settings group configurations.
**Example Response**
```json
{
"all_group_configurations": [
{
"active": true,
"description": "The groups in this configuration can be mapped to cohorts in the Instructor.",
"groups": [
{
"id": 593758473,
"name": "My Content Group",
"usage": [],
"version": 1
}
],
"id": 1791848226,
"name": "Content Groups",
"parameters": {},
"read_only": false,
"scheme": "cohort",
"version": 3
}
],
"experiment_group_configurations": null,
"mfe_proctored_exam_settings_url": "",
"should_show_enrollment_track": false,
"should_show_experiment_groups": false
}
```
"""
course_key = CourseKey.from_string(course_id)
store = modulestore()

if not has_studio_read_access(request.user, course_key):
self.permission_denied(request)

with store.bulk_operations(course_key):
course = modulestore().get_course(course_key)
group_configurations_context = get_group_configurations_context(course, store)
serializer = CourseGroupConfigurationsSerializer(group_configurations_context)
return Response(serializer.data)
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
"""
Unit tests for the course's setting group configuration.
"""
from django.urls import reverse
from rest_framework import status

from cms.djangoapps.contentstore.course_group_config import (
CONTENT_GROUP_CONFIGURATION_NAME,
)
from cms.djangoapps.contentstore.tests.utils import CourseTestCase
from cms.djangoapps.contentstore.views.tests.test_certificates import HelperMethods
from xmodule.partitions.partitions import (
Group,
UserPartition,
) # lint-amnesty, pylint: disable=wrong-import-order

from ...mixins import PermissionAccessMixin


class CourseGroupConfigurationsViewTest(
CourseTestCase, PermissionAccessMixin, HelperMethods
):
"""
Tests for CourseGroupConfigurationsView.
"""

def setUp(self):
super().setUp()
self.url = reverse(
"cms.djangoapps.contentstore:v1:group_configurations",
kwargs={"course_id": self.course.id},
)

def test_success_response(self):
"""
Check that endpoint is valid and success response.
"""
self.course.user_partitions = [
UserPartition(
0,
"First name",
"First description",
[Group(0, "Group A"), Group(1, "Group B"), Group(2, "Group C")],
), # lint-amnesty, pylint: disable=line-too-long
]
self.save_course()

if "split_test" not in self.course.advanced_modules:
self.course.advanced_modules.append("split_test")
self.store.update_item(self.course, self.user.id)

response = self.client.get(self.url)
self.assertEqual(len(response.data["all_group_configurations"]), 1)
self.assertEqual(len(response.data["experiment_group_configurations"]), 1)
self.assertContains(response, "First name", count=1)
self.assertContains(response, "Group C")
self.assertContains(response, CONTENT_GROUP_CONFIGURATION_NAME)
self.assertEqual(response.status_code, status.HTTP_200_OK)
73 changes: 73 additions & 0 deletions cms/djangoapps/contentstore/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -1974,6 +1974,79 @@ def get_textbooks_context(course):
}


def get_group_configurations_context(course, store):
"""
Utils is used to get context for course's group configurations.
It is used for both DRF and django views.
"""

from cms.djangoapps.contentstore.course_group_config import (
COHORT_SCHEME, ENROLLMENT_SCHEME, GroupConfiguration, RANDOM_SCHEME
)
from cms.djangoapps.contentstore.views.course import (
are_content_experiments_enabled
)
from xmodule.partitions.partitions import UserPartition # lint-amnesty, pylint: disable=wrong-import-order

course_key = course.id
group_configuration_url = reverse_course_url('group_configurations_list_handler', course_key)
course_outline_url = reverse_course_url('course_handler', course_key)
should_show_experiment_groups = are_content_experiments_enabled(course)
if should_show_experiment_groups:
experiment_group_configurations = GroupConfiguration.get_split_test_partitions_with_usage(store, course)
else:
experiment_group_configurations = None

all_partitions = GroupConfiguration.get_all_user_partition_details(store, course)
should_show_enrollment_track = False
has_content_groups = False
displayable_partitions = []
for partition in all_partitions:
partition['read_only'] = getattr(UserPartition.get_scheme(partition['scheme']), 'read_only', False)

if partition['scheme'] == COHORT_SCHEME:
has_content_groups = True
displayable_partitions.append(partition)
elif partition['scheme'] == CONTENT_TYPE_GATING_SCHEME:
# Add it to the front of the list if it should be shown.
if ContentTypeGatingConfig.current(course_key=course_key).studio_override_enabled:
displayable_partitions.append(partition)
elif partition['scheme'] == ENROLLMENT_SCHEME:
should_show_enrollment_track = len(partition['groups']) > 1

# Add it to the front of the list if it should be shown.
if should_show_enrollment_track:
displayable_partitions.insert(0, partition)
elif partition['scheme'] != RANDOM_SCHEME:
# Experiment group configurations are handled explicitly above. We don't
# want to display their groups twice.
displayable_partitions.append(partition)

# Set the sort-order. Higher numbers sort earlier
scheme_priority = defaultdict(lambda: -1, {
ENROLLMENT_SCHEME: 1,
CONTENT_TYPE_GATING_SCHEME: 0
})
displayable_partitions.sort(key=lambda p: scheme_priority[p['scheme']], reverse=True)
# Add empty content group if there is no COHORT User Partition in the list.
# This will add ability to add new groups in the view.
if not has_content_groups:
displayable_partitions.append(GroupConfiguration.get_or_create_content_group(store, course))

context = {
'context_course': course,
'group_configuration_url': group_configuration_url,
'course_outline_url': course_outline_url,
'experiment_group_configurations': experiment_group_configurations,
'should_show_experiment_groups': should_show_experiment_groups,
'all_group_configurations': displayable_partitions,
'should_show_enrollment_track': should_show_enrollment_track,
'mfe_proctored_exam_settings_url': get_proctored_exam_settings_url(course.id),
}

return context


class StudioPermissionsService:
"""
Service that can provide information about a user's permissions.
Expand Down
Loading

0 comments on commit 7aae58a

Please sign in to comment.