Commit 9f313fcb by jmclaus

Merge pull request #4613 from edx/jmclaus/easy-access-group-configurations-page

BLD 1020: Easy access to Group Configurations page from Content Experiment component.
parents 043d495a 459c241e
define([ define([
'jquery', 'underscore', 'js/views/pages/group_configurations', 'jquery', 'underscore', 'js/views/pages/group_configurations',
'js/collections/group_configuration' 'js/collections/group_configuration', 'js/models/group_configuration', 'js/spec_helpers/edit_helpers'
], function ($, _, GroupConfigurationsPage, GroupConfigurationCollection) { ], function ($, _, GroupConfigurationsPage, GroupConfigurationCollection, GroupConfigurationModel, view_helpers) {
'use strict'; 'use strict';
describe('GroupConfigurationsPage', function() { describe('GroupConfigurationsPage', function() {
var mockGroupConfigurationsPage = readFixtures( var mockGroupConfigurationsPage = readFixtures(
'mock/mock-group-configuration-page.underscore' 'mock/mock-group-configuration-page.underscore'
), ),
noGroupConfigurationsTpl = readFixtures( itemClassName = '.group-configurations-list-item';
'no-group-configurations.underscore'
),
groupConfigurationEditTpl = readFixtures(
'group-configuration-edit.underscore'
);
var initializePage = function (disableSpy) { var initializePage = function (disableSpy) {
var view = new GroupConfigurationsPage({ var view = new GroupConfigurationsPage({
el: $('#content'), el: $('#content'),
collection: new GroupConfigurationCollection({ collection: new GroupConfigurationCollection({
id: 0,
name: 'Configuration 1' name: 'Configuration 1'
}) })
}); });
...@@ -38,17 +34,17 @@ define([ ...@@ -38,17 +34,17 @@ define([
}; };
beforeEach(function () { beforeEach(function () {
setFixtures($('<script>', { setFixtures(mockGroupConfigurationsPage);
id: 'no-group-configurations-tpl', view_helpers.installTemplates([
type: 'text/template' 'no-group-configurations', 'group-configuration-edit',
}).text(noGroupConfigurationsTpl)); 'group-configuration-details'
]);
appendSetFixtures($('<script>', {
id: 'group-configuration-edit-tpl',
type: 'text/template'
}).text(groupConfigurationEditTpl));
appendSetFixtures(mockGroupConfigurationsPage); this.addMatchers({
toBeExpanded: function () {
return Boolean($('a.group-toggle.hide-groups', $(this.actual)).length);
}
});
}); });
describe('Initial display', function() { describe('Initial display', function() {
...@@ -56,7 +52,7 @@ define([ ...@@ -56,7 +52,7 @@ define([
var view = initializePage(); var view = initializePage();
expect(view.$('.ui-loading')).toBeVisible(); expect(view.$('.ui-loading')).toBeVisible();
view.render(); view.render();
expect(view.$('.no-group-configurations-content')).toBeTruthy(); expect(view.$(itemClassName)).toExist()
expect(view.$('.ui-loading')).toBeHidden(); expect(view.$('.ui-loading')).toBeHidden();
}); });
}); });
...@@ -74,10 +70,9 @@ define([ ...@@ -74,10 +70,9 @@ define([
it('I do not see notification message if the model is not changed', it('I do not see notification message if the model is not changed',
function() { function() {
var expectedMessage = var expectedMessage ='You have unsaved changes. Do you really want to leave this page?',
'You have unsaved changes. Do you really want to leave this page?', view = renderPage(),
view = renderPage(), message;
message;
view.collection.at(0).set('name', 'Configuration 2'); view.collection.at(0).set('name', 'Configuration 2');
message = view.onBeforeUnload(); message = view.onBeforeUnload();
...@@ -85,6 +80,36 @@ define([ ...@@ -85,6 +80,36 @@ define([
}); });
}); });
describe('Check that Group Configuration will focus and expand depending on content of url hash', function() {
beforeEach(function () {
spyOn($.fn, 'focus');
view_helpers.installTemplate('group-configuration-details');
this.view = initializePage(true);
});
it('should focus and expand group configuration if its id is part of url hash', function() {
spyOn(this.view, 'getLocationHash').andReturn('#0');
this.view.render();
// We cannot use .toBeFocused due to flakiness.
expect($.fn.focus).toHaveBeenCalled();
expect(this.view.$(itemClassName)).toBeExpanded();
});
it('should not focus on any group configuration if url hash is empty', function() {
spyOn(this.view, 'getLocationHash').andReturn('');
this.view.render();
expect($.fn.focus).not.toHaveBeenCalled();
expect(this.view.$(itemClassName)).not.toBeExpanded();
});
it('should not focus on any group configuration if url hash contains wrong id', function() {
spyOn(this.view, 'getLocationHash').andReturn('#1');
this.view.render();
expect($.fn.focus).not.toHaveBeenCalled();
expect(this.view.$(itemClassName)).not.toBeExpanded();
});
});
it('create new group configuration', function () { it('create new group configuration', function () {
var view = renderPage(); var view = renderPage();
......
...@@ -7,8 +7,11 @@ define([ ...@@ -7,8 +7,11 @@ define([
'use strict'; 'use strict';
var GroupConfigurationsItem = BaseView.extend({ var GroupConfigurationsItem = BaseView.extend({
tagName: 'section', tagName: 'section',
attributes: { attributes: function () {
'tabindex': -1 return {
'id': this.model.get('id'),
'tabindex': -1
};
}, },
events: { events: {
'click .delete': 'deleteConfiguration' 'click .delete': 'deleteConfiguration'
...@@ -65,7 +68,6 @@ define([ ...@@ -65,7 +68,6 @@ define([
} }
this.$el.html(this.view.render().el); this.$el.html(this.view.render().el);
this.$el.focus();
return this; return this;
} }
......
...@@ -13,10 +13,15 @@ function ($, _, gettext, BaseView, GroupConfigurationsList) { ...@@ -13,10 +13,15 @@ function ($, _, gettext, BaseView, GroupConfigurationsList) {
}, },
render: function() { render: function() {
var hash = this.getLocationHash();
this.hideLoadingIndicator(); this.hideLoadingIndicator();
this.$('.content-primary').append(this.listView.render().el); this.$('.content-primary').append(this.listView.render().el);
this.addButtonActions(); this.addButtonActions();
this.addWindowActions(); this.addWindowActions();
if (hash) {
// Strip leading '#' to get id string to match
this.expandConfiguration(hash.replace('#', ''))
}
}, },
addButtonActions: function () { addButtonActions: function () {
...@@ -39,6 +44,29 @@ function ($, _, gettext, BaseView, GroupConfigurationsList) { ...@@ -39,6 +44,29 @@ function ($, _, gettext, BaseView, GroupConfigurationsList) {
'You have unsaved changes. Do you really want to leave this page?' '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.collection.findWhere({
id: parseInt(id)
});
if (groupConfig) {
groupConfig.set('showGroups', true);
this.$('#' + id).focus();
}
} }
}); });
......
...@@ -127,6 +127,7 @@ class SplitTestFields(object): ...@@ -127,6 +127,7 @@ class SplitTestFields(object):
scope=Scope.content scope=Scope.content
) )
@XBlock.needs('user_tags') # pylint: disable=abstract-method @XBlock.needs('user_tags') # pylint: disable=abstract-method
@XBlock.wants('partitions') @XBlock.wants('partitions')
class SplitTestModule(SplitTestFields, XModule, StudioEditableModule): class SplitTestModule(SplitTestFields, XModule, StudioEditableModule):
...@@ -266,6 +267,7 @@ class SplitTestModule(SplitTestFields, XModule, StudioEditableModule): ...@@ -266,6 +267,7 @@ class SplitTestModule(SplitTestFields, XModule, StudioEditableModule):
is_root = root_xblock and root_xblock.location == self.location is_root = root_xblock and root_xblock.location == self.location
active_groups_preview = None active_groups_preview = None
inactive_groups_preview = None inactive_groups_preview = None
if is_root: if is_root:
[active_children, inactive_children] = self.descriptor.active_and_inactive_children() [active_children, inactive_children] = self.descriptor.active_and_inactive_children()
active_groups_preview = self.studio_render_children( active_groups_preview = self.studio_render_children(
...@@ -281,6 +283,7 @@ class SplitTestModule(SplitTestFields, XModule, StudioEditableModule): ...@@ -281,6 +283,7 @@ class SplitTestModule(SplitTestFields, XModule, StudioEditableModule):
'is_configured': is_configured, 'is_configured': is_configured,
'active_groups_preview': active_groups_preview, 'active_groups_preview': active_groups_preview,
'inactive_groups_preview': inactive_groups_preview, 'inactive_groups_preview': inactive_groups_preview,
'group_configuration_url': self.descriptor.group_configuration_url,
})) }))
fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/split_test_author_view.js')) fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/split_test_author_view.js'))
fragment.initialize_js('SplitTestAuthorView') fragment.initialize_js('SplitTestAuthorView')
...@@ -563,6 +566,23 @@ class SplitTestDescriptor(SplitTestFields, SequenceDescriptor, StudioEditableDes ...@@ -563,6 +566,23 @@ class SplitTestDescriptor(SplitTestFields, SequenceDescriptor, StudioEditableDes
self.system.modulestore.update_item(self, None) self.system.modulestore.update_item(self, None)
return Response() return Response()
@property
def group_configuration_url(self):
assert hasattr(self.system, 'modulestore') and hasattr(self.system.modulestore, 'get_course'), \
"modulestore has to be available"
course_module = self.system.modulestore.get_course(self.location.course_key)
group_configuration_url = None
if 'split_test' in course_module.advanced_modules:
user_partition = self.get_selected_partition()
if user_partition:
group_configuration_url = "{url}#{configuration_id}".format(
url='/group_configurations/' + unicode(self.location.course_key),
configuration_id=str(user_partition.id)
)
return group_configuration_url
def _create_vertical_for_group(self, group, user_id): def _create_vertical_for_group(self, group, user_id):
""" """
Creates a vertical to associate with the group. Creates a vertical to associate with the group.
......
...@@ -160,7 +160,8 @@ class SplitTestModuleStudioTest(SplitTestModuleTest): ...@@ -160,7 +160,8 @@ class SplitTestModuleStudioTest(SplitTestModuleTest):
Unit tests for how split test interacts with Studio. Unit tests for how split test interacts with Studio.
""" """
def test_render_author_view(self): @patch('xmodule.split_test_module.SplitTestDescriptor.group_configuration_url', return_value='http://example.com')
def test_render_author_view(self, group_configuration_url):
""" """
Test the rendering of the Studio author view. Test the rendering of the Studio author view.
""" """
...@@ -197,6 +198,22 @@ class SplitTestModuleStudioTest(SplitTestModuleTest): ...@@ -197,6 +198,22 @@ class SplitTestModuleStudioTest(SplitTestModuleTest):
self.assertIn('HTML FOR GROUP 0', html) self.assertIn('HTML FOR GROUP 0', html)
self.assertIn('HTML FOR GROUP 1', html) self.assertIn('HTML FOR GROUP 1', html)
def test_group_configuration_url(self):
"""
Test creation of correct Group Configuration URL.
"""
mocked_course = Mock(advanced_modules=['split_test'])
mocked_modulestore = Mock()
mocked_modulestore.get_course.return_value = mocked_course
self.split_test_module.system.modulestore = mocked_modulestore
self.split_test_module.user_partitions = [
UserPartition(0, 'first_partition', 'First Partition', [Group("0", 'alpha'), Group("1", 'beta')])
]
expected_url = '/group_configurations/edX/xml_test_course/101#0'
self.assertEqual(expected_url, self.split_test_module.group_configuration_url)
def test_editable_settings(self): def test_editable_settings(self):
""" """
Test the setting information passed back from editable_metadata_fields. Test the setting information passed back from editable_metadata_fields.
......
...@@ -56,6 +56,13 @@ class GroupConfiguration(object): ...@@ -56,6 +56,13 @@ class GroupConfiguration(object):
""" """
self.find_css('a.group-toggle').first.click() self.find_css('a.group-toggle').first.click()
@property
def is_expanded(self):
"""
Group configuration usage information is expanded.
"""
return self.find_css('a.group-toggle.hide-groups').present
def add_group(self): def add_group(self):
""" """
Add new group. Add new group.
......
...@@ -179,3 +179,16 @@ class Component(PageObject): ...@@ -179,3 +179,16 @@ class Component(PageObject):
Click on settings Save button. Click on settings Save button.
""" """
self._click_button('save_settings') self._click_button('save_settings')
def go_to_group_configuration_page(self):
"""
Go to the Group Configuration used by the component.
"""
self.q(css=self._bounded_selector('span.message-text a')).first.click()
@property
def group_configuration_link_name(self):
"""
Get Group Configuration name from link.
"""
return self.q(css=self._bounded_selector('span.message-text a')).first.text[0]
...@@ -713,7 +713,6 @@ class GroupConfigurationsTest(ContainerBase, SplitTestMixin): ...@@ -713,7 +713,6 @@ class GroupConfigurationsTest(ContainerBase, SplitTestMixin):
], ],
}, },
}) })
vertical = self.course_fixture.get_nested_xblocks(category="vertical")[0] vertical = self.course_fixture.get_nested_xblocks(category="vertical")[0]
self.course_fixture.create_xblock( self.course_fixture.create_xblock(
vertical.locator, vertical.locator,
...@@ -729,3 +728,50 @@ class GroupConfigurationsTest(ContainerBase, SplitTestMixin): ...@@ -729,3 +728,50 @@ class GroupConfigurationsTest(ContainerBase, SplitTestMixin):
config.edit() config.edit()
self.assertTrue(config.delete_button_is_disabled) self.assertTrue(config.delete_button_is_disabled)
self.assertIn('Cannot delete when in use by an experiment', config.delete_note) self.assertIn('Cannot delete when in use by an experiment', config.delete_note)
def test_easy_access_from_experiment(self):
"""
Scenario: When a Content Experiment uses a Group Configuration,
ensure that the link to that Group Configuration works correctly.
Given I have a course with two Group Configurations
And Content Experiment is assigned to one Group Configuration
Then I see a link to Group Configuration
When I click on the Group Configuration link
Then I see the Group Configurations page
And I see that appropriate Group Configuration is expanded.
"""
# Create a new group configurations
self.course_fixture._update_xblock(self.course_fixture._course_location, {
"metadata": {
u"user_partitions": [
UserPartition(0, "Name", "Description.", [Group("0", "Group A"), Group("1", "Group B")]).to_json(),
UserPartition(1, 'Name of second Group Configuration', 'Second group configuration.', [Group("0", 'Alpha'), Group("1", 'Beta'), Group("2", 'Gamma')]).to_json(),
],
},
})
# Assign newly created group configuration to unit
vertical = self.course_fixture.get_nested_xblocks(category="vertical")[0]
self.course_fixture.create_xblock(
vertical.locator,
XBlockFixtureDesc('split_test', 'Test Content Experiment', metadata={'user_partition_id': 1})
)
unit = UnitPage(self.browser, vertical.locator)
unit.visit()
experiment = unit.components[0]
group_configuration_link_name = experiment.group_configuration_link_name
experiment.go_to_group_configuration_page()
self.page.wait_for_page()
# Appropriate Group Configuration is expanded.
self.assertFalse(self.page.group_configurations[0].is_expanded)
self.assertTrue(self.page.group_configurations[1].is_expanded)
self.assertEqual(
group_configuration_link_name,
self.page.group_configurations[1].name
)
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
split_test = context.get('split_test') split_test = context.get('split_test')
user_partition = split_test.descriptor.get_selected_partition() user_partition = split_test.descriptor.get_selected_partition()
messages = split_test.descriptor.validation_messages() messages = split_test.descriptor.validation_messages()
show_link = settings.FEATURES.get('ENABLE_GROUP_CONFIGURATIONS') and group_configuration_url is not None
%> %>
% if is_root and not is_configured: % if is_root and not is_configured:
...@@ -17,7 +18,9 @@ messages = split_test.descriptor.validation_messages() ...@@ -17,7 +18,9 @@ messages = split_test.descriptor.validation_messages()
<div class="xblock-message information"> <div class="xblock-message information">
<p> <p>
<span class="message-text"> <span class="message-text">
${_("This content experiment uses group configuration '{experiment_name}'.").format(experiment_name=user_partition.name)} ${_("This content experiment uses group configuration '{group_configuration_name}'.").format(
group_configuration_name="<a href='{}'>{}</a>".format(group_configuration_url, user_partition.name) if show_link else user_partition.name
)}
</span> </span>
</p> </p>
</div> </div>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment