Skip to content

Commit 59c6218

Browse files
Albert St. Aubincahrens
Albert St. Aubin
authored and
cahrens
committedApr 19, 2017
Show Enrollment Tracks in Group Configurations.
TNL-6743

32 files changed

+524
-273
lines changed
 

‎cms/djangoapps/contentstore/course_group_config.py

+45-34
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,16 @@
88

99
from django.utils.translation import ugettext as _
1010
from contentstore.utils import reverse_usage_url
11+
from openedx.core.djangoapps.course_groups.partition_scheme import get_cohorted_user_partition
1112
from xmodule.partitions.partitions import UserPartition, MINIMUM_STATIC_PARTITION_ID
1213
from xmodule.partitions.partitions_service import get_all_partitions_for_course
1314
from xmodule.split_test_module import get_split_user_partitions
14-
from openedx.core.djangoapps.course_groups.partition_scheme import get_cohorted_user_partition
1515

1616
MINIMUM_GROUP_ID = MINIMUM_STATIC_PARTITION_ID
1717

1818
RANDOM_SCHEME = "random"
1919
COHORT_SCHEME = "cohort"
20+
ENROLLMENT_SCHEME = "enrollment_track"
2021

2122
CONTENT_GROUP_CONFIGURATION_DESCRIPTION = _(
2223
'The groups in this configuration can be mapped to cohorts in the Instructor Dashboard.'
@@ -187,21 +188,10 @@ def _get_content_experiment_usage_info(store, course, split_tests): # pylint: d
187188
return usage_info
188189

189190
@staticmethod
190-
def get_content_groups_usage_info(store, course):
191-
"""
192-
Get usage information for content groups.
193-
"""
194-
items = store.get_items(course.id, settings={'group_access': {'$exists': True}}, include_orphans=False)
195-
196-
return GroupConfiguration._get_content_groups_usage_info(course, items)
197-
198-
@staticmethod
199-
def _get_content_groups_usage_info(course, items):
191+
def get_partitions_usage_info(store, course):
200192
"""
201193
Returns all units names and their urls.
202194
203-
This will return only groups for the cohort user partition.
204-
205195
Returns:
206196
{'group_id':
207197
[
@@ -216,8 +206,10 @@ def _get_content_groups_usage_info(course, items):
216206
],
217207
}
218208
"""
209+
items = store.get_items(course.id, settings={'group_access': {'$exists': True}}, include_orphans=False)
210+
219211
usage_info = {}
220-
for item, group_id in GroupConfiguration._iterate_items_and_content_group_ids(course, items):
212+
for item, group_id in GroupConfiguration._iterate_items_and_group_ids(course, items):
221213
if group_id not in usage_info:
222214
usage_info[group_id] = []
223215

@@ -267,7 +259,7 @@ def _get_content_groups_items_usage_info(course, items):
267259
}
268260
"""
269261
usage_info = {}
270-
for item, group_id in GroupConfiguration._iterate_items_and_content_group_ids(course, items):
262+
for item, group_id in GroupConfiguration._iterate_items_and_group_ids(course, items):
271263
if group_id not in usage_info:
272264
usage_info[group_id] = []
273265

@@ -282,22 +274,23 @@ def _get_content_groups_items_usage_info(course, items):
282274
return usage_info
283275

284276
@staticmethod
285-
def _iterate_items_and_content_group_ids(course, items):
277+
def _iterate_items_and_group_ids(course, items):
286278
"""
287-
Iterate through items and content group IDs in a course.
279+
Iterate through items and group IDs in a course.
288280
289-
This will yield group IDs *only* for cohort user partitions.
281+
This will yield group IDs for all user partitions except those with a scheme of random.
290282
291283
Yields: tuple of (item, group_id)
292284
"""
293-
content_group_configuration = get_cohorted_user_partition(course)
294-
if content_group_configuration is not None:
295-
for item in items:
296-
if hasattr(item, 'group_access') and item.group_access:
297-
group_ids = item.group_access.get(content_group_configuration.id, [])
285+
all_partitions = get_all_partitions_for_course(course)
286+
for config in all_partitions:
287+
if config is not None and config.scheme.name != RANDOM_SCHEME:
288+
for item in items:
289+
if hasattr(item, 'group_access') and item.group_access:
290+
group_ids = item.group_access.get(config.id, [])
298291

299-
for group_id in group_ids:
300-
yield item, group_id
292+
for group_id in group_ids:
293+
yield item, group_id
301294

302295
@staticmethod
303296
def update_usage_info(store, course, configuration):
@@ -319,23 +312,23 @@ def update_usage_info(store, course, configuration):
319312
configuration_json['usage'] = usage_information.get(configuration.id, [])
320313
elif configuration.scheme.name == COHORT_SCHEME:
321314
# In case if scheme is "cohort"
322-
configuration_json = GroupConfiguration.update_content_group_usage_info(store, course, configuration)
315+
configuration_json = GroupConfiguration.update_partition_usage_info(store, course, configuration)
323316
return configuration_json
324317

325318
@staticmethod
326-
def update_content_group_usage_info(store, course, configuration):
319+
def update_partition_usage_info(store, course, configuration):
327320
"""
328-
Update usage information for particular Content Group Configuration.
321+
Update usage information for particular Partition Configuration.
329322
330-
Returns json of particular content group configuration updated with usage information.
323+
Returns json of particular partition configuration updated with usage information.
331324
"""
332-
usage_info = GroupConfiguration.get_content_groups_usage_info(store, course)
333-
content_group_configuration = configuration.to_json()
325+
usage_info = GroupConfiguration.get_partitions_usage_info(store, course)
326+
partition_configuration = configuration.to_json()
334327

335-
for group in content_group_configuration['groups']:
328+
for group in partition_configuration['groups']:
336329
group['usage'] = usage_info.get(group['id'], [])
337330

338-
return content_group_configuration
331+
return partition_configuration
339332

340333
@staticmethod
341334
def get_or_create_content_group(store, course):
@@ -357,9 +350,27 @@ def get_or_create_content_group(store, course):
357350
)
358351
return content_group_configuration.to_json()
359352

360-
content_group_configuration = GroupConfiguration.update_content_group_usage_info(
353+
content_group_configuration = GroupConfiguration.update_partition_usage_info(
361354
store,
362355
course,
363356
content_group_configuration
364357
)
365358
return content_group_configuration
359+
360+
@staticmethod
361+
def get_all_user_partition_details(store, course):
362+
"""
363+
Returns all the available partitions with updated usage information
364+
365+
:return: list of all partitions available with details
366+
"""
367+
all_partitions = get_all_partitions_for_course(course)
368+
all_updated_partitions = []
369+
for partition in all_partitions:
370+
configuration = GroupConfiguration.update_partition_usage_info(
371+
store,
372+
course,
373+
partition
374+
)
375+
all_updated_partitions.append(configuration)
376+
return all_updated_partitions

‎cms/djangoapps/contentstore/courseware_index.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -377,7 +377,7 @@ def do_course_reindex(cls, modulestore, course_key):
377377
@classmethod
378378
def fetch_group_usage(cls, modulestore, structure):
379379
groups_usage_dict = {}
380-
groups_usage_info = GroupConfiguration.get_content_groups_usage_info(modulestore, structure).items()
380+
groups_usage_info = GroupConfiguration.get_partitions_usage_info(modulestore, structure).items()
381381
groups_usage_info.extend(
382382
GroupConfiguration.get_content_groups_items_usage_info(
383383
modulestore,

‎cms/djangoapps/contentstore/views/course.py

+22-4
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
from ccx_keys.locator import CCXLocator
3131
from contentstore.course_group_config import (
3232
COHORT_SCHEME,
33+
ENROLLMENT_SCHEME,
3334
GroupConfiguration,
3435
GroupConfigurationsValidationError,
3536
RANDOM_SCHEME,
@@ -99,7 +100,6 @@
99100
from xmodule.modulestore.exceptions import ItemNotFoundError, DuplicateCourseError
100101
from xmodule.tabs import CourseTab, CourseTabList, InvalidTabsException
101102

102-
103103
log = logging.getLogger(__name__)
104104

105105
__all__ = ['course_info_handler', 'course_handler', 'course_listing',
@@ -1473,7 +1473,7 @@ def remove_content_or_experiment_group(request, store, course, configuration, gr
14731473
return JsonResponse(status=404)
14741474

14751475
group_id = int(group_id)
1476-
usages = GroupConfiguration.get_content_groups_usage_info(store, course)
1476+
usages = GroupConfiguration.get_partitions_usage_info(store, course)
14771477
used = group_id in usages
14781478

14791479
if used:
@@ -1521,15 +1521,33 @@ def group_configurations_list_handler(request, course_key_string):
15211521
else:
15221522
experiment_group_configurations = None
15231523

1524-
content_group_configuration = GroupConfiguration.get_or_create_content_group(store, course)
1524+
all_partitions = GroupConfiguration.get_all_user_partition_details(store, course)
1525+
should_show_enrollment_track = False
1526+
group_schemes = []
1527+
for partition in all_partitions:
1528+
group_schemes.append(partition['scheme'])
1529+
if partition['scheme'] == ENROLLMENT_SCHEME:
1530+
enrollment_track_configuration = partition
1531+
should_show_enrollment_track = len(enrollment_track_configuration['groups']) > 1
1532+
1533+
# Remove the enrollment track partition and add it to the front of the list if it should be shown.
1534+
all_partitions.remove(partition)
1535+
if should_show_enrollment_track:
1536+
all_partitions.insert(0, partition)
1537+
1538+
# Add empty content group if there is no COHORT User Partition in the list.
1539+
# This will add ability to add new groups in the view.
1540+
if COHORT_SCHEME not in group_schemes:
1541+
all_partitions.append(GroupConfiguration.get_or_create_content_group(store, course))
15251542

15261543
return render_to_response('group_configurations.html', {
15271544
'context_course': course,
15281545
'group_configuration_url': group_configuration_url,
15291546
'course_outline_url': course_outline_url,
15301547
'experiment_group_configurations': experiment_group_configurations,
15311548
'should_show_experiment_groups': should_show_experiment_groups,
1532-
'content_group_configuration': content_group_configuration
1549+
'all_group_configurations': all_partitions,
1550+
'should_show_enrollment_track': should_show_enrollment_track
15331551
})
15341552
elif "application/json" in request.META.get('HTTP_ACCEPT'):
15351553
if request.method == 'POST':

‎cms/djangoapps/contentstore/views/tests/test_group_configurations.py

+15-6
Original file line numberDiff line numberDiff line change
@@ -613,6 +613,15 @@ class GroupConfigurationsUsageInfoTestCase(CourseTestCase, HelperMethods):
613613
def setUp(self):
614614
super(GroupConfigurationsUsageInfoTestCase, self).setUp()
615615

616+
def _get_user_partition(self, scheme):
617+
"""
618+
Returns the first user partition with the specified scheme.
619+
"""
620+
for group in GroupConfiguration.get_all_user_partition_details(self.store, self.course):
621+
if group['scheme'] == scheme:
622+
return group
623+
return None
624+
616625
def _get_expected_content_group(self, usage_for_group):
617626
"""
618627
Returns the expected configuration with particular usage.
@@ -637,7 +646,7 @@ def test_content_group_not_used(self):
637646
Test that right data structure will be created if content group is not used.
638647
"""
639648
self._add_user_partitions(scheme_id='cohort')
640-
actual = GroupConfiguration.get_or_create_content_group(self.store, self.course)
649+
actual = self._get_user_partition('cohort')
641650
expected = self._get_expected_content_group(usage_for_group=[])
642651
self.assertEqual(actual, expected)
643652

@@ -650,7 +659,7 @@ def test_can_get_correct_usage_info_when_special_characters_are_in_content(self)
650659
cid=0, group_id=1, name_suffix='0', special_characters=u"JOSÉ ANDRÉS"
651660
)
652661

653-
actual = GroupConfiguration.get_or_create_content_group(self.store, self.course)
662+
actual = self._get_user_partition('cohort')
654663
expected = self._get_expected_content_group(
655664
usage_for_group=[
656665
{
@@ -669,7 +678,7 @@ def test_can_get_correct_usage_info_for_content_groups(self):
669678
self._add_user_partitions(count=1, scheme_id='cohort')
670679
vertical, __ = self._create_problem_with_content_group(cid=0, group_id=1, name_suffix='0')
671680

672-
actual = GroupConfiguration.get_or_create_content_group(self.store, self.course)
681+
actual = self._get_user_partition('cohort')
673682

674683
expected = self._get_expected_content_group(usage_for_group=[
675684
{
@@ -706,7 +715,7 @@ def test_can_get_correct_usage_info_with_orphan(self, module_store_type):
706715
expected = self._get_expected_content_group(usage_for_group=[])
707716

708717
# Get the actual content group information
709-
actual = GroupConfiguration.get_or_create_content_group(self.store, self.course)
718+
actual = self._get_user_partition('cohort')
710719

711720
# Assert that actual content group information is same as expected one.
712721
self.assertEqual(actual, expected)
@@ -720,7 +729,7 @@ def test_can_use_one_content_group_in_multiple_problems(self):
720729
vertical, __ = self._create_problem_with_content_group(cid=0, group_id=1, name_suffix='0')
721730
vertical1, __ = self._create_problem_with_content_group(cid=0, group_id=1, name_suffix='1')
722731

723-
actual = GroupConfiguration.get_or_create_content_group(self.store, self.course)
732+
actual = self._get_user_partition('cohort')
724733

725734
expected = self._get_expected_content_group(usage_for_group=[
726735
{
@@ -927,7 +936,7 @@ def test_can_handle_multiple_partitions(self):
927936

928937
# This used to cause an exception since the code assumed that
929938
# only one partition would be available.
930-
actual = GroupConfiguration.get_content_groups_usage_info(self.store, self.course)
939+
actual = GroupConfiguration.get_partitions_usage_info(self.store, self.course)
931940
self.assertEqual(actual.keys(), [0])
932941

933942
actual = GroupConfiguration.get_content_groups_items_usage_info(self.store, self.course)

‎cms/djangoapps/contentstore/views/tests/test_item.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -372,7 +372,7 @@ def test_get_user_partitions_and_groups(self):
372372
self.assertEqual(result["user_partitions"], [
373373
{
374374
"id": ENROLLMENT_TRACK_PARTITION_ID,
375-
"name": "Enrollment Tracks",
375+
"name": "Enrollment Track Groups",
376376
"scheme": "enrollment_track",
377377
"groups": [
378378
{

‎cms/lib/xblock/test/test_authoring_mixin.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ class AuthoringMixinTestCase(ModuleStoreTestCase):
2222
NO_CONTENT_ENROLLMENT_TRACK_ENABLED = "specific groups of learners based either on their enrollment track, or by content groups that you create"
2323
NO_CONTENT_ENROLLMENT_TRACK_DISABLED = "specific groups of learners based on content groups that you create"
2424
CONTENT_GROUPS_TITLE = "Content Groups"
25-
ENROLLMENT_GROUPS_TITLE = "Enrollment Tracks"
25+
ENROLLMENT_GROUPS_TITLE = "Enrollment Track Groups"
2626
STAFF_LOCKED = 'The unit that contains this component is hidden from learners'
2727

2828
FEATURES_WITH_ENROLLMENT_TRACK_DISABLED = settings.FEATURES.copy()

‎cms/static/js/factories/group_configurations.js

+17-8
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,33 @@ define([
22
'js/collections/group_configuration', 'js/models/group_configuration', 'js/views/pages/group_configurations'
33
], function(GroupConfigurationCollection, GroupConfigurationModel, GroupConfigurationsPage) {
44
'use strict';
5-
return function(experimentsEnabled, experimentGroupConfigurationsJson, contentGroupConfigurationJson,
6-
groupConfigurationUrl, courseOutlineUrl) {
5+
return function(experimentsEnabled,
6+
experimentGroupConfigurationsJson,
7+
allGroupConfigurationJson,
8+
groupConfigurationUrl,
9+
courseOutlineUrl) {
710
var experimentGroupConfigurations = new GroupConfigurationCollection(
811
experimentGroupConfigurationsJson, {parse: true}
912
),
10-
contentGroupConfiguration = new GroupConfigurationModel(contentGroupConfigurationJson, {
11-
parse: true, canBeEmpty: true
12-
});
13+
allGroupConfigurations = [],
14+
newGroupConfig,
15+
i;
16+
17+
for (i = 0; i < allGroupConfigurationJson.length; i++) {
18+
newGroupConfig = new GroupConfigurationModel(allGroupConfigurationJson[i],
19+
{parse: true, canBeEmpty: true});
20+
newGroupConfig.urlRoot = groupConfigurationUrl;
21+
newGroupConfig.outlineUrl = courseOutlineUrl;
22+
allGroupConfigurations.push(newGroupConfig);
23+
}
1324

1425
experimentGroupConfigurations.url = groupConfigurationUrl;
1526
experimentGroupConfigurations.outlineUrl = courseOutlineUrl;
16-
contentGroupConfiguration.urlRoot = groupConfigurationUrl;
17-
contentGroupConfiguration.outlineUrl = courseOutlineUrl;
1827
new GroupConfigurationsPage({
1928
el: $('#content'),
2029
experimentsEnabled: experimentsEnabled,
2130
experimentGroupConfigurations: experimentGroupConfigurations,
22-
contentGroupConfiguration: contentGroupConfiguration
31+
allGroupConfigurations: allGroupConfigurations
2332
}).render();
2433
};
2534
});

‎cms/static/js/spec/views/group_configuration_spec.js

+15-15
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@ define([
33
'common/js/spec_helpers/view_helpers', 'js/models/course', 'js/models/group_configuration', 'js/models/group',
44
'js/collections/group_configuration', 'js/collections/group', 'js/views/group_configuration_details',
55
'js/views/group_configurations_list', 'js/views/group_configuration_editor', 'js/views/group_configuration_item',
6-
'js/views/experiment_group_edit', 'js/views/content_group_list', 'js/views/content_group_details',
7-
'js/views/content_group_editor', 'js/views/content_group_item'
6+
'js/views/experiment_group_edit', 'js/views/partition_group_list', 'js/views/partition_group_details',
7+
'js/views/content_group_editor', 'js/views/partition_group_item'
88
], function(
99
_, AjaxHelpers, TemplateHelpers, ViewHelpers, Course, GroupConfigurationModel, GroupModel,
1010
GroupConfigurationCollection, GroupCollection, GroupConfigurationDetailsView, GroupConfigurationsListView,
1111
GroupConfigurationEditorView, GroupConfigurationItemView, ExperimentGroupEditView, GroupList,
12-
ContentGroupDetailsView, ContentGroupEditorView, ContentGroupItemView
12+
PartitionGroupDetailsView, ContentGroupEditorView, PartitionGroupItemView
1313
) {
1414
'use strict';
1515
var SELECTORS = {
@@ -675,7 +675,7 @@ define([
675675
verifyEditingGroup, respondToSave, expectGroupsVisible, correctValidationError;
676676

677677
scopedGroupSelector = function(groupIndex, additionalSelectors) {
678-
var groupSelector = '.content-groups-list-item-' + groupIndex;
678+
var groupSelector = '.partition-groups-list-item-' + groupIndex;
679679
if (additionalSelectors) {
680680
return groupSelector + ' ' + additionalSelectors;
681681
} else {
@@ -775,13 +775,13 @@ define([
775775

776776
expectGroupsVisible = function(view, groupNames) {
777777
_.each(groupNames, function(groupName) {
778-
expect(view.$('.content-groups-list-item')).toContainText(groupName);
778+
expect(view.$('.partition-groups-list-item')).toContainText(groupName);
779779
});
780780
};
781781

782782
beforeEach(function() {
783783
TemplateHelpers.installTemplates(
784-
['content-group-editor', 'content-group-details', 'list']
784+
['content-group-editor', 'partition-group-details', 'list']
785785
);
786786
});
787787

@@ -792,7 +792,7 @@ define([
792792

793793
it('can render groups', function() {
794794
var groupNames = ['Group 1', 'Group 2', 'Group 3'];
795-
renderView(groupNames).$('.content-group-details').each(function(index) {
795+
renderView(groupNames).$('.partition-group-details').each(function(index) {
796796
expect($(this)).toContainText(groupNames[index]);
797797
});
798798
});
@@ -874,7 +874,7 @@ define([
874874

875875
describe('Content groups details view', function() {
876876
beforeEach(function() {
877-
TemplateHelpers.installTemplate('content-group-details', true);
877+
TemplateHelpers.installTemplate('partition-group-details', true);
878878
this.model = new GroupModel({name: 'Content Group', id: 0, courseOutlineUrl: 'CourseOutlineUrl'});
879879

880880
var saveableModel = new GroupConfigurationModel({
@@ -889,7 +889,7 @@ define([
889889
this.collection = new GroupConfigurationCollection([saveableModel]);
890890
this.collection.outlineUrl = '/outline';
891891

892-
this.view = new ContentGroupDetailsView({
892+
this.view = new PartitionGroupDetailsView({
893893
model: this.model
894894
});
895895
appendSetFixtures(this.view.render().el);
@@ -901,7 +901,7 @@ define([
901901

902902
it('should show empty usage appropriately', function() {
903903
this.view.$('.show-groups').click();
904-
assertShowEmptyUsages(this.view, 'This content group is not in use. ');
904+
assertShowEmptyUsages(this.view, "Use this group to control a component's visibility in the ");
905905
});
906906

907907
it('should hide empty usage appropriately', function() {
@@ -915,7 +915,7 @@ define([
915915

916916
assertShowNonEmptyUsages(
917917
this.view,
918-
'This content group is used in:',
918+
'This group controls visibility of:',
919919
'Cannot delete when in use by a unit'
920920
);
921921
});
@@ -1015,7 +1015,7 @@ define([
10151015
describe('Content group controller view', function() {
10161016
beforeEach(function() {
10171017
TemplateHelpers.installTemplates([
1018-
'content-group-editor', 'content-group-details'
1018+
'content-group-editor', 'partition-group-details'
10191019
], true);
10201020

10211021
this.model = new GroupModel({name: 'Content Group', id: 0, courseOutlineUrl: 'CourseOutlineUrl'});
@@ -1029,14 +1029,14 @@ define([
10291029
this.saveableModel.urlRoot = '/group_configurations';
10301030
this.collection = new GroupConfigurationCollection([this.saveableModel]);
10311031
this.collection.url = '/group_configurations';
1032-
this.view = new ContentGroupItemView({
1032+
this.view = new PartitionGroupItemView({
10331033
model: this.model
10341034
});
10351035
appendSetFixtures(this.view.render().el);
10361036
});
10371037

10381038
it('should render properly', function() {
1039-
assertControllerView(this.view, '.content-group-details', '.content-group-edit');
1039+
assertControllerView(this.view, '.partition-group-details', '.content-group-edit');
10401040
});
10411041

10421042
it('should destroy itself on confirmation of deleting', function() {
@@ -1047,7 +1047,7 @@ define([
10471047
assertAndDeleteItemWithError(
10481048
this,
10491049
'/group_configurations/0/0',
1050-
'.content-groups-list-item',
1050+
'.partition-groups-list-item',
10511051
'Delete this content group'
10521052
);
10531053
});

‎cms/static/js/spec/views/pages/group_configurations_spec.js

+3-4
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,8 @@ define([
1919
name: 'Configuration 1',
2020
courseOutlineUrl: 'CourseOutlineUrl'
2121
}),
22-
contentGroupConfiguration: new GroupConfigurationModel({groups: []})
22+
allGroupConfigurations: [new GroupConfigurationModel({groups: []})]
2323
});
24-
2524
if (!disableSpy) {
2625
spyOn(view, 'addWindowActions');
2726
}
@@ -36,7 +35,7 @@ define([
3635
beforeEach(function() {
3736
setFixtures(mockGroupConfigurationsPage);
3837
TemplateHelpers.installTemplates([
39-
'group-configuration-editor', 'group-configuration-details', 'content-group-details',
38+
'group-configuration-editor', 'group-configuration-details', 'partition-group-details',
4039
'content-group-editor', 'group-edit', 'list'
4140
]);
4241

@@ -116,7 +115,7 @@ define([
116115
});
117116

118117
it('should show a notification message if a content group is changed', function() {
119-
this.view.contentGroupConfiguration.get('groups').add({id: 0, name: 'Content Group'});
118+
this.view.allGroupConfigurations[0].get('groups').add({id: 0, name: 'Content Group'});
120119
expect(this.view.onBeforeUnload())
121120
.toBe('You have unsaved changes. Do you really want to leave this page?');
122121
});

‎cms/static/js/views/content_group_list.js

-28
This file was deleted.

‎cms/static/js/views/list.js

+7-2
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
* of items this list contains. For example, 'Group Configuration'.
1212
* Note that it must be translated.
1313
* - emptyMessage (string): Text to render when the list is empty.
14+
* - restrictEditing (bool) : Boolean flag for hiding edit and remove options, defaults to false.
1415
*/
1516
define([
1617
'js/views/baseview'
@@ -25,6 +26,7 @@ define([
2526
listContainerCss: '.list-items',
2627

2728
initialize: function() {
29+
this.restrictEditing = this.options.restrictEditing || false;
2830
this.listenTo(this.collection, 'add', this.addNewItemView);
2931
this.listenTo(this.collection, 'remove', this.onRemoveItem);
3032
this.template = this.loadTemplate('list');
@@ -42,11 +44,14 @@ define([
4244
emptyMessage: this.emptyMessage,
4345
length: this.collection.length,
4446
isEditing: model && model.get('editing'),
45-
canCreateNewItem: this.canCreateItem(this.collection)
47+
canCreateNewItem: this.canCreateItem(this.collection),
48+
restrictEditing: this.restrictEditing
4649
}));
4750

4851
this.collection.each(function(model) {
49-
this.$(this.listContainerCss).append(this.createItemView({model: model}).render().el);
52+
this.$(this.listContainerCss).append(
53+
this.createItemView({model: model, restrictEditing: this.restrictEditing}).render().el
54+
);
5055
}, this);
5156

5257
return this;

‎cms/static/js/views/list_item.js

+1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ define([
2222
canDelete: false,
2323

2424
initialize: function() {
25+
this.restrictEditing = this.options.restrictEditing || false;
2526
this.listenTo(this.model, 'change:editing', this.render);
2627
this.listenTo(this.model, 'remove', this.remove);
2728
},

‎cms/static/js/views/pages/group_configurations.js

+44-9
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
define([
22
'jquery', 'underscore', 'gettext', 'js/views/pages/base_page',
3-
'js/views/group_configurations_list', 'js/views/content_group_list'
3+
'js/views/group_configurations_list', 'js/views/partition_group_list'
44
],
5-
function($, _, gettext, BasePage, GroupConfigurationsListView, ContentGroupListView) {
5+
function($, _, gettext, BasePage, GroupConfigurationsListView, PartitionGroupListView) {
66
'use strict';
77
var GroupConfigurationsPage = BasePage.extend({
88
initialize: function(options) {
9+
var currentScheme,
10+
i,
11+
enrollmentScheme = 'enrollment_track';
12+
913
BasePage.prototype.initialize.call(this);
1014
this.experimentsEnabled = options.experimentsEnabled;
1115
if (this.experimentsEnabled) {
@@ -14,18 +18,35 @@ function($, _, gettext, BasePage, GroupConfigurationsListView, ContentGroupListV
1418
collection: this.experimentGroupConfigurations
1519
});
1620
}
17-
this.contentGroupConfiguration = options.contentGroupConfiguration;
18-
this.cohortGroupsListView = new ContentGroupListView({
19-
collection: this.contentGroupConfiguration.get('groups')
20-
});
21+
22+
this.allGroupConfigurations = options.allGroupConfigurations || [];
23+
this.allGroupViewList = [];
24+
for (i = 0; i < this.allGroupConfigurations.length; i++) {
25+
currentScheme = this.allGroupConfigurations[i].get('scheme');
26+
this.allGroupViewList.push(
27+
new PartitionGroupListView({
28+
collection: this.allGroupConfigurations[i].get('groups'),
29+
restrictEditing: currentScheme === enrollmentScheme,
30+
scheme: currentScheme
31+
})
32+
);
33+
}
2134
},
2235

2336
renderPage: function() {
24-
var hash = this.getLocationHash();
37+
var hash = this.getLocationHash(),
38+
i,
39+
currentClass;
2540
if (this.experimentsEnabled) {
2641
this.$('.wrapper-groups.experiment-groups').append(this.experimentGroupsListView.render().el);
2742
}
28-
this.$('.wrapper-groups.content-groups').append(this.cohortGroupsListView.render().el);
43+
44+
// Render the remaining Configuration groups
45+
for (i = 0; i < this.allGroupViewList.length; i++) {
46+
currentClass = '.wrapper-groups.content-groups.' + this.allGroupViewList[i].scheme;
47+
this.$(currentClass).append(this.allGroupViewList[i].render().el);
48+
}
49+
2950
this.addWindowActions();
3051
if (hash) {
3152
// Strip leading '#' to get id string to match
@@ -38,8 +59,22 @@ function($, _, gettext, BasePage, GroupConfigurationsListView, ContentGroupListV
3859
$(window).on('beforeunload', this.onBeforeUnload.bind(this));
3960
},
4061

62+
/**
63+
* Checks the Partition Group Configurations to see if the isDirty bit is set
64+
* @returns {boolean} True if any partition group has the dirty bit set.
65+
*/
66+
areAnyConfigurationsDirty: function() {
67+
var i;
68+
for (i = 0; i < this.allGroupConfigurations.length; i++) {
69+
if (this.allGroupConfigurations[i].isDirty()) {
70+
return true;
71+
}
72+
}
73+
return false;
74+
},
75+
4176
onBeforeUnload: function() {
42-
var dirty = this.contentGroupConfiguration.isDirty() ||
77+
var dirty = this.areAnyConfigurationsDirty() ||
4378
(this.experimentsEnabled && this.experimentGroupConfigurations.find(function(configuration) {
4479
return configuration.isDirty();
4580
}));

‎cms/static/js/views/content_group_details.js ‎cms/static/js/views/partition_group_details.js

+11-9
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* This class defines a simple display view for a content group.
2+
* This class defines a simple display view for a partition group.
33
* It is expected to be backed by a Group model.
44
*/
55
define([
@@ -8,7 +8,7 @@ define([
88
], function(BaseView, _, gettext, str, StringUtils, HtmlUtils) {
99
'use strict';
1010

11-
var ContentGroupDetailsView = BaseView.extend({
11+
var PartitionGroupDetailsView = BaseView.extend({
1212
tagName: 'div',
1313
events: {
1414
'click .edit': 'editGroup',
@@ -21,17 +21,18 @@ define([
2121

2222
return [
2323
'collection',
24-
'content-group-details',
25-
'content-group-details-' + index
24+
'partition-group-details',
25+
'partition-group-details-' + index
2626
].join(' ');
2727
},
2828

2929
editGroup: function() {
30-
this.model.set({'editing': true});
30+
this.model.set({editing: true});
3131
},
3232

3333
initialize: function() {
34-
this.template = this.loadTemplate('content-group-details');
34+
this.template = this.loadTemplate('partition-group-details');
35+
this.restrictEditing = this.options.restrictEditing || false;
3536
this.listenTo(this.model, 'change', this.render);
3637
},
3738

@@ -41,9 +42,10 @@ define([
4142
courseOutlineUrl: this.model.collection.parents[0].outlineUrl,
4243
index: this.model.collection.indexOf(this.model),
4344
showContentGroupUsages: showContentGroupUsages || false,
44-
HtmlUtils: HtmlUtils
45+
HtmlUtils: HtmlUtils,
46+
restrictEditing: this.restrictEditing
4547
});
46-
this.$el.html(this.template(attrs));
48+
HtmlUtils.setHtml(this.$el, HtmlUtils.HTML(this.template(attrs)));
4749
return this;
4850
},
4951

@@ -78,5 +80,5 @@ define([
7880
}
7981
});
8082

81-
return ContentGroupDetailsView;
83+
return PartitionGroupDetailsView;
8284
});
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,32 @@
11
/**
2-
* This class defines an controller view for content groups.
2+
* This class defines an controller view for partition groups.
33
* It renders an editor view or a details view depending on the state
44
* of the underlying model.
55
* It is expected to be backed by a Group model.
66
*/
77
define([
8-
'js/views/list_item', 'js/views/content_group_editor', 'js/views/content_group_details', 'gettext', 'common/js/components/utils/view_utils'
9-
], function(ListItemView, ContentGroupEditorView, ContentGroupDetailsView, gettext) {
8+
'js/views/list_item', 'js/views/content_group_editor', 'js/views/partition_group_details',
9+
'gettext', 'common/js/components/utils/view_utils'
10+
], function(ListItemView, ContentGroupEditorView, PartitionGroupDetailsView, gettext) {
1011
'use strict';
1112

12-
var ContentGroupItemView = ListItemView.extend({
13+
var PartitionGroupItemView = ListItemView.extend({
1314
events: {
1415
'click .delete': 'deleteItem'
1516
},
1617

1718
tagName: 'section',
1819

19-
baseClassName: 'content-group',
20+
baseClassName: 'partition-group',
2021

2122
canDelete: true,
2223

2324
itemDisplayName: gettext('content group'),
2425

2526
attributes: function() {
2627
return {
27-
'id': this.model.get('id'),
28-
'tabindex': -1
28+
id: this.model.get('id'),
29+
tabindex: -1
2930
};
3031
},
3132

@@ -34,9 +35,11 @@ define([
3435
},
3536

3637
createDetailsView: function() {
37-
return new ContentGroupDetailsView({model: this.model});
38+
return new PartitionGroupDetailsView({model: this.model,
39+
restrictEditing: this.options.restrictEditing
40+
});
3841
}
3942
});
4043

41-
return ContentGroupItemView;
44+
return PartitionGroupItemView;
4245
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/**
2+
* This class defines a list view for partition groups.
3+
* It is expected to be backed by a Group collection.
4+
*/
5+
define([
6+
'underscore', 'js/views/list', 'js/views/partition_group_item', 'gettext'
7+
], function(_, ListView, PartitionGroupItemView, gettext) {
8+
'use strict';
9+
10+
var PartitionGroupListView = ListView.extend({
11+
initialize: function(options) {
12+
ListView.prototype.initialize.apply(this, [options]);
13+
this.scheme = options.scheme;
14+
},
15+
16+
tagName: 'div',
17+
18+
className: 'partition-group-list',
19+
20+
// Translators: This refers to a content group that can be linked to a student cohort.
21+
itemCategoryDisplayName: gettext('content group'),
22+
23+
newItemMessage: gettext('Add your first content group'),
24+
25+
emptyMessage: gettext('You have not created any content groups yet.'),
26+
27+
createItemView: function(options) {
28+
return new PartitionGroupItemView(_.extend({}, options, {scheme: this.scheme}));
29+
}
30+
});
31+
32+
return PartitionGroupListView;
33+
});

‎cms/templates/group_configurations.html

+45-27
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<%page expression_filter="h"/>
22
<%inherit file="base.html" />
33
<%def name="content_groups_help_token()"><% return "content_groups" %></%def>
4+
<%def name="enrollment_track_help_token()"><% return "enrollment_tracks" %></%def>
45
<%def name="experiment_group_configurations_help_token()"><% return "group_configurations" %></%def>
56
<%namespace name='static' file='static_content.html'/>
67
<%!
@@ -16,7 +17,7 @@
1617
<%block name="bodyclass">is-signedin course view-group-configurations</%block>
1718

1819
<%block name="header_extras">
19-
% for template_name in ["group-configuration-details", "group-configuration-editor", "group-edit", "content-group-editor", "content-group-details", "basic-modal", "modal-button", "list"]:
20+
% for template_name in ["group-configuration-details", "group-configuration-editor", "group-edit", "content-group-editor", "partition-group-details", "basic-modal", "modal-button", "list"]:
2021
<script type="text/template" id="${template_name}-tpl">
2122
<%static:include path="js/${template_name}.underscore" />
2223
</script>
@@ -28,9 +29,10 @@
2829
GroupConfigurationsFactory(
2930
${should_show_experiment_groups | n, dump_js_escaped_json},
3031
${experiment_group_configurations | n, dump_js_escaped_json},
31-
${content_group_configuration | n, dump_js_escaped_json},
32+
${all_group_configurations | n, dump_js_escaped_json},
3233
"${group_configuration_url | n, js_escaped_string}",
33-
"${course_outline_url | n, js_escaped_string}"
34+
"${course_outline_url | n, js_escaped_string}",
35+
${should_show_enrollment_track | n, dump_js_escaped_json}
3436
);
3537
});
3638
</%block>
@@ -47,37 +49,52 @@ <h1 class="page-header">
4749

4850
<div class="wrapper-content wrapper">
4951
<section class="content">
52+
5053
<article class="content-primary" role="main">
51-
<div class="wrapper-groups content-groups">
52-
<h3 class="title">${_("Content Groups")}</h3>
53-
<div class="ui-loading">
54-
<p><span class="spin"><span class="icon fa fa-refresh" aria-hidden="true"></span></span> <span class="copy">${_("Loading")}</span></p>
55-
</div>
56-
</div>
5754

58-
% if should_show_experiment_groups:
59-
<div class="wrapper-groups experiment-groups">
60-
<h3 class="title">${_("Experiment Group Configurations")}</h3>
61-
% if experiment_group_configurations is None:
62-
<div class="notice notice-incontext notice-moduledisabled">
63-
<p class="copy">
64-
${_("This module is disabled at the moment.")}
65-
</p>
66-
</div>
67-
% else:
68-
<div class="ui-loading">
69-
<p><span class="spin"><span class="icon fa fa-refresh" aria-hidden="true"></span></span> <span class="copy">${_("Loading")}</span></p>
70-
</div>
71-
% endif
72-
</div>
73-
% endif
55+
% for config in all_group_configurations:
56+
<div class="wrapper-groups content-groups ${config['scheme']}">
57+
<h3 class="title">${config['name']}</h3>
58+
<div class="ui-loading">
59+
<p><span class="spin"><span class="icon fa fa-refresh" aria-hidden="true"></span></span> <span class="copy">${_("Loading")}</span></p>
60+
</div>
61+
</div>
62+
% endfor
63+
64+
% if should_show_experiment_groups:
65+
<div class="wrapper-groups experiment-groups">
66+
<h3 class="title">${_("Experiment Group Configurations")}</h3>
67+
% if experiment_group_configurations is None:
68+
<div class="notice notice-incontext notice-moduledisabled">
69+
<p class="copy">
70+
${_("This module is disabled at the moment.")}
71+
</p>
72+
</div>
73+
% else:
74+
<div class="ui-loading">
75+
<p><span class="spin"><span class="icon fa fa-refresh" aria-hidden="true"></span></span> <span class="copy">${_("Loading")}</span></p>
76+
</div>
77+
% endif
78+
</div>
79+
% endif
7480
</article>
7581
<aside class="content-supplementary" role="complementary">
82+
% if should_show_enrollment_track:
83+
<div class="bit">
84+
<div class="enrollment-track-doc">
85+
<h3 class="title-3">${_("Enrollment Track Groups")}</h3>
86+
<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>
87+
<p>${_("On unit pages in the course outline, you can designate components as visible only to learners in a specific enrollment track.")}</p>
88+
<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>
89+
<p><a href="${get_online_help_info(enrollment_track_help_token())['doc_url']}" target="_blank" class="button external-help-button">${_("Learn More")}</a></p>
90+
</div>
91+
</div>
92+
% endif
7693
<div class="bit">
7794
<div class="content-groups-doc">
7895
<h3 class="title-3">${_("Content Groups")}</h3>
7996
<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>
80-
<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>
97+
<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>
8198
<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>
8299
<p><a href="${get_online_help_info(content_groups_help_token())['doc_url']}" target="_blank" class="button external-help-button">${_("Learn More")}</a></p>
83100
</div>
@@ -86,12 +103,13 @@ <h3 class="title-3">${_("Content Groups")}</h3>
86103
<div class="bit">
87104
<div class="experiment-groups-doc">
88105
<h3 class="title-3">${_("Experiment Group Configurations")}</h3>
89-
<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>
106+
<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>
90107
<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>
91108
<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>
92109
</div>
93110
</div>
94111
% endif
112+
95113
<div class="bit">
96114
% if context_course:
97115
<%

‎cms/templates/js/list.underscore

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
</div>
1111
<% } else { %>
1212
<div class="list-items"></div>
13-
<% if (!isEditing) { %>
13+
<% if (!isEditing && !restrictEditing) { %>
1414
<button class="action action-add <% if(!canCreateNewItem) {%> action-add-hidden <% }%>" >
1515
<span class="icon fa fa-plus" aria-hidden="true"></span>
1616
<%- interpolate(

‎cms/templates/js/content-group-details.underscore ‎cms/templates/js/partition-group-details.underscore

+26-19
Original file line numberDiff line numberDiff line change
@@ -23,25 +23,29 @@
2323
</ol>
2424

2525
<ul class="actions group-configuration-actions">
26-
<li class="action action-edit">
27-
<button class="edit"><span class="icon fa fa-pencil" aria-hidden="true"></span> <%- gettext("Edit") %></button>
28-
</li>
29-
<% if (_.isEmpty(usage)) { %>
30-
<li class="action action-delete wrapper-delete-button" data-tooltip="<%- gettext('Delete') %>">
31-
<button class="delete action-icon" title="<%- gettext('Delete') %>"><span class="icon fa fa-trash-o" aria-hidden="true"></span></button>
32-
</li>
33-
<% } else { %>
34-
<li class="action action-delete wrapper-delete-button" data-tooltip="<%- gettext('Cannot delete when in use by a unit') %>">
35-
<button class="delete action-icon is-disabled" disabled="disabled" title="<%- gettext('Delete') %>"><span class="icon fa fa-trash-o" aria-hidden="true"></span></button>
26+
<% if (!restrictEditing) { %>
27+
<li class="action action-edit">
28+
<button class="edit"><span class="icon fa fa-pencil" aria-hidden="true"></span> <%- gettext("Edit") %></button>
3629
</li>
30+
<% if (_.isEmpty(usage)) { %>
31+
<li class="action action-delete wrapper-delete-button" data-tooltip="<%- gettext('Delete') %>">
32+
<button class="delete action-icon" title="<%- gettext('Delete') %>"><span class="icon fa fa-trash-o" aria-hidden="true"></span></button>
33+
</li>
34+
<% } else { %>
35+
<li class="action action-delete wrapper-delete-button" data-tooltip="<%- gettext('Cannot delete when in use by a unit') %>">
36+
<button class="delete action-icon is-disabled" disabled="disabled" title="<%- gettext('Delete') %>"><span class="icon fa fa-trash-o" aria-hidden="true"></span></button>
37+
</li>
38+
<% } %>
3739
<% } %>
3840
</ul>
3941
</div>
4042

4143
<% if (showContentGroupUsages) { %>
4244
<div class="collection-references wrapper-group-configuration-usages">
4345
<% if (!_.isEmpty(usage)) { %>
44-
<h4 class="intro group-configuration-usage-text"><%- gettext('This content group is used in:') %></h4>
46+
<h4 class="intro group-configuration-usage-text">
47+
<%- gettext('This group controls visibility of:') %>
48+
</h4>
4549
<ol class="usage group-configuration-usage">
4650
<% _.each(usage, function(unit) { %>
4751
<li class="usage-unit group-configuration-usage-unit">
@@ -52,15 +56,18 @@
5256
<% } else { %>
5357
<p class="group-configuration-usage-text">
5458
<%= HtmlUtils.interpolateHtml(
55-
gettext('This content group is not in use. Add a content group to any unit from the {linkStart}Course Outline{linkEnd}.'),
56-
{
57-
linkStart: HtmlUtils.interpolateHtml(
58-
HtmlUtils.HTML('<a href="{courseOutlineUrl}" title="{courseOutlineTitle}">'),
59-
{courseOutlineUrl: courseOutlineUrl, courseOutlineTitle: gettext('Course Outline')}),
60-
linkEnd: HtmlUtils.HTML('</a>')
61-
})
59+
gettext("Use this group to control a component's visibility in the {linkStart}Course Outline{linkEnd}."),
60+
{
61+
linkStart: HtmlUtils.interpolateHtml(
62+
HtmlUtils.HTML('<a href="{courseOutlineUrl}" title="{courseOutlineTitle}">'),
63+
{courseOutlineUrl: courseOutlineUrl, courseOutlineTitle: gettext('Course Outline')}
64+
),
65+
linkEnd: HtmlUtils.HTML('</a>')
66+
}
67+
)
6268
%>
69+
6370
</p>
6471
<% } %>
6572
</div>
66-
<% } %>
73+
<% } %>

‎common/lib/xmodule/xmodule/partitions/partitions_service.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ def _create_enrollment_track_partition(course):
7474

7575
partition = enrollment_track_scheme.create_user_partition(
7676
id=ENROLLMENT_TRACK_PARTITION_ID,
77-
name=_(u"Enrollment Tracks"),
77+
name=_(u"Enrollment Track Groups"),
7878
description=_(u"Partition for segmenting users by enrollment track"),
7979
parameters={"course_id": unicode(course.id)}
8080
)

‎common/test/acceptance/pages/common/utils.py

+44
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
from bok_choy.promise import BrokenPromise
55
from common.test.acceptance.tests.helpers import disable_animations
66
from selenium.webdriver.common.action_chains import ActionChains
7+
from common.test.acceptance.pages.lms.pay_and_verify import PaymentAndVerificationFlow, FakePaymentPage
8+
from common.test.acceptance.pages.lms.track_selection import TrackSelectionPage
9+
from common.test.acceptance.pages.lms.create_mode import ModeCreationPage
710

811

912
def sync_on_notification(page, style='default', wait_for_hide=False):
@@ -100,3 +103,44 @@ def hover(browser, element):
100103
Hover over an element.
101104
"""
102105
ActionChains(browser).move_to_element(element).perform()
106+
107+
108+
def enroll_user_track(browser, course_id, track):
109+
"""
110+
Utility method to enroll a user in the audit or verified user track. Creates and connects to the
111+
necessary pages. Selects the track and handles payment for verified.
112+
Supported tracks are 'verified' or 'audit'.
113+
"""
114+
payment_and_verification_flow = PaymentAndVerificationFlow(browser, course_id)
115+
fake_payment_page = FakePaymentPage(browser, course_id)
116+
track_selection = TrackSelectionPage(browser, course_id)
117+
118+
# Select track and process payment
119+
track_selection.visit()
120+
track_selection.enroll(track)
121+
if track == 'verified':
122+
payment_and_verification_flow.proceed_to_payment()
123+
fake_payment_page.submit_payment()
124+
125+
126+
def add_enrollment_course_modes(browser, course_id, tracks):
127+
"""
128+
Add the specified array of tracks to the given course.
129+
Supported tracks are `verified` and `audit` (all others will be ignored),
130+
and display names assigned are `Verified` and `Audit`, respectively.
131+
"""
132+
for track in tracks:
133+
if track == 'audit':
134+
# Add an audit mode to the course
135+
ModeCreationPage(
136+
browser,
137+
course_id, mode_slug='audit',
138+
mode_display_name='Audit'
139+
).visit()
140+
141+
elif track == 'verified':
142+
# Add a verified mode to the course
143+
ModeCreationPage(
144+
browser, course_id, mode_slug='verified',
145+
mode_display_name='Verified', min_price=10
146+
).visit()

