Commit 5599a451 by Christina Roberts Committed by GitHub

Merge pull request #14803 from edx/aj/TNL-6743_GroupConfigEnrollmentPartition

Aj/tnl 6743 group config enrollment partition
parents 3f05d2e6 59c62188
...@@ -8,15 +8,16 @@ from util.db import generate_int_id, MYSQL_MAX_INT ...@@ -8,15 +8,16 @@ from util.db import generate_int_id, MYSQL_MAX_INT
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from contentstore.utils import reverse_usage_url from contentstore.utils import reverse_usage_url
from openedx.core.djangoapps.course_groups.partition_scheme import get_cohorted_user_partition
from xmodule.partitions.partitions import UserPartition, MINIMUM_STATIC_PARTITION_ID from xmodule.partitions.partitions import UserPartition, MINIMUM_STATIC_PARTITION_ID
from xmodule.partitions.partitions_service import get_all_partitions_for_course from xmodule.partitions.partitions_service import get_all_partitions_for_course
from xmodule.split_test_module import get_split_user_partitions from xmodule.split_test_module import get_split_user_partitions
from openedx.core.djangoapps.course_groups.partition_scheme import get_cohorted_user_partition
MINIMUM_GROUP_ID = MINIMUM_STATIC_PARTITION_ID MINIMUM_GROUP_ID = MINIMUM_STATIC_PARTITION_ID
RANDOM_SCHEME = "random" RANDOM_SCHEME = "random"
COHORT_SCHEME = "cohort" COHORT_SCHEME = "cohort"
ENROLLMENT_SCHEME = "enrollment_track"
CONTENT_GROUP_CONFIGURATION_DESCRIPTION = _( CONTENT_GROUP_CONFIGURATION_DESCRIPTION = _(
'The groups in this configuration can be mapped to cohorts in the Instructor Dashboard.' 'The groups in this configuration can be mapped to cohorts in the Instructor Dashboard.'
...@@ -187,21 +188,10 @@ class GroupConfiguration(object): ...@@ -187,21 +188,10 @@ class GroupConfiguration(object):
return usage_info return usage_info
@staticmethod @staticmethod
def get_content_groups_usage_info(store, course): def get_partitions_usage_info(store, course):
"""
Get usage information for content groups.
"""
items = store.get_items(course.id, settings={'group_access': {'$exists': True}}, include_orphans=False)
return GroupConfiguration._get_content_groups_usage_info(course, items)
@staticmethod
def _get_content_groups_usage_info(course, items):
""" """
Returns all units names and their urls. Returns all units names and their urls.
This will return only groups for the cohort user partition.
Returns: Returns:
{'group_id': {'group_id':
[ [
...@@ -216,8 +206,10 @@ class GroupConfiguration(object): ...@@ -216,8 +206,10 @@ class GroupConfiguration(object):
], ],
} }
""" """
items = store.get_items(course.id, settings={'group_access': {'$exists': True}}, include_orphans=False)
usage_info = {} usage_info = {}
for item, group_id in GroupConfiguration._iterate_items_and_content_group_ids(course, items): for item, group_id in GroupConfiguration._iterate_items_and_group_ids(course, items):
if group_id not in usage_info: if group_id not in usage_info:
usage_info[group_id] = [] usage_info[group_id] = []
...@@ -267,7 +259,7 @@ class GroupConfiguration(object): ...@@ -267,7 +259,7 @@ class GroupConfiguration(object):
} }
""" """
usage_info = {} usage_info = {}
for item, group_id in GroupConfiguration._iterate_items_and_content_group_ids(course, items): for item, group_id in GroupConfiguration._iterate_items_and_group_ids(course, items):
if group_id not in usage_info: if group_id not in usage_info:
usage_info[group_id] = [] usage_info[group_id] = []
...@@ -282,22 +274,23 @@ class GroupConfiguration(object): ...@@ -282,22 +274,23 @@ class GroupConfiguration(object):
return usage_info return usage_info
@staticmethod @staticmethod
def _iterate_items_and_content_group_ids(course, items): def _iterate_items_and_group_ids(course, items):
""" """
Iterate through items and content group IDs in a course. Iterate through items and group IDs in a course.
This will yield group IDs *only* for cohort user partitions. This will yield group IDs for all user partitions except those with a scheme of random.
Yields: tuple of (item, group_id) Yields: tuple of (item, group_id)
""" """
content_group_configuration = get_cohorted_user_partition(course) all_partitions = get_all_partitions_for_course(course)
if content_group_configuration is not None: for config in all_partitions:
for item in items: if config is not None and config.scheme.name != RANDOM_SCHEME:
if hasattr(item, 'group_access') and item.group_access: for item in items:
group_ids = item.group_access.get(content_group_configuration.id, []) if hasattr(item, 'group_access') and item.group_access:
group_ids = item.group_access.get(config.id, [])
for group_id in group_ids: for group_id in group_ids:
yield item, group_id yield item, group_id
@staticmethod @staticmethod
def update_usage_info(store, course, configuration): def update_usage_info(store, course, configuration):
...@@ -319,23 +312,23 @@ class GroupConfiguration(object): ...@@ -319,23 +312,23 @@ class GroupConfiguration(object):
configuration_json['usage'] = usage_information.get(configuration.id, []) configuration_json['usage'] = usage_information.get(configuration.id, [])
elif configuration.scheme.name == COHORT_SCHEME: elif configuration.scheme.name == COHORT_SCHEME:
# In case if scheme is "cohort" # In case if scheme is "cohort"
configuration_json = GroupConfiguration.update_content_group_usage_info(store, course, configuration) configuration_json = GroupConfiguration.update_partition_usage_info(store, course, configuration)
return configuration_json return configuration_json
@staticmethod @staticmethod
def update_content_group_usage_info(store, course, configuration): def update_partition_usage_info(store, course, configuration):
""" """
Update usage information for particular Content Group Configuration. Update usage information for particular Partition Configuration.
Returns json of particular content group configuration updated with usage information. Returns json of particular partition configuration updated with usage information.
""" """
usage_info = GroupConfiguration.get_content_groups_usage_info(store, course) usage_info = GroupConfiguration.get_partitions_usage_info(store, course)
content_group_configuration = configuration.to_json() partition_configuration = configuration.to_json()
for group in content_group_configuration['groups']: for group in partition_configuration['groups']:
group['usage'] = usage_info.get(group['id'], []) group['usage'] = usage_info.get(group['id'], [])
return content_group_configuration return partition_configuration
@staticmethod @staticmethod
def get_or_create_content_group(store, course): def get_or_create_content_group(store, course):
...@@ -357,9 +350,27 @@ class GroupConfiguration(object): ...@@ -357,9 +350,27 @@ class GroupConfiguration(object):
) )
return content_group_configuration.to_json() return content_group_configuration.to_json()
content_group_configuration = GroupConfiguration.update_content_group_usage_info( content_group_configuration = GroupConfiguration.update_partition_usage_info(
store, store,
course, course,
content_group_configuration content_group_configuration
) )
return content_group_configuration return content_group_configuration
@staticmethod
def get_all_user_partition_details(store, course):
"""
Returns all the available partitions with updated usage information
:return: list of all partitions available with details
"""
all_partitions = get_all_partitions_for_course(course)
all_updated_partitions = []
for partition in all_partitions:
configuration = GroupConfiguration.update_partition_usage_info(
store,
course,
partition
)
all_updated_partitions.append(configuration)
return all_updated_partitions
...@@ -377,7 +377,7 @@ class CoursewareSearchIndexer(SearchIndexerBase): ...@@ -377,7 +377,7 @@ class CoursewareSearchIndexer(SearchIndexerBase):
@classmethod @classmethod
def fetch_group_usage(cls, modulestore, structure): def fetch_group_usage(cls, modulestore, structure):
groups_usage_dict = {} groups_usage_dict = {}
groups_usage_info = GroupConfiguration.get_content_groups_usage_info(modulestore, structure).items() groups_usage_info = GroupConfiguration.get_partitions_usage_info(modulestore, structure).items()
groups_usage_info.extend( groups_usage_info.extend(
GroupConfiguration.get_content_groups_items_usage_info( GroupConfiguration.get_content_groups_items_usage_info(
modulestore, modulestore,
......
...@@ -30,6 +30,7 @@ from .library import LIBRARIES_ENABLED, get_library_creator_status ...@@ -30,6 +30,7 @@ from .library import LIBRARIES_ENABLED, get_library_creator_status
from ccx_keys.locator import CCXLocator from ccx_keys.locator import CCXLocator
from contentstore.course_group_config import ( from contentstore.course_group_config import (
COHORT_SCHEME, COHORT_SCHEME,
ENROLLMENT_SCHEME,
GroupConfiguration, GroupConfiguration,
GroupConfigurationsValidationError, GroupConfigurationsValidationError,
RANDOM_SCHEME, RANDOM_SCHEME,
...@@ -99,7 +100,6 @@ from xmodule.modulestore.django import modulestore ...@@ -99,7 +100,6 @@ from xmodule.modulestore.django import modulestore
from xmodule.modulestore.exceptions import ItemNotFoundError, DuplicateCourseError from xmodule.modulestore.exceptions import ItemNotFoundError, DuplicateCourseError
from xmodule.tabs import CourseTab, CourseTabList, InvalidTabsException from xmodule.tabs import CourseTab, CourseTabList, InvalidTabsException
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
__all__ = ['course_info_handler', 'course_handler', 'course_listing', __all__ = ['course_info_handler', 'course_handler', 'course_listing',
...@@ -1473,7 +1473,7 @@ def remove_content_or_experiment_group(request, store, course, configuration, gr ...@@ -1473,7 +1473,7 @@ def remove_content_or_experiment_group(request, store, course, configuration, gr
return JsonResponse(status=404) return JsonResponse(status=404)
group_id = int(group_id) group_id = int(group_id)
usages = GroupConfiguration.get_content_groups_usage_info(store, course) usages = GroupConfiguration.get_partitions_usage_info(store, course)
used = group_id in usages used = group_id in usages
if used: if used:
...@@ -1521,7 +1521,24 @@ def group_configurations_list_handler(request, course_key_string): ...@@ -1521,7 +1521,24 @@ def group_configurations_list_handler(request, course_key_string):
else: else:
experiment_group_configurations = None experiment_group_configurations = None
content_group_configuration = GroupConfiguration.get_or_create_content_group(store, course) all_partitions = GroupConfiguration.get_all_user_partition_details(store, course)
should_show_enrollment_track = False
group_schemes = []
for partition in all_partitions:
group_schemes.append(partition['scheme'])
if partition['scheme'] == ENROLLMENT_SCHEME:
enrollment_track_configuration = partition
should_show_enrollment_track = len(enrollment_track_configuration['groups']) > 1
# Remove the enrollment track partition and add it to the front of the list if it should be shown.
all_partitions.remove(partition)
if should_show_enrollment_track:
all_partitions.insert(0, partition)
# 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 COHORT_SCHEME not in group_schemes:
all_partitions.append(GroupConfiguration.get_or_create_content_group(store, course))
return render_to_response('group_configurations.html', { return render_to_response('group_configurations.html', {
'context_course': course, 'context_course': course,
...@@ -1529,7 +1546,8 @@ def group_configurations_list_handler(request, course_key_string): ...@@ -1529,7 +1546,8 @@ def group_configurations_list_handler(request, course_key_string):
'course_outline_url': course_outline_url, 'course_outline_url': course_outline_url,
'experiment_group_configurations': experiment_group_configurations, 'experiment_group_configurations': experiment_group_configurations,
'should_show_experiment_groups': should_show_experiment_groups, 'should_show_experiment_groups': should_show_experiment_groups,
'content_group_configuration': content_group_configuration 'all_group_configurations': all_partitions,
'should_show_enrollment_track': should_show_enrollment_track
}) })
elif "application/json" in request.META.get('HTTP_ACCEPT'): elif "application/json" in request.META.get('HTTP_ACCEPT'):
if request.method == 'POST': if request.method == 'POST':
......
...@@ -613,6 +613,15 @@ class GroupConfigurationsUsageInfoTestCase(CourseTestCase, HelperMethods): ...@@ -613,6 +613,15 @@ class GroupConfigurationsUsageInfoTestCase(CourseTestCase, HelperMethods):
def setUp(self): def setUp(self):
super(GroupConfigurationsUsageInfoTestCase, self).setUp() super(GroupConfigurationsUsageInfoTestCase, self).setUp()
def _get_user_partition(self, scheme):
"""
Returns the first user partition with the specified scheme.
"""
for group in GroupConfiguration.get_all_user_partition_details(self.store, self.course):
if group['scheme'] == scheme:
return group
return None
def _get_expected_content_group(self, usage_for_group): def _get_expected_content_group(self, usage_for_group):
""" """
Returns the expected configuration with particular usage. Returns the expected configuration with particular usage.
...@@ -637,7 +646,7 @@ class GroupConfigurationsUsageInfoTestCase(CourseTestCase, HelperMethods): ...@@ -637,7 +646,7 @@ class GroupConfigurationsUsageInfoTestCase(CourseTestCase, HelperMethods):
Test that right data structure will be created if content group is not used. Test that right data structure will be created if content group is not used.
""" """
self._add_user_partitions(scheme_id='cohort') self._add_user_partitions(scheme_id='cohort')
actual = GroupConfiguration.get_or_create_content_group(self.store, self.course) actual = self._get_user_partition('cohort')
expected = self._get_expected_content_group(usage_for_group=[]) expected = self._get_expected_content_group(usage_for_group=[])
self.assertEqual(actual, expected) self.assertEqual(actual, expected)
...@@ -650,7 +659,7 @@ class GroupConfigurationsUsageInfoTestCase(CourseTestCase, HelperMethods): ...@@ -650,7 +659,7 @@ class GroupConfigurationsUsageInfoTestCase(CourseTestCase, HelperMethods):
cid=0, group_id=1, name_suffix='0', special_characters=u"JOSÉ ANDRÉS" cid=0, group_id=1, name_suffix='0', special_characters=u"JOSÉ ANDRÉS"
) )
actual = GroupConfiguration.get_or_create_content_group(self.store, self.course) actual = self._get_user_partition('cohort')
expected = self._get_expected_content_group( expected = self._get_expected_content_group(
usage_for_group=[ usage_for_group=[
{ {
...@@ -669,7 +678,7 @@ class GroupConfigurationsUsageInfoTestCase(CourseTestCase, HelperMethods): ...@@ -669,7 +678,7 @@ class GroupConfigurationsUsageInfoTestCase(CourseTestCase, HelperMethods):
self._add_user_partitions(count=1, scheme_id='cohort') self._add_user_partitions(count=1, scheme_id='cohort')
vertical, __ = self._create_problem_with_content_group(cid=0, group_id=1, name_suffix='0') vertical, __ = self._create_problem_with_content_group(cid=0, group_id=1, name_suffix='0')
actual = GroupConfiguration.get_or_create_content_group(self.store, self.course) actual = self._get_user_partition('cohort')
expected = self._get_expected_content_group(usage_for_group=[ expected = self._get_expected_content_group(usage_for_group=[
{ {
...@@ -706,7 +715,7 @@ class GroupConfigurationsUsageInfoTestCase(CourseTestCase, HelperMethods): ...@@ -706,7 +715,7 @@ class GroupConfigurationsUsageInfoTestCase(CourseTestCase, HelperMethods):
expected = self._get_expected_content_group(usage_for_group=[]) expected = self._get_expected_content_group(usage_for_group=[])
# Get the actual content group information # Get the actual content group information
actual = GroupConfiguration.get_or_create_content_group(self.store, self.course) actual = self._get_user_partition('cohort')
# Assert that actual content group information is same as expected one. # Assert that actual content group information is same as expected one.
self.assertEqual(actual, expected) self.assertEqual(actual, expected)
...@@ -720,7 +729,7 @@ class GroupConfigurationsUsageInfoTestCase(CourseTestCase, HelperMethods): ...@@ -720,7 +729,7 @@ class GroupConfigurationsUsageInfoTestCase(CourseTestCase, HelperMethods):
vertical, __ = self._create_problem_with_content_group(cid=0, group_id=1, name_suffix='0') vertical, __ = self._create_problem_with_content_group(cid=0, group_id=1, name_suffix='0')
vertical1, __ = self._create_problem_with_content_group(cid=0, group_id=1, name_suffix='1') vertical1, __ = self._create_problem_with_content_group(cid=0, group_id=1, name_suffix='1')
actual = GroupConfiguration.get_or_create_content_group(self.store, self.course) actual = self._get_user_partition('cohort')
expected = self._get_expected_content_group(usage_for_group=[ expected = self._get_expected_content_group(usage_for_group=[
{ {
...@@ -927,7 +936,7 @@ class GroupConfigurationsUsageInfoTestCase(CourseTestCase, HelperMethods): ...@@ -927,7 +936,7 @@ class GroupConfigurationsUsageInfoTestCase(CourseTestCase, HelperMethods):
# This used to cause an exception since the code assumed that # This used to cause an exception since the code assumed that
# only one partition would be available. # only one partition would be available.
actual = GroupConfiguration.get_content_groups_usage_info(self.store, self.course) actual = GroupConfiguration.get_partitions_usage_info(self.store, self.course)
self.assertEqual(actual.keys(), [0]) self.assertEqual(actual.keys(), [0])
actual = GroupConfiguration.get_content_groups_items_usage_info(self.store, self.course) actual = GroupConfiguration.get_content_groups_items_usage_info(self.store, self.course)
......
...@@ -372,7 +372,7 @@ class GetItemTest(ItemTest): ...@@ -372,7 +372,7 @@ class GetItemTest(ItemTest):
self.assertEqual(result["user_partitions"], [ self.assertEqual(result["user_partitions"], [
{ {
"id": ENROLLMENT_TRACK_PARTITION_ID, "id": ENROLLMENT_TRACK_PARTITION_ID,
"name": "Enrollment Tracks", "name": "Enrollment Track Groups",
"scheme": "enrollment_track", "scheme": "enrollment_track",
"groups": [ "groups": [
{ {
......
...@@ -22,7 +22,7 @@ class AuthoringMixinTestCase(ModuleStoreTestCase): ...@@ -22,7 +22,7 @@ class AuthoringMixinTestCase(ModuleStoreTestCase):
NO_CONTENT_ENROLLMENT_TRACK_ENABLED = "specific groups of learners based either on their enrollment track, or by content groups that you create" NO_CONTENT_ENROLLMENT_TRACK_ENABLED = "specific groups of learners based either on their enrollment track, or by content groups that you create"
NO_CONTENT_ENROLLMENT_TRACK_DISABLED = "specific groups of learners based on content groups that you create" NO_CONTENT_ENROLLMENT_TRACK_DISABLED = "specific groups of learners based on content groups that you create"
CONTENT_GROUPS_TITLE = "Content Groups" CONTENT_GROUPS_TITLE = "Content Groups"
ENROLLMENT_GROUPS_TITLE = "Enrollment Tracks" ENROLLMENT_GROUPS_TITLE = "Enrollment Track Groups"
STAFF_LOCKED = 'The unit that contains this component is hidden from learners' STAFF_LOCKED = 'The unit that contains this component is hidden from learners'
FEATURES_WITH_ENROLLMENT_TRACK_DISABLED = settings.FEATURES.copy() FEATURES_WITH_ENROLLMENT_TRACK_DISABLED = settings.FEATURES.copy()
......
...@@ -2,24 +2,33 @@ define([ ...@@ -2,24 +2,33 @@ define([
'js/collections/group_configuration', 'js/models/group_configuration', 'js/views/pages/group_configurations' 'js/collections/group_configuration', 'js/models/group_configuration', 'js/views/pages/group_configurations'
], function(GroupConfigurationCollection, GroupConfigurationModel, GroupConfigurationsPage) { ], function(GroupConfigurationCollection, GroupConfigurationModel, GroupConfigurationsPage) {
'use strict'; 'use strict';
return function(experimentsEnabled, experimentGroupConfigurationsJson, contentGroupConfigurationJson, return function(experimentsEnabled,
groupConfigurationUrl, courseOutlineUrl) { experimentGroupConfigurationsJson,
allGroupConfigurationJson,
groupConfigurationUrl,
courseOutlineUrl) {
var experimentGroupConfigurations = new GroupConfigurationCollection( var experimentGroupConfigurations = new GroupConfigurationCollection(
experimentGroupConfigurationsJson, {parse: true} experimentGroupConfigurationsJson, {parse: true}
), ),
contentGroupConfiguration = new GroupConfigurationModel(contentGroupConfigurationJson, { allGroupConfigurations = [],
parse: true, canBeEmpty: true newGroupConfig,
}); i;
for (i = 0; i < allGroupConfigurationJson.length; i++) {
newGroupConfig = new GroupConfigurationModel(allGroupConfigurationJson[i],
{parse: true, canBeEmpty: true});
newGroupConfig.urlRoot = groupConfigurationUrl;
newGroupConfig.outlineUrl = courseOutlineUrl;
allGroupConfigurations.push(newGroupConfig);
}
experimentGroupConfigurations.url = groupConfigurationUrl; experimentGroupConfigurations.url = groupConfigurationUrl;
experimentGroupConfigurations.outlineUrl = courseOutlineUrl; experimentGroupConfigurations.outlineUrl = courseOutlineUrl;
contentGroupConfiguration.urlRoot = groupConfigurationUrl;
contentGroupConfiguration.outlineUrl = courseOutlineUrl;
new GroupConfigurationsPage({ new GroupConfigurationsPage({
el: $('#content'), el: $('#content'),
experimentsEnabled: experimentsEnabled, experimentsEnabled: experimentsEnabled,
experimentGroupConfigurations: experimentGroupConfigurations, experimentGroupConfigurations: experimentGroupConfigurations,
contentGroupConfiguration: contentGroupConfiguration allGroupConfigurations: allGroupConfigurations
}).render(); }).render();
}; };
}); });
...@@ -3,13 +3,13 @@ define([ ...@@ -3,13 +3,13 @@ define([
'common/js/spec_helpers/view_helpers', 'js/models/course', 'js/models/group_configuration', 'js/models/group', 'common/js/spec_helpers/view_helpers', 'js/models/course', 'js/models/group_configuration', 'js/models/group',
'js/collections/group_configuration', 'js/collections/group', 'js/views/group_configuration_details', 'js/collections/group_configuration', 'js/collections/group', 'js/views/group_configuration_details',
'js/views/group_configurations_list', 'js/views/group_configuration_editor', 'js/views/group_configuration_item', 'js/views/group_configurations_list', 'js/views/group_configuration_editor', 'js/views/group_configuration_item',
'js/views/experiment_group_edit', 'js/views/content_group_list', 'js/views/content_group_details', 'js/views/experiment_group_edit', 'js/views/partition_group_list', 'js/views/partition_group_details',
'js/views/content_group_editor', 'js/views/content_group_item' 'js/views/content_group_editor', 'js/views/partition_group_item'
], function( ], function(
_, AjaxHelpers, TemplateHelpers, ViewHelpers, Course, GroupConfigurationModel, GroupModel, _, AjaxHelpers, TemplateHelpers, ViewHelpers, Course, GroupConfigurationModel, GroupModel,
GroupConfigurationCollection, GroupCollection, GroupConfigurationDetailsView, GroupConfigurationsListView, GroupConfigurationCollection, GroupCollection, GroupConfigurationDetailsView, GroupConfigurationsListView,
GroupConfigurationEditorView, GroupConfigurationItemView, ExperimentGroupEditView, GroupList, GroupConfigurationEditorView, GroupConfigurationItemView, ExperimentGroupEditView, GroupList,
ContentGroupDetailsView, ContentGroupEditorView, ContentGroupItemView PartitionGroupDetailsView, ContentGroupEditorView, PartitionGroupItemView
) { ) {
'use strict'; 'use strict';
var SELECTORS = { var SELECTORS = {
...@@ -675,7 +675,7 @@ define([ ...@@ -675,7 +675,7 @@ define([
verifyEditingGroup, respondToSave, expectGroupsVisible, correctValidationError; verifyEditingGroup, respondToSave, expectGroupsVisible, correctValidationError;
scopedGroupSelector = function(groupIndex, additionalSelectors) { scopedGroupSelector = function(groupIndex, additionalSelectors) {
var groupSelector = '.content-groups-list-item-' + groupIndex; var groupSelector = '.partition-groups-list-item-' + groupIndex;
if (additionalSelectors) { if (additionalSelectors) {
return groupSelector + ' ' + additionalSelectors; return groupSelector + ' ' + additionalSelectors;
} else { } else {
...@@ -775,13 +775,13 @@ define([ ...@@ -775,13 +775,13 @@ define([
expectGroupsVisible = function(view, groupNames) { expectGroupsVisible = function(view, groupNames) {
_.each(groupNames, function(groupName) { _.each(groupNames, function(groupName) {
expect(view.$('.content-groups-list-item')).toContainText(groupName); expect(view.$('.partition-groups-list-item')).toContainText(groupName);
}); });
}; };
beforeEach(function() { beforeEach(function() {
TemplateHelpers.installTemplates( TemplateHelpers.installTemplates(
['content-group-editor', 'content-group-details', 'list'] ['content-group-editor', 'partition-group-details', 'list']
); );
}); });
...@@ -792,7 +792,7 @@ define([ ...@@ -792,7 +792,7 @@ define([
it('can render groups', function() { it('can render groups', function() {
var groupNames = ['Group 1', 'Group 2', 'Group 3']; var groupNames = ['Group 1', 'Group 2', 'Group 3'];
renderView(groupNames).$('.content-group-details').each(function(index) { renderView(groupNames).$('.partition-group-details').each(function(index) {
expect($(this)).toContainText(groupNames[index]); expect($(this)).toContainText(groupNames[index]);
}); });
}); });
...@@ -874,7 +874,7 @@ define([ ...@@ -874,7 +874,7 @@ define([
describe('Content groups details view', function() { describe('Content groups details view', function() {
beforeEach(function() { beforeEach(function() {
TemplateHelpers.installTemplate('content-group-details', true); TemplateHelpers.installTemplate('partition-group-details', true);
this.model = new GroupModel({name: 'Content Group', id: 0, courseOutlineUrl: 'CourseOutlineUrl'}); this.model = new GroupModel({name: 'Content Group', id: 0, courseOutlineUrl: 'CourseOutlineUrl'});
var saveableModel = new GroupConfigurationModel({ var saveableModel = new GroupConfigurationModel({
...@@ -889,7 +889,7 @@ define([ ...@@ -889,7 +889,7 @@ define([
this.collection = new GroupConfigurationCollection([saveableModel]); this.collection = new GroupConfigurationCollection([saveableModel]);
this.collection.outlineUrl = '/outline'; this.collection.outlineUrl = '/outline';
this.view = new ContentGroupDetailsView({ this.view = new PartitionGroupDetailsView({
model: this.model model: this.model
}); });
appendSetFixtures(this.view.render().el); appendSetFixtures(this.view.render().el);
...@@ -901,7 +901,7 @@ define([ ...@@ -901,7 +901,7 @@ define([
it('should show empty usage appropriately', function() { it('should show empty usage appropriately', function() {
this.view.$('.show-groups').click(); this.view.$('.show-groups').click();
assertShowEmptyUsages(this.view, 'This content group is not in use. '); assertShowEmptyUsages(this.view, "Use this group to control a component's visibility in the ");
}); });
it('should hide empty usage appropriately', function() { it('should hide empty usage appropriately', function() {
...@@ -915,7 +915,7 @@ define([ ...@@ -915,7 +915,7 @@ define([
assertShowNonEmptyUsages( assertShowNonEmptyUsages(
this.view, this.view,
'This content group is used in:', 'This group controls visibility of:',
'Cannot delete when in use by a unit' 'Cannot delete when in use by a unit'
); );
}); });
...@@ -1015,7 +1015,7 @@ define([ ...@@ -1015,7 +1015,7 @@ define([
describe('Content group controller view', function() { describe('Content group controller view', function() {
beforeEach(function() { beforeEach(function() {
TemplateHelpers.installTemplates([ TemplateHelpers.installTemplates([
'content-group-editor', 'content-group-details' 'content-group-editor', 'partition-group-details'
], true); ], true);
this.model = new GroupModel({name: 'Content Group', id: 0, courseOutlineUrl: 'CourseOutlineUrl'}); this.model = new GroupModel({name: 'Content Group', id: 0, courseOutlineUrl: 'CourseOutlineUrl'});
...@@ -1029,14 +1029,14 @@ define([ ...@@ -1029,14 +1029,14 @@ define([
this.saveableModel.urlRoot = '/group_configurations'; this.saveableModel.urlRoot = '/group_configurations';
this.collection = new GroupConfigurationCollection([this.saveableModel]); this.collection = new GroupConfigurationCollection([this.saveableModel]);
this.collection.url = '/group_configurations'; this.collection.url = '/group_configurations';
this.view = new ContentGroupItemView({ this.view = new PartitionGroupItemView({
model: this.model model: this.model
}); });
appendSetFixtures(this.view.render().el); appendSetFixtures(this.view.render().el);
}); });
it('should render properly', function() { it('should render properly', function() {
assertControllerView(this.view, '.content-group-details', '.content-group-edit'); assertControllerView(this.view, '.partition-group-details', '.content-group-edit');
}); });
it('should destroy itself on confirmation of deleting', function() { it('should destroy itself on confirmation of deleting', function() {
...@@ -1047,7 +1047,7 @@ define([ ...@@ -1047,7 +1047,7 @@ define([
assertAndDeleteItemWithError( assertAndDeleteItemWithError(
this, this,
'/group_configurations/0/0', '/group_configurations/0/0',
'.content-groups-list-item', '.partition-groups-list-item',
'Delete this content group' 'Delete this content group'
); );
}); });
......
...@@ -19,9 +19,8 @@ define([ ...@@ -19,9 +19,8 @@ define([
name: 'Configuration 1', name: 'Configuration 1',
courseOutlineUrl: 'CourseOutlineUrl' courseOutlineUrl: 'CourseOutlineUrl'
}), }),
contentGroupConfiguration: new GroupConfigurationModel({groups: []}) allGroupConfigurations: [new GroupConfigurationModel({groups: []})]
}); });
if (!disableSpy) { if (!disableSpy) {
spyOn(view, 'addWindowActions'); spyOn(view, 'addWindowActions');
} }
...@@ -36,7 +35,7 @@ define([ ...@@ -36,7 +35,7 @@ define([
beforeEach(function() { beforeEach(function() {
setFixtures(mockGroupConfigurationsPage); setFixtures(mockGroupConfigurationsPage);
TemplateHelpers.installTemplates([ TemplateHelpers.installTemplates([
'group-configuration-editor', 'group-configuration-details', 'content-group-details', 'group-configuration-editor', 'group-configuration-details', 'partition-group-details',
'content-group-editor', 'group-edit', 'list' 'content-group-editor', 'group-edit', 'list'
]); ]);
...@@ -116,7 +115,7 @@ define([ ...@@ -116,7 +115,7 @@ define([
}); });
it('should show a notification message if a content group is changed', function() { it('should show a notification message if a content group is changed', function() {
this.view.contentGroupConfiguration.get('groups').add({id: 0, name: 'Content Group'}); this.view.allGroupConfigurations[0].get('groups').add({id: 0, name: 'Content Group'});
expect(this.view.onBeforeUnload()) expect(this.view.onBeforeUnload())
.toBe('You have unsaved changes. Do you really want to leave this page?'); .toBe('You have unsaved changes. Do you really want to leave this page?');
}); });
......
...@@ -11,6 +11,7 @@ ...@@ -11,6 +11,7 @@
* of items this list contains. For example, 'Group Configuration'. * of items this list contains. For example, 'Group Configuration'.
* Note that it must be translated. * Note that it must be translated.
* - emptyMessage (string): Text to render when the list is empty. * - emptyMessage (string): Text to render when the list is empty.
* - restrictEditing (bool) : Boolean flag for hiding edit and remove options, defaults to false.
*/ */
define([ define([
'js/views/baseview' 'js/views/baseview'
...@@ -25,6 +26,7 @@ define([ ...@@ -25,6 +26,7 @@ define([
listContainerCss: '.list-items', listContainerCss: '.list-items',
initialize: function() { initialize: function() {
this.restrictEditing = this.options.restrictEditing || false;
this.listenTo(this.collection, 'add', this.addNewItemView); this.listenTo(this.collection, 'add', this.addNewItemView);
this.listenTo(this.collection, 'remove', this.onRemoveItem); this.listenTo(this.collection, 'remove', this.onRemoveItem);
this.template = this.loadTemplate('list'); this.template = this.loadTemplate('list');
...@@ -42,11 +44,14 @@ define([ ...@@ -42,11 +44,14 @@ define([
emptyMessage: this.emptyMessage, emptyMessage: this.emptyMessage,
length: this.collection.length, length: this.collection.length,
isEditing: model && model.get('editing'), isEditing: model && model.get('editing'),
canCreateNewItem: this.canCreateItem(this.collection) canCreateNewItem: this.canCreateItem(this.collection),
restrictEditing: this.restrictEditing
})); }));
this.collection.each(function(model) { this.collection.each(function(model) {
this.$(this.listContainerCss).append(this.createItemView({model: model}).render().el); this.$(this.listContainerCss).append(
this.createItemView({model: model, restrictEditing: this.restrictEditing}).render().el
);
}, this); }, this);
return this; return this;
......
...@@ -22,6 +22,7 @@ define([ ...@@ -22,6 +22,7 @@ define([
canDelete: false, canDelete: false,
initialize: function() { initialize: function() {
this.restrictEditing = this.options.restrictEditing || false;
this.listenTo(this.model, 'change:editing', this.render); this.listenTo(this.model, 'change:editing', this.render);
this.listenTo(this.model, 'remove', this.remove); this.listenTo(this.model, 'remove', this.remove);
}, },
......
define([ define([
'jquery', 'underscore', 'gettext', 'js/views/pages/base_page', 'jquery', 'underscore', 'gettext', 'js/views/pages/base_page',
'js/views/group_configurations_list', 'js/views/content_group_list' 'js/views/group_configurations_list', 'js/views/partition_group_list'
], ],
function($, _, gettext, BasePage, GroupConfigurationsListView, ContentGroupListView) { function($, _, gettext, BasePage, GroupConfigurationsListView, PartitionGroupListView) {
'use strict'; 'use strict';
var GroupConfigurationsPage = BasePage.extend({ var GroupConfigurationsPage = BasePage.extend({
initialize: function(options) { initialize: function(options) {
var currentScheme,
i,
enrollmentScheme = 'enrollment_track';
BasePage.prototype.initialize.call(this); BasePage.prototype.initialize.call(this);
this.experimentsEnabled = options.experimentsEnabled; this.experimentsEnabled = options.experimentsEnabled;
if (this.experimentsEnabled) { if (this.experimentsEnabled) {
...@@ -14,18 +18,35 @@ function($, _, gettext, BasePage, GroupConfigurationsListView, ContentGroupListV ...@@ -14,18 +18,35 @@ function($, _, gettext, BasePage, GroupConfigurationsListView, ContentGroupListV
collection: this.experimentGroupConfigurations collection: this.experimentGroupConfigurations
}); });
} }
this.contentGroupConfiguration = options.contentGroupConfiguration;
this.cohortGroupsListView = new ContentGroupListView({ this.allGroupConfigurations = options.allGroupConfigurations || [];
collection: this.contentGroupConfiguration.get('groups') this.allGroupViewList = [];
}); for (i = 0; i < this.allGroupConfigurations.length; i++) {
currentScheme = this.allGroupConfigurations[i].get('scheme');
this.allGroupViewList.push(
new PartitionGroupListView({
collection: this.allGroupConfigurations[i].get('groups'),
restrictEditing: currentScheme === enrollmentScheme,
scheme: currentScheme
})
);
}
}, },
renderPage: function() { renderPage: function() {
var hash = this.getLocationHash(); var hash = this.getLocationHash(),
i,
currentClass;
if (this.experimentsEnabled) { if (this.experimentsEnabled) {
this.$('.wrapper-groups.experiment-groups').append(this.experimentGroupsListView.render().el); this.$('.wrapper-groups.experiment-groups').append(this.experimentGroupsListView.render().el);
} }
this.$('.wrapper-groups.content-groups').append(this.cohortGroupsListView.render().el);
// Render the remaining Configuration groups
for (i = 0; i < this.allGroupViewList.length; i++) {
currentClass = '.wrapper-groups.content-groups.' + this.allGroupViewList[i].scheme;
this.$(currentClass).append(this.allGroupViewList[i].render().el);
}
this.addWindowActions(); this.addWindowActions();
if (hash) { if (hash) {
// Strip leading '#' to get id string to match // Strip leading '#' to get id string to match
...@@ -38,8 +59,22 @@ function($, _, gettext, BasePage, GroupConfigurationsListView, ContentGroupListV ...@@ -38,8 +59,22 @@ function($, _, gettext, BasePage, GroupConfigurationsListView, ContentGroupListV
$(window).on('beforeunload', this.onBeforeUnload.bind(this)); $(window).on('beforeunload', this.onBeforeUnload.bind(this));
}, },
/**
* Checks the Partition Group Configurations to see if the isDirty bit is set
* @returns {boolean} True if any partition group has the dirty bit set.
*/
areAnyConfigurationsDirty: function() {
var i;
for (i = 0; i < this.allGroupConfigurations.length; i++) {
if (this.allGroupConfigurations[i].isDirty()) {
return true;
}
}
return false;
},
onBeforeUnload: function() { onBeforeUnload: function() {
var dirty = this.contentGroupConfiguration.isDirty() || var dirty = this.areAnyConfigurationsDirty() ||
(this.experimentsEnabled && this.experimentGroupConfigurations.find(function(configuration) { (this.experimentsEnabled && this.experimentGroupConfigurations.find(function(configuration) {
return configuration.isDirty(); return configuration.isDirty();
})); }));
......
/** /**
* This class defines a simple display view for a content group. * This class defines a simple display view for a partition group.
* It is expected to be backed by a Group model. * It is expected to be backed by a Group model.
*/ */
define([ define([
...@@ -8,7 +8,7 @@ define([ ...@@ -8,7 +8,7 @@ define([
], function(BaseView, _, gettext, str, StringUtils, HtmlUtils) { ], function(BaseView, _, gettext, str, StringUtils, HtmlUtils) {
'use strict'; 'use strict';
var ContentGroupDetailsView = BaseView.extend({ var PartitionGroupDetailsView = BaseView.extend({
tagName: 'div', tagName: 'div',
events: { events: {
'click .edit': 'editGroup', 'click .edit': 'editGroup',
...@@ -21,17 +21,18 @@ define([ ...@@ -21,17 +21,18 @@ define([
return [ return [
'collection', 'collection',
'content-group-details', 'partition-group-details',
'content-group-details-' + index 'partition-group-details-' + index
].join(' '); ].join(' ');
}, },
editGroup: function() { editGroup: function() {
this.model.set({'editing': true}); this.model.set({editing: true});
}, },
initialize: function() { initialize: function() {
this.template = this.loadTemplate('content-group-details'); this.template = this.loadTemplate('partition-group-details');
this.restrictEditing = this.options.restrictEditing || false;
this.listenTo(this.model, 'change', this.render); this.listenTo(this.model, 'change', this.render);
}, },
...@@ -41,9 +42,10 @@ define([ ...@@ -41,9 +42,10 @@ define([
courseOutlineUrl: this.model.collection.parents[0].outlineUrl, courseOutlineUrl: this.model.collection.parents[0].outlineUrl,
index: this.model.collection.indexOf(this.model), index: this.model.collection.indexOf(this.model),
showContentGroupUsages: showContentGroupUsages || false, showContentGroupUsages: showContentGroupUsages || false,
HtmlUtils: HtmlUtils HtmlUtils: HtmlUtils,
restrictEditing: this.restrictEditing
}); });
this.$el.html(this.template(attrs)); HtmlUtils.setHtml(this.$el, HtmlUtils.HTML(this.template(attrs)));
return this; return this;
}, },
...@@ -78,5 +80,5 @@ define([ ...@@ -78,5 +80,5 @@ define([
} }
}); });
return ContentGroupDetailsView; return PartitionGroupDetailsView;
}); });
/** /**
* This class defines an controller view for content groups. * This class defines an controller view for partition groups.
* It renders an editor view or a details view depending on the state * It renders an editor view or a details view depending on the state
* of the underlying model. * of the underlying model.
* It is expected to be backed by a Group model. * It is expected to be backed by a Group model.
*/ */
define([ define([
'js/views/list_item', 'js/views/content_group_editor', 'js/views/content_group_details', 'gettext', 'common/js/components/utils/view_utils' 'js/views/list_item', 'js/views/content_group_editor', 'js/views/partition_group_details',
], function(ListItemView, ContentGroupEditorView, ContentGroupDetailsView, gettext) { 'gettext', 'common/js/components/utils/view_utils'
], function(ListItemView, ContentGroupEditorView, PartitionGroupDetailsView, gettext) {
'use strict'; 'use strict';
var ContentGroupItemView = ListItemView.extend({ var PartitionGroupItemView = ListItemView.extend({
events: { events: {
'click .delete': 'deleteItem' 'click .delete': 'deleteItem'
}, },
tagName: 'section', tagName: 'section',
baseClassName: 'content-group', baseClassName: 'partition-group',
canDelete: true, canDelete: true,
...@@ -24,8 +25,8 @@ define([ ...@@ -24,8 +25,8 @@ define([
attributes: function() { attributes: function() {
return { return {
'id': this.model.get('id'), id: this.model.get('id'),
'tabindex': -1 tabindex: -1
}; };
}, },
...@@ -34,9 +35,11 @@ define([ ...@@ -34,9 +35,11 @@ define([
}, },
createDetailsView: function() { createDetailsView: function() {
return new ContentGroupDetailsView({model: this.model}); return new PartitionGroupDetailsView({model: this.model,
restrictEditing: this.options.restrictEditing
});
} }
}); });
return ContentGroupItemView; return PartitionGroupItemView;
}); });
/** /**
* This class defines a list view for content groups. * This class defines a list view for partition groups.
* It is expected to be backed by a Group collection. * It is expected to be backed by a Group collection.
*/ */
define([ define([
'js/views/list', 'js/views/content_group_item', 'gettext' 'underscore', 'js/views/list', 'js/views/partition_group_item', 'gettext'
], function(ListView, ContentGroupItemView, gettext) { ], function(_, ListView, PartitionGroupItemView, gettext) {
'use strict'; 'use strict';
var ContentGroupListView = ListView.extend({ var PartitionGroupListView = ListView.extend({
initialize: function(options) {
ListView.prototype.initialize.apply(this, [options]);
this.scheme = options.scheme;
},
tagName: 'div', tagName: 'div',
className: 'content-group-list', className: 'partition-group-list',
// Translators: This refers to a content group that can be linked to a student cohort. // Translators: This refers to a content group that can be linked to a student cohort.
itemCategoryDisplayName: gettext('content group'), itemCategoryDisplayName: gettext('content group'),
...@@ -20,9 +25,9 @@ define([ ...@@ -20,9 +25,9 @@ define([
emptyMessage: gettext('You have not created any content groups yet.'), emptyMessage: gettext('You have not created any content groups yet.'),
createItemView: function(options) { createItemView: function(options) {
return new ContentGroupItemView(options); return new PartitionGroupItemView(_.extend({}, options, {scheme: this.scheme}));
} }
}); });
return ContentGroupListView; return PartitionGroupListView;
}); });
<%page expression_filter="h"/> <%page expression_filter="h"/>
<%inherit file="base.html" /> <%inherit file="base.html" />
<%def name="content_groups_help_token()"><% return "content_groups" %></%def> <%def name="content_groups_help_token()"><% return "content_groups" %></%def>
<%def name="enrollment_track_help_token()"><% return "enrollment_tracks" %></%def>
<%def name="experiment_group_configurations_help_token()"><% return "group_configurations" %></%def> <%def name="experiment_group_configurations_help_token()"><% return "group_configurations" %></%def>
<%namespace name='static' file='static_content.html'/> <%namespace name='static' file='static_content.html'/>
<%! <%!
...@@ -16,7 +17,7 @@ from openedx.core.djangolib.markup import HTML, Text ...@@ -16,7 +17,7 @@ from openedx.core.djangolib.markup import HTML, Text
<%block name="bodyclass">is-signedin course view-group-configurations</%block> <%block name="bodyclass">is-signedin course view-group-configurations</%block>
<%block name="header_extras"> <%block name="header_extras">
% for template_name in ["group-configuration-details", "group-configuration-editor", "group-edit", "content-group-editor", "content-group-details", "basic-modal", "modal-button", "list"]: % for template_name in ["group-configuration-details", "group-configuration-editor", "group-edit", "content-group-editor", "partition-group-details", "basic-modal", "modal-button", "list"]:
<script type="text/template" id="${template_name}-tpl"> <script type="text/template" id="${template_name}-tpl">
<%static:include path="js/${template_name}.underscore" /> <%static:include path="js/${template_name}.underscore" />
</script> </script>
...@@ -28,9 +29,10 @@ from openedx.core.djangolib.markup import HTML, Text ...@@ -28,9 +29,10 @@ from openedx.core.djangolib.markup import HTML, Text
GroupConfigurationsFactory( GroupConfigurationsFactory(
${should_show_experiment_groups | n, dump_js_escaped_json}, ${should_show_experiment_groups | n, dump_js_escaped_json},
${experiment_group_configurations | n, dump_js_escaped_json}, ${experiment_group_configurations | n, dump_js_escaped_json},
${content_group_configuration | n, dump_js_escaped_json}, ${all_group_configurations | n, dump_js_escaped_json},
"${group_configuration_url | n, js_escaped_string}", "${group_configuration_url | n, js_escaped_string}",
"${course_outline_url | n, js_escaped_string}" "${course_outline_url | n, js_escaped_string}",
${should_show_enrollment_track | n, dump_js_escaped_json}
); );
}); });
</%block> </%block>
...@@ -47,37 +49,52 @@ from openedx.core.djangolib.markup import HTML, Text ...@@ -47,37 +49,52 @@ from openedx.core.djangolib.markup import HTML, Text
<div class="wrapper-content wrapper"> <div class="wrapper-content wrapper">
<section class="content"> <section class="content">
<article class="content-primary" role="main"> <article class="content-primary" role="main">
<div class="wrapper-groups content-groups">
<h3 class="title">${_("Content Groups")}</h3>
<div class="ui-loading">
<p><span class="spin"><span class="icon fa fa-refresh" aria-hidden="true"></span></span> <span class="copy">${_("Loading")}</span></p>
</div>
</div>
% if should_show_experiment_groups: % for config in all_group_configurations:
<div class="wrapper-groups experiment-groups"> <div class="wrapper-groups content-groups ${config['scheme']}">
<h3 class="title">${_("Experiment Group Configurations")}</h3> <h3 class="title">${config['name']}</h3>
% if experiment_group_configurations is None: <div class="ui-loading">
<div class="notice notice-incontext notice-moduledisabled"> <p><span class="spin"><span class="icon fa fa-refresh" aria-hidden="true"></span></span> <span class="copy">${_("Loading")}</span></p>
<p class="copy"> </div>
${_("This module is disabled at the moment.")} </div>
</p> % endfor
</div>
% else: % if should_show_experiment_groups:
<div class="ui-loading"> <div class="wrapper-groups experiment-groups">
<p><span class="spin"><span class="icon fa fa-refresh" aria-hidden="true"></span></span> <span class="copy">${_("Loading")}</span></p> <h3 class="title">${_("Experiment Group Configurations")}</h3>
</div> % if experiment_group_configurations is None:
% endif <div class="notice notice-incontext notice-moduledisabled">
</div> <p class="copy">
% endif ${_("This module is disabled at the moment.")}
</p>
</div>
% else:
<div class="ui-loading">
<p><span class="spin"><span class="icon fa fa-refresh" aria-hidden="true"></span></span> <span class="copy">${_("Loading")}</span></p>
</div>
% endif
</div>
% endif
</article> </article>
<aside class="content-supplementary" role="complementary"> <aside class="content-supplementary" role="complementary">
% if should_show_enrollment_track:
<div class="bit">
<div class="enrollment-track-doc">
<h3 class="title-3">${_("Enrollment Track Groups")}</h3>
<p>${_("Enrollment track groups allow you to offer different course content to learners in each enrollment track. Learners enrolled in each enrollment track in your course are automatically included in the corresponding enrollment track group.")}</p>
<p>${_("On unit pages in the course outline, you can designate components as visible only to learners in a specific enrollment track.")}</p>
<p>${_("You cannot edit enrollment track groups, but you can expand each group to view details of the course content that is designated for learners in the group.")}</p>
<p><a href="${get_online_help_info(enrollment_track_help_token())['doc_url']}" target="_blank" class="button external-help-button">${_("Learn More")}</a></p>
</div>
</div>
% endif
<div class="bit"> <div class="bit">
<div class="content-groups-doc"> <div class="content-groups-doc">
<h3 class="title-3">${_("Content Groups")}</h3> <h3 class="title-3">${_("Content Groups")}</h3>
<p>${_("If you have cohorts enabled in your course, you can use content groups to create cohort-specific courseware. In other words, you can customize the content that particular cohorts see in your course.")}</p> <p>${_("If you have cohorts enabled in your course, you can use content groups to create cohort-specific courseware. In other words, you can customize the content that particular cohorts see in your course.")}</p>
<p>${_("Each content group that you create can be associated with one or more cohorts. In addition to course content that is intended for all students, you can designate some content as visible only to specified content groups. Only learners in the cohorts that are associated with the specified content groups see the additional content.")}</p> <p>${_("Each content group that you create can be associated with one or more cohorts. In addition to course content that is intended for all learners, you can designate some content as visible only to specified content groups. Only learners in the cohorts that are associated with the specified content groups see the additional content.")}</p>
<p>${Text(_("Click {em_start}New content group{em_end} to add a new content group. To edit the name of a content group, hover over its box and click {em_start}Edit{em_end}. You can delete a content group only if it is not in use by a unit. To delete a content group, hover over its box and click the delete icon.")).format(em_start=HTML("<strong>"), em_end=HTML("</strong>"))}</p> <p>${Text(_("Click {em_start}New content group{em_end} to add a new content group. To edit the name of a content group, hover over its box and click {em_start}Edit{em_end}. You can delete a content group only if it is not in use by a unit. To delete a content group, hover over its box and click the delete icon.")).format(em_start=HTML("<strong>"), em_end=HTML("</strong>"))}</p>
<p><a href="${get_online_help_info(content_groups_help_token())['doc_url']}" target="_blank" class="button external-help-button">${_("Learn More")}</a></p> <p><a href="${get_online_help_info(content_groups_help_token())['doc_url']}" target="_blank" class="button external-help-button">${_("Learn More")}</a></p>
</div> </div>
...@@ -86,12 +103,13 @@ from openedx.core.djangolib.markup import HTML, Text ...@@ -86,12 +103,13 @@ from openedx.core.djangolib.markup import HTML, Text
<div class="bit"> <div class="bit">
<div class="experiment-groups-doc"> <div class="experiment-groups-doc">
<h3 class="title-3">${_("Experiment Group Configurations")}</h3> <h3 class="title-3">${_("Experiment Group Configurations")}</h3>
<p>${_("Use experiment group configurations if you are conducting content experiments, also known as A/B testing, in your course. Experiment group configurations define how many groups of students are in a content experiment. When you create a content experiment for a course, you select the group configuration to use.")}</p> <p>${_("Use experiment group configurations if you are conducting content experiments, also known as A/B testing, in your course. Experiment group configurations define how many groups of learners are in a content experiment. When you create a content experiment for a course, you select the group configuration to use.")}</p>
<p>${Text(_("Click {em_start}New Group Configuration{em_end} to add a new configuration. To edit a configuration, hover over its box and click {em_start}Edit{em_end}. You can delete a group configuration only if it is not in use in an experiment. To delete a configuration, hover over its box and click the delete icon.")).format(em_start=HTML("<strong>"), em_end=HTML("</strong>"))}</p> <p>${Text(_("Click {em_start}New Group Configuration{em_end} to add a new configuration. To edit a configuration, hover over its box and click {em_start}Edit{em_end}. You can delete a group configuration only if it is not in use in an experiment. To delete a configuration, hover over its box and click the delete icon.")).format(em_start=HTML("<strong>"), em_end=HTML("</strong>"))}</p>
<p><a href="${get_online_help_info(experiment_group_configurations_help_token())['doc_url']}" target="_blank" class="button external-help-button">${_("Learn More")}</a></p> <p><a href="${get_online_help_info(experiment_group_configurations_help_token())['doc_url']}" target="_blank" class="button external-help-button">${_("Learn More")}</a></p>
</div> </div>
</div> </div>
% endif % endif
<div class="bit"> <div class="bit">
% if context_course: % if context_course:
<% <%
......
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
</div> </div>
<% } else { %> <% } else { %>
<div class="list-items"></div> <div class="list-items"></div>
<% if (!isEditing) { %> <% if (!isEditing && !restrictEditing) { %>
<button class="action action-add <% if(!canCreateNewItem) {%> action-add-hidden <% }%>" > <button class="action action-add <% if(!canCreateNewItem) {%> action-add-hidden <% }%>" >
<span class="icon fa fa-plus" aria-hidden="true"></span> <span class="icon fa fa-plus" aria-hidden="true"></span>
<%- interpolate( <%- interpolate(
......
...@@ -23,17 +23,19 @@ ...@@ -23,17 +23,19 @@
</ol> </ol>
<ul class="actions group-configuration-actions"> <ul class="actions group-configuration-actions">
<li class="action action-edit"> <% if (!restrictEditing) { %>
<button class="edit"><span class="icon fa fa-pencil" aria-hidden="true"></span> <%- gettext("Edit") %></button> <li class="action action-edit">
</li> <button class="edit"><span class="icon fa fa-pencil" aria-hidden="true"></span> <%- gettext("Edit") %></button>
<% if (_.isEmpty(usage)) { %>
<li class="action action-delete wrapper-delete-button" data-tooltip="<%- gettext('Delete') %>">
<button class="delete action-icon" title="<%- gettext('Delete') %>"><span class="icon fa fa-trash-o" aria-hidden="true"></span></button>
</li>
<% } else { %>
<li class="action action-delete wrapper-delete-button" data-tooltip="<%- gettext('Cannot delete when in use by a unit') %>">
<button class="delete action-icon is-disabled" disabled="disabled" title="<%- gettext('Delete') %>"><span class="icon fa fa-trash-o" aria-hidden="true"></span></button>
</li> </li>
<% if (_.isEmpty(usage)) { %>
<li class="action action-delete wrapper-delete-button" data-tooltip="<%- gettext('Delete') %>">
<button class="delete action-icon" title="<%- gettext('Delete') %>"><span class="icon fa fa-trash-o" aria-hidden="true"></span></button>
</li>
<% } else { %>
<li class="action action-delete wrapper-delete-button" data-tooltip="<%- gettext('Cannot delete when in use by a unit') %>">
<button class="delete action-icon is-disabled" disabled="disabled" title="<%- gettext('Delete') %>"><span class="icon fa fa-trash-o" aria-hidden="true"></span></button>
</li>
<% } %>
<% } %> <% } %>
</ul> </ul>
</div> </div>
...@@ -41,7 +43,9 @@ ...@@ -41,7 +43,9 @@
<% if (showContentGroupUsages) { %> <% if (showContentGroupUsages) { %>
<div class="collection-references wrapper-group-configuration-usages"> <div class="collection-references wrapper-group-configuration-usages">
<% if (!_.isEmpty(usage)) { %> <% if (!_.isEmpty(usage)) { %>
<h4 class="intro group-configuration-usage-text"><%- gettext('This content group is used in:') %></h4> <h4 class="intro group-configuration-usage-text">
<%- gettext('This group controls visibility of:') %>
</h4>
<ol class="usage group-configuration-usage"> <ol class="usage group-configuration-usage">
<% _.each(usage, function(unit) { %> <% _.each(usage, function(unit) { %>
<li class="usage-unit group-configuration-usage-unit"> <li class="usage-unit group-configuration-usage-unit">
...@@ -52,15 +56,18 @@ ...@@ -52,15 +56,18 @@
<% } else { %> <% } else { %>
<p class="group-configuration-usage-text"> <p class="group-configuration-usage-text">
<%= HtmlUtils.interpolateHtml( <%= HtmlUtils.interpolateHtml(
gettext('This content group is not in use. Add a content group to any unit from the {linkStart}Course Outline{linkEnd}.'), gettext("Use this group to control a component's visibility in the {linkStart}Course Outline{linkEnd}."),
{ {
linkStart: HtmlUtils.interpolateHtml( linkStart: HtmlUtils.interpolateHtml(
HtmlUtils.HTML('<a href="{courseOutlineUrl}" title="{courseOutlineTitle}">'), HtmlUtils.HTML('<a href="{courseOutlineUrl}" title="{courseOutlineTitle}">'),
{courseOutlineUrl: courseOutlineUrl, courseOutlineTitle: gettext('Course Outline')}), {courseOutlineUrl: courseOutlineUrl, courseOutlineTitle: gettext('Course Outline')}
linkEnd: HtmlUtils.HTML('</a>') ),
}) linkEnd: HtmlUtils.HTML('</a>')
}
)
%> %>
</p> </p>
<% } %> <% } %>
</div> </div>
<% } %> <% } %>
\ No newline at end of file
...@@ -74,7 +74,7 @@ def _create_enrollment_track_partition(course): ...@@ -74,7 +74,7 @@ def _create_enrollment_track_partition(course):
partition = enrollment_track_scheme.create_user_partition( partition = enrollment_track_scheme.create_user_partition(
id=ENROLLMENT_TRACK_PARTITION_ID, id=ENROLLMENT_TRACK_PARTITION_ID,
name=_(u"Enrollment Tracks"), name=_(u"Enrollment Track Groups"),
description=_(u"Partition for segmenting users by enrollment track"), description=_(u"Partition for segmenting users by enrollment track"),
parameters={"course_id": unicode(course.id)} parameters={"course_id": unicode(course.id)}
) )
......
...@@ -4,6 +4,9 @@ Utility methods common to Studio and the LMS. ...@@ -4,6 +4,9 @@ Utility methods common to Studio and the LMS.
from bok_choy.promise import BrokenPromise from bok_choy.promise import BrokenPromise
from common.test.acceptance.tests.helpers import disable_animations from common.test.acceptance.tests.helpers import disable_animations
from selenium.webdriver.common.action_chains import ActionChains from selenium.webdriver.common.action_chains import ActionChains
from common.test.acceptance.pages.lms.pay_and_verify import PaymentAndVerificationFlow, FakePaymentPage
from common.test.acceptance.pages.lms.track_selection import TrackSelectionPage
from common.test.acceptance.pages.lms.create_mode import ModeCreationPage
def sync_on_notification(page, style='default', wait_for_hide=False): def sync_on_notification(page, style='default', wait_for_hide=False):
...@@ -100,3 +103,44 @@ def hover(browser, element): ...@@ -100,3 +103,44 @@ def hover(browser, element):
Hover over an element. Hover over an element.
""" """
ActionChains(browser).move_to_element(element).perform() ActionChains(browser).move_to_element(element).perform()
def enroll_user_track(browser, course_id, track):
"""
Utility method to enroll a user in the audit or verified user track. Creates and connects to the
necessary pages. Selects the track and handles payment for verified.
Supported tracks are 'verified' or 'audit'.
"""
payment_and_verification_flow = PaymentAndVerificationFlow(browser, course_id)
fake_payment_page = FakePaymentPage(browser, course_id)
track_selection = TrackSelectionPage(browser, course_id)
# Select track and process payment
track_selection.visit()
track_selection.enroll(track)
if track == 'verified':
payment_and_verification_flow.proceed_to_payment()
fake_payment_page.submit_payment()
def add_enrollment_course_modes(browser, course_id, tracks):
"""
Add the specified array of tracks to the given course.
Supported tracks are `verified` and `audit` (all others will be ignored),
and display names assigned are `Verified` and `Audit`, respectively.
"""
for track in tracks:
if track == 'audit':
# Add an audit mode to the course
ModeCreationPage(
browser,
course_id, mode_slug='audit',
mode_display_name='Audit'
).visit()
elif track == 'verified':
# Add a verified mode to the course
ModeCreationPage(
browser, course_id, mode_slug='verified',
mode_display_name='Verified', min_price=10
).visit()
...@@ -34,24 +34,24 @@ class TrackSelectionPage(PageObject): ...@@ -34,24 +34,24 @@ class TrackSelectionPage(PageObject):
"""Check if the track selection page has loaded.""" """Check if the track selection page has loaded."""
return self.q(css=".wrapper-register-choose").is_present() return self.q(css=".wrapper-register-choose").is_present()
def enroll(self, mode="honor"): def enroll(self, mode="audit"):
"""Interact with one of the enrollment buttons on the page. """Interact with one of the enrollment buttons on the page.
Keyword Arguments: Keyword Arguments:
mode (str): Can be "honor" or "verified" mode (str): Can be "audit" or "verified"
Raises: Raises:
ValueError ValueError
""" """
if mode == "honor": if mode == "verified":
self.q(css="input[name='honor_mode']").click()
return DashboardPage(self.browser).wait_for_page()
elif mode == "verified":
# Check the first contribution option, then click the enroll button # Check the first contribution option, then click the enroll button
self.q(css=".contribution-option > input").first.click() self.q(css=".contribution-option > input").first.click()
self.q(css="input[name='verified_mode']").click() self.q(css="input[name='verified_mode']").click()
return PaymentAndVerificationFlow(self.browser, self._course_id).wait_for_page() return PaymentAndVerificationFlow(self.browser, self._course_id).wait_for_page()
elif mode == "audit":
self.q(css="input[name='audit_mode']").click()
return DashboardPage(self.browser).wait_for_page()
else: else:
raise ValueError("Mode must be either 'honor' or 'verified'.") raise ValueError("Mode must be either 'audit' or 'verified'.")
...@@ -112,7 +112,7 @@ class ComponentVisibilityEditorView(BaseComponentEditorView): ...@@ -112,7 +112,7 @@ class ComponentVisibilityEditorView(BaseComponentEditorView):
OPTION_SELECTOR = '.partition-group-control .field' OPTION_SELECTOR = '.partition-group-control .field'
ALL_LEARNERS_AND_STAFF = 'All Learners and Staff' ALL_LEARNERS_AND_STAFF = 'All Learners and Staff'
CONTENT_GROUP_PARTITION = 'Content Groups' CONTENT_GROUP_PARTITION = 'Content Groups'
ENROLLMENT_TRACK_PARTITION = "Enrollment Tracks" ENROLLMENT_TRACK_PARTITION = "Enrollment Track Groups"
@property @property
def all_group_options(self): def all_group_options(self):
......
...@@ -91,6 +91,17 @@ class GroupConfigurationsPage(CoursePage): ...@@ -91,6 +91,17 @@ class GroupConfigurationsPage(CoursePage):
""" """
return self.q(css=self.experiment_groups_css).present or self.q(css=".experiment-groups-doc").present return self.q(css=self.experiment_groups_css).present or self.q(css=".experiment-groups-doc").present
@property
def enrollment_track_section_present(self):
return self.q(css='.wrapper-groups.content-groups.enrollment_track').present
@property
def enrollment_track_edit_present(self):
return self.q(css='.wrapper-groups.content-groups.enrollment_track .action.action-edit').present
def get_enrollment_groups(self):
return self.q(css='.wrapper-groups.content-groups.enrollment_track .collection-details .title').text
class GroupConfiguration(object): class GroupConfiguration(object):
""" """
......
...@@ -38,8 +38,8 @@ from common.test.acceptance.pages.lms.pay_and_verify import PaymentAndVerificati ...@@ -38,8 +38,8 @@ from common.test.acceptance.pages.lms.pay_and_verify import PaymentAndVerificati
from common.test.acceptance.pages.lms.progress import ProgressPage from common.test.acceptance.pages.lms.progress import ProgressPage
from common.test.acceptance.pages.lms.problem import ProblemPage from common.test.acceptance.pages.lms.problem import ProblemPage
from common.test.acceptance.pages.lms.tab_nav import TabNavPage from common.test.acceptance.pages.lms.tab_nav import TabNavPage
from common.test.acceptance.pages.lms.track_selection import TrackSelectionPage
from common.test.acceptance.pages.lms.video.video import VideoPage from common.test.acceptance.pages.lms.video.video import VideoPage
from common.test.acceptance.pages.common.utils import enroll_user_track
from common.test.acceptance.pages.studio.settings import SettingsPage from common.test.acceptance.pages.studio.settings import SettingsPage
from common.test.acceptance.fixtures.course import CourseFixture, XBlockFixtureDesc, CourseUpdateDesc from common.test.acceptance.fixtures.course import CourseFixture, XBlockFixtureDesc, CourseUpdateDesc
...@@ -417,7 +417,6 @@ class PayAndVerifyTest(EventsTestMixin, UniqueCourseTest): ...@@ -417,7 +417,6 @@ class PayAndVerifyTest(EventsTestMixin, UniqueCourseTest):
""" """
super(PayAndVerifyTest, self).setUp() super(PayAndVerifyTest, self).setUp()
self.track_selection_page = TrackSelectionPage(self.browser, self.course_id)
self.payment_and_verification_flow = PaymentAndVerificationFlow(self.browser, self.course_id) self.payment_and_verification_flow = PaymentAndVerificationFlow(self.browser, self.course_id)
self.immediate_verification_page = PaymentAndVerificationFlow(self.browser, self.course_id, entry_point='verify-now') self.immediate_verification_page = PaymentAndVerificationFlow(self.browser, self.course_id, entry_point='verify-now')
self.upgrade_page = PaymentAndVerificationFlow(self.browser, self.course_id, entry_point='upgrade') self.upgrade_page = PaymentAndVerificationFlow(self.browser, self.course_id, entry_point='upgrade')
...@@ -443,17 +442,7 @@ class PayAndVerifyTest(EventsTestMixin, UniqueCourseTest): ...@@ -443,17 +442,7 @@ class PayAndVerifyTest(EventsTestMixin, UniqueCourseTest):
# Create a user and log them in # Create a user and log them in
student_id = AutoAuthPage(self.browser).visit().get_user_id() student_id = AutoAuthPage(self.browser).visit().get_user_id()
# Navigate to the track selection page enroll_user_track(self.browser, self.course_id, 'verified')
self.track_selection_page.visit()
# Enter the payment and verification flow by choosing to enroll as verified
self.track_selection_page.enroll('verified')
# Proceed to the fake payment page
self.payment_and_verification_flow.proceed_to_payment()
# Submit payment
self.fake_payment_page.submit_payment()
# Proceed to verification # Proceed to verification
self.payment_and_verification_flow.immediate_verification() self.payment_and_verification_flow.immediate_verification()
...@@ -480,17 +469,7 @@ class PayAndVerifyTest(EventsTestMixin, UniqueCourseTest): ...@@ -480,17 +469,7 @@ class PayAndVerifyTest(EventsTestMixin, UniqueCourseTest):
# Create a user and log them in # Create a user and log them in
student_id = AutoAuthPage(self.browser).visit().get_user_id() student_id = AutoAuthPage(self.browser).visit().get_user_id()
# Navigate to the track selection page enroll_user_track(self.browser, self.course_id, 'verified')
self.track_selection_page.visit()
# Enter the payment and verification flow by choosing to enroll as verified
self.track_selection_page.enroll('verified')
# Proceed to the fake payment page
self.payment_and_verification_flow.proceed_to_payment()
# Submit payment
self.fake_payment_page.submit_payment()
# Navigate to the dashboard # Navigate to the dashboard
self.dashboard_page.visit() self.dashboard_page.visit()
......
...@@ -19,9 +19,9 @@ from common.test.acceptance.pages.lms.instructor_dashboard import InstructorDash ...@@ -19,9 +19,9 @@ from common.test.acceptance.pages.lms.instructor_dashboard import InstructorDash
from common.test.acceptance.fixtures.course import CourseFixture, XBlockFixtureDesc from common.test.acceptance.fixtures.course import CourseFixture, XBlockFixtureDesc
from common.test.acceptance.pages.lms.dashboard import DashboardPage from common.test.acceptance.pages.lms.dashboard import DashboardPage
from common.test.acceptance.pages.lms.problem import ProblemPage from common.test.acceptance.pages.lms.problem import ProblemPage
from common.test.acceptance.pages.lms.track_selection import TrackSelectionPage from common.test.acceptance.pages.lms.pay_and_verify import PaymentAndVerificationFlow
from common.test.acceptance.pages.lms.pay_and_verify import PaymentAndVerificationFlow, FakePaymentPage
from common.test.acceptance.pages.lms.login_and_register import CombinedLoginAndRegisterPage from common.test.acceptance.pages.lms.login_and_register import CombinedLoginAndRegisterPage
from common.test.acceptance.pages.common.utils import enroll_user_track
from common.test.acceptance.tests.helpers import disable_animations from common.test.acceptance.tests.helpers import disable_animations
from common.test.acceptance.fixtures.certificates import CertificateConfigFixture from common.test.acceptance.fixtures.certificates import CertificateConfigFixture
...@@ -247,13 +247,6 @@ class ProctoredExamsTest(BaseInstructorDashboardTest): ...@@ -247,13 +247,6 @@ class ProctoredExamsTest(BaseInstructorDashboardTest):
) )
).install() ).install()
self.track_selection_page = TrackSelectionPage(self.browser, self.course_id)
self.payment_and_verification_flow = PaymentAndVerificationFlow(self.browser, self.course_id)
self.immediate_verification_page = PaymentAndVerificationFlow(
self.browser, self.course_id, entry_point='verify-now'
)
self.upgrade_page = PaymentAndVerificationFlow(self.browser, self.course_id, entry_point='upgrade')
self.fake_payment_page = FakePaymentPage(self.browser, self.course_id)
self.dashboard_page = DashboardPage(self.browser) self.dashboard_page = DashboardPage(self.browser)
self.problem_page = ProblemPage(self.browser) self.problem_page = ProblemPage(self.browser)
...@@ -279,19 +272,7 @@ class ProctoredExamsTest(BaseInstructorDashboardTest): ...@@ -279,19 +272,7 @@ class ProctoredExamsTest(BaseInstructorDashboardTest):
""" """
self._auto_auth(self.USERNAME, self.EMAIL, False) self._auto_auth(self.USERNAME, self.EMAIL, False)
enroll_user_track(self.browser, self.course_id, 'verified')
# the track selection page cannot be visited. see the other tests to see if any prereq is there.
# Navigate to the track selection page
self.track_selection_page.visit()
# Enter the payment and verification flow by choosing to enroll as verified
self.track_selection_page.enroll('verified')
# Proceed to the fake payment page
self.payment_and_verification_flow.proceed_to_payment()
# Submit payment
self.fake_payment_page.submit_payment()
def _create_a_proctored_exam_and_attempt(self): def _create_a_proctored_exam_and_attempt(self):
""" """
......
...@@ -18,6 +18,8 @@ from common.test.acceptance.pages.studio.settings_advanced import AdvancedSettin ...@@ -18,6 +18,8 @@ from common.test.acceptance.pages.studio.settings_advanced import AdvancedSettin
from common.test.acceptance.pages.studio.settings_group_configurations import GroupConfigurationsPage from common.test.acceptance.pages.studio.settings_group_configurations import GroupConfigurationsPage
from common.test.acceptance.pages.lms.courseware import CoursewarePage from common.test.acceptance.pages.lms.courseware import CoursewarePage
from common.test.acceptance.pages.studio.utils import get_input_value from common.test.acceptance.pages.studio.utils import get_input_value
from common.test.acceptance.pages.common.utils import add_enrollment_course_modes
from textwrap import dedent from textwrap import dedent
from xmodule.partitions.partitions import Group from xmodule.partitions.partitions import Group
...@@ -231,6 +233,56 @@ class ContentGroupConfigurationTest(StudioCourseTest): ...@@ -231,6 +233,56 @@ class ContentGroupConfigurationTest(StudioCourseTest):
self.outline_page.wait_for_page() self.outline_page.wait_for_page()
@attr(shard=5)
class EnrollmentTrackModeTest(StudioCourseTest):
def setUp(self, is_staff=True, test_xss=True):
super(EnrollmentTrackModeTest, self).setUp(is_staff=is_staff)
self.audit_track = "Audit"
self.verified_track = "Verified"
self.staff_user = self.user
def test_all_course_modes_present(self):
"""
This test is meant to ensure that all the course modes show up as groups
on the Group configuration page within the Enrollment Tracks section.
It also checks to make sure that the edit buttons are not available.
"""
add_enrollment_course_modes(self.browser, self.course_id, ['audit', 'verified'])
group_configurations_page = GroupConfigurationsPage(
self.browser,
self.course_info['org'],
self.course_info['number'],
self.course_info['run']
)
group_configurations_page.visit()
self.assertTrue(group_configurations_page.enrollment_track_section_present)
# Make sure the edit buttons are not available.
self.assertFalse(group_configurations_page.enrollment_track_edit_present)
groups = group_configurations_page.get_enrollment_groups()
for g in [self.audit_track, self.verified_track]:
self.assertTrue(g in groups)
def test_one_course_mode(self):
"""
The purpose of this test is to ensure that when there is 1 or fewer course modes
the enrollment track section is not shown.
"""
add_enrollment_course_modes(self.browser, self.course_id, ['audit'])
group_configurations_page = GroupConfigurationsPage(
self.browser,
self.course_info['org'],
self.course_info['number'],
self.course_info['run']
)
group_configurations_page.visit()
self.assertFalse(group_configurations_page.enrollment_track_section_present)
groups = group_configurations_page.get_enrollment_groups()
self.assertEqual(len(groups), 0)
@attr(shard=8) @attr(shard=8)
class AdvancedSettingsValidationTest(StudioCourseTest): class AdvancedSettingsValidationTest(StudioCourseTest):
""" """
......
...@@ -39,6 +39,7 @@ login = getting_started/index.html ...@@ -39,6 +39,7 @@ login = getting_started/index.html
register = getting_started/index.html register = getting_started/index.html
content_libraries = course_components/libraries.html content_libraries = course_components/libraries.html
content_groups = course_features/cohorts/cohorted_courseware.html content_groups = course_features/cohorts/cohorted_courseware.html
enrollment_tracks = course_features/cohorts/cohorted_courseware.html
group_configurations = course_features/content_experiments/content_experiments_configure.html#set-up-group-configurations-in-edx-studio group_configurations = course_features/content_experiments/content_experiments_configure.html#set-up-group-configurations-in-edx-studio
container = developing_course/course_components.html#components-that-contain-other-components container = developing_course/course_components.html#components-that-contain-other-components
video = video/video_uploads.html video = video/video_uploads.html
......
...@@ -135,6 +135,7 @@ ...@@ -135,6 +135,7 @@
.entry-title { .entry-title {
padding-bottom: 8px; padding-bottom: 8px;
margin-bottom: 22px; margin-bottom: 22px;
margin-top: 0;
border-bottom: 1px solid $light-gray; border-bottom: 1px solid $light-gray;
font-size: 1.6em; font-size: 1.6em;
font-weight: bold; font-weight: bold;
......
...@@ -45,17 +45,11 @@ class EnrollmentTrackUserPartition(UserPartition): ...@@ -45,17 +45,11 @@ class EnrollmentTrackUserPartition(UserPartition):
for mode in CourseMode.modes_for_course(course_key, include_expired=True, only_selectable=False) for mode in CourseMode.modes_for_course(course_key, include_expired=True, only_selectable=False)
] ]
def to_json(self):
"""
Because this partition is dynamic, to_json and from_json are not supported.
Calling this method will raise a TypeError.
"""
raise TypeError("Because EnrollmentTrackUserPartition is a dynamic partition, 'to_json' is not supported.")
def from_json(self): def from_json(self):
""" """
Because this partition is dynamic, to_json and from_json are not supported. Because this partition is dynamic, `from_json` is not supported.
`to_json` is supported, but shouldn't be used to persist this partition
within the course itself (used by Studio for sending data to front-end code)
Calling this method will raise a TypeError. Calling this method will raise a TypeError.
""" """
...@@ -116,7 +110,7 @@ class EnrollmentTrackPartitionScheme(object): ...@@ -116,7 +110,7 @@ class EnrollmentTrackPartitionScheme(object):
Any group access rule referencing inactive partitions will be ignored Any group access rule referencing inactive partitions will be ignored
when performing access checks. when performing access checks.
""" """
return EnrollmentTrackUserPartition(id, name, description, [], cls, parameters, active) return EnrollmentTrackUserPartition(id, unicode(name), unicode(description), [], cls, parameters, active)
def is_course_using_cohort_instead(course_key): def is_course_using_cohort_instead(course_key):
......
...@@ -53,10 +53,11 @@ class EnrollmentTrackUserPartitionTest(SharedModuleStoreTestCase): ...@@ -53,10 +53,11 @@ class EnrollmentTrackUserPartitionTest(SharedModuleStoreTestCase):
self.assertIsNotNone(self.get_group_by_name(partition, "Verified Enrollment Track")) self.assertIsNotNone(self.get_group_by_name(partition, "Verified Enrollment Track"))
self.assertIsNotNone(self.get_group_by_name(partition, "Credit Mode")) self.assertIsNotNone(self.get_group_by_name(partition, "Credit Mode"))
def test_to_json_not_supported(self): def test_to_json_supported(self):
user_partition = create_enrollment_track_partition(self.course) user_partition_json = create_enrollment_track_partition(self.course).to_json()
with self.assertRaises(TypeError): self.assertEqual('Test Enrollment Track Partition', user_partition_json['name'])
user_partition.to_json() self.assertEqual('enrollment_track', user_partition_json['scheme'])
self.assertEqual('Test partition for segmenting users by enrollment track', user_partition_json['description'])
def test_from_json_not_supported(self): def test_from_json_not_supported(self):
with self.assertRaises(TypeError): with self.assertRaises(TypeError):
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment