Commit 59c62188 by Albert St. Aubin Committed by cahrens

Show Enrollment Tracks in Group Configurations.

TNL-6743
parent 447f5c2b
......@@ -8,15 +8,16 @@ from util.db import generate_int_id, MYSQL_MAX_INT
from django.utils.translation import ugettext as _
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_service import get_all_partitions_for_course
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
RANDOM_SCHEME = "random"
COHORT_SCHEME = "cohort"
ENROLLMENT_SCHEME = "enrollment_track"
CONTENT_GROUP_CONFIGURATION_DESCRIPTION = _(
'The groups in this configuration can be mapped to cohorts in the Instructor Dashboard.'
......@@ -187,21 +188,10 @@ class GroupConfiguration(object):
return usage_info
@staticmethod
def get_content_groups_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):
def get_partitions_usage_info(store, course):
"""
Returns all units names and their urls.
This will return only groups for the cohort user partition.
Returns:
{'group_id':
[
......@@ -216,8 +206,10 @@ class GroupConfiguration(object):
],
}
"""
items = store.get_items(course.id, settings={'group_access': {'$exists': True}}, include_orphans=False)
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:
usage_info[group_id] = []
......@@ -267,7 +259,7 @@ class GroupConfiguration(object):
}
"""
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:
usage_info[group_id] = []
......@@ -282,22 +274,23 @@ class GroupConfiguration(object):
return usage_info
@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)
"""
content_group_configuration = get_cohorted_user_partition(course)
if content_group_configuration is not None:
for item in items:
if hasattr(item, 'group_access') and item.group_access:
group_ids = item.group_access.get(content_group_configuration.id, [])
all_partitions = get_all_partitions_for_course(course)
for config in all_partitions:
if config is not None and config.scheme.name != RANDOM_SCHEME:
for item in items:
if hasattr(item, 'group_access') and item.group_access:
group_ids = item.group_access.get(config.id, [])
for group_id in group_ids:
yield item, group_id
for group_id in group_ids:
yield item, group_id
@staticmethod
def update_usage_info(store, course, configuration):
......@@ -319,23 +312,23 @@ class GroupConfiguration(object):
configuration_json['usage'] = usage_information.get(configuration.id, [])
elif configuration.scheme.name == COHORT_SCHEME:
# 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
@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)
content_group_configuration = configuration.to_json()
usage_info = GroupConfiguration.get_partitions_usage_info(store, course)
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'], [])
return content_group_configuration
return partition_configuration
@staticmethod
def get_or_create_content_group(store, course):
......@@ -357,9 +350,27 @@ class GroupConfiguration(object):
)
return content_group_configuration.to_json()
content_group_configuration = GroupConfiguration.update_content_group_usage_info(
content_group_configuration = GroupConfiguration.update_partition_usage_info(
store,
course,
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):
@classmethod
def fetch_group_usage(cls, modulestore, structure):
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(
GroupConfiguration.get_content_groups_items_usage_info(
modulestore,
......
......@@ -30,6 +30,7 @@ from .library import LIBRARIES_ENABLED, get_library_creator_status
from ccx_keys.locator import CCXLocator
from contentstore.course_group_config import (
COHORT_SCHEME,
ENROLLMENT_SCHEME,
GroupConfiguration,
GroupConfigurationsValidationError,
RANDOM_SCHEME,
......@@ -99,7 +100,6 @@ from xmodule.modulestore.django import modulestore
from xmodule.modulestore.exceptions import ItemNotFoundError, DuplicateCourseError
from xmodule.tabs import CourseTab, CourseTabList, InvalidTabsException
log = logging.getLogger(__name__)
__all__ = ['course_info_handler', 'course_handler', 'course_listing',
......@@ -1473,7 +1473,7 @@ def remove_content_or_experiment_group(request, store, course, configuration, gr
return JsonResponse(status=404)
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
if used:
......@@ -1521,7 +1521,24 @@ def group_configurations_list_handler(request, course_key_string):
else:
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', {
'context_course': course,
......@@ -1529,7 +1546,8 @@ def group_configurations_list_handler(request, course_key_string):
'course_outline_url': course_outline_url,
'experiment_group_configurations': experiment_group_configurations,
'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'):
if request.method == 'POST':
......
......@@ -613,6 +613,15 @@ class GroupConfigurationsUsageInfoTestCase(CourseTestCase, HelperMethods):
def setUp(self):
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):
"""
Returns the expected configuration with particular usage.
......@@ -637,7 +646,7 @@ class GroupConfigurationsUsageInfoTestCase(CourseTestCase, HelperMethods):
Test that right data structure will be created if content group is not used.
"""
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=[])
self.assertEqual(actual, expected)
......@@ -650,7 +659,7 @@ class GroupConfigurationsUsageInfoTestCase(CourseTestCase, HelperMethods):
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(
usage_for_group=[
{
......@@ -669,7 +678,7 @@ class GroupConfigurationsUsageInfoTestCase(CourseTestCase, HelperMethods):
self._add_user_partitions(count=1, scheme_id='cohort')
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=[
{
......@@ -706,7 +715,7 @@ class GroupConfigurationsUsageInfoTestCase(CourseTestCase, HelperMethods):
expected = self._get_expected_content_group(usage_for_group=[])
# 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.
self.assertEqual(actual, expected)
......@@ -720,7 +729,7 @@ class GroupConfigurationsUsageInfoTestCase(CourseTestCase, HelperMethods):
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')
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=[
{
......@@ -927,7 +936,7 @@ class GroupConfigurationsUsageInfoTestCase(CourseTestCase, HelperMethods):
# This used to cause an exception since the code assumed that
# 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])
actual = GroupConfiguration.get_content_groups_items_usage_info(self.store, self.course)
......
......@@ -372,7 +372,7 @@ class GetItemTest(ItemTest):
self.assertEqual(result["user_partitions"], [
{
"id": ENROLLMENT_TRACK_PARTITION_ID,
"name": "Enrollment Tracks",
"name": "Enrollment Track Groups",
"scheme": "enrollment_track",
"groups": [
{
......
......@@ -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_DISABLED = "specific groups of learners based on content groups that you create"
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'
FEATURES_WITH_ENROLLMENT_TRACK_DISABLED = settings.FEATURES.copy()
......
......@@ -2,24 +2,33 @@ define([
'js/collections/group_configuration', 'js/models/group_configuration', 'js/views/pages/group_configurations'
], function(GroupConfigurationCollection, GroupConfigurationModel, GroupConfigurationsPage) {
'use strict';
return function(experimentsEnabled, experimentGroupConfigurationsJson, contentGroupConfigurationJson,
groupConfigurationUrl, courseOutlineUrl) {
return function(experimentsEnabled,
experimentGroupConfigurationsJson,
allGroupConfigurationJson,
groupConfigurationUrl,
courseOutlineUrl) {
var experimentGroupConfigurations = new GroupConfigurationCollection(
experimentGroupConfigurationsJson, {parse: true}
),
contentGroupConfiguration = new GroupConfigurationModel(contentGroupConfigurationJson, {
parse: true, canBeEmpty: true
});
allGroupConfigurations = [],
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.outlineUrl = courseOutlineUrl;
contentGroupConfiguration.urlRoot = groupConfigurationUrl;
contentGroupConfiguration.outlineUrl = courseOutlineUrl;
new GroupConfigurationsPage({
el: $('#content'),
experimentsEnabled: experimentsEnabled,
experimentGroupConfigurations: experimentGroupConfigurations,
contentGroupConfiguration: contentGroupConfiguration
allGroupConfigurations: allGroupConfigurations
}).render();
};
});
......@@ -3,13 +3,13 @@ define([
'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/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/content_group_editor', 'js/views/content_group_item'
'js/views/experiment_group_edit', 'js/views/partition_group_list', 'js/views/partition_group_details',
'js/views/content_group_editor', 'js/views/partition_group_item'
], function(
_, AjaxHelpers, TemplateHelpers, ViewHelpers, Course, GroupConfigurationModel, GroupModel,
GroupConfigurationCollection, GroupCollection, GroupConfigurationDetailsView, GroupConfigurationsListView,
GroupConfigurationEditorView, GroupConfigurationItemView, ExperimentGroupEditView, GroupList,
ContentGroupDetailsView, ContentGroupEditorView, ContentGroupItemView
PartitionGroupDetailsView, ContentGroupEditorView, PartitionGroupItemView
) {
'use strict';
var SELECTORS = {
......@@ -675,7 +675,7 @@ define([
verifyEditingGroup, respondToSave, expectGroupsVisible, correctValidationError;
scopedGroupSelector = function(groupIndex, additionalSelectors) {
var groupSelector = '.content-groups-list-item-' + groupIndex;
var groupSelector = '.partition-groups-list-item-' + groupIndex;
if (additionalSelectors) {
return groupSelector + ' ' + additionalSelectors;
} else {
......@@ -775,13 +775,13 @@ define([
expectGroupsVisible = function(view, groupNames) {
_.each(groupNames, function(groupName) {
expect(view.$('.content-groups-list-item')).toContainText(groupName);
expect(view.$('.partition-groups-list-item')).toContainText(groupName);
});
};
beforeEach(function() {
TemplateHelpers.installTemplates(
['content-group-editor', 'content-group-details', 'list']
['content-group-editor', 'partition-group-details', 'list']
);
});
......@@ -792,7 +792,7 @@ define([
it('can render groups', function() {
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]);
});
});
......@@ -874,7 +874,7 @@ define([
describe('Content groups details view', 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'});
var saveableModel = new GroupConfigurationModel({
......@@ -889,7 +889,7 @@ define([
this.collection = new GroupConfigurationCollection([saveableModel]);
this.collection.outlineUrl = '/outline';
this.view = new ContentGroupDetailsView({
this.view = new PartitionGroupDetailsView({
model: this.model
});
appendSetFixtures(this.view.render().el);
......@@ -901,7 +901,7 @@ define([
it('should show empty usage appropriately', function() {
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() {
......@@ -915,7 +915,7 @@ define([
assertShowNonEmptyUsages(
this.view,
'This content group is used in:',
'This group controls visibility of:',
'Cannot delete when in use by a unit'
);
});
......@@ -1015,7 +1015,7 @@ define([
describe('Content group controller view', function() {
beforeEach(function() {
TemplateHelpers.installTemplates([
'content-group-editor', 'content-group-details'
'content-group-editor', 'partition-group-details'
], true);
this.model = new GroupModel({name: 'Content Group', id: 0, courseOutlineUrl: 'CourseOutlineUrl'});
......@@ -1029,14 +1029,14 @@ define([
this.saveableModel.urlRoot = '/group_configurations';
this.collection = new GroupConfigurationCollection([this.saveableModel]);
this.collection.url = '/group_configurations';
this.view = new ContentGroupItemView({
this.view = new PartitionGroupItemView({
model: this.model
});
appendSetFixtures(this.view.render().el);
});
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() {
......@@ -1047,7 +1047,7 @@ define([
assertAndDeleteItemWithError(
this,
'/group_configurations/0/0',
'.content-groups-list-item',
'.partition-groups-list-item',
'Delete this content group'
);
});
......
......@@ -19,9 +19,8 @@ define([
name: 'Configuration 1',
courseOutlineUrl: 'CourseOutlineUrl'
}),
contentGroupConfiguration: new GroupConfigurationModel({groups: []})
allGroupConfigurations: [new GroupConfigurationModel({groups: []})]
});
if (!disableSpy) {
spyOn(view, 'addWindowActions');
}
......@@ -36,7 +35,7 @@ define([
beforeEach(function() {
setFixtures(mockGroupConfigurationsPage);
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'
]);
......@@ -116,7 +115,7 @@ define([
});
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())
.toBe('You have unsaved changes. Do you really want to leave this page?');
});
......
......@@ -11,6 +11,7 @@
* of items this list contains. For example, 'Group Configuration'.
* Note that it must be translated.
* - emptyMessage (string): Text to render when the list is empty.
* - restrictEditing (bool) : Boolean flag for hiding edit and remove options, defaults to false.
*/
define([
'js/views/baseview'
......@@ -25,6 +26,7 @@ define([
listContainerCss: '.list-items',
initialize: function() {
this.restrictEditing = this.options.restrictEditing || false;
this.listenTo(this.collection, 'add', this.addNewItemView);
this.listenTo(this.collection, 'remove', this.onRemoveItem);
this.template = this.loadTemplate('list');
......@@ -42,11 +44,14 @@ define([
emptyMessage: this.emptyMessage,
length: this.collection.length,
isEditing: model && model.get('editing'),
canCreateNewItem: this.canCreateItem(this.collection)
canCreateNewItem: this.canCreateItem(this.collection),
restrictEditing: this.restrictEditing
}));
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);
return this;
......
......@@ -22,6 +22,7 @@ define([
canDelete: false,
initialize: function() {
this.restrictEditing = this.options.restrictEditing || false;
this.listenTo(this.model, 'change:editing', this.render);
this.listenTo(this.model, 'remove', this.remove);
},
......
define([
'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';
var GroupConfigurationsPage = BasePage.extend({
initialize: function(options) {
var currentScheme,
i,
enrollmentScheme = 'enrollment_track';
BasePage.prototype.initialize.call(this);
this.experimentsEnabled = options.experimentsEnabled;
if (this.experimentsEnabled) {
......@@ -14,18 +18,35 @@ function($, _, gettext, BasePage, GroupConfigurationsListView, ContentGroupListV
collection: this.experimentGroupConfigurations
});
}
this.contentGroupConfiguration = options.contentGroupConfiguration;
this.cohortGroupsListView = new ContentGroupListView({
collection: this.contentGroupConfiguration.get('groups')
});
this.allGroupConfigurations = options.allGroupConfigurations || [];
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() {
var hash = this.getLocationHash();
var hash = this.getLocationHash(),
i,
currentClass;
if (this.experimentsEnabled) {
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();
if (hash) {
// Strip leading '#' to get id string to match
......@@ -38,8 +59,22 @@ function($, _, gettext, BasePage, GroupConfigurationsListView, ContentGroupListV
$(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() {
var dirty = this.contentGroupConfiguration.isDirty() ||
var dirty = this.areAnyConfigurationsDirty() ||
(this.experimentsEnabled && this.experimentGroupConfigurations.find(function(configuration) {
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.
*/
define([
......@@ -8,7 +8,7 @@ define([
], function(BaseView, _, gettext, str, StringUtils, HtmlUtils) {
'use strict';
var ContentGroupDetailsView = BaseView.extend({
var PartitionGroupDetailsView = BaseView.extend({
tagName: 'div',
events: {
'click .edit': 'editGroup',
......@@ -21,17 +21,18 @@ define([
return [
'collection',
'content-group-details',
'content-group-details-' + index
'partition-group-details',
'partition-group-details-' + index
].join(' ');
},
editGroup: function() {
this.model.set({'editing': true});
this.model.set({editing: true});
},
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);
},
......@@ -41,9 +42,10 @@ define([
courseOutlineUrl: this.model.collection.parents[0].outlineUrl,
index: this.model.collection.indexOf(this.model),
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;
},
......@@ -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
* of the underlying model.
* It is expected to be backed by a Group model.
*/
define([
'js/views/list_item', 'js/views/content_group_editor', 'js/views/content_group_details', 'gettext', 'common/js/components/utils/view_utils'
], function(ListItemView, ContentGroupEditorView, ContentGroupDetailsView, gettext) {
'js/views/list_item', 'js/views/content_group_editor', 'js/views/partition_group_details',
'gettext', 'common/js/components/utils/view_utils'
], function(ListItemView, ContentGroupEditorView, PartitionGroupDetailsView, gettext) {
'use strict';
var ContentGroupItemView = ListItemView.extend({
var PartitionGroupItemView = ListItemView.extend({
events: {
'click .delete': 'deleteItem'
},
tagName: 'section',
baseClassName: 'content-group',
baseClassName: 'partition-group',
canDelete: true,
......@@ -24,8 +25,8 @@ define([
attributes: function() {
return {
'id': this.model.get('id'),
'tabindex': -1
id: this.model.get('id'),
tabindex: -1
};
},
......@@ -34,9 +35,11 @@ define([
},
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.
*/
define([
'js/views/list', 'js/views/content_group_item', 'gettext'
], function(ListView, ContentGroupItemView, gettext) {
'underscore', 'js/views/list', 'js/views/partition_group_item', 'gettext'
], function(_, ListView, PartitionGroupItemView, gettext) {
'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',
className: 'content-group-list',
className: 'partition-group-list',
// Translators: This refers to a content group that can be linked to a student cohort.
itemCategoryDisplayName: gettext('content group'),
......@@ -20,9 +25,9 @@ define([
emptyMessage: gettext('You have not created any content groups yet.'),
createItemView: function(options) {
return new ContentGroupItemView(options);
return new PartitionGroupItemView(_.extend({}, options, {scheme: this.scheme}));
}
});
return ContentGroupListView;
return PartitionGroupListView;
});
<%page expression_filter="h"/>
<%inherit file="base.html" />
<%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>
<%namespace name='static' file='static_content.html'/>
<%!
......@@ -16,7 +17,7 @@ from openedx.core.djangolib.markup import HTML, Text
<%block name="bodyclass">is-signedin course view-group-configurations</%block>
<%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">
<%static:include path="js/${template_name}.underscore" />
</script>
......@@ -28,9 +29,10 @@ from openedx.core.djangolib.markup import HTML, Text
GroupConfigurationsFactory(
${should_show_experiment_groups | 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}",
"${course_outline_url | n, js_escaped_string}"
"${course_outline_url | n, js_escaped_string}",
${should_show_enrollment_track | n, dump_js_escaped_json}
);
});
</%block>
......@@ -47,37 +49,52 @@ from openedx.core.djangolib.markup import HTML, Text
<div class="wrapper-content wrapper">
<section class="content">
<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:
<div class="wrapper-groups experiment-groups">
<h3 class="title">${_("Experiment Group Configurations")}</h3>
% if experiment_group_configurations is None:
<div class="notice notice-incontext notice-moduledisabled">
<p class="copy">
${_("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
% for config in all_group_configurations:
<div class="wrapper-groups content-groups ${config['scheme']}">
<h3 class="title">${config['name']}</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>
% endfor
% if should_show_experiment_groups:
<div class="wrapper-groups experiment-groups">
<h3 class="title">${_("Experiment Group Configurations")}</h3>
% if experiment_group_configurations is None:
<div class="notice notice-incontext notice-moduledisabled">
<p class="copy">
${_("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>
<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="content-groups-doc">
<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>${_("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><a href="${get_online_help_info(content_groups_help_token())['doc_url']}" target="_blank" class="button external-help-button">${_("Learn More")}</a></p>
</div>
......@@ -86,12 +103,13 @@ from openedx.core.djangolib.markup import HTML, Text
<div class="bit">
<div class="experiment-groups-doc">
<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><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>
% endif
<div class="bit">
% if context_course:
<%
......
......@@ -10,7 +10,7 @@
</div>
<% } else { %>
<div class="list-items"></div>
<% if (!isEditing) { %>
<% if (!isEditing && !restrictEditing) { %>
<button class="action action-add <% if(!canCreateNewItem) {%> action-add-hidden <% }%>" >
<span class="icon fa fa-plus" aria-hidden="true"></span>
<%- interpolate(
......
......@@ -23,17 +23,19 @@
</ol>
<ul class="actions group-configuration-actions">
<li class="action action-edit">
<button class="edit"><span class="icon fa fa-pencil" aria-hidden="true"></span> <%- gettext("Edit") %></button>
</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>
<% if (!restrictEditing) { %>
<li class="action action-edit">
<button class="edit"><span class="icon fa fa-pencil" aria-hidden="true"></span> <%- gettext("Edit") %></button>
</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>
</div>
......@@ -41,7 +43,9 @@
<% if (showContentGroupUsages) { %>
<div class="collection-references wrapper-group-configuration-usages">
<% 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">
<% _.each(usage, function(unit) { %>
<li class="usage-unit group-configuration-usage-unit">
......@@ -52,15 +56,18 @@
<% } else { %>
<p class="group-configuration-usage-text">
<%= HtmlUtils.interpolateHtml(
gettext('This content group is not in use. Add a content group to any unit from the {linkStart}Course Outline{linkEnd}.'),
{
linkStart: HtmlUtils.interpolateHtml(
HtmlUtils.HTML('<a href="{courseOutlineUrl}" title="{courseOutlineTitle}">'),
{courseOutlineUrl: courseOutlineUrl, courseOutlineTitle: gettext('Course Outline')}),
linkEnd: HtmlUtils.HTML('</a>')
})
gettext("Use this group to control a component's visibility in the {linkStart}Course Outline{linkEnd}."),
{
linkStart: HtmlUtils.interpolateHtml(
HtmlUtils.HTML('<a href="{courseOutlineUrl}" title="{courseOutlineTitle}">'),
{courseOutlineUrl: courseOutlineUrl, courseOutlineTitle: gettext('Course Outline')}
),
linkEnd: HtmlUtils.HTML('</a>')
}
)
%>
</p>
<% } %>
</div>
<% } %>
\ No newline at end of file
<% } %>
......@@ -74,7 +74,7 @@ def _create_enrollment_track_partition(course):
partition = enrollment_track_scheme.create_user_partition(
id=ENROLLMENT_TRACK_PARTITION_ID,
name=_(u"Enrollment Tracks"),
name=_(u"Enrollment Track Groups"),
description=_(u"Partition for segmenting users by enrollment track"),
parameters={"course_id": unicode(course.id)}
)
......
......@@ -4,6 +4,9 @@ Utility methods common to Studio and the LMS.
from bok_choy.promise import BrokenPromise
from common.test.acceptance.tests.helpers import disable_animations
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):
......@@ -100,3 +103,44 @@ def hover(browser, element):
Hover over an element.
"""
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):
"""Check if the track selection page has loaded."""
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.
Keyword Arguments:
mode (str): Can be "honor" or "verified"
mode (str): Can be "audit" or "verified"
Raises:
ValueError
"""
if mode == "honor":
self.q(css="input[name='honor_mode']").click()
return DashboardPage(self.browser).wait_for_page()
elif mode == "verified":
if mode == "verified":
# Check the first contribution option, then click the enroll button
self.q(css=".contribution-option > input").first.click()
self.q(css="input[name='verified_mode']").click()
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:
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):
OPTION_SELECTOR = '.partition-group-control .field'
ALL_LEARNERS_AND_STAFF = 'All Learners and Staff'
CONTENT_GROUP_PARTITION = 'Content Groups'
ENROLLMENT_TRACK_PARTITION = "Enrollment Tracks"
ENROLLMENT_TRACK_PARTITION = "Enrollment Track Groups"
@property
def all_group_options(self):
......
......@@ -91,6 +91,17 @@ class GroupConfigurationsPage(CoursePage):
"""
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):
"""
......
......@@ -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.problem import ProblemPage
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.common.utils import enroll_user_track
from common.test.acceptance.pages.studio.settings import SettingsPage
from common.test.acceptance.fixtures.course import CourseFixture, XBlockFixtureDesc, CourseUpdateDesc
......@@ -417,7 +417,6 @@ class PayAndVerifyTest(EventsTestMixin, UniqueCourseTest):
"""
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.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')
......@@ -443,17 +442,7 @@ class PayAndVerifyTest(EventsTestMixin, UniqueCourseTest):
# Create a user and log them in
student_id = AutoAuthPage(self.browser).visit().get_user_id()
# 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()
enroll_user_track(self.browser, self.course_id, 'verified')
# Proceed to verification
self.payment_and_verification_flow.immediate_verification()
......@@ -480,17 +469,7 @@ class PayAndVerifyTest(EventsTestMixin, UniqueCourseTest):
# Create a user and log them in
student_id = AutoAuthPage(self.browser).visit().get_user_id()
# 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()
enroll_user_track(self.browser, self.course_id, 'verified')
# Navigate to the dashboard
self.dashboard_page.visit()
......
......@@ -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.pages.lms.dashboard import DashboardPage
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, FakePaymentPage
from common.test.acceptance.pages.lms.pay_and_verify import PaymentAndVerificationFlow
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.fixtures.certificates import CertificateConfigFixture
......@@ -247,13 +247,6 @@ class ProctoredExamsTest(BaseInstructorDashboardTest):
)
).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.problem_page = ProblemPage(self.browser)
......@@ -279,19 +272,7 @@ class ProctoredExamsTest(BaseInstructorDashboardTest):
"""
self._auto_auth(self.USERNAME, self.EMAIL, False)
# 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()
enroll_user_track(self.browser, self.course_id, 'verified')
def _create_a_proctored_exam_and_attempt(self):
"""
......
......@@ -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.lms.courseware import CoursewarePage
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 xmodule.partitions.partitions import Group
......@@ -231,6 +233,56 @@ class ContentGroupConfigurationTest(StudioCourseTest):
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)
class AdvancedSettingsValidationTest(StudioCourseTest):
"""
......
......@@ -39,6 +39,7 @@ login = getting_started/index.html
register = getting_started/index.html
content_libraries = course_components/libraries.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
container = developing_course/course_components.html#components-that-contain-other-components
video = video/video_uploads.html
......
......@@ -135,6 +135,7 @@
.entry-title {
padding-bottom: 8px;
margin-bottom: 22px;
margin-top: 0;
border-bottom: 1px solid $light-gray;
font-size: 1.6em;
font-weight: bold;
......
......@@ -45,17 +45,11 @@ class EnrollmentTrackUserPartition(UserPartition):
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):
"""
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.
"""
......@@ -116,7 +110,7 @@ class EnrollmentTrackPartitionScheme(object):
Any group access rule referencing inactive partitions will be ignored
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):
......
......@@ -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, "Credit Mode"))
def test_to_json_not_supported(self):
user_partition = create_enrollment_track_partition(self.course)
with self.assertRaises(TypeError):
user_partition.to_json()
def test_to_json_supported(self):
user_partition_json = create_enrollment_track_partition(self.course).to_json()
self.assertEqual('Test Enrollment Track Partition', user_partition_json['name'])
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):
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