‎common/test/acceptance/pages/lms/track_selection.py

+9-9
Original file line numberDiff line numberDiff line change
@@ -34,24 +34,24 @@ def is_browser_on_page(self):
3434
"""Check if the track selection page has loaded."""
3535
return self.q(css=".wrapper-register-choose").is_present()
3636

37-
def enroll(self, mode="honor"):
37+
def enroll(self, mode="audit"):
3838
"""Interact with one of the enrollment buttons on the page.
3939
4040
Keyword Arguments:
41-
mode (str): Can be "honor" or "verified"
41+
mode (str): Can be "audit" or "verified"
4242
4343
Raises:
4444
ValueError
4545
"""
46-
if mode == "honor":
47-
self.q(css="input[name='honor_mode']").click()
48-
49-
return DashboardPage(self.browser).wait_for_page()
50-
elif mode == "verified":
46+
if mode == "verified":
5147
# Check the first contribution option, then click the enroll button
5248
self.q(css=".contribution-option > input").first.click()
5349
self.q(css="input[name='verified_mode']").click()
54-
5550
return PaymentAndVerificationFlow(self.browser, self._course_id).wait_for_page()
51+
52+
elif mode == "audit":
53+
self.q(css="input[name='audit_mode']").click()
54+
return DashboardPage(self.browser).wait_for_page()
55+
5656
else:
57-
raise ValueError("Mode must be either 'honor' or 'verified'.")
57+
raise ValueError("Mode must be either 'audit' or 'verified'.")

