${config['name']}
-${_("Loading")}
-${_("Experiment Group Configurations")}
- % if experiment_group_configurations is None: -- ${_("This module is disabled at the moment.")} -
-${_("Loading")}
-diff --git a/cms/djangoapps/contentstore/rest_api/v1/serializers/course_waffle_flags.py b/cms/djangoapps/contentstore/rest_api/v1/serializers/course_waffle_flags.py index 3a03f2eff244..0f6f3d6ebe1e 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/serializers/course_waffle_flags.py +++ b/cms/djangoapps/contentstore/rest_api/v1/serializers/course_waffle_flags.py @@ -193,8 +193,7 @@ def get_use_new_group_configurations_page(self, obj): """ Method to get the use_new_group_configurations_page switch """ - course_key = self.get_course_key() - return toggles.use_new_group_configurations_page(course_key) + return True def get_enable_course_optimizer(self, obj): """ diff --git a/cms/djangoapps/contentstore/toggles.py b/cms/djangoapps/contentstore/toggles.py index 064c9e7aec0a..5ec301e9b57f 100644 --- a/cms/djangoapps/contentstore/toggles.py +++ b/cms/djangoapps/contentstore/toggles.py @@ -255,24 +255,6 @@ def use_new_unit_page(course_key): -# .. toggle_name: legacy_studio.configurations -# .. toggle_implementation: WaffleFlag -# .. toggle_default: False -# .. toggle_description: Temporarily fall back to the old Studio Configurations page. -# .. toggle_use_cases: temporary -# .. toggle_creation_date: 2025-03-14 -# .. toggle_target_removal_date: 2025-09-14 -# .. toggle_tickets: https://github.com/openedx/edx-platform/issues/36275 -# .. toggle_warning: In Ulmo, this toggle will be removed. Only the new (React-based) experience will be available. -LEGACY_STUDIO_CONFIGURATIONS = CourseWaffleFlag('legacy_studio.configurations', __name__) - - -def use_new_group_configurations_page(course_key): - """ - Returns a boolean if new studio group configurations mfe is enabled - """ - return not LEGACY_STUDIO_CONFIGURATIONS.is_enabled(course_key) - # .. toggle_name: contentstore.mock_video_uploads # .. toggle_implementation: WaffleFlag diff --git a/cms/djangoapps/contentstore/utils.py b/cms/djangoapps/contentstore/utils.py index fa81de76b488..82583de88ad1 100644 --- a/cms/djangoapps/contentstore/utils.py +++ b/cms/djangoapps/contentstore/utils.py @@ -42,7 +42,6 @@ libraries_v2_enabled, split_library_view_on_dashboard, use_new_export_page, - use_new_group_configurations_page, use_new_import_page, use_new_unit_page, ) @@ -475,13 +474,8 @@ def get_group_configurations_url(course_locator) -> str: """ Gets course authoring microfrontend URL for group configurations page view. """ - group_configurations_url = None - if use_new_group_configurations_page(course_locator): - mfe_base_url = get_course_authoring_url(course_locator) - course_mfe_url = f'{mfe_base_url}/course/{course_locator}/group_configurations' - if mfe_base_url: - group_configurations_url = course_mfe_url - return group_configurations_url + mfe_base_url = get_course_authoring_url(course_locator) + return f'{mfe_base_url}/course/{course_locator}/group_configurations' if mfe_base_url else None def get_custom_pages_url(course_locator) -> str: diff --git a/cms/djangoapps/contentstore/views/course.py b/cms/djangoapps/contentstore/views/course.py index b84e7d9ca7b8..569871b95bea 100644 --- a/cms/djangoapps/contentstore/views/course.py +++ b/cms/djangoapps/contentstore/views/course.py @@ -98,7 +98,6 @@ from ..tasks import rerun_course as rerun_course_task from ..toggles import ( default_enable_flexible_peer_openassessments, - use_new_group_configurations_page, ) from ..utils import ( add_instructor, @@ -106,7 +105,6 @@ get_course_outline_url, get_course_rerun_context, get_grading_url, - get_group_configurations_context, get_group_configurations_url, get_lms_link_for_item, get_schedule_details_url, @@ -1843,10 +1841,7 @@ def group_configurations_list_handler(request, course_key_string): course = get_course_and_check_manage_group_configurations_access(course_key, request.user) if 'text/html' in request.META.get('HTTP_ACCEPT', 'text/html'): - if use_new_group_configurations_page(course_key): - return redirect(get_group_configurations_url(course_key)) - group_configurations_context = get_group_configurations_context(course, store) - return render_to_response('group_configurations.html', group_configurations_context) + return redirect(get_group_configurations_url(course_key)) elif "application/json" in request.META.get('HTTP_ACCEPT'): if request.method == 'POST': # create a new group configuration for the course diff --git a/cms/djangoapps/contentstore/views/tests/test_exam_settings_view.py b/cms/djangoapps/contentstore/views/tests/test_exam_settings_view.py index 8f545f2c8166..6a746b8d67ec 100644 --- a/cms/djangoapps/contentstore/views/tests/test_exam_settings_view.py +++ b/cms/djangoapps/contentstore/views/tests/test_exam_settings_view.py @@ -1,18 +1,14 @@ """ Exam Settings View Tests """ -import ddt from django.conf import settings from django.test.utils import override_settings -from edx_toggles.toggles.testutils import override_waffle_flag -from cms.djangoapps.contentstore import toggles from cms.djangoapps.contentstore.tests.utils import CourseTestCase from cms.djangoapps.contentstore.utils import reverse_course_url from common.djangoapps.util.testing import UrlResetMixin -@ddt.ddt @override_settings( FEATURES={ **settings.FEATURES, @@ -20,7 +16,6 @@ "ENABLE_PROCTORED_EXAMS": True, }, ) -@override_waffle_flag(toggles.LEGACY_STUDIO_CONFIGURATIONS, True) @override_settings(COURSE_AUTHORING_MICROFRONTEND_URL='https://mfe.example') class TestExamSettingsView(CourseTestCase, UrlResetMixin): """ @@ -33,33 +28,6 @@ def setUp(self): super().setUp() self.reset_urls() - @override_waffle_flag(toggles.LEGACY_STUDIO_EXAM_SETTINGS, True) - @ddt.data( - "group_configurations_list_handler", - ) - def test_view_without_exam_settings_enabled(self, handler): - """ - Tests pages should not have `Exam Settings` item - if course does not have the Exam Settings view enabled. - """ - outline_url = reverse_course_url(handler, self.course.id) - resp = self.client.get(outline_url, HTTP_ACCEPT='text/html') - self.assertEqual(resp.status_code, 200) # noqa: PT009 - self.assertNotContains(resp, 'Proctored Exam Settings') - - @ddt.data( - "group_configurations_list_handler", - ) - def test_view_with_exam_settings_enabled(self, handler): - """ - Tests pages should have `Exam Settings` item - if course does have Exam Settings view enabled. - """ - outline_url = reverse_course_url(handler, self.course.id) - resp = self.client.get(outline_url, HTTP_ACCEPT='text/html') - self.assertEqual(resp.status_code, 200) # noqa: PT009 - self.assertContains(resp, 'Proctored Exam Settings') - def test_grading_handler_redirects_to_mfe(self): """grading_handler redirects to the authoring MFE.""" url = reverse_course_url('grading_handler', self.course.id) @@ -83,3 +51,9 @@ def test_advanced_settings_handler_redirects_to_mfe(self): url = reverse_course_url('advanced_settings_handler', self.course.id) resp = self.client.get(url, HTTP_ACCEPT='text/html') self.assertEqual(resp.status_code, 302) # noqa: PT009 + + def test_group_configurations_list_handler_redirects_to_mfe(self): + """group_configurations_list_handler redirects to the authoring MFE.""" + url = reverse_course_url('group_configurations_list_handler', self.course.id) + resp = self.client.get(url, HTTP_ACCEPT='text/html') + self.assertEqual(resp.status_code, 302) # noqa: PT009 diff --git a/cms/djangoapps/contentstore/views/tests/test_group_configurations.py b/cms/djangoapps/contentstore/views/tests/test_group_configurations.py index c8af13343aa0..dcf9e18a52d3 100644 --- a/cms/djangoapps/contentstore/views/tests/test_group_configurations.py +++ b/cms/djangoapps/contentstore/views/tests/test_group_configurations.py @@ -9,14 +9,11 @@ import ddt from django.test import Client -from edx_toggles.toggles.testutils import override_waffle_flag from openedx_authz.constants.roles import COURSE_DATA_RESEARCHER, COURSE_STAFF from rest_framework import status -from cms.djangoapps.contentstore import toggles from cms.djangoapps.contentstore.api.tests.base import BaseCourseViewTest from cms.djangoapps.contentstore.course_group_config import ( - CONTENT_GROUP_CONFIGURATION_NAME, ENROLLMENT_SCHEME, GroupConfiguration, ) @@ -274,27 +271,12 @@ def _url(self): """ return reverse_course_url('group_configurations_list_handler', self.course.id) - @override_waffle_flag(toggles.LEGACY_STUDIO_CONFIGURATIONS, True) def test_view_index_ok(self): """ - Basic check that the groups configuration page responds correctly. + Basic check that the groups configuration page redirects to the MFE. """ - - # This creates a random UserPartition. - self.course.user_partitions = [ - UserPartition(0, 'First name', 'First description', [Group(0, 'Group A'), Group(1, 'Group B'), Group(2, 'Group C')]), # pylint: disable=line-too-long - ] - self.save_course() - - if 'split_test' not in self.course.advanced_modules: - self.course.advanced_modules.append('split_test') - self.store.update_item(self.course, self.user.id) - response = self.client.get(self._url()) - self.assertEqual(response.status_code, 200) # noqa: PT009 - self.assertContains(response, 'First name', count=1) - self.assertContains(response, 'Group C') - self.assertContains(response, CONTENT_GROUP_CONFIGURATION_NAME) + self.assertEqual(response.status_code, 302) # noqa: PT009 def test_unsupported_http_accept_header(self): """ diff --git a/cms/static/cms/js/build.js b/cms/static/cms/js/build.js index 764cf870fd1c..ff746bbae093 100644 --- a/cms/static/cms/js/build.js +++ b/cms/static/cms/js/build.js @@ -21,7 +21,6 @@ 'js/factories/base', 'js/factories/course_create_rerun', 'js/factories/export', - 'js/factories/group_configurations', 'js/factories/index', 'js/factories/outline', ]), diff --git a/cms/static/cms/js/spec/main.js b/cms/static/cms/js/spec/main.js index c002b48f4668..de76a6dda5a5 100644 --- a/cms/static/cms/js/spec/main.js +++ b/cms/static/cms/js/spec/main.js @@ -250,13 +250,11 @@ 'js/spec/views/assets_spec', 'js/spec/views/baseview_spec', 'js/spec/views/paged_container_spec', - 'js/spec/views/group_configuration_spec', 'js/spec/views/unit_outline_spec', 'js/spec/views/xblock_spec', 'js/spec/views/xblock_validation_spec', 'js/spec/views/license_spec', 'js/spec/views/paging_spec', - 'js/spec/views/pages/group_configurations_spec', 'js/spec/views/pages/course_rerun_spec', 'js/spec/views/pages/index_spec', 'js/spec/views/pages/library_users_spec', diff --git a/cms/static/cms/js/spec/main_squire.js b/cms/static/cms/js/spec/main_squire.js index 8feb05692273..ace0f36b1f2d 100644 --- a/cms/static/cms/js/spec/main_squire.js +++ b/cms/static/cms/js/spec/main_squire.js @@ -192,8 +192,7 @@ testFiles = [ 'js/spec/views/assets_squire_spec', 'js/spec/video/translations_editor_spec', - 'js/spec/video/file_uploader_editor_spec', - 'js/spec/models/group_configuration_spec' + 'js/spec/video/file_uploader_editor_spec' ]; i = 0; diff --git a/cms/static/js/collections/group_configuration.js b/cms/static/js/collections/group_configuration.js deleted file mode 100644 index 29dc8e50319e..000000000000 --- a/cms/static/js/collections/group_configuration.js +++ /dev/null @@ -1,12 +0,0 @@ -define([ - 'backbone', 'js/models/group_configuration' -], -function(Backbone, GroupConfigurationModel) { - 'use strict'; - - var GroupConfigurationCollection = Backbone.Collection.extend({ - model: GroupConfigurationModel - }); - - return GroupConfigurationCollection; -}); diff --git a/cms/static/js/factories/group_configurations.js b/cms/static/js/factories/group_configurations.js deleted file mode 100644 index 648ad78f0be5..000000000000 --- a/cms/static/js/factories/group_configurations.js +++ /dev/null @@ -1,35 +0,0 @@ -define([ - 'js/collections/group_configuration', 'js/models/group_configuration', 'js/views/pages/group_configurations' -], function(GroupConfigurationCollection, GroupConfigurationModel, GroupConfigurationsPage) { - 'use strict'; - - return function(experimentsEnabled, - experimentGroupConfigurationsJson, - allGroupConfigurationJson, - groupConfigurationUrl, - courseOutlineUrl) { - var experimentGroupConfigurations = new GroupConfigurationCollection( - experimentGroupConfigurationsJson, {parse: 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; - new GroupConfigurationsPage({ - el: $('#content'), - experimentsEnabled: experimentsEnabled, - experimentGroupConfigurations: experimentGroupConfigurations, - allGroupConfigurations: allGroupConfigurations - }).render(); - }; -}); diff --git a/cms/static/js/models/group_configuration.js b/cms/static/js/models/group_configuration.js deleted file mode 100644 index 4fc87c89e8e7..000000000000 --- a/cms/static/js/models/group_configuration.js +++ /dev/null @@ -1,136 +0,0 @@ -define([ - 'backbone', 'underscore', 'gettext', 'js/models/group', 'js/collections/group', - 'backbone.associations', 'cms/js/main' -], -function(Backbone, _, gettext, GroupModel, GroupCollection) { - 'use strict'; - - var GroupConfiguration = Backbone.AssociatedModel.extend({ - defaults: function() { - return { - name: '', - scheme: 'random', - description: '', - version: 2, - groups: new GroupCollection([ - { - name: gettext('Group A'), - order: 0 - }, - { - name: gettext('Group B'), - order: 1 - } - ]), - showGroups: false, - editing: false, - usage: [], - read_only: false - }; - }, - - relations: [{ - type: Backbone.Many, - key: 'groups', - relatedModel: GroupModel, - collectionType: GroupCollection - }], - - initialize: function(attributes, options) { - this.on('remove:groups', this.groupRemoved); - - this.canBeEmpty = options && options.canBeEmpty; - this.setOriginalAttributes(); - - return this; - }, - - setOriginalAttributes: function() { - this._originalAttributes = this.parse(this.toJSON()); - }, - - reset: function() { - this.set(this._originalAttributes, {parse: true, validate: true}); - }, - - isDirty: function() { - return !_.isEqual( - this._originalAttributes, this.parse(this.toJSON()) - ); - }, - - isEmpty: function() { - return !this.get('name') && this.get('groups').isEmpty(); - }, - - parse: function(response) { - var attrs = $.extend(true, {}, response); - - _.each(attrs.groups, function(group, index) { - group.order = group.order || index; - }); - - return attrs; - }, - - toJSON: function() { - return { - id: this.get('id'), - name: this.get('name'), - scheme: this.get('scheme'), - description: this.get('description'), - version: this.get('version'), - groups: this.get('groups').toJSON(), - read_only: this.get('read_only') - }; - }, - - validate: function(attrs) { - if (!attrs.name.trim()) { - return { - message: gettext('Group Configuration name is required.'), - attributes: {name: true} - }; - } - - if (!this.canBeEmpty && attrs.groups.length < 1) { - return { - message: gettext('There must be at least one group.'), - attributes: {groups: true} - }; - } else { - // validate all groups - var validGroups = new Backbone.Collection(), - invalidGroups = new Backbone.Collection(); - attrs.groups.each(function(group) { - if (!group.isValid()) { - invalidGroups.add(group); - } else { - validGroups.add(group); - } - }); - - if (!invalidGroups.isEmpty()) { - return { - message: gettext('All groups must have a name.'), - attributes: {groups: invalidGroups.toJSON()} - }; - } - - var groupNames = validGroups.map(function(group) { return group.get('name'); }); - if (groupNames.length !== _.uniq(groupNames).length) { - return { - message: gettext('All groups must have a unique name.'), - attributes: {groups: validGroups.toJSON()} - }; - } - } - }, - - groupRemoved: function() { - this.setOriginalAttributes(); - } - }); - - return GroupConfiguration; -}); diff --git a/cms/static/js/spec/models/group_configuration_spec.js b/cms/static/js/spec/models/group_configuration_spec.js deleted file mode 100644 index 2206904bfa81..000000000000 --- a/cms/static/js/spec/models/group_configuration_spec.js +++ /dev/null @@ -1,337 +0,0 @@ -define([ - 'backbone', 'cms/js/main', 'js/models/group_configuration', - 'js/models/group', 'js/collections/group', 'squire' -], function(Backbone, main, GroupConfigurationModel, GroupModel, GroupCollection, Squire) { - 'use strict'; - - describe('GroupConfigurationModel', function() { - beforeEach(function() { - main(); - this.model = new GroupConfigurationModel(); - }); - - describe('Basic', function() { - it('should have an empty name by default', function() { - expect(this.model.get('name')).toEqual(''); - }); - - it('should have an empty description by default', function() { - expect(this.model.get('description')).toEqual(''); - }); - - it('should not show groups by default', function() { - expect(this.model.get('showGroups')).toBeFalsy(); - }); - - it('should have a collection with 2 groups by default', function() { - var groups = this.model.get('groups'); - - expect(groups).toBeInstanceOf(GroupCollection); - expect(groups.at(0).get('name')).toBe('Group A'); - expect(groups.at(1).get('name')).toBe('Group B'); - }); - - it('should have an empty usage by default', function() { - expect(this.model.get('usage').length).toBe(0); - }); - - it('should be able to reset itself', function() { - var originalName = 'Original Name', - model = new GroupConfigurationModel({name: originalName}); - model.set({name: 'New Name'}); - model.reset(); - - expect(model.get('name')).toEqual(originalName); - }); - - it('should be dirty after it\'s been changed', function() { - this.model.set('name', 'foobar'); - - expect(this.model.isDirty()).toBeTruthy(); - }); - - describe('should not be dirty', function() { - it('by default', function() { - expect(this.model.isDirty()).toBeFalsy(); - }); - - it('after calling setOriginalAttributes', function() { - this.model.set('name', 'foobar'); - this.model.setOriginalAttributes(); - - expect(this.model.isDirty()).toBeFalsy(); - }); - }); - }); - - describe('Input/Output', function() { - var deepAttributes = function(obj) { - if (obj instanceof Backbone.Model) { - return deepAttributes(obj.attributes); - } else if (obj instanceof Backbone.Collection) { - return obj.map(deepAttributes); - } else if ($.isPlainObject(obj)) { - var attributes = {}; - - for (var prop in obj) { - if (obj.hasOwnProperty(prop)) { - attributes[prop] = deepAttributes(obj[prop]); - } - } - return attributes; - } else { - return obj; - } - }; - - it('should match server model to client model', function() { - var serverModelSpec = { - id: 10, - name: 'My Group Configuration', - description: 'Some description', - version: 2, - scheme: 'random', - groups: [ - { - version: 1, - name: 'Group 1', - usage: [] - }, { - version: 1, - name: 'Group 2', - usage: [] - } - ], - read_only: true - }, - clientModelSpec = { - id: 10, - name: 'My Group Configuration', - description: 'Some description', - scheme: 'random', - showGroups: false, - editing: false, - version: 2, - groups: [ - { - version: 1, - order: 0, - name: 'Group 1', - usage: [] - }, { - version: 1, - order: 1, - name: 'Group 2', - usage: [] - } - ], - usage: [], - read_only: true - }, - model = new GroupConfigurationModel( - serverModelSpec, {parse: true} - ); - - expect(deepAttributes(model)).toEqual(clientModelSpec); - expect(JSON.parse(JSON.stringify(model))).toEqual(serverModelSpec); - }); - }); - - describe('Validation', function() { - it('requires a name', function() { - var model = new GroupConfigurationModel({name: ''}); - - expect(model.isValid()).toBeFalsy(); - }); - - it('can pass validation', function() { - // Note that two groups - Group A and Group B - are - // created by default. - var model = new GroupConfigurationModel({name: 'foo'}); - - expect(model.isValid()).toBeTruthy(); - }); - - it('requires at least one group', function() { - var group1 = new GroupModel({name: 'Group A'}), - model = new GroupConfigurationModel({name: 'foo', groups: []}); - - expect(model.isValid()).toBeFalsy(); - - model.get('groups').add(group1); - expect(model.isValid()).toBeTruthy(); - }); - - it('requires a valid group', function() { - var model = new GroupConfigurationModel({name: 'foo', groups: [{name: ''}]}); - - expect(model.isValid()).toBeFalsy(); - }); - - it('requires all groups to be valid', function() { - var model = new GroupConfigurationModel({name: 'foo', groups: [{name: 'Group A'}, {name: ''}]}); - - expect(model.isValid()).toBeFalsy(); - }); - - it('requires all groups to have unique names', function() { - var model = new GroupConfigurationModel({ - name: 'foo', groups: [{name: 'Group A'}, {name: 'Group A'}] - }); - - expect(model.isValid()).toBeFalsy(); - }); - }); - }); - - describe('GroupModel', function() { - beforeEach(function() { - this.collection = new GroupCollection([{}]); - this.model = this.collection.at(0); - }); - - describe('Basic', function() { - it('should have an empty name by default', function() { - expect(this.model.get('name')).toEqual(''); - }); - - it('should be empty by default', function() { - expect(this.model.isEmpty()).toBeTruthy(); - }); - }); - - describe('Validation', function() { - it('requires a name', function() { - var model = new GroupModel({name: ''}); - - expect(model.isValid()).toBeFalsy(); - }); - - it('can pass validation', function() { - var model = new GroupConfigurationModel({name: 'foo'}); - - expect(model.isValid()).toBeTruthy(); - }); - }); - }); - - describe('GroupCollection', function() { - beforeEach(function() { - this.collection = new GroupCollection(); - }); - - it('is empty by default', function() { - expect(this.collection.isEmpty()).toBeTruthy(); - }); - - it('is empty if all groups are empty', function() { - this.collection.add([{name: ''}, {name: ''}, {name: ''}]); - - expect(this.collection.isEmpty()).toBeTruthy(); - }); - - it('is not empty if a group is not empty', function() { - this.collection.add([ - {name: ''}, {name: 'full'}, {name: ''} - ]); - - expect(this.collection.isEmpty()).toBeFalsy(); - }); - - describe('getGroupId', function() { - var collection, injector, mockGettext, initializeGroupModel, cleanUp; - - mockGettext = function(returnedValue) { - // eslint-disable-next-line no-shadow - var injector = new Squire(); - - injector.mock('gettext', function() { - return function() { - return returnedValue; - }; - }); - - return injector; - }; - - initializeGroupModel = function(dict) { - var deferred = $.Deferred(); - - injector = mockGettext(dict); - injector.require(['js/collections/group'], - // eslint-disable-next-line no-shadow - function(GroupCollection) { - collection = new GroupCollection(); - deferred.resolve(collection); - }); - - return deferred.promise(); - }; - - cleanUp = function() { - collection = null; - injector.clean(); - injector.remove(); - }; - - it('returns correct ids', function() { - // eslint-disable-next-line no-shadow - var collection = new GroupCollection(); - - expect(collection.getGroupId(0)).toBe('A'); - expect(collection.getGroupId(1)).toBe('B'); - expect(collection.getGroupId(25)).toBe('Z'); - expect(collection.getGroupId(702)).toBe('AAA'); - expect(collection.getGroupId(704)).toBe('AAC'); - expect(collection.getGroupId(475253)).toBe('ZZZZ'); - expect(collection.getGroupId(475254)).toBe('AAAAA'); - expect(collection.getGroupId(475279)).toBe('AAAAZ'); - }); - - it('just 1 character in the dictionary', function(done) { - initializeGroupModel('1') - // eslint-disable-next-line no-shadow - .then(function(collection) { - expect(collection.getGroupId(0)).toBe('1'); - expect(collection.getGroupId(1)).toBe('11'); - expect(collection.getGroupId(5)).toBe('111111'); - }) - .always(function() { - cleanUp(); - done(); - }); - }); - - it('allow to use unicode characters in the dict', function(done) { - initializeGroupModel('ö诶úeœ') - // eslint-disable-next-line no-shadow - .then(function(collection) { - expect(collection.getGroupId(0)).toBe('ö'); - expect(collection.getGroupId(1)).toBe('诶'); - expect(collection.getGroupId(5)).toBe('öö'); - expect(collection.getGroupId(29)).toBe('œœ'); - expect(collection.getGroupId(30)).toBe('ööö'); - expect(collection.getGroupId(43)).toBe('öúe'); - }) - .always(function() { - cleanUp(); - done(); - }); - }); - - it('return initial value if dictionary is empty', function(done) { - initializeGroupModel('') - // eslint-disable-next-line no-shadow - .then(function(collection) { - expect(collection.getGroupId(0)).toBe('0'); - expect(collection.getGroupId(5)).toBe('5'); - expect(collection.getGroupId(30)).toBe('30'); - }) - .always(function() { - cleanUp(); - done(); - }); - }); - }); - }); -}); diff --git a/cms/static/js/spec/views/group_configuration_spec.js b/cms/static/js/spec/views/group_configuration_spec.js deleted file mode 100644 index d05be0f1515d..000000000000 --- a/cms/static/js/spec/views/group_configuration_spec.js +++ /dev/null @@ -1,1079 +0,0 @@ -define([ - 'underscore', 'sinon', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers', 'common/js/spec_helpers/template_helpers', - '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/partition_group_list', 'js/views/partition_group_details', - 'js/views/content_group_editor', 'js/views/partition_group_item' -], function( - _, sinon, AjaxHelpers, TemplateHelpers, ViewHelpers, Course, GroupConfigurationModel, GroupModel, - GroupConfigurationCollection, GroupCollection, GroupConfigurationDetailsView, GroupConfigurationsListView, - GroupConfigurationEditorView, GroupConfigurationItemView, ExperimentGroupEditView, GroupList, - PartitionGroupDetailsView, ContentGroupEditorView, PartitionGroupItemView -) { - 'use strict'; - - // Wrap all specs in a single describe so that the beforeEach/afterEach below are - // scoped to this file only. Without this wrapper those hooks sit at the top level of - // the AMD factory, which Jasmine treats as global hooks — they run before/after every - // spec in the entire test suite, not just the ones in this file. That causes two problems: - // - // 1. sinon conflict: other spec files also call sinon.useFakeXMLHttpRequest() in their - // own beforeEach. In sinon v19, restoring one factory clears the .restore() method - // on all other active factories. When the global afterEach then calls - // xhrFactory.restore(), it throws "TypeError: xhrFactory.restore is not a function". - // - // 2. State leak: the global afterEach deletes window.course, corrupting tests in other - // spec files that set up window.course in their own beforeEach. - describe('Group Configurations', function() { - - // Pre-populate the jasmine-fixtures cache for every template used in the inner - // beforeEach blocks below. The outer beforeEach installs sinon.useFakeXMLHttpRequest(), - // which intercepts the synchronous XHR that readFixtures() uses to load .underscore - // files from the Karma server. Calling readFixtures() here — at describe-body level, - // before any beforeEach runs — loads the files over real XHR and caches the results. - // Subsequent readFixtures() calls inside beforeEach then return from cache without - // making a new (intercepted) request. - readFixtures( - 'group-configuration-details.underscore', - 'group-configuration-editor.underscore', - 'group-edit.underscore', - 'list.underscore', - 'content-group-editor.underscore', - 'partition-group-details.underscore' - ); - - var requests, xhrFactory; - var SELECTORS = { - detailsView: '.group-configuration-details', - editView: '.group-configuration-edit', - itemView: '.group-configurations-list-item', - group: '.group', - groupFields: '.groups-fields', - name: '.group-configuration-name', - description: '.group-configuration-description', - groupsCount: '.group-configuration-groups-count', - groupsAllocation: '.group-allocation', - errorMessage: '.group-configuration-edit-error', - inputGroupName: '.group-name', - inputName: '.collection-name-input', - inputDescription: '.group-configuration-description-input', - usageCount: '.group-configuration-usage-count', - usage: '.group-configuration-usage', - usageText: '.group-configuration-usage-text', - usageTextAnchor: '.group-configuration-usage-text > a', - usageUnit: '.group-configuration-usage-unit', - usageUnitAnchor: '.group-configuration-usage-unit a', - usageUnitMessage: '.group-configuration-validation-message', - usageUnitWarningIcon: '.group-configuration-usage-unit .fa-warning', - usageUnitErrorIcon: '.group-configuration-usage-unit .fa-exclamation-circle', - warningMessage: '.group-configuration-validation-text', - warningIcon: '.wrapper-group-configuration-validation > .fa-warning', - note: '.wrapper-delete-button' - }; - - var assertTheDetailsView = function(view, text) { - expect(view.$el).toContainText(text); - expect(view.$el).toContainText('ID: 0'); - expect(view.$('.delete')).toExist(); - }; - var assertShowEmptyUsages = function(view, usageText) { - expect(view.$(SELECTORS.usageCount)).not.toExist(); - expect(view.$(SELECTORS.usageText)).toContainText(usageText); - expect(view.$(SELECTORS.usageTextAnchor)).toExist(); - expect(view.$(SELECTORS.usageUnit)).not.toExist(); - }; - var assertHideEmptyUsages = function(view) { - expect(view.$(SELECTORS.usageText)).not.toExist(); - expect(view.$(SELECTORS.usageUnit)).not.toExist(); - expect(view.$(SELECTORS.usageCount)).toContainText('Not in Use'); - }; - var assertShowNonEmptyUsages = function(view, usageText, toolTipText) { - var usageUnitAnchors = view.$(SELECTORS.usageUnitAnchor); - - expect(view.$(SELECTORS.note)).toHaveAttr( - 'data-tooltip', toolTipText - ); - expect(view.$('.delete')).toHaveClass('is-disabled'); - expect(view.$(SELECTORS.usageCount)).not.toExist(); - expect(view.$(SELECTORS.usageText)).toContainText(usageText); - expect(view.$(SELECTORS.usageUnit).length).toBe(2); - expect(usageUnitAnchors.length).toBe(2); - expect(usageUnitAnchors.eq(0)).toContainText('label1'); - expect(usageUnitAnchors.eq(0).attr('href')).toBe('url1'); - expect(usageUnitAnchors.eq(1)).toContainText('label2'); - expect(usageUnitAnchors.eq(1).attr('href')).toBe('url2'); - }; - var assertHideNonEmptyUsages = function(view) { - expect(view.$('.delete')).toHaveClass('is-disabled'); - expect(view.$(SELECTORS.usageText)).not.toExist(); - expect(view.$(SELECTORS.usageUnit)).not.toExist(); - expect(view.$(SELECTORS.usageCount)).toContainText('Used in 2 locations'); - }; - var setUsageInfo = function(model) { - model.set('usage', [ - {label: 'label1', url: 'url1'}, - {label: 'label2', url: 'url2'} - ]); - }; - var assertHideValidationContent = function(view) { - expect(view.$(SELECTORS.usageUnitMessage)).not.toExist(); - expect(view.$(SELECTORS.usageUnitWarningIcon)).not.toExist(); - expect(view.$(SELECTORS.usageUnitErrorIcon)).not.toExist(); - }; - var assertControllerView = function(view, detailsView, editView) { - // Details view by default - expect(view.$(detailsView)).toExist(); - view.$('.action-edit .edit').click(); - expect(view.$(editView)).toExist(); - expect(view.$(detailsView)).not.toExist(); - view.$('.action-cancel').click(); - expect(view.$(detailsView)).toExist(); - expect(view.$(editView)).not.toExist(); - }; - var assertAndDeleteItemError = function(that, url, promptText) { - var promptSpy = ViewHelpers.createPromptSpy(), - notificationSpy = ViewHelpers.createNotificationSpy(); - - ViewHelpers.clickDeleteItem(that, promptSpy, promptText); - - ViewHelpers.patchAndVerifyRequest(requests, url, notificationSpy); - - AjaxHelpers.respondWithNoContent(requests); - ViewHelpers.verifyNotificationHidden(notificationSpy); - expect($(SELECTORS.itemView)).not.toExist(); - }; - var assertAndDeleteItemWithError = function(that, url, listItemView, promptText) { - var promptSpy = ViewHelpers.createPromptSpy(), - notificationSpy = ViewHelpers.createNotificationSpy(); - - ViewHelpers.clickDeleteItem(that, promptSpy, promptText); - ViewHelpers.patchAndVerifyRequest(requests, url, notificationSpy); - - AjaxHelpers.respondWithError(requests); - ViewHelpers.verifyNotificationShowing(notificationSpy, /Deleting/); - expect($(listItemView)).toExist(); - }; - var assertCannotDeleteUsed = function(that, toolTipText, warningText) { - setUsageInfo(that.model); - that.view.render(); - expect(that.view.$(SELECTORS.note)).toHaveAttr( - 'data-tooltip', toolTipText - ); - expect(that.view.$(SELECTORS.warningMessage)).toContainText(warningText); - expect(that.view.$(SELECTORS.warningIcon)).toExist(); - expect(that.view.$('.delete')).toHaveClass('is-disabled'); - }; - var assertUnusedOptions = function(that) { - that.model.set('usage', []); - that.view.render(); - expect(that.view.$(SELECTORS.warningMessage)).not.toExist(); - expect(that.view.$(SELECTORS.warningIcon)).not.toExist(); - }; - - beforeEach(function() { - xhrFactory = sinon.useFakeXMLHttpRequest(); - requests = []; - requests.currentIndex = 0; - requests.restore = function() { xhrFactory.restore(); }; - xhrFactory.onCreate = function(req) { requests.push(req); }; - window.course = new Course({ - id: '5', - name: 'Course Name', - url_name: 'course_name', - org: 'course_org', - num: 'course_num', - revision: 'course_rev' - }); - - jasmine.addMatchers({ - toContainText: function() { - return { - compare: function(actual, text) { - var trimmedText = $.trim(actual.text()), - passed; - - if (text && $.isFunction(text.test)) { - passed = text.test(trimmedText); - } else { - passed = trimmedText.indexOf(text) !== -1; - } - - return { - pass: passed - }; - } - }; - }, - toBeCorrectValuesInInputs: function() { - return { - compare: function(actual, values) { - var expected = { - name: actual.$(SELECTORS.inputName).val(), - description: actual - .$(SELECTORS.inputDescription).val() - }; - - var passed = _.isEqual(values, expected); - - return { - pass: passed - }; - } - }; - }, - toBeCorrectValuesInModel: function() { - return { - compare: function(actual, values) { - var passed = _.every(values, function(value, key) { - return actual.get(key) === value; - }); - - return { - pass: passed - }; - } - }; - }, - toHaveDefaultNames: function() { - return { - compare: function(actual, values) { - var actualValues = $.map(actual, function(item) { - return $(item).val(); - }); - - var passed = _.isEqual(actualValues, values); - - return { - pass: passed - }; - } - }; - } - }); - }); - - afterEach(function() { - requests.restore(); - delete window.course; - }); - - describe('Experiment group configurations details view', function() { - beforeEach(function() { - TemplateHelpers.installTemplate('group-configuration-details', true); - - this.model = new GroupConfigurationModel({ - name: 'Configuration', - description: 'Configuration Description', - id: 0 - }); - - this.collection = new GroupConfigurationCollection([this.model]); - this.collection.outlineUrl = '/outline'; - this.view = new GroupConfigurationDetailsView({ - model: this.model - }); - appendSetFixtures(this.view.render().el); - }); - - it('should render properly', function() { - assertTheDetailsView(this.view, 'Configuration'); - }); - - it('should show groups appropriately', function() { - this.model.get('groups').add([{}]); - this.model.set('showGroups', false); - this.view.$('.show-groups').click(); - - expect(this.model.get('showGroups')).toBeTruthy(); - expect(this.view.$(SELECTORS.group).length).toBe(3); - expect(this.view.$(SELECTORS.groupsCount)).not.toExist(); - expect(this.view.$(SELECTORS.description)) - .toContainText('Configuration Description'); - expect(this.view.$(SELECTORS.groupsAllocation)) - .toContainText('33%'); - }); - - it('should hide groups appropriately', function() { - this.model.get('groups').add([{}]); - this.model.set('showGroups', true); - this.view.$('.hide-groups').click(); - - expect(this.model.get('showGroups')).toBeFalsy(); - expect(this.view.$(SELECTORS.group)).not.toExist(); - expect(this.view.$(SELECTORS.groupsCount)) - .toContainText('Contains 3 groups'); - expect(this.view.$(SELECTORS.description)).not.toExist(); - expect(this.view.$(SELECTORS.groupsAllocation)).not.toExist(); - }); - - it('should show empty usage appropriately', function() { - this.model.set('showGroups', false); - this.view.$('.show-groups').click(); - assertShowEmptyUsages( - this.view, - 'This Group Configuration is not in use. ' - + 'Start by adding a content experiment to any Unit via the' - ); - }); - - it('should hide empty usage appropriately', function() { - this.model.set('showGroups', true); - this.view.$('.hide-groups').click(); - assertHideEmptyUsages(this.view); - }); - - it('should show non-empty usage appropriately', function() { - setUsageInfo(this.model); - this.model.set('showGroups', false); - this.view.$('.show-groups').click(); - - assertShowNonEmptyUsages( - this.view, - 'This Group Configuration is used in:', - 'Cannot delete when in use by an experiment' - ); - }); - - it('should hide non-empty usage appropriately', function() { - setUsageInfo(this.model); - this.model.set('showGroups', true); - this.view.$('.hide-groups').click(); - - expect(this.view.$(SELECTORS.note)).toHaveAttr( - 'data-tooltip', 'Cannot delete when in use by an experiment' - ); - assertHideNonEmptyUsages(this.view); - }); - - it('should show validation warning icon and message appropriately', function() { - this.model.set('usage', [ - { - label: 'label1', - url: 'url1', - validation: { - text: 'Warning message', - type: 'warning' - } - } - ]); - this.model.set('showGroups', false); - this.view.$('.show-groups').click(); - - expect(this.view.$(SELECTORS.usageUnitMessage)).toContainText('Warning message'); - expect(this.view.$(SELECTORS.usageUnitWarningIcon)).toExist(); - }); - - it('should show validation error icon and message appropriately', function() { - this.model.set('usage', [ - { - label: 'label1', - url: 'url1', - validation: { - text: 'Error message', - type: 'error' - } - } - ]); - this.model.set('showGroups', false); - this.view.$('.show-groups').click(); - - expect(this.view.$(SELECTORS.usageUnitMessage)).toContainText('Error message'); - expect(this.view.$(SELECTORS.usageUnitErrorIcon)).toExist(); - }); - - it('should hide validation icons and messages appropriately', function() { - setUsageInfo(this.model); - this.model.set('showGroups', true); - this.view.$('.hide-groups').click(); - - assertHideValidationContent(this.view); - }); - }); - - describe('Experiment group configurations editor view', function() { - var setValuesToInputs = function(view, values) { - _.each(values, function(value, selector) { - if (SELECTORS[selector]) { - view.$(SELECTORS[selector]).val(value); - } - }); - }; - - beforeEach(function() { - ViewHelpers.installViewTemplates(); - TemplateHelpers.installTemplates([ - 'group-configuration-editor', 'group-edit' - ]); - - this.model = new GroupConfigurationModel({ - name: 'Configuration', - description: 'Configuration Description', - id: 0, - editing: true - }); - this.collection = new GroupConfigurationCollection([this.model]); - this.collection.url = '/group_configurations'; - this.view = new GroupConfigurationEditorView({ - model: this.model - }); - appendSetFixtures(this.view.render().el); - }); - - it('should render properly', function() { - expect(this.view).toBeCorrectValuesInInputs({ - name: 'Configuration', - description: 'Configuration Description' - }); - expect(this.view.$('.delete')).toExist(); - }); - - it('should allow you to create new groups', function() { - var numGroups = this.model.get('groups').length; - this.view.$('.action-add-group').click(); - expect(this.model.get('groups').length).toEqual(numGroups + 1); - }); - - it('should save properly', function() { - var notificationSpy = ViewHelpers.createNotificationSpy(), - groups; - - this.view.$('.action-add-group').click(); - setValuesToInputs(this.view, { - inputName: 'New Configuration', - inputDescription: 'New Description' - }); - - ViewHelpers.submitAndVerifyFormSuccess(this.view, requests, notificationSpy); - - expect(this.model).toBeCorrectValuesInModel({ - name: 'New Configuration', - description: 'New Description' - }); - - groups = this.model.get('groups'); - expect(groups.length).toBe(3); - expect(groups.at(2).get('name')).toBe('Group C'); - expect(this.view.$el).not.toBeInDOM(); - }); - - it('does not hide saving message if failure', function() { - var notificationSpy = ViewHelpers.createNotificationSpy(); - - setValuesToInputs(this.view, {inputName: 'New Configuration'}); - ViewHelpers.submitAndVerifyFormError(this.view, requests, notificationSpy); - }); - - it('does not save on cancel', function() { - this.view.$('.action-add-group').click(); - setValuesToInputs(this.view, { - inputName: 'New Configuration', - inputDescription: 'New Description' - }); - - expect(this.model.get('groups').length).toBe(3); - - this.view.$('.action-cancel').click(); - expect(this.model).toBeCorrectValuesInModel({ - name: 'Configuration', - description: 'Configuration Description' - }); - // Model is still exist in the collection - expect(this.collection.indexOf(this.model)).toBeGreaterThan(-1); - expect(this.collection.length).toBe(1); - expect(this.model.get('groups').length).toBe(2); - }); - - it('should be removed on cancel if it is a new item', function() { - spyOn(this.model, 'isNew').and.returnValue(true); - setValuesToInputs(this.view, { - inputName: 'New Configuration', - inputDescription: 'New Description' - }); - this.view.$('.action-cancel').click(); - // Model is removed from the collection - expect(this.collection.length).toBe(0); - }); - - it('should be possible to correct validation errors', function() { - // Set incorrect value - setValuesToInputs(this.view, {inputName: ''}); - // Try to save - this.view.$('form').submit(); - // See error message - expect(this.view.$(SELECTORS.errorMessage)).toContainText( - 'Group Configuration name is required' - ); - // No request - AjaxHelpers.expectNoRequests(requests); - // Set correct value - setValuesToInputs(this.view, {inputName: 'New Configuration'}); - // Try to save - this.view.$('form').submit(); - AjaxHelpers.respondWithJson(requests, {}); - // Model is updated - expect(this.model).toBeCorrectValuesInModel({ - name: 'New Configuration' - }); - // Error message disappear - expect(this.view.$(SELECTORS.errorMessage)).not.toBeInDOM(); - AjaxHelpers.expectNoRequests(requests); - }); - - describe('removes all newly created groups on cancel', function() { - it('if the model has a non-empty groups', function() { - var groups = this.model.get('groups'); - - this.view.render(); - groups.add([{name: 'non-empty'}]); - expect(groups.length).toEqual(3); - this.view.$('.action-cancel').click(); - // Restore to default state (2 groups by default). - expect(groups.length).toEqual(2); - }); - - it('if the model has no non-empty groups', function() { - var groups = this.model.get('groups'); - - this.view.render(); - groups.add([{}, {}, {}]); - expect(groups.length).toEqual(5); - this.view.$('.action-cancel').click(); - // Restore to default state (2 groups by default). - expect(groups.length).toEqual(2); - }); - }); - - it('groups have correct default names', function() { - var group1 = new GroupModel({name: 'Group A'}), - group2 = new GroupModel({name: 'Group B'}), - collection = this.model.get('groups'); - - collection.reset([group1, group2]); // Group A, Group B - this.view.$('.action-add-group').click(); // Add Group C - this.view.$('.action-add-group').click(); // Add Group D - this.view.$('.action-add-group').click(); // Add Group E - - expect(this.view.$(SELECTORS.inputGroupName)).toHaveDefaultNames([ - 'Group A', 'Group B', 'Group C', 'Group D', 'Group E' - ]); - - // Remove Group B - this.view.$('.group-1 .action-close').click(); - - expect(this.view.$(SELECTORS.inputGroupName)).toHaveDefaultNames([ - 'Group A', 'Group C', 'Group D', 'Group E' - ]); - - this.view.$('.action-add-group').click(); // Add Group F - this.view.$('.action-add-group').click(); // Add Group G - - expect(this.view.$(SELECTORS.inputGroupName)).toHaveDefaultNames([ - 'Group A', 'Group C', 'Group D', 'Group E', 'Group F', 'Group G' - ]); - }); - - it('cannot be deleted if it is in use', function() { - assertCannotDeleteUsed( - this, - 'Cannot delete when in use by an experiment', - 'This configuration is currently used in content ' - + 'experiments. If you make changes to the groups, you may ' - + 'need to edit those experiments.' - ); - }); - - it('does not contain warning message if it is not in use', function() { - assertUnusedOptions(this); - }); - }); - - describe('Experiment group configurations list view', function() { - var emptyMessage = 'You have not created any group configurations yet.'; - - beforeEach(function() { - TemplateHelpers.installTemplates( - ['group-configuration-editor', 'group-edit', 'list'] - ); - - this.model = new GroupConfigurationModel({id: 0}); - this.collection = new GroupConfigurationCollection(); - this.view = new GroupConfigurationsListView({ - collection: this.collection - }); - appendSetFixtures(this.view.render().el); - }); - - describe('empty template', function() { - it('should be rendered if no group configurations', function() { - expect(this.view.$el).toContainText(emptyMessage); - expect(this.view.$('.new-button')).toExist(); - expect(this.view.$(SELECTORS.itemView)).not.toExist(); - }); - - it('should disappear if group configuration is added', function() { - expect(this.view.$el).toContainText(emptyMessage); - expect(this.view.$(SELECTORS.itemView)).not.toExist(); - this.collection.add(this.model); - expect(this.view.$el).not.toContainText(emptyMessage); - expect(this.view.$(SELECTORS.itemView)).toExist(); - }); - - it('should appear if configurations were removed', function() { - this.collection.add(this.model); - expect(this.view.$(SELECTORS.itemView)).toExist(); - this.collection.remove(this.model); - expect(this.view.$el).toContainText(emptyMessage); - expect(this.view.$(SELECTORS.itemView)).not.toExist(); - }); - - it('can create a new group configuration', function() { - this.view.$('.new-button').click(); - expect($('.group-configuration-edit').length).toBeGreaterThan(0); - }); - }); - }); - - describe('Experiment group configurations controller view', function() { - beforeEach(function() { - TemplateHelpers.installTemplates([ - 'group-configuration-editor', 'group-configuration-details' - ], true); - this.model = new GroupConfigurationModel({id: 0}); - this.collection = new GroupConfigurationCollection([this.model]); - this.collection.url = '/group_configurations'; - this.view = new GroupConfigurationItemView({ - model: this.model - }); - appendSetFixtures(this.view.render().el); - }); - - it('should render properly', function() { - assertControllerView(this.view, SELECTORS.detailsView, SELECTORS.editView); - }); - - it('should destroy itself on confirmation of deleting', function() { - assertAndDeleteItemError(this, '/group_configurations/0', 'Delete this group configuration?'); - }); - - it('does not hide deleting message if failure', function() { - assertAndDeleteItemWithError( - this, - '/group_configurations/0', - SELECTORS.itemView, - 'Delete this group configuration?' - ); - }); - }); - - describe('Experiment group configurations group editor view', function() { - beforeEach(function() { - TemplateHelpers.installTemplate('group-edit', true); - - this.model = new GroupModel({ - name: 'Group A' - }); - - this.collection = new GroupCollection([this.model]); - - this.view = new ExperimentGroupEditView({ - model: this.model - }); - }); - - describe('Basic', function() { - it('can render properly', function() { - this.view.render(); - expect(this.view.$('.group-name').val()).toBe('Group A'); - expect(this.view.$('.group-allocation')).toContainText('100%'); - }); - - it('can delete itself', function() { - this.view.render().$('.action-close').click(); - expect(this.collection.length).toEqual(0); - }); - }); - }); - - describe('Content groups list view', function() { - var newGroupCss = '.new-button', - addGroupCss = '.action-add', - inputCss = '.collection-name-input', - saveButtonCss = '.action-primary', - cancelButtonCss = '.action-cancel', - validationErrorCss = '.content-group-edit-error', - scopedGroupSelector, createGroups, renderView, saveOrCancel, editNewGroup, editExistingGroup, - verifyEditingGroup, respondToSave, expectGroupsVisible, correctValidationError; - - scopedGroupSelector = function(groupIndex, additionalSelectors) { - var groupSelector = '.partition-groups-list-item-' + groupIndex; - if (additionalSelectors) { - return groupSelector + ' ' + additionalSelectors; - } else { - return groupSelector; - } - }; - - createGroups = function(groupNamesWithId) { - var groups = new GroupCollection(_.map(groupNamesWithId, function(groupName, id) { - return {id: id, name: groupName}; - })), - groupConfiguration = new GroupConfigurationModel({ - id: 0, - name: 'Content Group Configuration', - groups: groups - }, {canBeEmpty: true}); - groupConfiguration.urlRoot = '/mock_url'; - groupConfiguration.outlineUrl = '/mock_url'; - return groups; - }; - - renderView = function(groupNamesWithId) { - var view = new GroupList({collection: createGroups(groupNamesWithId || {})}).render(); - appendSetFixtures(view.el); - return view; - }; - - saveOrCancel = function(view, options, groupIndex) { - if (options.save) { - view.$(scopedGroupSelector(groupIndex, saveButtonCss)).click(); - } else if (options.cancel) { - view.$(scopedGroupSelector(groupIndex, cancelButtonCss)).click(); - } - }; - - editNewGroup = function(view, options) { - var newGroupIndex; - if (view.collection.length === 0) { - view.$(newGroupCss).click(); - } else { - view.$(addGroupCss).click(); - } - newGroupIndex = view.collection.length - 1; - view.$(inputCss).val(options.newName); - verifyEditingGroup(view, true, newGroupIndex); - saveOrCancel(view, options, newGroupIndex); - }; - - editExistingGroup = function(view, options) { - var groupIndex = options.groupIndex || 0; - view.$(scopedGroupSelector(groupIndex, '.edit')).click(); - view.$(scopedGroupSelector(groupIndex, inputCss)).val(options.newName); - saveOrCancel(view, options, groupIndex); - }; - - verifyEditingGroup = function(view, expectEditing, index) { - // Should prevent the user from opening more than one edit - // form at a time by removing the add button(s) when - // editing a group. - index = index || 0; - if (expectEditing) { - expect(view.$(scopedGroupSelector(index, '.content-group-edit'))).toExist(); - expect(view.$(newGroupCss)).not.toExist(); - expect(view.$(addGroupCss)).toHaveClass('is-hidden'); - } else { - expect(view.$('.content-group-edit')).not.toExist(); - if (view.collection.length === 0) { - expect(view.$(newGroupCss)).toExist(); - expect(view.$(addGroupCss)).not.toExist(); - } else { - expect(view.$(newGroupCss)).not.toExist(); - expect(view.$(addGroupCss)).not.toHaveClass('is-hidden'); - } - } - }; - - respondToSave = function(requests, view) { - var request = AjaxHelpers.currentRequest(requests); - expect(request.method).toBe('POST'); - expect(request.url).toBe('/mock_url/0'); - AjaxHelpers.respondWithJson(requests, { - name: 'Content Group Configuration', - groups: view.collection.map(function(groupModel, index) { - return _.extend(groupModel.toJSON(), {id: index}); - }) - }); - }; - - correctValidationError = function(view, requests, newGroupName) { - expect(view.$(validationErrorCss)).toExist(); - verifyEditingGroup(view, true); - view.$(inputCss).val(newGroupName); - view.$(saveButtonCss).click(); - respondToSave(requests, view); - expect(view.$(validationErrorCss)).not.toExist(); - }; - - expectGroupsVisible = function(view, groupNames) { - _.each(groupNames, function(groupName) { - expect(view.$('.partition-groups-list-item')).toContainText(groupName); - }); - }; - - beforeEach(function() { - TemplateHelpers.installTemplates( - ['content-group-editor', 'partition-group-details', 'list'] - ); - }); - - it('shows a message when no groups are present', function() { - expect(renderView().$('.no-content')) - .toContainText('You have not created any content groups yet.'); - }); - - it('can render groups', function() { - var groupNames = ['Group 1', 'Group 2', 'Group 3']; - renderView(groupNames).$('.partition-group-details').each(function(index) { - expect($(this)).toContainText(groupNames[index]); - }); - }); - - it('can create an initial group and save', function() { - var newGroupName = 'New Group Name', - view = renderView(); - editNewGroup(view, {newName: newGroupName, save: true}); - respondToSave(requests, view); - verifyEditingGroup(view, false); - expectGroupsVisible(view, [newGroupName]); - }); - - it('can add another group and save', function() { - var oldGroupName = 'Old Group Name', - newGroupName = 'New Group Name', - view = renderView({1: oldGroupName}); - editNewGroup(view, {newName: newGroupName, save: true}); - respondToSave(requests, view); - verifyEditingGroup(view, false, 1); - expectGroupsVisible(view, [oldGroupName, newGroupName]); - }); - - it('can cancel adding a group', function() { - var newGroupName = 'New Group Name', - view = renderView(); - editNewGroup(view, {newName: newGroupName, cancel: true}); - AjaxHelpers.expectNoRequests(requests); - verifyEditingGroup(view, false); - expect(view.$()).not.toContainText(newGroupName); - }); - - it('can cancel editing a group', function() { - var originalGroupName = 'Original Group Name', - view = renderView([originalGroupName]); - editExistingGroup(view, {newName: 'New Group Name', cancel: true}); - verifyEditingGroup(view, false); - AjaxHelpers.expectNoRequests(requests); - expect(view.collection.at(0).get('name')).toBe(originalGroupName); - }); - - it('can show and correct a validation error', function() { - var newGroupName = 'New Group Name', - view = renderView(); - editNewGroup(view, {newName: '', save: true}); - AjaxHelpers.expectNoRequests(requests); - correctValidationError(view, requests, newGroupName); - }); - - it('can not invalidate an existing content group', function() { - var oldGroupName = 'Old Group Name', - view = renderView([oldGroupName]); - editExistingGroup(view, {newName: '', save: true}); - AjaxHelpers.expectNoRequests(requests); - correctValidationError(view, requests, oldGroupName); - }); - - it('trims whitespace', function() { - var newGroupName = 'New Group Name', - view = renderView(); - editNewGroup(view, {newName: ' ' + newGroupName + ' ', save: true}); - respondToSave(requests, view); - expect(view.collection.at(0).get('name')).toBe(newGroupName); - }); - - it('only edits one form at a time', function() { - var view = renderView(); - view.collection.add({name: 'Editing Group', editing: true}); - verifyEditingGroup(view, true); - }); - }); - - describe('Content groups details view', function() { - beforeEach(function() { - TemplateHelpers.installTemplate('partition-group-details', true); - this.model = new GroupModel({name: 'Content Group', id: 0, courseOutlineUrl: 'CourseOutlineUrl'}); - - var saveableModel = new GroupConfigurationModel({ - name: 'Content Group Configuration', - id: 0, - scheme: 'cohort', - groups: new GroupCollection([this.model]) - }, {canBeEmpty: true}); - - saveableModel.urlRoot = '/mock_url'; - - this.collection = new GroupConfigurationCollection([saveableModel]); - this.collection.outlineUrl = '/outline'; - - this.view = new PartitionGroupDetailsView({ - model: this.model - }); - appendSetFixtures(this.view.render().el); - }); - - it('should render properly', function() { - assertTheDetailsView(this.view, 'Content Group'); - }); - - it('should show empty usage appropriately', function() { - this.view.$('.show-groups').click(); - assertShowEmptyUsages(this.view, 'use this group to control access to a component'); - }); - - it('should hide empty usage appropriately', function() { - this.view.$('.hide-groups').click(); - assertHideEmptyUsages(this.view); - }); - - it('should show non-empty usage appropriately', function() { - setUsageInfo(this.model); - this.view.$('.show-groups').click(); - - assertShowNonEmptyUsages( - this.view, - 'This group controls access to:', - 'Cannot delete when in use by a unit' - ); - }); - - it('should hide non-empty usage appropriately', function() { - setUsageInfo(this.model); - this.view.$('.hide-groups').click(); - - expect(this.view.$('li.action-delete')).toHaveAttr( - 'data-tooltip', 'Cannot delete when in use by a unit' - ); - assertHideNonEmptyUsages(this.view); - }); - - it('should hide validation icons and messages appropriately', function() { - setUsageInfo(this.model); - this.view.$('.hide-groups').click(); - assertHideValidationContent(this.view); - }); - }); - - describe('Content groups editor view', function() { - beforeEach(function() { - ViewHelpers.installViewTemplates(); - TemplateHelpers.installTemplates(['content-group-editor']); - - this.model = new GroupModel({name: 'Content Group', id: 0}); - - this.saveableModel = new GroupConfigurationModel({ - name: 'Content Group Configuration', - id: 0, - scheme: 'cohort', - groups: new GroupCollection([this.model]), - editing: true - }); - - this.collection = new GroupConfigurationCollection([this.saveableModel]); - this.collection.outlineUrl = '/outline'; - this.collection.url = '/group_configurations'; - - this.view = new ContentGroupEditorView({ - model: this.model - }); - appendSetFixtures(this.view.render().el); - }); - - it('should save properly', function() { - var notificationSpy = ViewHelpers.createNotificationSpy(); - - this.view.$('.action-add').click(); - this.view.$(SELECTORS.inputName).val('New Content Group'); - - ViewHelpers.submitAndVerifyFormSuccess(this.view, requests, notificationSpy); - - expect(this.model).toBeCorrectValuesInModel({ - name: 'New Content Group' - }); - expect(this.view.$el).not.toBeInDOM(); - }); - - it('does not hide saving message if failure', function() { - var notificationSpy = ViewHelpers.createNotificationSpy(); - this.view.$(SELECTORS.inputName).val('New Content Group'); - - ViewHelpers.submitAndVerifyFormError(this.view, requests, notificationSpy); - }); - - it('does not save on cancel', function() { - expect(this.view.$('.action-add')); - this.view.$('.action-add').click(); - this.view.$(SELECTORS.inputName).val('New Content Group'); - - this.view.$('.action-cancel').click(); - expect(this.model).toBeCorrectValuesInModel({ - name: 'Content Group' - }); - // Model is still exist in the collection - expect(this.collection.indexOf(this.saveableModel)).toBeGreaterThan(-1); - expect(this.collection.length).toBe(1); - }); - - it('cannot be deleted if it is in use', function() { - assertCannotDeleteUsed( - this, - 'Cannot delete when in use by a unit', - 'This content group is used in one or more units.' - ); - }); - - it('does not contain warning message if it is not in use', function() { - assertUnusedOptions(this); - }); - }); - - describe('Content group controller view', function() { - beforeEach(function() { - TemplateHelpers.installTemplates([ - 'content-group-editor', 'partition-group-details' - ], true); - - this.model = new GroupModel({name: 'Content Group', id: 0, courseOutlineUrl: 'CourseOutlineUrl'}); - - this.saveableModel = new GroupConfigurationModel({ - name: 'Content Group Configuration', - id: 0, - scheme: 'cohort', - groups: new GroupCollection([this.model]) - }); - this.saveableModel.urlRoot = '/group_configurations'; - this.collection = new GroupConfigurationCollection([this.saveableModel]); - this.collection.url = '/group_configurations'; - this.view = new PartitionGroupItemView({ - model: this.model - }); - appendSetFixtures(this.view.render().el); - }); - - it('should render properly', function() { - assertControllerView(this.view, '.partition-group-details', '.content-group-edit'); - }); - - it('should destroy itself on confirmation of deleting', function() { - assertAndDeleteItemError(this, '/group_configurations/0/0', 'Delete this content group'); - }); - - it('does not hide deleting message if failure', function() { - assertAndDeleteItemWithError( - this, - '/group_configurations/0/0', - '.partition-groups-list-item', - 'Delete this content group' - ); - }); - }); - - }); // end describe('Group Configurations') -}); diff --git a/cms/static/js/spec/views/pages/group_configurations_spec.js b/cms/static/js/spec/views/pages/group_configurations_spec.js deleted file mode 100644 index 7534681aac7a..000000000000 --- a/cms/static/js/spec/views/pages/group_configurations_spec.js +++ /dev/null @@ -1,125 +0,0 @@ -define([ - 'jquery', 'underscore', 'js/views/pages/group_configurations', - 'js/models/group_configuration', 'js/collections/group_configuration', - 'common/js/spec_helpers/template_helpers' -], function($, _, GroupConfigurationsPage, GroupConfigurationModel, GroupConfigurationCollection, TemplateHelpers) { - 'use strict'; - - describe('GroupConfigurationsPage', function() { - var mockGroupConfigurationsPage = readFixtures( - 'mock/mock-group-configuration-page.underscore' - ), - groupConfigItemClassName = '.group-configurations-list-item'; - - var initializePage = function(disableSpy) { - var view = new GroupConfigurationsPage({ - el: $('#content'), - experimentsEnabled: true, - experimentGroupConfigurations: new GroupConfigurationCollection({ - id: 0, - name: 'Configuration 1', - courseOutlineUrl: 'CourseOutlineUrl' - }), - allGroupConfigurations: [new GroupConfigurationModel({groups: []})] - }); - if (!disableSpy) { - spyOn(view, 'addWindowActions'); - } - - return view; - }; - - var renderPage = function() { - return initializePage().render(); - }; - - beforeEach(function() { - setFixtures(mockGroupConfigurationsPage); - TemplateHelpers.installTemplates([ - 'group-configuration-editor', 'group-configuration-details', 'partition-group-details', - 'content-group-editor', 'group-edit', 'list' - ]); - - jasmine.addMatchers({ - toBeExpanded: function() { - return { - compare: function(actual) { - return { - pass: Boolean($('a.group-toggle.hide-groups', $(actual)).length) - }; - } - }; - } - }); - }); - - describe('Initial display', function() { - // TODO fix this, see TNL-1475 - xit('can render itself', function() { - var view = initializePage(); - expect(view.$('.ui-loading')).toBeVisible(); - view.render(); - expect(view.$(groupConfigItemClassName)).toExist(); - expect(view.$('.content-groups .no-content')).toExist(); - expect(view.$('.ui-loading')).toHaveClass('is-hidden'); - }); - }); - - describe('Experiment group configurations', function() { - beforeEach(function() { - spyOn($.fn, 'focus'); - TemplateHelpers.installTemplate('group-configuration-details'); - this.view = initializePage(true); - }); - - it('should focus and expand if its id is part of url hash', function() { - spyOn(this.view, 'getLocationHash').and.returnValue('#0'); - this.view.render(); - // We cannot use .toBeFocused due to flakiness. - expect($.fn.focus).toHaveBeenCalled(); - expect(this.view.$(groupConfigItemClassName)).toBeExpanded(); - }); - - it('should not focus on any experiment configuration if url hash is empty', function() { - spyOn(this.view, 'getLocationHash').and.returnValue(''); - this.view.render(); - expect($.fn.focus).not.toHaveBeenCalled(); - expect(this.view.$(groupConfigItemClassName)).not.toBeExpanded(); - }); - - it('should not focus on any experiment configuration if url hash contains wrong id', function() { - spyOn(this.view, 'getLocationHash').and.returnValue('#1'); - this.view.render(); - expect($.fn.focus).not.toHaveBeenCalled(); - expect(this.view.$(groupConfigItemClassName)).not.toBeExpanded(); - }); - - it('should not show a notification message if an experiment configuration is not changed', function() { - this.view.render(); - expect(this.view.onBeforeUnload()).toBeUndefined(); - }); - - it('should show a notification message if an experiment configuration is changed', function() { - this.view.experimentGroupConfigurations.at(0).set('name', 'Configuration 2'); - expect(this.view.onBeforeUnload()) - .toBe('You have unsaved changes. Do you really want to leave this page?'); - }); - }); - - describe('Content groups', function() { - beforeEach(function() { - this.view = renderPage(); - }); - - it('should not show a notification message if a content group is not changed', function() { - expect(this.view.onBeforeUnload()).toBeUndefined(); - }); - - it('should show a notification message if a content group is changed', function() { - 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_editor.js b/cms/static/js/views/content_group_editor.js deleted file mode 100644 index 6423e700ff26..000000000000 --- a/cms/static/js/views/content_group_editor.js +++ /dev/null @@ -1,46 +0,0 @@ -/** - * This class defines an editing view for content groups. - * It is expected to be backed by a Group model. - */ -define([ - 'js/views/list_item_editor', 'underscore' -], -function(ListItemEditorView, _) { - 'use strict'; - - var ContentGroupEditorView = ListItemEditorView.extend({ - tagName: 'div', - className: 'content-group-edit collection-edit', - events: { - submit: 'setAndClose', - 'click .action-cancel': 'cancel' - }, - - initialize: function() { - ListItemEditorView.prototype.initialize.call(this); - this.template = this.loadTemplate('content-group-editor'); - }, - - getTemplateOptions: function() { - return { - id: this.model.get('id'), - name: this.model.get('name'), - index: this.model.collection.indexOf(this.model), - isNew: this.model.isNew(), - usage: this.model.get('usage'), - uniqueId: _.uniqueId() - }; - }, - - setValues: function() { - this.model.set({name: this.$('input').val().trim()}); - return this; - }, - - getSaveableModel: function() { - return this.model.collection.parents[0]; - } - }); - - return ContentGroupEditorView; -}); diff --git a/cms/static/js/views/experiment_group_edit.js b/cms/static/js/views/experiment_group_edit.js deleted file mode 100644 index 48abd6c7710b..000000000000 --- a/cms/static/js/views/experiment_group_edit.js +++ /dev/null @@ -1,72 +0,0 @@ -/** - * This class defines an edit view for groups within content experiment group configurations. - * It is expected to be backed by a Group model. - */ -define([ - 'js/views/baseview', 'underscore', 'underscore.string', 'gettext', 'text!templates/group-edit.underscore' -], -function(BaseView, _, str, gettext, groupEditTemplate) { - 'use strict'; - - var ExperimentGroupEditView = BaseView.extend({ - tagName: 'li', - events: { - 'click .action-close': 'removeGroup', - 'change .group-name': 'changeName', - 'focus .group-name': 'onFocus', - 'blur .group-name': 'onBlur' - }, - - className: function() { - var index = this.model.collection.indexOf(this.model); - return 'field-group group group-' + index; - }, - - initialize: function() { - this.listenTo(this.model, 'change', this.render); - }, - - render: function() { - var collection = this.model.collection, - index = collection.indexOf(this.model); - - edx.HtmlUtils.setHtml(this.$el, edx.HtmlUtils.template(groupEditTemplate)({ - name: this.model.get('name'), - allocation: this.getAllocation(), - index: index, - error: this.model.validationError - })); - - return this; - }, - - changeName: function(event) { - if (event && event.preventDefault) { event.preventDefault(); } - this.model.set({ - name: this.$('.group-name').val() - }, {silent: true}); - - return this; - }, - - removeGroup: function(event) { - if (event && event.preventDefault) { event.preventDefault(); } - this.model.collection.remove(this.model); - return this.remove(); - }, - - getAllocation: function() { - return Math.floor(100 / this.model.collection.length); - }, - - onFocus: function() { - this.$el.closest('.groups-fields').addClass('is-focused'); - }, - - onBlur: function() { - this.$el.closest('.groups-fields').removeClass('is-focused'); - } - }); - - return ExperimentGroupEditView; -}); diff --git a/cms/static/js/views/group_configuration_details.js b/cms/static/js/views/group_configuration_details.js deleted file mode 100644 index 133f464cf9bc..000000000000 --- a/cms/static/js/views/group_configuration_details.js +++ /dev/null @@ -1,100 +0,0 @@ -/** - * This class defines a details view for content experiment group configurations. - * It is expected to be instantiated with a GroupConfiguration model. - */ -define([ - 'js/views/baseview', 'underscore', 'gettext', 'underscore.string', - 'edx-ui-toolkit/js/utils/string-utils', 'edx-ui-toolkit/js/utils/html-utils' -], -function(BaseView, _, gettext, str, StringUtils, HtmlUtils) { - 'use strict'; - - var GroupConfigurationDetailsView = BaseView.extend({ - tagName: 'div', - events: { - 'click .edit': 'editConfiguration', - 'click .show-groups': 'showGroups', - 'click .hide-groups': 'hideGroups' - }, - - className: function() { - var index = this.model.collection.indexOf(this.model); - - return [ - 'collection', - 'group-configuration-details', - 'group-configuration-details-' + index - ].join(' '); - }, - - initialize: function() { - this.template = HtmlUtils.template( - $('#group-configuration-details-tpl').text() - ); - this.listenTo(this.model, 'change', this.render); - }, - - render: function() { - var attrs = $.extend({}, this.model.attributes, { - groupsCountMessage: this.getGroupsCountTitle(), - usageCountMessage: this.getUsageCountTitle(), - courseOutlineUrl: this.model.collection.outlineUrl, - index: this.model.collection.indexOf(this.model) - }); - HtmlUtils.setHtml(this.$el, this.template(attrs)); - return this; - }, - - editConfiguration: function(event) { - if (event && event.preventDefault) { event.preventDefault(); } - this.model.set('editing', true); - }, - - showGroups: function(event) { - if (event && event.preventDefault) { event.preventDefault(); } - this.model.set('showGroups', true); - }, - - hideGroups: function(event) { - if (event && event.preventDefault) { event.preventDefault(); } - this.model.set('showGroups', false); - }, - - getGroupsCountTitle: function() { - var count = this.model.get('groups').length, - /* globals ngettext */ - message = ngettext( - /* - Translators: 'count' is number of groups that the group - configuration contains. - */ - 'Contains {count} group', 'Contains {count} groups', - count - ); - - return StringUtils.interpolate(message, {count: count}); - }, - - getUsageCountTitle: function() { - var count = this.model.get('usage').length; - - if (count === 0) { - return gettext('Not in Use'); - } else { - return StringUtils.interpolate(ngettext( - - /* - Translators: 'count' is number of units that the group - configuration is used in. - */ - 'Used in {count} location', 'Used in {count} locations', - count - ), - {count: count} - ); - } - } - }); - - return GroupConfigurationDetailsView; -}); diff --git a/cms/static/js/views/group_configuration_editor.js b/cms/static/js/views/group_configuration_editor.js deleted file mode 100644 index e4187475d282..000000000000 --- a/cms/static/js/views/group_configuration_editor.js +++ /dev/null @@ -1,122 +0,0 @@ -/** - * This class defines an editing view for content experiment group configurations. - * It is expected to be backed by a GroupConfiguration model. - */ -define([ - 'js/views/list_item_editor', 'underscore', 'jquery', 'gettext', - 'js/views/experiment_group_edit' -], -function(ListItemEditorView, _, $, gettext, ExperimentGroupEditView) { - 'use strict'; - - var GroupConfigurationEditorView = ListItemEditorView.extend({ - tagName: 'div', - events: { - 'change .collection-name-input': 'setName', - 'change .group-configuration-description-input': 'setDescription', - 'click .action-add-group': 'createGroup', - 'focus .input-text': 'onFocus', - 'blur .input-text': 'onBlur', - submit: 'setAndClose', - 'click .action-cancel': 'cancel' - }, - - className: function() { - var index = this.model.collection.indexOf(this.model); - - return [ - 'collection-edit', - 'group-configuration-edit', - 'group-configuration-edit-' + index - ].join(' '); - }, - - initialize: function() { - var groups = this.model.get('groups'); - - ListItemEditorView.prototype.initialize.call(this); - - this.template = this.loadTemplate('group-configuration-editor'); - this.listenTo(groups, 'add', this.onAddItem); - this.listenTo(groups, 'reset', this.addAll); - this.listenTo(groups, 'all', this.render); - }, - - render: function() { - ListItemEditorView.prototype.render.call(this); - this.addAll(); - return this; - }, - - getTemplateOptions: function() { - return { - id: this.model.get('id'), - uniqueId: _.uniqueId(), - name: this.model.get('name'), - description: this.model.get('description'), - usage: this.model.get('usage'), - isNew: this.model.isNew() - }; - }, - - getSaveableModel: function() { - return this.model; - }, - - onAddItem: function(group) { - var view = new ExperimentGroupEditView({model: group}); - this.$('ol.groups').append(view.render().el); - - return this; - }, - - addAll: function() { - this.model.get('groups').each(this.onAddItem, this); - }, - - createGroup: function(event) { - if (event && event.preventDefault) { event.preventDefault(); } - var collection = this.model.get('groups'); - collection.add([{ - name: collection.getNextDefaultGroupName(), - order: collection.nextOrder() - }]); - }, - - setName: function(event) { - if (event && event.preventDefault) { event.preventDefault(); } - this.model.set( - 'name', this.$('.collection-name-input').val(), - {silent: true} - ); - }, - - setDescription: function(event) { - if (event && event.preventDefault) { event.preventDefault(); } - this.model.set( - 'description', - this.$('.group-configuration-description-input').val(), - {silent: true} - ); - }, - - setValues: function() { - this.setName(); - this.setDescription(); - - _.each(this.$('.groups li'), function(li, i) { - var group = this.model.get('groups').at(i); - - if (group) { - group.set({ - name: $('.group-name', li).val() - }); - } - }, this); - - return this; - } - }); - - return GroupConfigurationEditorView; -}); diff --git a/cms/static/js/views/group_configuration_item.js b/cms/static/js/views/group_configuration_item.js deleted file mode 100644 index 4f548a1d193e..000000000000 --- a/cms/static/js/views/group_configuration_item.js +++ /dev/null @@ -1,45 +0,0 @@ -/** - * This class defines an controller view for content experiment group configurations. - * 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/group_configuration_details', 'js/views/group_configuration_editor', 'gettext' -], function( - ListItemView, GroupConfigurationDetailsView, GroupConfigurationEditorView, gettext -) { - 'use strict'; - - var GroupConfigurationItemView = ListItemView.extend({ - events: { - 'click .delete': 'deleteItem' - }, - - tagName: 'section', - - baseClassName: 'group-configuration', - - canDelete: true, - - // Translators: this refers to a collection of groups. - itemDisplayName: gettext('group configuration'), - - attributes: function() { - return { - id: this.model.get('id'), - tabindex: -1 - }; - }, - - createEditView: function() { - return new GroupConfigurationEditorView({model: this.model}); - }, - - createDetailsView: function() { - return new GroupConfigurationDetailsView({model: this.model}); - } - }); - - return GroupConfigurationItemView; -}); diff --git a/cms/static/js/views/group_configurations_list.js b/cms/static/js/views/group_configurations_list.js deleted file mode 100644 index a1996430c48a..000000000000 --- a/cms/static/js/views/group_configurations_list.js +++ /dev/null @@ -1,30 +0,0 @@ -/** - * This class defines a list view for content experiment group configurations. - * It is expected to be backed by a GroupConfiguration collection. - */ -define([ - 'js/views/list', 'js/views/group_configuration_item', 'gettext' -], function(ListView, GroupConfigurationItemView, gettext) { - 'use strict'; - - var GroupConfigurationsListView = ListView.extend({ - tagName: 'div', - - className: 'group-configurations-list', - - newModelOptions: {addDefaultGroups: true}, - - // Translators: this refers to a collection of groups. - itemCategoryDisplayName: gettext('group configuration'), - - newItemMessage: gettext('Add your first group configuration'), - - emptyMessage: gettext('You have not created any group configurations yet.'), - - createItemView: function(options) { - return new GroupConfigurationItemView(options); - } - }); - - return GroupConfigurationsListView; -}); diff --git a/cms/static/js/views/pages/group_configurations.js b/cms/static/js/views/pages/group_configurations.js deleted file mode 100644 index 04de16838bb5..000000000000 --- a/cms/static/js/views/pages/group_configurations.js +++ /dev/null @@ -1,113 +0,0 @@ -define([ - 'jquery', 'underscore', 'gettext', 'js/views/pages/base_page', - 'js/views/group_configurations_list', 'js/views/partition_group_list' -], -function($, _, gettext, BasePage, GroupConfigurationsListView, PartitionGroupListView) { - 'use strict'; - - var GroupConfigurationsPage = BasePage.extend({ - initialize: function(options) { - var currentScheme, - i; - - BasePage.prototype.initialize.call(this); - this.experimentsEnabled = options.experimentsEnabled; - if (this.experimentsEnabled) { - this.experimentGroupConfigurations = options.experimentGroupConfigurations; - this.experimentGroupsListView = new GroupConfigurationsListView({ - collection: this.experimentGroupConfigurations - }); - } - - 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({ - id: this.allGroupConfigurations[i].get('id'), - collection: this.allGroupConfigurations[i].get('groups'), - restrictEditing: this.allGroupConfigurations[i].get('read_only'), - scheme: currentScheme - }) - ); - } - }, - - renderPage: function() { - var hash = this.getLocationHash(), - i, - currentClass; - if (this.experimentsEnabled) { - this.$('.wrapper-groups.experiment-groups').append(this.experimentGroupsListView.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.allGroupViewList[i].id}`; - this.$(currentClass).append(this.allGroupViewList[i].render().el); - } - - this.addWindowActions(); - if (hash) { - // Strip leading '#' to get id string to match - this.expandConfiguration(hash.replace('#', '')); - } - return $.Deferred().resolve().promise(); - }, - - addWindowActions: function() { - $(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.areAnyConfigurationsDirty() - || (this.experimentsEnabled && this.experimentGroupConfigurations.find(function(configuration) { - return configuration.isDirty(); - })); - - if (dirty) { - return gettext('You have unsaved changes. Do you really want to leave this page?'); - } - }, - - /** - * Helper method that returns url hash. - * @return {String} Returns anchor part of current url. - */ - getLocationHash: function() { - return window.location.hash; - }, - - /** - * Focus on and expand group configuration with peculiar id. - * @param {String|Number} Id of the group configuration. - */ - expandConfiguration: function(id) { - var groupConfig = this.experimentsEnabled && this.experimentGroupConfigurations.findWhere({ - id: parseInt(id) - }); - - if (groupConfig) { - groupConfig.set('showGroups', true); - this.$('#' + id).focus(); - } - } - }); - - return GroupConfigurationsPage; -}); // end define(); diff --git a/cms/static/js/views/partition_group_details.js b/cms/static/js/views/partition_group_details.js deleted file mode 100644 index 4740e460b50a..000000000000 --- a/cms/static/js/views/partition_group_details.js +++ /dev/null @@ -1,84 +0,0 @@ -/** - * This class defines a simple display view for a partition group. - * It is expected to be backed by a Group model. - */ -define([ - 'js/views/baseview', 'underscore', 'gettext', 'underscore.string', - 'edx-ui-toolkit/js/utils/string-utils', 'edx-ui-toolkit/js/utils/html-utils' -], function(BaseView, _, gettext, str, StringUtils, HtmlUtils) { - 'use strict'; - - var PartitionGroupDetailsView = BaseView.extend({ - tagName: 'div', - events: { - 'click .edit': 'editGroup', - 'click .show-groups': 'showContentGroupUsages', - 'click .hide-groups': 'hideContentGroupUsages' - }, - - className: function() { - var index = this.model.collection.indexOf(this.model); - - return [ - 'collection', - 'partition-group-details', - 'partition-group-details-' + index - ].join(' '); - }, - - editGroup: function() { - this.model.set({editing: true}); - }, - - initialize: function() { - this.template = this.loadTemplate('partition-group-details'); - this.restrictEditing = this.options.restrictEditing || false; - this.listenTo(this.model, 'change', this.render); - }, - - render: function(showContentGroupUsages) { - var attrs = $.extend({}, this.model.attributes, { - usageCountMessage: this.getUsageCountTitle(), - courseOutlineUrl: this.model.collection.parents[0].outlineUrl, - index: this.model.collection.indexOf(this.model), - showContentGroupUsages: showContentGroupUsages || false, - HtmlUtils: HtmlUtils, - restrictEditing: this.restrictEditing - }); - HtmlUtils.setHtml(this.$el, HtmlUtils.HTML(this.template(attrs))); - return this; - }, - - showContentGroupUsages: function(event) { - if (event && event.preventDefault) { event.preventDefault(); } - this.render(true); - }, - - hideContentGroupUsages: function(event) { - if (event && event.preventDefault) { event.preventDefault(); } - this.render(false); - }, - - getUsageCountTitle: function() { - var count = this.model.get('usage').length; - - if (count === 0) { - return gettext('Not in Use'); - } else { - /* globals ngettext */ - return StringUtils.interpolate(ngettext( - /* - Translators: 'count' is number of locations that the group - configuration is used in. - */ - 'Used in {count} location', 'Used in {count} locations', - count - ), - {count: count} - ); - } - } - }); - - return PartitionGroupDetailsView; -}); diff --git a/cms/static/js/views/partition_group_item.js b/cms/static/js/views/partition_group_item.js deleted file mode 100644 index d4d391e35457..000000000000 --- a/cms/static/js/views/partition_group_item.js +++ /dev/null @@ -1,46 +0,0 @@ -/** - * 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/partition_group_details', - 'gettext', 'common/js/components/utils/view_utils' -], function(ListItemView, ContentGroupEditorView, PartitionGroupDetailsView, gettext) { - 'use strict'; - - var PartitionGroupItemView = ListItemView.extend({ - events: { - 'click .delete': 'deleteItem' - }, - - tagName: 'section', - - baseClassName: 'partition-group', - - canDelete: true, - - itemDisplayName: gettext('content group'), - - attributes: function() { - return { - id: this.model.get('id'), - tabindex: -1 - }; - }, - - createEditView: function() { - return new ContentGroupEditorView({model: this.model}); - }, - - createDetailsView: function() { - return new PartitionGroupDetailsView({ - model: this.model, - restrictEditing: this.options.restrictEditing - }); - } - }); - - return PartitionGroupItemView; -}); diff --git a/cms/static/js/views/partition_group_list.js b/cms/static/js/views/partition_group_list.js deleted file mode 100644 index 602889e1b76e..000000000000 --- a/cms/static/js/views/partition_group_list.js +++ /dev/null @@ -1,33 +0,0 @@ -/** - * 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/static/sass/_build-v1.scss b/cms/static/sass/_build-v1.scss index 97d21c3f4a53..831cdb277747 100644 --- a/cms/static/sass/_build-v1.scss +++ b/cms/static/sass/_build-v1.scss @@ -71,7 +71,6 @@ @import 'views/container'; @import 'views/users'; @import 'views/export-git'; -@import 'views/group-configuration'; // +Base - Contexts // ==================== diff --git a/cms/static/sass/elements/_header.scss b/cms/static/sass/elements/_header.scss index f1033d80d7c9..0552a7107ff5 100644 --- a/cms/static/sass/elements/_header.scss +++ b/cms/static/sass/elements/_header.scss @@ -367,10 +367,8 @@ body.course.view-uploads .nav-course-courseware-uploads, body.course.view-textbooks .nav-course-courseware-textbooks, body.course.view-video-uploads .nav-course-courseware-videos, body.course.view-team .nav-course-settings .title, -body.course.view-group-configurations .nav-course-settings .title, body.course.advanced .nav-course-settings .title, body.course.view-team .nav-course-settings-team, -body.course.view-group-configurations .nav-course-settings-group-configurations, body.course.advanced .nav-course-settings-advanced, body.course.view-import .nav-course-tools .title, body.course.view-export .nav-course-tools .title, diff --git a/cms/static/sass/views/_group-configuration.scss b/cms/static/sass/views/_group-configuration.scss deleted file mode 100644 index 95d332de78ea..000000000000 --- a/cms/static/sass/views/_group-configuration.scss +++ /dev/null @@ -1,673 +0,0 @@ -// studio - views - group-configurations -// ==================== -.view-group-configurations { - .content-primary, - .content-supplementary { - box-sizing: border-box; - - @include float(left); - } - - .content-primary { - @include margin-right(flex-gutter()); - - width: flex-grid(9, 12); - - .notice-moduledisabled { - @extend %ui-well; - @extend %t-copy-base; - - background-color: $white; - padding: ($baseline*1.5) $baseline; - text-align: center; - } - - .no-content { - @extend %no-content; - - color: $gray-d1; - } - - .wrapper-groups { - margin-bottom: ($baseline*1.5); - - .title { - @extend %t-title4; - @extend %t-strong; - - margin-bottom: ($baseline/2); - } - - .copy { - @extend %t-copy-sub1; - } - } - - .wrapper-collection { - @extend %ui-window; - - position: relative; - outline: none; - - &:hover .collection .actions { - opacity: 1; - } - - .collection-details { - padding: $baseline ($baseline*1.5); - } - - .collection-header { - margin-bottom: 0; - border-bottom: 0; - padding-bottom: 0; - - .title { - @extend %cont-truncated; - @extend %t-title5; - @extend %t-strong; - - @include margin-right($baseline*14); - - color: $black; - - .toggle { - @include padding-left($baseline); - - display: inline-block; - color: $black; - - &:hover, - &:focus { - color: $blue; - } - - .ui-toggle-expansion { - @include transition(rotate 0.15s ease-in-out 0.25s); - - @extend %t-action1; - - @include margin-left(-$baseline); - - display: inline-block; - width: ($baseline*0.75); - vertical-align: baseline; - - &.fa-caret-right::before { - @include rtl { - // Flip to fa-caret-left on RTL - content: "\f0d9"; - } - } - } - - &.is-selectable { - @extend %ui-fake-link; - - &:hover { - color: $blue; - - .ui-toggle-expansion { - color: $blue; - } - } - } - } - } - } - - .collection-info { - @extend %t-copy-sub1; - - @include margin-left($baseline); - - color: $gray-l1; - - &.group-configuration-info-inline { - @include margin($baseline/4, 0, $baseline/2, $baseline); - - display: table; - width: 70%; - - li { - box-sizing: border-box; - - @include margin-right(1%); - - display: table-cell; - - &.group-configuration-usage-count { - font-style: italic; - } - } - } - - &.group-configuration-info-block { - li { - padding: ($baseline/4) 0; - } - } - - &.collection-info-inline { - @include margin($baseline/4, 0, $baseline/2, $baseline); - - display: table; - width: 70%; - - li { - box-sizing: border-box; - - @include margin-right(1%); - - display: table-cell; - padding: ($baseline/4) 0; - - &.collection-usage-count { - font-style: italic; - } - } - } - - .collection-label { - text-transform: uppercase; - } - - .collection-description { - overflow: hidden; - text-overflow: ellipsis; - } - } - - .collection-items { - @include margin-left($baseline); - - margin-bottom: ($baseline*0.75); - - .item { - @extend %t-copy-lead1; - - padding: ($baseline/7) 0 ($baseline/4); - border-top: 1px solid $gray-l4; - white-space: nowrap; - - &:first-child { - border-top: none; - } - - .name { - @include margin-right(5%); - - overflow: hidden; - text-overflow: ellipsis; - display: inline-block; - vertical-align: middle; - width: 75%; - } - } - } - - .collection-details { - .actions { - @include transition(opacity 0.15s 0.25s ease-in-out); - @include right($baseline); - - position: absolute; - top: $baseline; - opacity: 0; - - .action { - @include margin-right($baseline/4); - - display: inline-block; - vertical-align: middle; - - .edit { - @extend %ui-btn-non-blue; - } - - .delete { - @extend %ui-btn-non; - - &.is-disabled { - background-color: $gray-l3; - color: $gray-l6; - } - } - } - } - } - - .collection-references { - @extend %t-copy-sub1; - - @include padding($baseline, $baseline*1.5, $baseline, $baseline*2.5); - - box-shadow: 0 2px 2px 0 $shadow inset; - color: $gray-l1; - - .usage { - margin-left: $baseline; - - .usage-unit { - padding: ($baseline/4) 0; - - a { - @extend %t-strong; - } - - .fa-warning { - @include margin($baseline/4, $baseline/2, 0, $baseline*1.5); - - color: $orange; - } - - .fa-times-circle { - @include margin($baseline/4, $baseline/2, 0, $baseline*1.5); - - color: $red-l2; - } - } - } - } - - .usage-validation { - @extend %t-copy-sub1; - - @include padding($baseline, $baseline*1.5, $baseline, $baseline*1.5); - - background-color: $gray-l6; - margin-top: $baseline; - - .fa-warning { - @include margin($baseline/2, $baseline, 0, 0); - @include float(left); - - color: $orange; - } - - .collection-validation-text { - overflow: auto; - } - } - - .collection-edit { - box-sizing: border-box; - border-radius: 2px; - width: 100%; - background: $white; - - .message { - margin-bottom: 0; - } - - .wrapper-form { - padding: $baseline ($baseline*1.5); - } - - .tip { - @extend %t-copy-sub2; - - @include transition(color, 0.15s, ease-in-out); - - display: block; - margin-top: ($baseline/4); - color: $gray-d1; - } - - .is-focused .tip { - color: $gray-d2; - } - - - .collection-fields { - @extend %cont-no-list; - - margin-bottom: $baseline; - } - - .field { - @include margin(0, 0, $baseline*0.75, 0); - - &:last-child { - @extend %wipe-last-child; - } - - &.required { - label { - @extend %t-strong; - } - - label::after { - @include margin-left($baseline/4); - - content: "*"; - } - } - - label, - input, - textarea { - display: block; - } - - textarea { - resize: vertical; - } - - label { - @extend %t-copy-sub1; - - @include transition(color, 0.15s, ease-in-out); - @include margin(0, 0, $baseline/4, 0); - - &.is-focused { - color: $blue; - } - } - - //this section is borrowed from _account.scss - we should clean up and unify later - input, - textarea { - @extend %t-copy-base; - - height: 100%; - width: 100%; - padding: ($baseline/2); - - &.long { - width: 100%; - } - - &.short { - width: 25%; - } - - ::-webkit-input-placeholder { - color: $gray-l4; - } - - :-moz-placeholder { - color: $gray-l3; - } - - ::-moz-placeholder { - color: $gray-l3; - } - - :-ms-input-placeholder { - color: $gray-l3; - } - - &:focus { - + .tip { - color: $gray-d1; - } - } - } - - &.error { - label { - color: $red; - } - - input { - border-color: $red; - } - } - } - - label.required { - @extend %t-strong; - - &::after { - @include margin-left($baseline/4); - - content: "*"; - } - } - - .field.add-collection-name { - label { - width: 50%; - - @extend %t-title5; - - display: inline-block; - vertical-align: bottom; - } - - .group-configuration-id { - @include text-align(right); - - display: inline-block; - width: 45%; - vertical-align: top; - color: $gray-l1; - - .group-configuration-value { - @extend %t-strong; - - @include margin-left($baseline*0.5); - - white-space: nowrap; - } - } - } - - - .actions { - box-shadow: inset 0 1px 2px $shadow; - border-top: 1px solid $gray-l1; - padding: ($baseline*0.75) $baseline; - background: $gray-l6; - - .action { - @include margin-right($baseline/4); - - &:last-child { - @include margin-right(0); - } - } - - // add a group is below with groups styling - .action-primary { - @extend %btn-primary-blue; - - padding: ($baseline/4) $baseline; - } - - .action-secondary { - @extend %btn-secondary-gray; - - padding: ($baseline/4) $baseline; - } - - .wrapper-delete-button { - @include float(right); - - padding: ($baseline/4) ($baseline/2); - - .is-disabled { - color: $gray-l3; - } - } - } - - .copy { - @extend %t-copy-sub2; - - @include margin($baseline, 0, $baseline/2, 0); - - color: $gray; - - strong { - @extend %t-strong; - } - } - } - - .action-add-item { - @extend %ui-btn-flat-outline; - @extend %t-action2; - @extend %t-strong; - - @include margin($baseline*1.5, 0, 0, 0); - - display: block; - width: 100%; - padding: ($baseline/2); - } - } - - // add/new collection - .action-add { - @extend %ui-btn-flat-outline; - - display: block; - width: 100%; - margin-top: ($baseline*0.75); - padding: ($baseline/2) $baseline; - - &.is-hidden { - display: none; - } - - .icon { - @include margin-right($baseline/2); - - display: inline-block; - vertical-align: middle; - } - } - - // specific group-type styles - .content-groups { - .collection-header { - .title { - margin-bottom: 0; - } - } - } - - .experiment-groups { - .group-configuration-details { - .group-configuration-info { - @extend %t-copy-sub1; - - @include margin-left($baseline); - - color: $gray-l1; - - .group-configuration-label { - text-transform: uppercase; - } - - .group-configuration-description { - overflow: hidden; - text-overflow: ellipsis; - } - } - - .groups { - @include margin-left($baseline); - - margin-bottom: ($baseline*0.75); - - .group { - @extend %t-copy-lead1; - - padding: ($baseline/7) 0 ($baseline/4); - border-top: 1px solid $gray-l4; - white-space: nowrap; - - &:first-child { - border-top: none; - } - - .group-name { - @include margin-right(5%); - - overflow: hidden; - text-overflow: ellipsis; - display: inline-block; - vertical-align: middle; - width: 75%; - } - - .group-allocation { - @include text-align(right); - - display: inline-block; - vertical-align: middle; - width: 20%; - color: $gray-l1; - } - } - } - } - - .group-configuration-edit { - .add-collection-name label { - @include padding-right(5%); - - overflow: hidden; - text-overflow: ellipsis; - vertical-align: bottom; - } - - .field-group { - @include clearfix(); - @include margin(0, 0, $baseline/2, 0); - @include padding($baseline/4, 0, 0, 0); - - .group-allocation, - .field { - @include margin(0, 3%, 0, 0); - - display: inline-block; - vertical-align: middle; - } - - .group-allocation { - max-width: 10%; - min-width: 5%; - color: $gray-l1; - } - - .field { - position: relative; - - &.long { - width: 80%; - } - - &.short { - width: 10%; - } - } - - .action-close { - @include transition(color $tmg-f2 ease-in-out); - - @extend %t-action1; - - display: inline-block; - border: 0; - padding: 0; - background: transparent; - color: $blue-l3; - vertical-align: middle; - - &:hover { - color: $blue; - } - } - } - } - } - } - - .content-supplementary { - width: flex-grid(3, 12); - } -} diff --git a/cms/templates/group_configurations.html b/cms/templates/group_configurations.html deleted file mode 100644 index 607cbc965321..000000000000 --- a/cms/templates/group_configurations.html +++ /dev/null @@ -1,140 +0,0 @@ -<%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'/> -<%! -from cms.djangoapps.contentstore import utils -from django.utils.translation import gettext as _ -from openedx.core.djangolib.js_utils import ( - dump_js_escaped_json, js_escaped_string -) -from openedx.core.djangolib.markup import HTML, Text -from six.moves.urllib.parse import quote -%> - -<%block name="title">${_("Group Configurations")}%block> -<%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", "partition-group-details", "basic-modal", "modal-button", "list"]: - -% endfor -%block> - -<%block name="requirejs"> - require(["js/factories/group_configurations"], function(GroupConfigurationsFactory) { - GroupConfigurationsFactory( - ${should_show_experiment_groups | n, dump_js_escaped_json}, - ${experiment_group_configurations | 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}", - ${should_show_enrollment_track | n, dump_js_escaped_json} - ); - }); -%block> - -<%block name="content"> -
${_("Loading")}
-- ${_("This module is disabled at the moment.")} -
-${_("Loading")}
-- <% if (unit.validation.type === 'warning') { %> - - <% } else if (unit.validation.type === 'error') { %> - - <% } %> - -
- <% } %> -- <%= HtmlUtils.interpolateHtml( - gettext('This Group Configuration is not in use. Start by adding a content experiment to any Unit via the {linkStart}Course Outline{linkEnd}.'), - { - linkStart: HtmlUtils.interpolateHtml( - HtmlUtils.HTML(''), - { courseOutlineUrl: courseOutlineUrl, courseOutlineTitle: gettext('Course Outline')}), - linkEnd: HtmlUtils.HTML('') - }) - %> -
- <% } %> -Loading
-Loading
-- <%= HtmlUtils.interpolateHtml( - gettext("In the {linkStart}Course Outline{linkEnd}, use this group to control access to a component."), - { - linkStart: HtmlUtils.interpolateHtml( - HtmlUtils.HTML(''), - {courseOutlineUrl: courseOutlineUrl, courseOutlineTitle: gettext('Course Outline')} - ), - linkEnd: HtmlUtils.HTML('') - } - ) - %> - -
- <% } %> -