From 59c62188859be0e71ad02d31e8c529012f3aba55 Mon Sep 17 00:00:00 2001 From: "Albert St. Aubin" Date: Thu, 30 Mar 2017 09:08:27 -0400 Subject: [PATCH] Show Enrollment Tracks in Group Configurations. TNL-6743 --- .../contentstore/course_group_config.py | 79 ++++++------ .../contentstore/courseware_index.py | 2 +- cms/djangoapps/contentstore/views/course.py | 26 +++- .../views/tests/test_group_configurations.py | 21 +++- .../contentstore/views/tests/test_item.py | 2 +- cms/lib/xblock/test/test_authoring_mixin.py | 2 +- .../js/factories/group_configurations.js | 25 ++-- .../js/spec/views/group_configuration_spec.js | 30 ++--- .../views/pages/group_configurations_spec.js | 7 +- cms/static/js/views/content_group_list.js | 28 ----- cms/static/js/views/list.js | 9 +- cms/static/js/views/list_item.js | 1 + .../js/views/pages/group_configurations.js | 53 ++++++-- ..._details.js => partition_group_details.js} | 20 +-- ..._group_item.js => partition_group_item.js} | 21 ++-- cms/static/js/views/partition_group_list.js | 33 +++++ cms/templates/group_configurations.html | 74 ++++++----- cms/templates/js/list.underscore | 2 +- ...ore => partition-group-details.underscore} | 45 ++++--- .../xmodule/partitions/partitions_service.py | 2 +- common/test/acceptance/pages/common/utils.py | 44 +++++++ .../acceptance/pages/lms/track_selection.py | 18 +-- .../pages/studio/component_editor.py | 2 +- .../studio/settings_group_configurations.py | 11 ++ common/test/acceptance/tests/lms/test_lms.py | 27 +--- .../lms/test_lms_instructor_dashboard.py | 25 +--- .../tests/studio/test_studio_settings.py | 52 ++++++++ .../tests/test_cohorted_courseware.py | 115 ++++++++++++++---- docs/cms_config.ini | 1 + lms/static/sass/course/wiki/_wiki.scss | 1 + .../partition_scheme.py | 14 +-- .../tests/test_partition_scheme.py | 9 +- 32 files changed, 526 insertions(+), 275 deletions(-) delete mode 100644 cms/static/js/views/content_group_list.js rename cms/static/js/views/{content_group_details.js => partition_group_details.js} (78%) rename cms/static/js/views/{content_group_item.js => partition_group_item.js} (53%) create mode 100644 cms/static/js/views/partition_group_list.js rename cms/templates/js/{content-group-details.underscore => partition-group-details.underscore} (52%) diff --git a/cms/djangoapps/contentstore/course_group_config.py b/cms/djangoapps/contentstore/course_group_config.py index aaae31bf42..6bcbee47a7 100644 --- a/cms/djangoapps/contentstore/course_group_config.py +++ b/cms/djangoapps/contentstore/course_group_config.py @@ -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 diff --git a/cms/djangoapps/contentstore/courseware_index.py b/cms/djangoapps/contentstore/courseware_index.py index 99e0052e2e..ea4b819283 100644 --- a/cms/djangoapps/contentstore/courseware_index.py +++ b/cms/djangoapps/contentstore/courseware_index.py @@ -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, diff --git a/cms/djangoapps/contentstore/views/course.py b/cms/djangoapps/contentstore/views/course.py index 4b883b0176..e3a5e45cea 100644 --- a/cms/djangoapps/contentstore/views/course.py +++ b/cms/djangoapps/contentstore/views/course.py @@ -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': diff --git a/cms/djangoapps/contentstore/views/tests/test_group_configurations.py b/cms/djangoapps/contentstore/views/tests/test_group_configurations.py index 8aafc88a8e..45f82f5c83 100644 --- a/cms/djangoapps/contentstore/views/tests/test_group_configurations.py +++ b/cms/djangoapps/contentstore/views/tests/test_group_configurations.py @@ -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) diff --git a/cms/djangoapps/contentstore/views/tests/test_item.py b/cms/djangoapps/contentstore/views/tests/test_item.py index e53e8937e4..43df60059e 100644 --- a/cms/djangoapps/contentstore/views/tests/test_item.py +++ b/cms/djangoapps/contentstore/views/tests/test_item.py @@ -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": [ { diff --git a/cms/lib/xblock/test/test_authoring_mixin.py b/cms/lib/xblock/test/test_authoring_mixin.py index b16eae9092..c45270c70a 100644 --- a/cms/lib/xblock/test/test_authoring_mixin.py +++ b/cms/lib/xblock/test/test_authoring_mixin.py @@ -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() diff --git a/cms/static/js/factories/group_configurations.js b/cms/static/js/factories/group_configurations.js index 32ab12172a..424f60a50c 100644 --- a/cms/static/js/factories/group_configurations.js +++ b/cms/static/js/factories/group_configurations.js @@ -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(); }; }); diff --git a/cms/static/js/spec/views/group_configuration_spec.js b/cms/static/js/spec/views/group_configuration_spec.js index 3e6bc8d390..f90934b2b6 100644 --- a/cms/static/js/spec/views/group_configuration_spec.js +++ b/cms/static/js/spec/views/group_configuration_spec.js @@ -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' ); }); diff --git a/cms/static/js/spec/views/pages/group_configurations_spec.js b/cms/static/js/spec/views/pages/group_configurations_spec.js index 187c648c97..204b25c4fb 100644 --- a/cms/static/js/spec/views/pages/group_configurations_spec.js +++ b/cms/static/js/spec/views/pages/group_configurations_spec.js @@ -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?'); }); diff --git a/cms/static/js/views/content_group_list.js b/cms/static/js/views/content_group_list.js deleted file mode 100644 index 8f5e420a96..0000000000 --- a/cms/static/js/views/content_group_list.js +++ /dev/null @@ -1,28 +0,0 @@ -/** - * This class defines a list view for content 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) { - 'use strict'; - - var ContentGroupListView = ListView.extend({ - tagName: 'div', - - className: 'content-group-list', - - // Translators: This refers to a content group that can be linked to a student cohort. - itemCategoryDisplayName: gettext('content group'), - - newItemMessage: gettext('Add your first content group'), - - emptyMessage: gettext('You have not created any content groups yet.'), - - createItemView: function(options) { - return new ContentGroupItemView(options); - } - }); - - return ContentGroupListView; -}); diff --git a/cms/static/js/views/list.js b/cms/static/js/views/list.js index a0cd130a0b..7ec03927f2 100644 --- a/cms/static/js/views/list.js +++ b/cms/static/js/views/list.js @@ -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; diff --git a/cms/static/js/views/list_item.js b/cms/static/js/views/list_item.js index 692fc2c54f..e51c9caede 100644 --- a/cms/static/js/views/list_item.js +++ b/cms/static/js/views/list_item.js @@ -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); }, diff --git a/cms/static/js/views/pages/group_configurations.js b/cms/static/js/views/pages/group_configurations.js index 69b0b779f1..9da77189bc 100644 --- a/cms/static/js/views/pages/group_configurations.js +++ b/cms/static/js/views/pages/group_configurations.js @@ -1,11 +1,15 @@ 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(); })); diff --git a/cms/static/js/views/content_group_details.js b/cms/static/js/views/partition_group_details.js similarity index 78% rename from cms/static/js/views/content_group_details.js rename to cms/static/js/views/partition_group_details.js index 562b7073c0..bfd96808b7 100644 --- a/cms/static/js/views/content_group_details.js +++ b/cms/static/js/views/partition_group_details.js @@ -1,5 +1,5 @@ /** - * 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; }); diff --git a/cms/static/js/views/content_group_item.js b/cms/static/js/views/partition_group_item.js similarity index 53% rename from cms/static/js/views/content_group_item.js rename to cms/static/js/views/partition_group_item.js index 0b647d3435..73d7668f92 100644 --- a/cms/static/js/views/content_group_item.js +++ b/cms/static/js/views/partition_group_item.js @@ -1,22 +1,23 @@ /** - * 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; }); diff --git a/cms/static/js/views/partition_group_list.js b/cms/static/js/views/partition_group_list.js new file mode 100644 index 0000000000..602889e1b7 --- /dev/null +++ b/cms/static/js/views/partition_group_list.js @@ -0,0 +1,33 @@ +/** + * This class defines a list view for partition groups. + * It is expected to be backed by a Group collection. + */ +define([ + 'underscore', 'js/views/list', 'js/views/partition_group_item', 'gettext' +], function(_, ListView, PartitionGroupItemView, gettext) { + 'use strict'; + + var PartitionGroupListView = ListView.extend({ + initialize: function(options) { + ListView.prototype.initialize.apply(this, [options]); + this.scheme = options.scheme; + }, + + tagName: 'div', + + className: 'partition-group-list', + + // Translators: This refers to a content group that can be linked to a student cohort. + itemCategoryDisplayName: gettext('content group'), + + newItemMessage: gettext('Add your first content group'), + + emptyMessage: gettext('You have not created any content groups yet.'), + + createItemView: function(options) { + return new PartitionGroupItemView(_.extend({}, options, {scheme: this.scheme})); + } + }); + + return PartitionGroupListView; +}); diff --git a/cms/templates/group_configurations.html b/cms/templates/group_configurations.html index e883d2fed9..c8f06d7207 100644 --- a/cms/templates/group_configurations.html +++ b/cms/templates/group_configurations.html @@ -1,6 +1,7 @@ <%page expression_filter="h"/> <%inherit file="base.html" /> <%def name="content_groups_help_token()"><% return "content_groups" %> +<%def name="enrollment_track_help_token()"><% return "enrollment_tracks" %> <%def name="experiment_group_configurations_help_token()"><% return "group_configurations" %> <%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 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"]: @@ -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} ); }); @@ -47,37 +49,52 @@ from openedx.core.djangolib.markup import HTML, Text
-
-
-

${_("Content Groups")}

-
-

${_("Loading")}

-
-
- % if should_show_experiment_groups: -
-

${_("Experiment Group Configurations")}

- % if experiment_group_configurations is None: -
-

- ${_("This module is disabled at the moment.")} -

-
- % else: -
-

${_("Loading")}

-
- % endif -
- % endif +
+ + % for config in all_group_configurations: +
+

${config['name']}

+
+

${_("Loading")}

+
+
+ % endfor + + % if should_show_experiment_groups: +
+

${_("Experiment Group Configurations")}

+ % if experiment_group_configurations is None: +
+

+ ${_("This module is disabled at the moment.")} +

+
+ % else: +
+

${_("Loading")}

+
+ % endif +
+ % endif