‎common/test/acceptance/pages/studio/component_editor.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ class ComponentVisibilityEditorView(BaseComponentEditorView):
112112
OPTION_SELECTOR = '.partition-group-control .field'
113113
ALL_LEARNERS_AND_STAFF = 'All Learners and Staff'
114114
CONTENT_GROUP_PARTITION = 'Content Groups'
115-
ENROLLMENT_TRACK_PARTITION = "Enrollment Tracks"
115+
ENROLLMENT_TRACK_PARTITION = "Enrollment Track Groups"
116116

117117
@property
118118
def all_group_options(self):

‎common/test/acceptance/pages/studio/settings_group_configurations.py

+11
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,17 @@ def experiment_group_sections_present(self):
9191
"""
9292
return self.q(css=self.experiment_groups_css).present or self.q(css=".experiment-groups-doc").present
9393

94+
@property
95+
def enrollment_track_section_present(self):
96+
return self.q(css='.wrapper-groups.content-groups.enrollment_track').present
97+
98+
@property
99+
def enrollment_track_edit_present(self):
100+
return self.q(css='.wrapper-groups.content-groups.enrollment_track .action.action-edit').present
101+
102+
def get_enrollment_groups(self):
103+
return self.q(css='.wrapper-groups.content-groups.enrollment_track .collection-details .title').text
104+
94105

95106
class GroupConfiguration(object):
96107
"""

