feat: [FC-0044] group configurations API DRF (#34389)
This commit is contained in:
@@ -3,24 +3,25 @@ Serializers for v1 contentstore API.
|
||||
"""
|
||||
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
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
"""
|
||||
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()
|
||||
validation = serializers.DictField(required=False)
|
||||
|
||||
|
||||
class GroupConfigurationGroupSerializer(serializers.Serializer):
|
||||
"""
|
||||
Serializer for representing nested group inside configuration.
|
||||
"""
|
||||
|
||||
id = serializers.IntegerField()
|
||||
name = serializers.CharField()
|
||||
usage = GroupConfigurationUsageSerializer(required=False, 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()
|
||||
usage = GroupConfigurationUsageSerializer(required=False, allow_null=True, many=True)
|
||||
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()
|
||||
@@ -13,6 +13,7 @@ from .views import (
|
||||
CourseTextbooksView,
|
||||
CourseIndexView,
|
||||
CourseGradingView,
|
||||
CourseGroupConfigurationsView,
|
||||
CourseRerunView,
|
||||
CourseSettingsView,
|
||||
CourseVideosView,
|
||||
@@ -115,6 +116,11 @@ urlpatterns = [
|
||||
CourseCertificatesView.as_view(),
|
||||
name="certificates"
|
||||
),
|
||||
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(),
|
||||
|
||||
@@ -4,17 +4,18 @@ Views for v1 contentstore API.
|
||||
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 .vertical_block import ContainerHandlerView, VerticalContainerView
|
||||
from .videos import (
|
||||
CourseVideosView,
|
||||
VideoDownloadView,
|
||||
VideoUsageView,
|
||||
VideoDownloadView
|
||||
)
|
||||
from .help_urls import HelpUrlsView
|
||||
from .vertical_block import ContainerHandlerView, VerticalContainerView
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
""" 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": "Partition for segmenting users by enrollment track",
|
||||
"groups": [
|
||||
{
|
||||
"id": 2,
|
||||
"name": "Enroll",
|
||||
"usage": [
|
||||
{
|
||||
"label": "Subsection / Unit",
|
||||
"url": "/container/block-v1:org+101+101+type@vertical+block@08772238547242848cef9"
|
||||
}
|
||||
],
|
||||
"version": 1
|
||||
}
|
||||
],
|
||||
"id": 50,
|
||||
"usage": null,
|
||||
"name": "Enrollment Track Groups",
|
||||
"parameters": {
|
||||
"course_id": "course-v1:org+101+101"
|
||||
},
|
||||
"read_only": true,
|
||||
"scheme": "enrollment_track",
|
||||
"version": 3
|
||||
},
|
||||
{
|
||||
"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": [
|
||||
{
|
||||
"active": true,
|
||||
"description": "desc",
|
||||
"groups": [
|
||||
{
|
||||
"id": 276408623,
|
||||
"name": "Group A",
|
||||
"usage": null,
|
||||
"version": 1
|
||||
},
|
||||
...
|
||||
],
|
||||
"id": 875961582,
|
||||
"usage": [
|
||||
{
|
||||
"label": "Unit / Content Experiment",
|
||||
"url": "/container/block-v1:org+101+101+type@split_test+block@90ccbbad0dac48b18c5c80",
|
||||
"validation": null
|
||||
},
|
||||
...
|
||||
],
|
||||
"name": "Experiment Group Configurations 5",
|
||||
"parameters": {},
|
||||
"scheme": "random",
|
||||
"version": 3
|
||||
},
|
||||
...
|
||||
],
|
||||
"mfe_proctored_exam_settings_url": "",
|
||||
"should_show_enrollment_track": true,
|
||||
"should_show_experiment_groups": true,
|
||||
}
|
||||
```
|
||||
"""
|
||||
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)
|
||||
@@ -0,0 +1,55 @@
|
||||
"""
|
||||
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 xmodule.partitions.partitions import (
|
||||
Group,
|
||||
UserPartition,
|
||||
) # lint-amnesty, pylint: disable=wrong-import-order
|
||||
|
||||
from ...mixins import PermissionAccessMixin
|
||||
|
||||
|
||||
class CourseGroupConfigurationsViewTest(CourseTestCase, PermissionAccessMixin):
|
||||
"""
|
||||
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)
|
||||
@@ -2096,6 +2096,80 @@ def get_certificates_context(course, user):
|
||||
'certificate_activation_handler_url': activation_handler_url,
|
||||
'mfe_proctored_exam_settings_url': get_proctored_exam_settings_url(course_key),
|
||||
}
|
||||
|
||||
return context
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ import logging
|
||||
import random
|
||||
import re
|
||||
import string
|
||||
from collections import defaultdict
|
||||
from typing import Dict
|
||||
|
||||
import django.utils
|
||||
@@ -63,8 +62,6 @@ from openedx.core.djangoapps.models.course_details import CourseDetails
|
||||
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
|
||||
from openedx.core.djangolib.js_utils import dump_js_escaped_json
|
||||
from openedx.core.lib.course_tabs import CourseTabPluginManager
|
||||
from openedx.features.content_type_gating.models import ContentTypeGatingConfig
|
||||
from openedx.features.content_type_gating.partitions import CONTENT_TYPE_GATING_SCHEME
|
||||
from organizations.models import Organization
|
||||
from xmodule.contentstore.content import StaticContent # lint-amnesty, pylint: disable=wrong-import-order
|
||||
from xmodule.course_block import CourseBlock, CourseFields # lint-amnesty, pylint: disable=wrong-import-order
|
||||
@@ -72,12 +69,10 @@ from xmodule.error_block import ErrorBlock # lint-amnesty, pylint: disable=wron
|
||||
from xmodule.modulestore import EdxJSONEncoder # lint-amnesty, pylint: disable=wrong-import-order
|
||||
from xmodule.modulestore.django import modulestore # lint-amnesty, pylint: disable=wrong-import-order
|
||||
from xmodule.modulestore.exceptions import DuplicateCourseError # lint-amnesty, pylint: disable=wrong-import-order
|
||||
from xmodule.partitions.partitions import UserPartition # lint-amnesty, pylint: disable=wrong-import-order
|
||||
from xmodule.tabs import CourseTab, CourseTabList, InvalidTabsException # lint-amnesty, pylint: disable=wrong-import-order
|
||||
|
||||
from ..course_group_config import (
|
||||
COHORT_SCHEME,
|
||||
ENROLLMENT_SCHEME,
|
||||
RANDOM_SCHEME,
|
||||
GroupConfiguration,
|
||||
GroupConfigurationsValidationError
|
||||
@@ -92,25 +87,28 @@ from ..toggles import (
|
||||
use_new_updates_page,
|
||||
use_new_advanced_settings_page,
|
||||
use_new_grading_page,
|
||||
use_new_schedule_details_page,
|
||||
use_new_textbooks_page,
|
||||
use_new_group_configurations_page,
|
||||
use_new_schedule_details_page
|
||||
)
|
||||
from ..utils import (
|
||||
add_instructor,
|
||||
get_course_settings,
|
||||
get_advanced_settings_url,
|
||||
get_course_grading,
|
||||
get_course_index_context,
|
||||
get_course_outline_url,
|
||||
get_course_rerun_context,
|
||||
get_course_settings,
|
||||
get_grading_url,
|
||||
get_group_configurations_context,
|
||||
get_group_configurations_url,
|
||||
get_home_context,
|
||||
get_library_context,
|
||||
get_course_index_context,
|
||||
get_lms_link_for_item,
|
||||
get_proctored_exam_settings_url,
|
||||
get_course_outline_url,
|
||||
get_schedule_details_url,
|
||||
get_studio_home_url,
|
||||
get_updates_url,
|
||||
get_advanced_settings_url,
|
||||
get_grading_url,
|
||||
get_schedule_details_url,
|
||||
get_course_rerun_context,
|
||||
get_textbooks_context,
|
||||
get_textbooks_url,
|
||||
initialize_permissions,
|
||||
@@ -1636,59 +1634,10 @@ def group_configurations_list_handler(request, course_key_string):
|
||||
course = get_course_and_check_access(course_key, request.user)
|
||||
|
||||
if 'text/html' in request.META.get('HTTP_ACCEPT', 'text/html'):
|
||||
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))
|
||||
return render_to_response('group_configurations.html', {
|
||||
'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),
|
||||
})
|
||||
if use_new_group_configurations_page(course_key):
|
||||
return redirect(get_group_configurations_url(course_key))
|
||||
group_configurations_context = get_group_configurations_context(course, store)
|
||||
return render_to_response('group_configurations.html', group_configurations_context)
|
||||
elif "application/json" in request.META.get('HTTP_ACCEPT'):
|
||||
if request.method == 'POST':
|
||||
# create a new group configuration for the course
|
||||
|
||||
Reference in New Issue
Block a user