‎common/test/acceptance/tests/lms/test_lms.py

+3-24
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,8 @@
3838
from common.test.acceptance.pages.lms.progress import ProgressPage
3939
from common.test.acceptance.pages.lms.problem import ProblemPage
4040
from common.test.acceptance.pages.lms.tab_nav import TabNavPage
41-
from common.test.acceptance.pages.lms.track_selection import TrackSelectionPage
4241
from common.test.acceptance.pages.lms.video.video import VideoPage
42+
from common.test.acceptance.pages.common.utils import enroll_user_track
4343
from common.test.acceptance.pages.studio.settings import SettingsPage
4444
from common.test.acceptance.fixtures.course import CourseFixture, XBlockFixtureDesc, CourseUpdateDesc
4545

@@ -417,7 +417,6 @@ def setUp(self):
417417
"""
418418
super(PayAndVerifyTest, self).setUp()
419419

420-
self.track_selection_page = TrackSelectionPage(self.browser, self.course_id)
421420
self.payment_and_verification_flow = PaymentAndVerificationFlow(self.browser, self.course_id)
422421
self.immediate_verification_page = PaymentAndVerificationFlow(self.browser, self.course_id, entry_point='verify-now')
423422
self.upgrade_page = PaymentAndVerificationFlow(self.browser, self.course_id, entry_point='upgrade')
@@ -443,17 +442,7 @@ def test_immediate_verification_enrollment(self):
443442
# Create a user and log them in
444443
student_id = AutoAuthPage(self.browser).visit().get_user_id()
445444

446-
# Navigate to the track selection page
447-
self.track_selection_page.visit()
448-
449-
# Enter the payment and verification flow by choosing to enroll as verified
450-
self.track_selection_page.enroll('verified')
451-
452-
# Proceed to the fake payment page
453-
self.payment_and_verification_flow.proceed_to_payment()
454-
455-
# Submit payment
456-
self.fake_payment_page.submit_payment()
445+
enroll_user_track(self.browser, self.course_id, 'verified')
457446

458447
# Proceed to verification
459448
self.payment_and_verification_flow.immediate_verification()
@@ -480,17 +469,7 @@ def test_deferred_verification_enrollment(self):
480469
# Create a user and log them in
481470
student_id = AutoAuthPage(self.browser).visit().get_user_id()
482471

483-
# Navigate to the track selection page
484-
self.track_selection_page.visit()
485-
486-
# Enter the payment and verification flow by choosing to enroll as verified
487-
self.track_selection_page.enroll('verified')
488-
489-
# Proceed to the fake payment page
490-
self.payment_and_verification_flow.proceed_to_payment()
491-
492-
# Submit payment
493-
self.fake_payment_page.submit_payment()
472+
enroll_user_track(self.browser, self.course_id, 'verified')
494473

495474
# Navigate to the dashboard
496475
self.dashboard_page.visit()

‎common/test/acceptance/tests/lms/test_lms_instructor_dashboard.py

+3-22
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,9 @@
1919
from common.test.acceptance.fixtures.course import CourseFixture, XBlockFixtureDesc
2020
from common.test.acceptance.pages.lms.dashboard import DashboardPage
2121
from common.test.acceptance.pages.lms.problem import ProblemPage
22-
from common.test.acceptance.pages.lms.track_selection import TrackSelectionPage
23-
from common.test.acceptance.pages.lms.pay_and_verify import PaymentAndVerificationFlow, FakePaymentPage
22+
from common.test.acceptance.pages.lms.pay_and_verify import PaymentAndVerificationFlow
2423
from common.test.acceptance.pages.lms.login_and_register import CombinedLoginAndRegisterPage
24+
from common.test.acceptance.pages.common.utils import enroll_user_track
2525
from common.test.acceptance.tests.helpers import disable_animations
2626
from common.test.acceptance.fixtures.certificates import CertificateConfigFixture
2727

@@ -247,13 +247,6 @@ def setUp(self):
247247
)
248248
).install()
249249

250-
self.track_selection_page = TrackSelectionPage(self.browser, self.course_id)
251-
self.payment_and_verification_flow = PaymentAndVerificationFlow(self.browser, self.course_id)
252-
self.immediate_verification_page = PaymentAndVerificationFlow(
253-
self.browser, self.course_id, entry_point='verify-now'
254-
)
255-
self.upgrade_page = PaymentAndVerificationFlow(self.browser, self.course_id, entry_point='upgrade')
256-
self.fake_payment_page = FakePaymentPage(self.browser, self.course_id)
257250
self.dashboard_page = DashboardPage(self.browser)
258251
self.problem_page = ProblemPage(self.browser)
259252

@@ -279,19 +272,7 @@ def _login_as_a_verified_user(self):
279272
"""
280273

281274
self._auto_auth(self.USERNAME, self.EMAIL, False)
282-
283-
# the track selection page cannot be visited. see the other tests to see if any prereq is there.
284-
# Navigate to the track selection page
285-
self.track_selection_page.visit()
286-
287-
# Enter the payment and verification flow by choosing to enroll as verified
288-
self.track_selection_page.enroll('verified')
289-
290-
# Proceed to the fake payment page
291-
self.payment_and_verification_flow.proceed_to_payment()
292-
293-
# Submit payment
294-
self.fake_payment_page.submit_payment()
275+
enroll_user_track(self.browser, self.course_id, 'verified')
295276

296277
def _create_a_proctored_exam_and_attempt(self):
297278
"""

‎common/test/acceptance/tests/studio/test_studio_settings.py

+52
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
from common.test.acceptance.pages.studio.settings_group_configurations import GroupConfigurationsPage
1919
from common.test.acceptance.pages.lms.courseware import CoursewarePage
2020
from common.test.acceptance.pages.studio.utils import get_input_value
21+
from common.test.acceptance.pages.common.utils import add_enrollment_course_modes
22+
2123
from textwrap import dedent
2224
from xmodule.partitions.partitions import Group
2325

@@ -231,6 +233,56 @@ def test_content_group_empty_usage(self):
231233
self.outline_page.wait_for_page()
232234

233235

236+
@attr(shard=5)
237+
class EnrollmentTrackModeTest(StudioCourseTest):
238+
239+
def setUp(self, is_staff=True, test_xss=True):
240+
super(EnrollmentTrackModeTest, self).setUp(is_staff=is_staff)
241+
242+
self.audit_track = "Audit"
243+
self.verified_track = "Verified"
244+
self.staff_user = self.user
245+
246+
def test_all_course_modes_present(self):
247+
"""
248+
This test is meant to ensure that all the course modes show up as groups
249+
on the Group configuration page within the Enrollment Tracks section.
250+
It also checks to make sure that the edit buttons are not available.
251+
"""
252+
add_enrollment_course_modes(self.browser, self.course_id, ['audit', 'verified'])
253+
group_configurations_page = GroupConfigurationsPage(
254+
self.browser,
255+
self.course_info['org'],
256+
self.course_info['number'],
257+
self.course_info['run']
258+
)
259+
group_configurations_page.visit()
260+
self.assertTrue(group_configurations_page.enrollment_track_section_present)
261+
262+
# Make sure the edit buttons are not available.
263+
self.assertFalse(group_configurations_page.enrollment_track_edit_present)
264+
groups = group_configurations_page.get_enrollment_groups()
265+
for g in [self.audit_track, self.verified_track]:
266+
self.assertTrue(g in groups)
267+
268+
def test_one_course_mode(self):
269+
"""
270+
The purpose of this test is to ensure that when there is 1 or fewer course modes
271+
the enrollment track section is not shown.
272+
"""
273+
add_enrollment_course_modes(self.browser, self.course_id, ['audit'])
274+
group_configurations_page = GroupConfigurationsPage(
275+
self.browser,
276+
self.course_info['org'],
277+
self.course_info['number'],
278+
self.course_info['run']
279+
)
280+
group_configurations_page.visit()
281+
self.assertFalse(group_configurations_page.enrollment_track_section_present)
282+
groups = group_configurations_page.get_enrollment_groups()
283+
self.assertEqual(len(groups), 0)
284+
285+
234286
@attr(shard=8)
235287
class AdvancedSettingsValidationTest(StudioCourseTest):
236288
"""

‎common/test/acceptance/tests/test_cohorted_courseware.py

+89-24
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,24 @@
33
"""
44

55
import json
6-
from nose.plugins.attrib import attr
7-
8-
from studio.base_studio_test import ContainerBase
96

10-
from common.test.acceptance.pages.studio.settings_group_configurations import GroupConfigurationsPage
11-
from common.test.acceptance.pages.studio.auto_auth import AutoAuthPage as StudioAutoAuthPage
12-
from common.test.acceptance.fixtures.course import XBlockFixtureDesc
7+
from bok_choy.page_object import XSS_INJECTION
138
from common.test.acceptance.fixtures import LMS_BASE_URL
14-
from common.test.acceptance.pages.studio.component_editor import ComponentVisibilityEditorView
15-
from common.test.acceptance.pages.lms.instructor_dashboard import InstructorDashboardPage
16-
from common.test.acceptance.pages.lms.courseware import CoursewarePage
9+
from common.test.acceptance.fixtures.course import XBlockFixtureDesc
10+
from common.test.acceptance.pages.common.utils import enroll_user_track, add_enrollment_course_modes
1711
from common.test.acceptance.pages.lms.auto_auth import AutoAuthPage as LmsAutoAuthPage
12+
from common.test.acceptance.pages.lms.courseware import CoursewarePage
13+
from common.test.acceptance.pages.lms.instructor_dashboard import InstructorDashboardPage
14+
from common.test.acceptance.pages.studio.auto_auth import AutoAuthPage as StudioAutoAuthPage
15+
from common.test.acceptance.pages.studio.component_editor import ComponentVisibilityEditorView
16+
from common.test.acceptance.pages.studio.settings_group_configurations import GroupConfigurationsPage
1817
from common.test.acceptance.tests.lms.test_lms_user_preview import verify_expected_problem_visibility
18+
from nose.plugins.attrib import attr
1919

20-
from bok_choy.page_object import XSS_INJECTION
20+
from studio.base_studio_test import ContainerBase
21+
22+
AUDIT_TRACK = "Audit"
23+
VERIFIED_TRACK = "Verified"
2124

2225

2326
@attr(shard=5)
@@ -34,6 +37,9 @@ def setUp(self, is_staff=True):
3437
self.content_group_a = "Content Group A" + XSS_INJECTION
3538
self.content_group_b = "Content Group B" + XSS_INJECTION
3639

40+
# Creates the Course modes needed to test enrollment tracks
41+
add_enrollment_course_modes(self.browser, self.course_id, ["audit", "verified"])
42+
3743
# Create a student who will be in "Cohort A"
3844
self.cohort_a_student_username = "cohort_a_student"
3945
self.cohort_a_student_email = "cohort_a_student@example.com"
@@ -48,6 +54,26 @@ def setUp(self, is_staff=True):
4854
self.browser, username=self.cohort_b_student_username, email=self.cohort_b_student_email, no_login=True
4955
).visit()
5056

57+
# Create a Verified Student
58+
self.cohort_verified_student_username = "cohort_verified_student"
59+
self.cohort_verified_student_email = "cohort_verified_student@example.com"
60+
StudioAutoAuthPage(
61+
self.browser,
62+
username=self.cohort_verified_student_username,
63+
email=self.cohort_verified_student_email,
64+
no_login=True
65+
).visit()
66+
67+
# Create audit student
68+
self.cohort_audit_student_username = "cohort_audit_student"
69+
self.cohort_audit_student_email = "cohort_audit_student@example.com"
70+
StudioAutoAuthPage(
71+
self.browser,
72+
username=self.cohort_audit_student_username,
73+
email=self.cohort_audit_student_email,
74+
no_login=True
75+
).visit()
76+
5177
# Create a student who will end up in the default cohort group
5278
self.cohort_default_student_username = "cohort_default_student"
5379
self.cohort_default_student_email = "cohort_default_student@example.com"
@@ -67,14 +93,20 @@ def populate_course_fixture(self, course_fixture):
6793
"""
6894
self.group_a_problem = 'GROUP A CONTENT'
6995
self.group_b_problem = 'GROUP B CONTENT'
96+
self.group_verified_problem = 'GROUP VERIFIED CONTENT'
97+
self.group_audit_problem = 'GROUP AUDIT CONTENT'
98+
7099
self.group_a_and_b_problem = 'GROUP A AND B CONTENT'
100+
71101
self.visible_to_all_problem = 'VISIBLE TO ALL CONTENT'
72102
course_fixture.add_children(
73103
XBlockFixtureDesc('chapter', 'Test Section').add_children(
74104
XBlockFixtureDesc('sequential', 'Test Subsection').add_children(
75105
XBlockFixtureDesc('vertical', 'Test Unit').add_children(
76106
XBlockFixtureDesc('problem', self.group_a_problem, data='<problem></problem>'),
77107
XBlockFixtureDesc('problem', self.group_b_problem, data='<problem></problem>'),
108+
XBlockFixtureDesc('problem', self.group_verified_problem, data='<problem></problem>'),
109+
XBlockFixtureDesc('problem', self.group_audit_problem, data='<problem></problem>'),
78110
XBlockFixtureDesc('problem', self.group_a_and_b_problem, data='<problem></problem>'),
79111
XBlockFixtureDesc('problem', self.visible_to_all_problem, data='<problem></problem>')
80112
)
@@ -115,20 +147,26 @@ def create_content_groups(self):
115147

116148
def link_problems_to_content_groups_and_publish(self):
117149
"""
118-
Updates 3 of the 4 existing problems to limit their visibility by content group.
150+
Updates 5 of the 6 existing problems to limit their visibility by content group.
119151
Publishes the modified units.
120152
"""
121153
container_page = self.go_to_unit_page()
154+
enrollment_group = 'enrollment_track_group'
122155

123-
def set_visibility(problem_index, groups):
156+
def set_visibility(problem_index, groups, group_partition='content_group'):
124157
problem = container_page.xblocks[problem_index]
125158
problem.edit_visibility()
126159
visibility_dialog = ComponentVisibilityEditorView(self.browser, problem.locator)
127-
visibility_dialog.select_groups_in_partition_scheme(visibility_dialog.CONTENT_GROUP_PARTITION, groups)
160+
partition_name = (visibility_dialog.ENROLLMENT_TRACK_PARTITION
161+
if group_partition == enrollment_group
162+
else visibility_dialog.CONTENT_GROUP_PARTITION)
163+
visibility_dialog.select_groups_in_partition_scheme(partition_name, groups)
128164

129165
set_visibility(1, [self.content_group_a])
130166
set_visibility(2, [self.content_group_b])
131-
set_visibility(3, [self.content_group_a, self.content_group_b])
167+
set_visibility(3, [VERIFIED_TRACK], enrollment_group)
168+
set_visibility(4, [AUDIT_TRACK], enrollment_group)
169+
set_visibility(5, [self.content_group_a, self.content_group_b])
132170

133171
container_page.publish_action.click()
134172

@@ -150,54 +188,81 @@ def add_cohort_with_student(cohort_name, content_group, student):
150188

151189
def view_cohorted_content_as_different_users(self):
152190
"""
153-
View content as staff, student in Cohort A, student in Cohort B, and student in Default Cohort.
191+
View content as staff, student in Cohort A, student in Cohort B, Verified Student, Audit student,
192+
and student in Default Cohort.
154193
"""
155194
courseware_page = CoursewarePage(self.browser, self.course_id)
156195

157-
def login_and_verify_visible_problems(username, email, expected_problems):
196+
def login_and_verify_visible_problems(username, email, expected_problems, track=None):
158197
LmsAutoAuthPage(
159198
self.browser, username=username, email=email, course_id=self.course_id
160199
).visit()
200+
if track is not None:
201+
enroll_user_track(self.browser, self.course_id, track)
161202
courseware_page.visit()
162203
verify_expected_problem_visibility(self, courseware_page, expected_problems)
163204

164205
login_and_verify_visible_problems(
165206
self.staff_user["username"], self.staff_user["email"],
166-
[self.group_a_problem, self.group_b_problem, self.group_a_and_b_problem, self.visible_to_all_problem]
207+
[self.group_a_problem,
208+
self.group_b_problem,
209+
self.group_verified_problem,
210+
self.group_audit_problem,
211+
self.group_a_and_b_problem,
212+
self.visible_to_all_problem
213+
],
167214
)
168215

169216
login_and_verify_visible_problems(
170217
self.cohort_a_student_username, self.cohort_a_student_email,
171-
[self.group_a_problem, self.group_a_and_b_problem, self.visible_to_all_problem]
218+
[self.group_a_problem, self.group_audit_problem, self.group_a_and_b_problem, self.visible_to_all_problem]
172219
)
173220

174221
login_and_verify_visible_problems(
175222
self.cohort_b_student_username, self.cohort_b_student_email,
176-
[self.group_b_problem, self.group_a_and_b_problem, self.visible_to_all_problem]
223+
[self.group_b_problem, self.group_audit_problem, self.group_a_and_b_problem, self.visible_to_all_problem]
224+
)
225+
226+
login_and_verify_visible_problems(
227+
self.cohort_verified_student_username, self.cohort_verified_student_email,
228+
[self.group_verified_problem, self.visible_to_all_problem],
229+
'verified'
230+
)
231+
232+
login_and_verify_visible_problems(
233+
self.cohort_audit_student_username, self.cohort_audit_student_email,
234+
[self.group_audit_problem, self.visible_to_all_problem],
235+
'audit'
177236
)
178237

179238
login_and_verify_visible_problems(
180239
self.cohort_default_student_username, self.cohort_default_student_email,
181-
[self.visible_to_all_problem]
240+
[self.group_audit_problem, self.visible_to_all_problem],
182241
)
183242

184243
def test_cohorted_courseware(self):
185244
"""
186245
Scenario: Can create content that is only visible to students in particular cohorts
187-
Given that I have course with 4 problems, 1 staff member, and 3 students
246+
Given that I have course with 6 problems, 1 staff member, and 6 students
188247
When I enable cohorts in the course
248+
And I add the Course Modes for Verified and Audit
189249
And I create two content groups, Content Group A, and Content Group B, in the course
190250
And I link one problem to Content Group A
191251
And I link one problem to Content Group B
252+
And I link one problem to the Verified Group
253+
And I link one problem to the Audit Group
192254
And I link one problem to both Content Group A and Content Group B
193255
And one problem remains unlinked to any Content Group
194256
And I create two manual cohorts, Cohort A and Cohort B,
195257
linked to Content Group A and Content Group B, respectively
196258
And I assign one student to each manual cohort
259+
And I assign one student to each enrollment track
197260
And one student remains in the default cohort
198-
Then the staff member can see all 4 problems
199-
And the student in Cohort A can see all the problems except the one linked to Content Group B
200-
And the student in Cohort B can see all the problems except the one linked to Content Group A
261+
Then the staff member can see all 6 problems
262+
And the student in Cohort A can see all the problems linked to A
263+
And the student in Cohort B can see all the problems linked to B
264+
And the student in Verified can see the problems linked to Verified and those not linked to a Group
265+
And the student in Audit can see the problems linked to Audit and those not linked to a Group
201266
And the student in the default cohort can ony see the problem that is unlinked to any Content Group
202267
"""
203268
self.enable_cohorting(self.course_fixture)

‎docs/cms_config.ini

+1
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ login = getting_started/index.html
3939
register = getting_started/index.html
4040
content_libraries = course_components/libraries.html
4141
content_groups = course_features/cohorts/cohorted_courseware.html
42+
enrollment_tracks = course_features/cohorts/cohorted_courseware.html
4243
group_configurations = course_features/content_experiments/content_experiments_configure.html#set-up-group-configurations-in-edx-studio
4344
container = developing_course/course_components.html#components-that-contain-other-components
4445
video = video/video_uploads.html

‎lms/static/sass/course/wiki/_wiki.scss

+1
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@
135135
.entry-title {
136136
padding-bottom: 8px;
137137
margin-bottom: 22px;
138+
margin-top: 0;
138139
border-bottom: 1px solid $light-gray;
139140
font-size: 1.6em;
140141
font-weight: bold;

‎openedx/core/djangoapps/verified_track_content/partition_scheme.py

+4-10
Original file line numberDiff line numberDiff line change
@@ -45,17 +45,11 @@ def groups(self):
4545
for mode in CourseMode.modes_for_course(course_key, include_expired=True, only_selectable=False)
4646
]
4747

48-
def to_json(self):
49-
"""
50-
Because this partition is dynamic, to_json and from_json are not supported.
51-
52-
Calling this method will raise a TypeError.
53-
"""
54-
raise TypeError("Because EnrollmentTrackUserPartition is a dynamic partition, 'to_json' is not supported.")
55-
5648
def from_json(self):
5749
"""
58-
Because this partition is dynamic, to_json and from_json are not supported.
50+
Because this partition is dynamic, `from_json` is not supported.
51+
`to_json` is supported, but shouldn't be used to persist this partition
52+
within the course itself (used by Studio for sending data to front-end code)
5953
6054
Calling this method will raise a TypeError.
6155
"""
@@ -116,7 +110,7 @@ def create_user_partition(cls, id, name, description, groups=None, parameters=No
116110
Any group access rule referencing inactive partitions will be ignored
117111
when performing access checks.
118112
"""
119-
return EnrollmentTrackUserPartition(id, name, description, [], cls, parameters, active)
113+
return EnrollmentTrackUserPartition(id, unicode(name), unicode(description), [], cls, parameters, active)
120114

121115

122116
def is_course_using_cohort_instead(course_key):

‎openedx/core/djangoapps/verified_track_content/tests/test_partition_scheme.py

+5-4
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,11 @@ def test_multiple_groups(self):
5353
self.assertIsNotNone(self.get_group_by_name(partition, "Verified Enrollment Track"))
5454
self.assertIsNotNone(self.get_group_by_name(partition, "Credit Mode"))
5555

56-
def test_to_json_not_supported(self):
57-
user_partition = create_enrollment_track_partition(self.course)
58-
with self.assertRaises(TypeError):
59-
user_partition.to_json()
56+
def test_to_json_supported(self):
57+
user_partition_json = create_enrollment_track_partition(self.course).to_json()
58+
self.assertEqual('Test Enrollment Track Partition', user_partition_json['name'])
59+
self.assertEqual('enrollment_track', user_partition_json['scheme'])
60+
self.assertEqual('Test partition for segmenting users by enrollment track', user_partition_json['description'])
6061

6162
def test_from_json_not_supported(self):
6263
with self.assertRaises(TypeError):

0 commit comments

Comments
 (0)
Please sign in to comment.