Commit fd587b97 by Christina Roberts

Merge pull request #5710 from edx/christina/general-component-errors

Support validation messages for any xblock (on container page).
parents f0a37cf4 181715f2
...@@ -7,6 +7,8 @@ the top. Include a label indicating the component affected. ...@@ -7,6 +7,8 @@ the top. Include a label indicating the component affected.
Common: Add configurable reset button to units Common: Add configurable reset button to units
Studio: Add support xblock validation messages on Studio unit/container page. TNL-683
LMS: Support adding cohorts from the instructor dashboard. TNL-162 LMS: Support adding cohorts from the instructor dashboard. TNL-162
LMS: Support adding students to a cohort via the instructor dashboard. TNL-163 LMS: Support adding students to a cohort via the instructor dashboard. TNL-163
......
...@@ -1290,10 +1290,12 @@ class GroupConfiguration(object): ...@@ -1290,10 +1290,12 @@ class GroupConfiguration(object):
'container_handler', 'container_handler',
course.location.course_key.make_usage_key(unit.location.block_type, unit.location.name) course.location.course_key.make_usage_key(unit.location.block_type, unit.location.name)
) )
validation_summary = split_test.general_validation_message()
usage_info[split_test.user_partition_id].append({ usage_info[split_test.user_partition_id].append({
'label': '{} / {}'.format(unit.display_name, split_test.display_name), 'label': '{} / {}'.format(unit.display_name, split_test.display_name),
'url': unit_url, 'url': unit_url,
'validation': split_test.general_validation_message, 'validation': validation_summary.to_json() if validation_summary else None,
}) })
return usage_info return usage_info
......
...@@ -9,7 +9,7 @@ from contentstore.views.course import GroupConfiguration ...@@ -9,7 +9,7 @@ from contentstore.views.course import GroupConfiguration
from contentstore.tests.utils import CourseTestCase from contentstore.tests.utils import CourseTestCase
from xmodule.partitions.partitions import Group, UserPartition from xmodule.partitions.partitions import Group, UserPartition
from xmodule.modulestore.tests.factories import ItemFactory from xmodule.modulestore.tests.factories import ItemFactory
from xmodule.split_test_module import ValidationMessage, ValidationMessageType from xmodule.validation import StudioValidation, StudioValidationMessage
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.modulestore import ModuleStoreEnum from xmodule.modulestore import ModuleStoreEnum
...@@ -541,87 +541,75 @@ class GroupConfigurationsValidationTestCase(CourseTestCase, HelperMethods): ...@@ -541,87 +541,75 @@ class GroupConfigurationsValidationTestCase(CourseTestCase, HelperMethods):
def setUp(self): def setUp(self):
super(GroupConfigurationsValidationTestCase, self).setUp() super(GroupConfigurationsValidationTestCase, self).setUp()
@patch('xmodule.split_test_module.SplitTestDescriptor.validation_messages') @patch('xmodule.split_test_module.SplitTestDescriptor.validate_split_test')
def test_error_message_present(self, mocked_validation_messages): def verify_validation_add_usage_info(self, expected_result, mocked_message, mocked_validation_messages):
""" """
Tests if validation message is present. Helper method for testing validation information present after add_usage_info.
""" """
self._add_user_partitions() self._add_user_partitions()
split_test = self._create_content_experiment(cid=0, name_suffix='0')[1] split_test = self._create_content_experiment(cid=0, name_suffix='0')[1]
mocked_validation_messages.return_value = [ validation = StudioValidation(split_test.location)
ValidationMessage( validation.add(mocked_message)
split_test, mocked_validation_messages.return_value = validation
u"Validation message",
ValidationMessageType.error
)
]
group_configuration = GroupConfiguration.add_usage_info(self.course, self.store)[0] group_configuration = GroupConfiguration.add_usage_info(self.course, self.store)[0]
self.assertEqual( self.assertEqual(expected_result.to_json(), group_configuration['usage'][0]['validation'])
group_configuration['usage'][0]['validation'],
{ def test_error_message_present(self):
'message': u'This content experiment has issues that affect content visibility.', """
'type': 'error' Tests if validation message is present (error case).
} """
mocked_message = StudioValidationMessage(StudioValidationMessage.ERROR, u"Validation message")
expected_result = StudioValidationMessage(
StudioValidationMessage.ERROR, u"This content experiment has issues that affect content visibility."
) )
self.verify_validation_add_usage_info(expected_result, mocked_message) # pylint: disable=no-value-for-parameter
@patch('xmodule.split_test_module.SplitTestDescriptor.validation_messages') def test_warning_message_present(self):
def test_warning_message_present(self, mocked_validation_messages): """
Tests if validation message is present (warning case).
""" """
Tests if validation message is present. mocked_message = StudioValidationMessage(StudioValidationMessage.WARNING, u"Validation message")
expected_result = StudioValidationMessage(
StudioValidationMessage.WARNING, u"This content experiment has issues that affect content visibility."
)
self.verify_validation_add_usage_info(expected_result, mocked_message) # pylint: disable=no-value-for-parameter
@patch('xmodule.split_test_module.SplitTestDescriptor.validate_split_test')
def verify_validation_update_usage_info(self, expected_result, mocked_message, mocked_validation_messages):
"""
Helper method for testing validation information present after update_usage_info.
""" """
self._add_user_partitions() self._add_user_partitions()
split_test = self._create_content_experiment(cid=0, name_suffix='0')[1] split_test = self._create_content_experiment(cid=0, name_suffix='0')[1]
mocked_validation_messages.return_value = [ validation = StudioValidation(split_test.location)
ValidationMessage( if mocked_message is not None:
split_test, validation.add(mocked_message)
u"Validation message", mocked_validation_messages.return_value = validation
ValidationMessageType.warning
group_configuration = GroupConfiguration.update_usage_info(
self.store, self.course, self.course.user_partitions[0]
) )
]
group_configuration = GroupConfiguration.add_usage_info(self.course, self.store)[0]
self.assertEqual( self.assertEqual(
group_configuration['usage'][0]['validation'], expected_result.to_json() if expected_result is not None else None,
{ group_configuration['usage'][0]['validation']
'message': u'This content experiment has issues that affect content visibility.',
'type': 'warning'
}
) )
@patch('xmodule.split_test_module.SplitTestDescriptor.validation_messages') def test_update_usage_info(self):
def test_update_usage_info(self, mocked_validation_messages):
""" """
Tests if validation message is present when updating usage info. Tests if validation message is present when updating usage info.
""" """
self._add_user_partitions() mocked_message = StudioValidationMessage(StudioValidationMessage.WARNING, u"Validation message")
split_test = self._create_content_experiment(cid=0, name_suffix='0')[1] expected_result = StudioValidationMessage(
StudioValidationMessage.WARNING, u"This content experiment has issues that affect content visibility."
mocked_validation_messages.return_value = [
ValidationMessage(
split_test,
u"Validation message",
ValidationMessageType.warning
)
]
group_configuration = GroupConfiguration.update_usage_info(self.store, self.course, self.course.user_partitions[0])
self.assertEqual(
group_configuration['usage'][0]['validation'],
{
'message': u'This content experiment has issues that affect content visibility.',
'type': 'warning'
}
) )
# pylint: disable=no-value-for-parameter
self.verify_validation_update_usage_info(expected_result, mocked_message)
@patch('xmodule.split_test_module.SplitTestDescriptor.validation_messages') def test_update_usage_info_no_message(self):
def test_update_usage_info_no_message(self, mocked_validation_messages):
""" """
Tests if validation message is not present when updating usage info. Tests if validation message is not present when updating usage info.
""" """
self._add_user_partitions() self.verify_validation_update_usage_info(None, None) # pylint: disable=no-value-for-parameter
self._create_content_experiment(cid=0, name_suffix='0')
mocked_validation_messages.return_value = []
group_configuration = GroupConfiguration.update_usage_info(self.store, self.course, self.course.user_partitions[0])
self.assertEqual(group_configuration['usage'][0]['validation'], None)
...@@ -130,8 +130,10 @@ class GetItemTest(ItemTest): ...@@ -130,8 +130,10 @@ class GetItemTest(ItemTest):
root_usage_key = self._create_vertical() root_usage_key = self._create_vertical()
html, __ = self._get_container_preview(root_usage_key) html, __ = self._get_container_preview(root_usage_key)
# Verify that the Studio wrapper is not added # XBlock messages are added by the Studio wrapper.
self.assertNotIn('wrapper-xblock', html) self.assertIn('wrapper-xblock-message', html)
# Make sure that "wrapper-xblock" does not appear by itself (without -message at end).
self.assertNotRegexpMatches(html, r'wrapper-xblock[^-]+')
# Verify that the header and article tags are still added # Verify that the header and article tags are still added
self.assertIn('<header class="xblock-header xblock-header-vertical">', html) self.assertIn('<header class="xblock-header xblock-header-vertical">', html)
......
...@@ -213,6 +213,7 @@ define([ ...@@ -213,6 +213,7 @@ define([
"js/spec/models/component_template_spec", "js/spec/models/component_template_spec",
"js/spec/models/explicit_url_spec", "js/spec/models/explicit_url_spec",
"js/spec/models/xblock_info_spec", "js/spec/models/xblock_info_spec",
"js/spec/models/xblock_validation_spec",
"js/spec/utils/drag_and_drop_spec", "js/spec/utils/drag_and_drop_spec",
"js/spec/utils/handle_iframe_binding_spec", "js/spec/utils/handle_iframe_binding_spec",
...@@ -228,6 +229,7 @@ define([ ...@@ -228,6 +229,7 @@ define([
"js/spec/views/xblock_spec", "js/spec/views/xblock_spec",
"js/spec/views/xblock_editor_spec", "js/spec/views/xblock_editor_spec",
"js/spec/views/xblock_string_field_editor_spec", "js/spec/views/xblock_string_field_editor_spec",
"js/spec/views/xblock_validation_spec",
"js/spec/views/utils/view_utils_spec", "js/spec/views/utils/view_utils_spec",
......
define(["backbone", "gettext", "underscore"], function (Backbone, gettext, _) {
/**
* Model for xblock validation messages as displayed in Studio.
*/
var XBlockValidationModel = Backbone.Model.extend({
defaults: {
summary: {},
messages: [],
empty: true,
xblock_id: null
},
WARNING : "warning",
ERROR: "error",
NOT_CONFIGURED: "not-configured",
parse: function(response) {
if (!response.empty) {
var summary = "summary" in response ? response.summary : {};
var messages = "messages" in response ? response.messages : [];
if (!(_.has(summary, "text")) || !summary.text) {
summary.text = gettext("This component has validation issues.");
}
if (!(_.has(summary, "type")) || !summary.type) {
summary.type = this.WARNING;
// Possible types are ERROR, WARNING, and NOT_CONFIGURED. NOT_CONFIGURED is treated as a warning.
_.find(messages, function (message) {
if (message.type === this.ERROR) {
summary.type = this.ERROR;
return true;
}
return false;
}, this);
}
response.summary = summary;
if (response.showSummaryOnly) {
messages = [];
}
response.messages = messages;
}
return response;
}
});
return XBlockValidationModel;
});
define(['js/models/xblock_validation'],
function(XBlockValidationModel) {
var verifyModel;
verifyModel = function(model, expected_empty, expected_summary, expected_messages, expected_xblock_id) {
expect(model.get("empty")).toBe(expected_empty);
expect(model.get("summary")).toEqual(expected_summary);
expect(model.get("messages")).toEqual(expected_messages);
expect(model.get("xblock_id")).toBe(expected_xblock_id);
};
describe('XBlockValidationModel', function() {
it('handles empty variable', function() {
verifyModel(new XBlockValidationModel({parse: true}), true, {}, [], null);
verifyModel(new XBlockValidationModel({"empty": true}, {parse: true}), true, {}, [], null);
// It is assumed that the "empty" state on the JSON object passed in is correct
// (no attempt is made to correct other variables based on empty==true).
verifyModel(
new XBlockValidationModel(
{"empty": true, "messages": [{"text": "Bad JSON case"}], "xblock_id": "id"},
{parse: true}
),
true,
{},
[{"text": "Bad JSON case"}], "id"
);
});
it('creates a summary if not defined', function() {
// Single warning message.
verifyModel(
new XBlockValidationModel({
"empty": false,
"xblock_id": "id"
}, {parse: true}),
false,
{"text": "This component has validation issues.", "type": "warning"},
[],
"id"
);
// Two messages that compute to a "warning" state in the summary.
verifyModel(
new XBlockValidationModel({
"empty": false,
"messages": [{"text": "one", "type": "not-configured"}, {"text": "two", "type": "warning"}],
"xblock_id": "id"
}, {parse: true}),
false,
{"text": "This component has validation issues.", "type": "warning"},
[{"text": "one", "type": "not-configured"}, {"text": "two", "type": "warning"}],
"id"
);
// Two messages, with one of them "error", resulting in an "error" state in the summary.
verifyModel(
new XBlockValidationModel({
"empty": false,
"messages": [{"text": "one", "type": "warning"}, {"text": "two", "type": "error"}],
"xblock_id": "id"
}, {parse: true}),
false,
{"text": "This component has validation issues.", "type": "error"},
[{"text": "one", "type": "warning"}, {"text": "two", "type": "error"}],
"id"
);
});
it('respects summary properties that are defined', function() {
// Summary already present (both text and type), no messages.
verifyModel(
new XBlockValidationModel({
"empty": false,
"xblock_id": "id",
"summary": {"text": "my summary", "type": "custom type"}
}, {parse: true}),
false,
{"text": "my summary", "type": "custom type"},
[],
"id"
);
// Summary text present, but not type (will get default value of warning).
verifyModel(
new XBlockValidationModel({
"empty": false,
"xblock_id": "id",
"summary": {"text": "my summary"}
}, {parse: true}),
false,
{"text": "my summary", "type": "warning"},
[],
"id"
);
// Summary type present, but not text.
verifyModel(
new XBlockValidationModel({
"empty": false,
"summary": {"type": "custom type"},
"messages": [{"text": "one", "type": "not-configured"}, {"text": "two", "type": "warning"}],
"xblock_id": "id"
}, {parse: true}),
false,
{"text": "This component has validation issues.", "type": "custom type"},
[{"text": "one", "type": "not-configured"}, {"text": "two", "type": "warning"}],
"id"
);
// Summary text present, type will be computed as error.
verifyModel(
new XBlockValidationModel({
"empty": false,
"summary": {"text": "my summary"},
"messages": [{"text": "one", "type": "warning"}, {"text": "two", "type": "error"}],
"xblock_id": "id"
}, {parse: true}),
false,
{"text": "my summary", "type": "error"},
[{"text": "one", "type": "warning"}, {"text": "two", "type": "error"}],
"id"
);
});
it('clears messages if showSummaryOnly is true', function() {
verifyModel(
new XBlockValidationModel({
"empty": false,
"xblock_id": "id",
"summary": {"text": "my summary"},
"messages": [{"text": "one", "type": "warning"}, {"text": "two", "type": "error"}],
"showSummaryOnly": true
}, {parse: true}),
false,
{"text": "my summary", "type": "error"},
[],
"id"
);
verifyModel(
new XBlockValidationModel({
"empty": false,
"xblock_id": "id",
"summary": {"text": "my summary"},
"messages": [{"text": "one", "type": "warning"}, {"text": "two", "type": "error"}],
"showSummaryOnly": false
}, {parse: true}),
false,
{"text": "my summary", "type": "error"},
[{"text": "one", "type": "warning"}, {"text": "two", "type": "error"}],
"id"
);
});
});
}
);
...@@ -215,7 +215,7 @@ define([ ...@@ -215,7 +215,7 @@ define([
'label': 'label1', 'label': 'label1',
'url': 'url1', 'url': 'url1',
'validation': { 'validation': {
'message': "Warning message", 'text': "Warning message",
'type': 'warning' 'type': 'warning'
} }
} }
...@@ -233,7 +233,7 @@ define([ ...@@ -233,7 +233,7 @@ define([
'label': 'label1', 'label': 'label1',
'url': 'url1', 'url': 'url1',
'validation': { 'validation': {
'message': "Error message", 'text': "Error message",
'type': 'error' 'type': 'error'
} }
} }
......
...@@ -102,6 +102,14 @@ define([ "jquery", "js/common_helpers/ajax_helpers", "URI", "js/views/xblock", " ...@@ -102,6 +102,14 @@ define([ "jquery", "js/common_helpers/ajax_helpers", "URI", "js/views/xblock", "
]); ]);
expect(promise.isRejected()).toBe(true); expect(promise.isRejected()).toBe(true);
}); });
it('Triggers an event to the runtime when a notification-action-button is clicked', function () {
var notifySpy = spyOn(xblockView, "notifyRuntime").andCallThrough();
postXBlockRequest(AjaxHelpers.requests(this), []);
xblockView.$el.find(".notification-action-button").click();
expect(notifySpy).toHaveBeenCalledWith("add-missing-groups", model.get("id"));
})
}); });
}); });
}); });
define(['jquery', 'js/models/xblock_validation', 'js/views/xblock_validation', 'js/common_helpers/template_helpers'],
function($, XBlockValidationModel, XBlockValidationView, TemplateHelpers) {
beforeEach(function () {
TemplateHelpers.installTemplate('xblock-validation-messages');
});
describe('XBlockValidationView helper methods', function() {
var model, view;
beforeEach(function () {
model = new XBlockValidationModel({parse: true});
view = new XBlockValidationView({model: model});
view.render();
});
it('has a getIcon method', function() {
var getIcon = view.getIcon.bind(view);
expect(getIcon(model.WARNING)).toBe('icon-warning-sign');
expect(getIcon(model.NOT_CONFIGURED)).toBe('icon-warning-sign');
expect(getIcon(model.ERROR)).toBe('icon-exclamation-sign');
expect(getIcon("unknown")).toBeNull();
});
it('has a getDisplayName method', function() {
var getDisplayName = view.getDisplayName.bind(view);
expect(getDisplayName(model.WARNING)).toBe("Warning");
expect(getDisplayName(model.NOT_CONFIGURED)).toBe("Warning");
expect(getDisplayName(model.ERROR)).toBe("Error");
expect(getDisplayName("unknown")).toBeNull();
});
it('can add additional classes', function() {
var noContainerContent = "no-container-content", notConfiguredModel, nonRootView, rootView;
expect(view.getAdditionalClasses()).toBe("");
expect(view.$('.validation')).not.toHaveClass(noContainerContent);
notConfiguredModel = new XBlockValidationModel({
"empty": false, "summary": {"text": "Not configured", "type": model.NOT_CONFIGURED},
"xblock_id": "id"
},
{parse: true}
);
nonRootView = new XBlockValidationView({model: notConfiguredModel});
nonRootView.render();
expect(nonRootView.getAdditionalClasses()).toBe("");
expect(view.$('.validation')).not.toHaveClass(noContainerContent);
rootView = new XBlockValidationView({model: notConfiguredModel, root: true});
rootView.render();
expect(rootView.getAdditionalClasses()).toBe(noContainerContent);
expect(rootView.$('.validation')).toHaveClass(noContainerContent);
});
});
describe('XBlockValidationView rendering', function() {
var model, view;
beforeEach(function () {
model = new XBlockValidationModel({
"empty": false,
"summary": {
"text": "Summary message", "type": "error",
"action_label": "Summary Action", "action_class": "edit-button"
},
"messages": [
{
"text": "First message", "type": "warning",
"action_label": "First Message Action", "action_runtime_event": "fix-up"
},
{"text": "Second message", "type": "error"}
],
"xblock_id": "id"
});
view = new XBlockValidationView({model: model});
view.render();
});
it('renders summary and detailed messages types', function() {
var details;
expect(view.$('.xblock-message')).toHaveClass("has-errors");
details = view.$('.xblock-message-item');
expect(details.length).toBe(2);
expect(details[0]).toHaveClass("warning");
expect(details[1]).toHaveClass("error");
});
it('renders summary and detailed messages text', function() {
var details;
expect(view.$('.xblock-message').text()).toContain("Summary message");
details = view.$('.xblock-message-item');
expect(details.length).toBe(2);
expect($(details[0]).text()).toContain("Warning");
expect($(details[0]).text()).toContain("First message");
expect($(details[1]).text()).toContain("Error");
expect($(details[1]).text()).toContain("Second message");
});
it('renders action info', function() {
expect(view.$('a.edit-button .action-button-text').text()).toContain("Summary Action");
expect(view.$('a.notification-action-button .action-button-text').text()).
toContain("First Message Action");
expect(view.$('a.notification-action-button').data("notification-action")).toBe("fix-up");
});
it('renders a summary only', function() {
var summaryOnlyModel = new XBlockValidationModel({
"empty": false,
"summary": {"text": "Summary message", "type": "warning"},
"xblock_id": "id"
}), summaryOnlyView, details;
summaryOnlyView = new XBlockValidationView({model: summaryOnlyModel});
summaryOnlyView.render();
expect(summaryOnlyView.$('.xblock-message')).toHaveClass("has-warnings");
expect(view.$('.xblock-message').text()).toContain("Summary message");
details = summaryOnlyView.$('.xblock-message-item');
expect(details.length).toBe(0);
});
});
}
);
...@@ -13,6 +13,12 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views ...@@ -13,6 +13,12 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views
var XBlockContainerPage = BasePage.extend({ var XBlockContainerPage = BasePage.extend({
// takes XBlockInfo as a model // takes XBlockInfo as a model
events: {
"click .edit-button": "editXBlock",
"click .duplicate-button": "duplicateXBlock",
"click .delete-button": "deleteXBlock"
},
options: { options: {
collapsedClass: 'is-collapsed' collapsedClass: 'is-collapsed'
}, },
...@@ -81,12 +87,6 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views ...@@ -81,12 +87,6 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views
// Hide both blocks until we know which one to show // Hide both blocks until we know which one to show
xblockView.$el.addClass(hiddenCss); xblockView.$el.addClass(hiddenCss);
if (!options || !options.refresh) {
// Add actions to any top level buttons, e.g. "Edit" of the container itself.
// Do not add the actions on "refresh" though, as the handlers are already registered.
self.addButtonActions(this.$el);
}
// Render the xblock // Render the xblock
xblockView.render({ xblockView.render({
done: function() { done: function() {
...@@ -119,7 +119,6 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views ...@@ -119,7 +119,6 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views
}, },
onXBlockRefresh: function(xblockView) { onXBlockRefresh: function(xblockView) {
this.addButtonActions(xblockView.$el);
this.xblockView.refresh(); this.xblockView.refresh();
// Update publish and last modified information from the server. // Update publish and last modified information from the server.
this.model.fetch(); this.model.fetch();
...@@ -137,25 +136,12 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views ...@@ -137,25 +136,12 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views
}); });
}, },
addButtonActions: function(element) { editXBlock: function(event) {
var self = this; var xblockElement = this.findXBlockElement(event.target),
element.find('.edit-button').click(function(event) { self = this,
event.preventDefault(); modal = new EditXBlockModal({ });
self.editComponent(self.findXBlockElement(event.target));
});
element.find('.duplicate-button').click(function(event) {
event.preventDefault();
self.duplicateComponent(self.findXBlockElement(event.target));
});
element.find('.delete-button').click(function(event) {
event.preventDefault(); event.preventDefault();
self.deleteComponent(self.findXBlockElement(event.target));
});
},
editComponent: function(xblockElement) {
var self = this,
modal = new EditXBlockModal({ });
modal.edit(xblockElement, this.model, { modal.edit(xblockElement, this.model, {
refresh: function() { refresh: function() {
self.refreshXBlock(xblockElement); self.refreshXBlock(xblockElement);
...@@ -163,6 +149,16 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views ...@@ -163,6 +149,16 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views
}); });
}, },
duplicateXBlock: function(event) {
event.preventDefault();
this.duplicateComponent(this.findXBlockElement(event.target));
},
deleteXBlock: function(event) {
event.preventDefault();
this.deleteComponent(this.findXBlockElement(event.target));
},
createPlaceholderElement: function() { createPlaceholderElement: function() {
return $("<div/>", { class: "studio-xblock-wrapper" }); return $("<div/>", { class: "studio-xblock-wrapper" });
}, },
......
...@@ -4,6 +4,10 @@ define(["jquery", "underscore", "js/views/baseview", "xblock/runtime.v1"], ...@@ -4,6 +4,10 @@ define(["jquery", "underscore", "js/views/baseview", "xblock/runtime.v1"],
var XBlockView = BaseView.extend({ var XBlockView = BaseView.extend({
// takes XBlockInfo as a model // takes XBlockInfo as a model
events: {
"click .notification-action-button": "fireNotificationActionEvent"
},
initialize: function() { initialize: function() {
BaseView.prototype.initialize.call(this); BaseView.prototype.initialize.call(this);
this.view = this.options.view; this.view = this.options.view;
...@@ -195,6 +199,14 @@ define(["jquery", "underscore", "js/views/baseview", "xblock/runtime.v1"], ...@@ -195,6 +199,14 @@ define(["jquery", "underscore", "js/views/baseview", "xblock/runtime.v1"],
} }
// Return an already resolved promise for synchronous updates // Return an already resolved promise for synchronous updates
return $.Deferred().resolve().promise(); return $.Deferred().resolve().promise();
},
fireNotificationActionEvent: function(event) {
var eventName = $(event.currentTarget).data("notification-action");
if (eventName) {
event.preventDefault();
this.notifyRuntime(eventName, this.model.get("id"));
}
} }
}); });
......
define(["jquery", "underscore", "js/views/baseview", "gettext"],
function ($, _, BaseView, gettext) {
/**
* View for xblock validation messages as displayed in Studio.
*/
var XBlockValidationView = BaseView.extend({
// Takes XBlockValidationModel as a model
initialize: function(options) {
BaseView.prototype.initialize.call(this);
this.template = this.loadTemplate('xblock-validation-messages');
this.root = options.root;
},
render: function () {
this.$el.html(this.template({
validation: this.model,
additionalClasses: this.getAdditionalClasses(),
getIcon: this.getIcon.bind(this),
getDisplayName: this.getDisplayName.bind(this)
}));
return this;
},
/**
* Returns the icon css class based on the message type.
* @param messageType
* @returns string representation of css class that will render the correct icon, or null if unknown type
*/
getIcon: function (messageType) {
if (messageType === this.model.ERROR) {
return 'icon-exclamation-sign';
}
else if (messageType === this.model.WARNING || messageType === this.model.NOT_CONFIGURED) {
return 'icon-warning-sign';
}
return null;
},
/**
* Returns a display name for a message (useful for screen readers), based on the message type.
* @param messageType
* @returns string display name (translated)
*/
getDisplayName: function (messageType) {
if (messageType === this.model.WARNING || messageType === this.model.NOT_CONFIGURED) {
// Translators: This message will be added to the front of messages of type warning,
// e.g. "Warning: this component has not been configured yet".
return gettext("Warning");
}
else if (messageType === this.model.ERROR) {
// Translators: This message will be added to the front of messages of type error,
// e.g. "Error: required field is missing".
return gettext("Error");
}
return null;
},
/**
* Returns additional css classes that can be added to HTML containing the validation messages.
* Useful for rendering NOT_CONFIGURED in a special way.
*
* @returns string of additional css classes (or empty string)
*/
getAdditionalClasses: function () {
if (this.root && this.model.get("summary").type === this.model.NOT_CONFIGURED &&
this.model.get("messages").length === 0) {
return "no-container-content";
}
return "";
}
});
return XBlockValidationView;
});
...@@ -124,6 +124,23 @@ div.wrapper-comp-editor.is-inactive ~ div.launch-latex-compiler{ ...@@ -124,6 +124,23 @@ div.wrapper-comp-editor.is-inactive ~ div.launch-latex-compiler{
// ==================== // ====================
// xblock message UI override
// (to fake the Studio no-content pattern design since validation classes cannot be removed in logic layer)
.wrapper-xblock .xblock-message.no-container-content.xblock-message.no-container-content {
border: 0;
padding: ($baseline*1.5) ($baseline*2);
background-color: $gray-l4;
text-align: center;
color: $gray;
.button,
.action {
@extend %btn-primary-green;
}
}
// ====================
// TODOs: // TODOs:
// * font-weight syncing // * font-weight syncing
......
...@@ -11,8 +11,8 @@ ...@@ -11,8 +11,8 @@
background: $white; background: $white;
box-shadow: 0px 1px 1px $shadow-l1; box-shadow: 0px 1px 1px $shadow-l1;
// STATE: hover/focus
&:hover { &:hover, &:focus {
box-shadow: 0 0 1px $shadow; box-shadow: 0 0 1px $shadow;
} }
...@@ -165,7 +165,7 @@ ...@@ -165,7 +165,7 @@
.xblock-message { .xblock-message {
@extend %t-copy-sub1; @extend %t-copy-sub1;
background-color: $gray-d2; background-color: $gray-d2;
padding: ($baseline/2) ($baseline*.75); padding: ($baseline*0.75);
color: $white; color: $white;
[class^="icon-"] { [class^="icon-"] {
...@@ -187,7 +187,7 @@ ...@@ -187,7 +187,7 @@
} }
&.has-warnings { &.has-warnings {
border-bottom: 3px solid $orange; border-top: 3px solid $orange;
.icon-warning-sign { .icon-warning-sign {
margin-right: ($baseline/2); margin-right: ($baseline/2);
...@@ -196,7 +196,7 @@ ...@@ -196,7 +196,7 @@
} }
&.has-errors { &.has-errors {
border-bottom: 3px solid $red-l2; border-top: 3px solid $red-l2;
.icon-exclamation-sign { .icon-exclamation-sign {
margin-right: ($baseline/2); margin-right: ($baseline/2);
...@@ -231,14 +231,13 @@ ...@@ -231,14 +231,13 @@
} }
.xblock-message { .xblock-message {
border-radius: 3px 3px 0 0;
&.validation { &.validation {
padding-top: ($baseline*.75); padding-top: ($baseline*0.75);
} }
.xblock-message-list { .xblock-message-list {
margin: ($baseline/5) ($baseline*2.5); margin: 0 ($baseline*2.25);
list-style-type: disc; list-style-type: disc;
color: $gray-l3; color: $gray-l3;
} }
...@@ -248,7 +247,7 @@ ...@@ -248,7 +247,7 @@
} }
&.information { &.information {
padding: 0 0 ($baseline/2) 0; padding: ($baseline/2) 0;
background-color: $gray-l5; background-color: $gray-l5;
color: $gray-d1; color: $gray-d1;
} }
...@@ -342,28 +341,12 @@ ...@@ -342,28 +341,12 @@
.wrapper-xblock-message { .wrapper-xblock-message {
.xblock-message { .xblock-message {
border-radius: 0 0 3px 3px;
.xblock-message-list {
margin: 0;
list-style-type: none;
}
&.information { &.information {
@extend %t-copy-sub2; @extend %t-copy-sub2;
padding: 0 0 ($baseline/2) $baseline; padding: 0 $baseline ($baseline*0.75) $baseline;
color: $gray-l1; color: $gray-l1;
} }
&.validation.has-warnings {
border: 0;
border-top: 3px solid $orange;
}
&.validation.has-errors {
border: 0;
border-top: 3px solid $red-l2;
}
} }
} }
} }
......
...@@ -71,7 +71,7 @@ ...@@ -71,7 +71,7 @@
<i class="icon-exclamation-sign"></i> <i class="icon-exclamation-sign"></i>
<% } %> <% } %>
<span class="group-configuration-validation-message"> <span class="group-configuration-validation-message">
<%= unit.validation.message %> <%= unit.validation.text %>
</span> </span>
</p> </p>
<% } %> <% } %>
......
...@@ -9,6 +9,9 @@ ...@@ -9,6 +9,9 @@
<span data-tooltip="Drag to reorder" class="drag-handle action"></span> <span data-tooltip="Drag to reorder" class="drag-handle action"></span>
</li> </li>
</ul> </ul>
<a href="#" class="button action-button notification-action-button" data-notification-action="add-missing-groups">
<span class="action-button-text">Add Missing Groups</span>
</a>
</div> </div>
</header> </header>
<article class="xblock-render"> <article class="xblock-render">
......
<%
var summaryMessage = validation.get("summary");
var aggregateMessageType = summaryMessage.type;
var aggregateValidationClass = aggregateMessageType === "error"? "has-errors" : "has-warnings";
%>
<div class="xblock-message validation <%= aggregateValidationClass %> <%= additionalClasses %>">
<p class="<%= aggregateMessageType %>"><i class="<%= getIcon(aggregateMessageType) %>"></i>
<%- summaryMessage.text %>
<% if (summaryMessage.action_class) { %>
<a href="#" class="button action-button <%- summaryMessage.action_class %>">
<span class="action-button-text"><%- summaryMessage.action_label %></span>
</a>
<% } else if (summaryMessage.action_runtime_event) {%>
<a href="#" class="button action-button notification-action-button" data-notification-action="<%- summaryMessage.action_runtime_event %>">
<span class="action-button-text"><%- summaryMessage.action_label %></span>
</a>
<% } %>
</p>
<% var detailedMessages = validation.get("messages"); %>
<% if (detailedMessages.length > 0) { %>
<ul class="xblock-message-list">
<% for (var i = 0; i < detailedMessages.length; i++) { %>
<%
var message = detailedMessages[i];
var messageType = message.type
var messageTypeDisplayName = getDisplayName(messageType)
%>
<li class="xblock-message-item <%= messageType %>">
<span class="message-text">
<% if (messageTypeDisplayName) { %>
<span class="sr"><%- messageTypeDisplayName %>:</span>
<% } %>
<%- message.text %>
<% if (message.action_class) { %>
<a href="#" class="button action-button <%- message.action_class %>">
<span class="action-button-text"><%- message.action_label %></span>
</a>
<% } else if (message.action_runtime_event) {%>
<a href="#" class="button action-button notification-action-button" data-notification-action="<%- message.action_runtime_event %>">
<span class="action-button-text"><%- message.action_label %></span>
</a>
<% } %>
</span>
</li>
<% } %>
</ul>
<% } %>
</div>
<%! <%!
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from contentstore.views.helpers import xblock_studio_url from contentstore.views.helpers import xblock_studio_url
import json
%> %>
<% <%
xblock_url = xblock_studio_url(xblock) xblock_url = xblock_studio_url(xblock)
show_inline = xblock.has_children and not xblock_url show_inline = xblock.has_children and not xblock_url
section_class = "level-nesting" if show_inline else "level-element" section_class = "level-nesting" if show_inline else "level-element"
collapsible_class = "is-collapsible" if xblock.has_children else "" collapsible_class = "is-collapsible" if xblock.has_children else ""
label = xblock.display_name or xblock.scope_ids.block_type label = xblock.display_name or xblock.scope_ids.block_type
messages = json.dumps(xblock.validate().to_json())
%> %>
<%namespace name='static' file='static_content.html'/>
<%block name="header_extras">
<script type="text/template" id="xblock-validation-messages-tpl">
<%static:include path="js/xblock-validation-messages.underscore" />
</script>
</%block>
<script type='text/javascript'>
require(["js/views/xblock_validation", "js/models/xblock_validation"],
function (XBlockValidationView, XBlockValidationModel) {
var validationMessages = ${messages};
% if xblock_url and not is_root:
validationMessages.showSummaryOnly = true;
% endif
var model = new XBlockValidationModel(validationMessages, {parse: true});
if (!model.get("empty")) {
var validationEle = $('div.xblock-validation-messages[data-locator="${xblock.location | h}"]');
var viewOptions = {
el: validationEle,
model: model
};
% if is_root:
viewOptions.root = true;
% endif
var view = new XBlockValidationView(viewOptions);
view.render();
}
});
</script>
% if not is_root: % if not is_root:
% if is_reorderable: % if is_reorderable:
<li class="studio-xblock-wrapper is-draggable" data-locator="${xblock.location | h}" data-course-key="${xblock.location.course_key | h}"> <li class="studio-xblock-wrapper is-draggable" data-locator="${xblock.location | h}" data-course-key="${xblock.location.course_key | h}">
...@@ -64,7 +99,9 @@ label = xblock.display_name or xblock.scope_ids.block_type ...@@ -64,7 +99,9 @@ label = xblock.display_name or xblock.scope_ids.block_type
</ul> </ul>
</div> </div>
</div> </div>
% if xblock_url and not is_root: % if not is_root:
<div class="wrapper-xblock-message xblock-validation-messages" data-locator="${xblock.location | h}"/>
% if xblock_url:
<div class="xblock-header-secondary"> <div class="xblock-header-secondary">
<div class="meta-info">${_('This block contains multiple components.')}</div> <div class="meta-info">${_('This block contains multiple components.')}</div>
<ul class="actions-list"> <ul class="actions-list">
...@@ -78,7 +115,13 @@ label = xblock.display_name or xblock.scope_ids.block_type ...@@ -78,7 +115,13 @@ label = xblock.display_name or xblock.scope_ids.block_type
</ul> </ul>
</div> </div>
% endif % endif
% endif
</header> </header>
% if is_root:
<div class="wrapper-xblock-message xblock-validation-messages" data-locator="${xblock.location | h}"/>
% endif
% if is_root or not xblock_url: % if is_root or not xblock_url:
<article class="xblock-render"> <article class="xblock-render">
${content} ${content}
...@@ -86,7 +129,6 @@ label = xblock.display_name or xblock.scope_ids.block_type ...@@ -86,7 +129,6 @@ label = xblock.display_name or xblock.scope_ids.block_type
% else: % else:
<div class="xblock-message-area"> <div class="xblock-message-area">
${content} ${content}
</div>
% endif % endif
% if not is_root: % if not is_root:
......
/* JavaScript for editing operations that can be done on the split test author view. */ /* JavaScript for editing operations that can be done on the split test author view. */
window.SplitTestAuthorView = function (runtime, element) { window.SplitTestAuthorView = function (runtime, element) {
var $element = $(element); var $element = $(element);
var splitTestLocator = $element.closest('.studio-xblock-wrapper').data('locator');
$element.find('.add-missing-groups-button').click(function () { runtime.listenTo("add-missing-groups", function (parentLocator) {
if (splitTestLocator === parentLocator) {
runtime.notify('save', { runtime.notify('save', {
state: 'start', state: 'start',
element: element, element: element,
...@@ -14,12 +16,12 @@ window.SplitTestAuthorView = function (runtime, element) { ...@@ -14,12 +16,12 @@ window.SplitTestAuthorView = function (runtime, element) {
element: element element: element
}); });
}); });
}
}); });
// Listen to delete events so that the view can refresh when the last inactive group is removed. // Listen to delete events so that the view can refresh when the last inactive group is removed.
runtime.listenTo('deleted-child', function(parentLocator) { runtime.listenTo('deleted-child', function(parentLocator) {
var splitTestLocator = $element.closest('.studio-xblock-wrapper').data('locator'), var inactiveGroups = $element.find('.is-inactive .studio-xblock-wrapper');
inactiveGroups = $element.find('.is-inactive .studio-xblock-wrapper');
if (splitTestLocator === parentLocator && inactiveGroups.length === 0) { if (splitTestLocator === parentLocator && inactiveGroups.length === 0) {
runtime.refreshXBlock($element); runtime.refreshXBlock($element);
} }
......
...@@ -12,6 +12,7 @@ from xmodule.progress import Progress ...@@ -12,6 +12,7 @@ from xmodule.progress import Progress
from xmodule.seq_module import SequenceDescriptor from xmodule.seq_module import SequenceDescriptor
from xmodule.studio_editable import StudioEditableModule, StudioEditableDescriptor from xmodule.studio_editable import StudioEditableModule, StudioEditableDescriptor
from xmodule.x_module import XModule, module_attr, STUDENT_VIEW from xmodule.x_module import XModule, module_attr, STUDENT_VIEW
from xmodule.validation import StudioValidation, StudioValidationMessage
from xmodule.modulestore.inheritance import UserPartitionList from xmodule.modulestore.inheritance import UserPartitionList
from lxml import etree from lxml import etree
...@@ -28,48 +29,6 @@ _ = lambda text: text ...@@ -28,48 +29,6 @@ _ = lambda text: text
DEFAULT_GROUP_NAME = _(u'Group ID {group_id}') DEFAULT_GROUP_NAME = _(u'Group ID {group_id}')
class ValidationMessageType(object):
"""
The type for a validation message -- currently 'information', 'warning' or 'error'.
"""
information = 'information'
warning = 'warning'
error = 'error'
@staticmethod
def display_name(message_type):
"""
Returns the display name for the specified validation message type.
"""
if message_type == ValidationMessageType.warning:
# Translators: This message will be added to the front of messages of type warning,
# e.g. "Warning: this component has not been configured yet".
return _(u"Warning")
elif message_type == ValidationMessageType.error:
# Translators: This message will be added to the front of messages of type error,
# e.g. "Error: required field is missing".
return _(u"Error")
else:
return None
# TODO: move this into the xblock repo once it has a formal validation contract
class ValidationMessage(object):
"""
Represents a single validation message for an xblock.
"""
def __init__(self, xblock, message_text, message_type, action_class=None, action_label=None):
assert isinstance(message_text, unicode)
self.xblock = xblock
self.message_text = message_text
self.message_type = message_type
self.action_class = action_class
self.action_label = action_label
def __unicode__(self):
return self.message_text
class SplitTestFields(object): class SplitTestFields(object):
"""Fields needed for split test module""" """Fields needed for split test module"""
has_children = True has_children = True
...@@ -231,6 +190,13 @@ class SplitTestModule(SplitTestFields, XModule, StudioEditableModule): ...@@ -231,6 +190,13 @@ class SplitTestModule(SplitTestFields, XModule, StudioEditableModule):
return None return None
return partitions_service.get_user_group_for_partition(self.user_partition_id) return partitions_service.get_user_group_for_partition(self.user_partition_id)
@property
def is_configured(self):
"""
Returns true if the split_test instance is associated with a UserPartition.
"""
return self.descriptor.is_configured
def _staff_view(self, context): def _staff_view(self, context):
""" """
Render the staff view for a split test module. Render the staff view for a split test module.
...@@ -283,7 +249,6 @@ class SplitTestModule(SplitTestFields, XModule, StudioEditableModule): ...@@ -283,7 +249,6 @@ class SplitTestModule(SplitTestFields, XModule, StudioEditableModule):
""" """
fragment = Fragment() fragment = Fragment()
root_xblock = context.get('root_xblock') root_xblock = context.get('root_xblock')
is_configured = not self.user_partition_id == SplitTestFields.no_partition_selected['value']
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
...@@ -300,7 +265,7 @@ class SplitTestModule(SplitTestFields, XModule, StudioEditableModule): ...@@ -300,7 +265,7 @@ class SplitTestModule(SplitTestFields, XModule, StudioEditableModule):
fragment.add_content(self.system.render_template('split_test_author_view.html', { fragment.add_content(self.system.render_template('split_test_author_view.html', {
'split_test': self, 'split_test': self,
'is_root': is_root, 'is_root': is_root,
'is_configured': is_configured, 'is_configured': self.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, 'group_configuration_url': self.descriptor.group_configuration_url,
...@@ -384,6 +349,13 @@ class SplitTestModule(SplitTestFields, XModule, StudioEditableModule): ...@@ -384,6 +349,13 @@ class SplitTestModule(SplitTestFields, XModule, StudioEditableModule):
return (group.name, group.id) return (group.name, group.id)
return (None, None) return (None, None)
def validate(self):
"""
Message for either error or warning validation message/s.
Returns message and type. Priority given to error type message.
"""
return self.descriptor.validate()
@XBlock.needs('user_tags') # pylint: disable=abstract-method @XBlock.needs('user_tags') # pylint: disable=abstract-method
@XBlock.wants('partitions') @XBlock.wants('partitions')
...@@ -544,46 +516,94 @@ class SplitTestDescriptor(SplitTestFields, SequenceDescriptor, StudioEditableDes ...@@ -544,46 +516,94 @@ class SplitTestDescriptor(SplitTestFields, SequenceDescriptor, StudioEditableDes
return active_children, inactive_children return active_children, inactive_children
def validation_messages(self): @property
def is_configured(self):
""" """
Returns a list of validation messages describing the current state of the block. Each message Returns true if the split_test instance is associated with a UserPartition.
includes a message type indicating whether the message represents information, a warning or an error. """
return not self.user_partition_id == SplitTestFields.no_partition_selected['value']
def validate(self):
"""
Validates the state of this split_test instance. This is the override of the general XBlock method,
and it will also ask its superclass to validate.
"""
validation = super(SplitTestDescriptor, self).validate()
split_test_validation = self.validate_split_test()
if split_test_validation:
return validation
validation = StudioValidation.copy(validation)
if validation and (not self.is_configured and len(split_test_validation.messages) == 1):
validation.summary = split_test_validation.messages[0]
else:
validation.summary = self.general_validation_message(split_test_validation)
validation.add_messages(split_test_validation)
return validation
def validate_split_test(self):
"""
Returns a StudioValidation object describing the current state of the split_test_module
(not including superclass validation messages).
""" """
_ = self.runtime.service(self, "i18n").ugettext # pylint: disable=redefined-outer-name _ = self.runtime.service(self, "i18n").ugettext # pylint: disable=redefined-outer-name
messages = [] split_validation = StudioValidation(self.location)
if self.user_partition_id < 0: if self.user_partition_id < 0:
messages.append(ValidationMessage( split_validation.add(
self, StudioValidationMessage(
StudioValidationMessage.NOT_CONFIGURED,
_(u"The experiment is not associated with a group configuration."), _(u"The experiment is not associated with a group configuration."),
ValidationMessageType.warning, action_class='edit-button',
'edit-button', action_label=_(u"Select a Group Configuration")
_(u"Select a Group Configuration") )
)) )
else: else:
user_partition = self.get_selected_partition() user_partition = self.get_selected_partition()
if not user_partition: if not user_partition:
messages.append(ValidationMessage( split_validation.add(
self, StudioValidationMessage(
_(u"The experiment uses a deleted group configuration. Select a valid group configuration or delete this experiment."), StudioValidationMessage.ERROR,
ValidationMessageType.error _(u"The experiment uses a deleted group configuration. Select a valid group configuration or delete this experiment.")
)) )
)
else: else:
[active_children, inactive_children] = self.active_and_inactive_children() [active_children, inactive_children] = self.active_and_inactive_children()
if len(active_children) < len(user_partition.groups): if len(active_children) < len(user_partition.groups):
messages.append(ValidationMessage( split_validation.add(
self, StudioValidationMessage(
StudioValidationMessage.ERROR,
_(u"The experiment does not contain all of the groups in the configuration."), _(u"The experiment does not contain all of the groups in the configuration."),
ValidationMessageType.error, action_runtime_event='add-missing-groups',
'add-missing-groups-button', action_label=_(u"Add Missing Groups")
_(u"Add Missing Groups") )
)) )
if len(inactive_children) > 0: if len(inactive_children) > 0:
messages.append(ValidationMessage( split_validation.add(
self, StudioValidationMessage(
_(u"The experiment has an inactive group. Move content into active groups, then delete the inactive group."), StudioValidationMessage.WARNING,
ValidationMessageType.warning _(u"The experiment has an inactive group. Move content into active groups, then delete the inactive group.")
)) )
return messages )
return split_validation
def general_validation_message(self, validation=None):
"""
Returns just a summary message about whether or not this split_test instance has
validation issues (not including superclass validation messages). If the split_test instance
validates correctly, this method returns None.
"""
if validation is None:
validation = self.validate_split_test()
if not validation:
has_error = any(message.type == StudioValidationMessage.ERROR for message in validation.messages)
return StudioValidationMessage(
StudioValidationMessage.ERROR if has_error else StudioValidationMessage.WARNING,
_(u"This content experiment has issues that affect content visibility.")
)
return None
@XBlock.handler @XBlock.handler
def add_missing_groups(self, request, suffix=''): # pylint: disable=unused-argument def add_missing_groups(self, request, suffix=''): # pylint: disable=unused-argument
...@@ -603,7 +623,7 @@ class SplitTestDescriptor(SplitTestFields, SequenceDescriptor, StudioEditableDes ...@@ -603,7 +623,7 @@ class SplitTestDescriptor(SplitTestFields, SequenceDescriptor, StudioEditableDes
changed = True changed = True
if changed: if changed:
# TODO user.id - to be fixed by Publishing team # user.id - to be fixed by Publishing team
self.system.modulestore.update_item(self, None) self.system.modulestore.update_item(self, None)
return Response() return Response()
...@@ -648,19 +668,3 @@ class SplitTestDescriptor(SplitTestFields, SequenceDescriptor, StudioEditableDes ...@@ -648,19 +668,3 @@ class SplitTestDescriptor(SplitTestFields, SequenceDescriptor, StudioEditableDes
) )
self.children.append(dest_usage_key) # pylint: disable=no-member self.children.append(dest_usage_key) # pylint: disable=no-member
self.group_id_to_child[unicode(group.id)] = dest_usage_key self.group_id_to_child[unicode(group.id)] = dest_usage_key
@property
def general_validation_message(self):
"""
Message for either error or warning validation message/s.
Returns message and type. Priority given to error type message.
"""
validation_messages = self.validation_messages()
if validation_messages:
has_error = any(message.message_type == ValidationMessageType.error for message in validation_messages)
return {
'message': _(u"This content experiment has issues that affect content visibility."),
'type': ValidationMessageType.error if has_error else ValidationMessageType.warning,
}
return None
...@@ -10,7 +10,8 @@ from xmodule.tests.xml import factories as xml ...@@ -10,7 +10,8 @@ from xmodule.tests.xml import factories as xml
from xmodule.tests.xml import XModuleXmlImportTest from xmodule.tests.xml import XModuleXmlImportTest
from xmodule.tests import get_test_system from xmodule.tests import get_test_system
from xmodule.x_module import AUTHOR_VIEW, STUDENT_VIEW from xmodule.x_module import AUTHOR_VIEW, STUDENT_VIEW
from xmodule.split_test_module import SplitTestDescriptor, SplitTestFields, ValidationMessageType from xmodule.validation import StudioValidationMessage
from xmodule.split_test_module import SplitTestDescriptor, SplitTestFields
from xmodule.partitions.partitions import Group, UserPartition from xmodule.partitions.partitions import Group, UserPartition
from xmodule.partitions.test_partitions import StaticPartitionService, MemoryUserTagsService from xmodule.partitions.test_partitions import StaticPartitionService, MemoryUserTagsService
...@@ -320,14 +321,6 @@ class SplitTestModuleStudioTest(SplitTestModuleTest): ...@@ -320,14 +321,6 @@ class SplitTestModuleStudioTest(SplitTestModuleTest):
self.assertEqual(active_children, []) self.assertEqual(active_children, [])
self.assertEqual(inactive_children, children) self.assertEqual(inactive_children, children)
def test_validation_message_types(self):
"""
Test the behavior of validation message types.
"""
self.assertEqual(ValidationMessageType.display_name(ValidationMessageType.error), u"Error")
self.assertEqual(ValidationMessageType.display_name(ValidationMessageType.warning), u"Warning")
self.assertIsNone(ValidationMessageType.display_name(ValidationMessageType.information))
def test_validation_messages(self): def test_validation_messages(self):
""" """
Test the validation messages produced for different split test configurations. Test the validation messages produced for different split test configurations.
...@@ -335,122 +328,128 @@ class SplitTestModuleStudioTest(SplitTestModuleTest): ...@@ -335,122 +328,128 @@ class SplitTestModuleStudioTest(SplitTestModuleTest):
split_test_module = self.split_test_module split_test_module = self.split_test_module
def verify_validation_message(message, expected_message, expected_message_type, def verify_validation_message(message, expected_message, expected_message_type,
expected_action_class=None, expected_action_label=None): expected_action_class=None, expected_action_label=None,
expected_action_runtime_event=None):
""" """
Verify that the validation message has the expected validation message and type. Verify that the validation message has the expected validation message and type.
""" """
self.assertEqual(unicode(message), expected_message) self.assertEqual(message.text, expected_message)
self.assertEqual(message.message_type, expected_message_type) self.assertEqual(message.type, expected_message_type)
if expected_action_class:
self.assertEqual(message.action_class, expected_action_class) self.assertEqual(message.action_class, expected_action_class)
else:
self.assertFalse(hasattr(message, "action_class"))
if expected_action_label:
self.assertEqual(message.action_label, expected_action_label) self.assertEqual(message.action_label, expected_action_label)
else:
def verify_general_validation_message(general_validation, expected_message, expected_message_type): self.assertFalse(hasattr(message, "action_label"))
if expected_action_runtime_event:
self.assertEqual(message.action_runtime_event, expected_action_runtime_event)
else:
self.assertFalse(hasattr(message, "action_runtime_event"))
def verify_summary_message(general_validation, expected_message, expected_message_type):
""" """
Verify that the general validation message has the expected validation message and type. Verify that the general validation message has the expected validation message and type.
""" """
self.assertEqual(unicode(general_validation['message']), expected_message) self.assertEqual(general_validation.text, expected_message)
self.assertEqual(general_validation['type'], expected_message_type) self.assertEqual(general_validation.type, expected_message_type)
# Verify the messages for an unconfigured user partition # Verify the messages for an unconfigured user partition
split_test_module.user_partition_id = -1 split_test_module.user_partition_id = -1
messages = split_test_module.validation_messages() validation = split_test_module.validate()
self.assertEqual(len(messages), 1) self.assertEqual(len(validation.messages), 0)
verify_validation_message( verify_validation_message(
messages[0], validation.summary,
u"The experiment is not associated with a group configuration.", u"The experiment is not associated with a group configuration.",
ValidationMessageType.warning, StudioValidationMessage.NOT_CONFIGURED,
'edit-button', 'edit-button',
u"Select a Group Configuration", u"Select a Group Configuration",
) )
verify_general_validation_message(
split_test_module.general_validation_message,
u"This content experiment has issues that affect content visibility.",
ValidationMessageType.warning
)
# Verify the messages for a correctly configured split_test # Verify the messages for a correctly configured split_test
split_test_module.user_partition_id = 0 split_test_module.user_partition_id = 0
split_test_module.user_partitions = [ split_test_module.user_partitions = [
UserPartition(0, 'first_partition', 'First Partition', [Group("0", 'alpha'), Group("1", 'beta')]) UserPartition(0, 'first_partition', 'First Partition', [Group("0", 'alpha'), Group("1", 'beta')])
] ]
messages = split_test_module.validation_messages() validation = split_test_module.validate_split_test()
self.assertEqual(len(messages), 0) self.assertTrue(validation)
self.assertIsNone(split_test_module.general_validation_message, None) self.assertIsNone(split_test_module.general_validation_message(), None)
# Verify the messages for a split test with too few groups # Verify the messages for a split test with too few groups
split_test_module.user_partitions = [ split_test_module.user_partitions = [
UserPartition(0, 'first_partition', 'First Partition', UserPartition(0, 'first_partition', 'First Partition',
[Group("0", 'alpha'), Group("1", 'beta'), Group("2", 'gamma')]) [Group("0", 'alpha'), Group("1", 'beta'), Group("2", 'gamma')])
] ]
messages = split_test_module.validation_messages() validation = split_test_module.validate()
self.assertEqual(len(messages), 1) self.assertEqual(len(validation.messages), 1)
verify_validation_message( verify_validation_message(
messages[0], validation.messages[0],
u"The experiment does not contain all of the groups in the configuration.", u"The experiment does not contain all of the groups in the configuration.",
ValidationMessageType.error, StudioValidationMessage.ERROR,
'add-missing-groups-button', expected_action_runtime_event='add-missing-groups',
u"Add Missing Groups" expected_action_label=u"Add Missing Groups"
) )
verify_general_validation_message( verify_summary_message(
split_test_module.general_validation_message, validation.summary,
u"This content experiment has issues that affect content visibility.", u"This content experiment has issues that affect content visibility.",
ValidationMessageType.error StudioValidationMessage.ERROR
) )
# Verify the messages for a split test with children that are not associated with any group # Verify the messages for a split test with children that are not associated with any group
split_test_module.user_partitions = [ split_test_module.user_partitions = [
UserPartition(0, 'first_partition', 'First Partition', UserPartition(0, 'first_partition', 'First Partition',
[Group("0", 'alpha')]) [Group("0", 'alpha')])
] ]
messages = split_test_module.validation_messages() validation = split_test_module.validate()
self.assertEqual(len(messages), 1) self.assertEqual(len(validation.messages), 1)
verify_validation_message( verify_validation_message(
messages[0], validation.messages[0],
u"The experiment has an inactive group. Move content into active groups, then delete the inactive group.", u"The experiment has an inactive group. Move content into active groups, then delete the inactive group.",
ValidationMessageType.warning StudioValidationMessage.WARNING
) )
verify_general_validation_message( verify_summary_message(
split_test_module.general_validation_message, validation.summary,
u"This content experiment has issues that affect content visibility.", u"This content experiment has issues that affect content visibility.",
ValidationMessageType.warning StudioValidationMessage.WARNING
) )
# Verify the messages for a split test with both missing and inactive children # Verify the messages for a split test with both missing and inactive children
split_test_module.user_partitions = [ split_test_module.user_partitions = [
UserPartition(0, 'first_partition', 'First Partition', UserPartition(0, 'first_partition', 'First Partition',
[Group("0", 'alpha'), Group("2", 'gamma')]) [Group("0", 'alpha'), Group("2", 'gamma')])
] ]
messages = split_test_module.validation_messages() validation = split_test_module.validate()
self.assertEqual(len(messages), 2) self.assertEqual(len(validation.messages), 2)
verify_validation_message( verify_validation_message(
messages[0], validation.messages[0],
u"The experiment does not contain all of the groups in the configuration.", u"The experiment does not contain all of the groups in the configuration.",
ValidationMessageType.error, StudioValidationMessage.ERROR,
'add-missing-groups-button', expected_action_runtime_event='add-missing-groups',
u"Add Missing Groups" expected_action_label=u"Add Missing Groups"
) )
verify_validation_message( verify_validation_message(
messages[1], validation.messages[1],
u"The experiment has an inactive group. Move content into active groups, then delete the inactive group.", u"The experiment has an inactive group. Move content into active groups, then delete the inactive group.",
ValidationMessageType.warning StudioValidationMessage.WARNING
) )
# With two messages of type error and warning priority given to error. # With two messages of type error and warning priority given to error.
verify_general_validation_message( verify_summary_message(
split_test_module.general_validation_message, validation.summary,
u"This content experiment has issues that affect content visibility.", u"This content experiment has issues that affect content visibility.",
ValidationMessageType.error StudioValidationMessage.ERROR
) )
# Verify the messages for a split test referring to a non-existent user partition # Verify the messages for a split test referring to a non-existent user partition
split_test_module.user_partition_id = 2 split_test_module.user_partition_id = 2
messages = split_test_module.validation_messages() validation = split_test_module.validate()
self.assertEqual(len(messages), 1) self.assertEqual(len(validation.messages), 1)
verify_validation_message( verify_validation_message(
messages[0], validation.messages[0],
u"The experiment uses a deleted group configuration. " u"The experiment uses a deleted group configuration. "
u"Select a valid group configuration or delete this experiment.", u"Select a valid group configuration or delete this experiment.",
ValidationMessageType.error StudioValidationMessage.ERROR
) )
verify_general_validation_message( verify_summary_message(
split_test_module.general_validation_message, validation.summary,
u"This content experiment has issues that affect content visibility.", u"This content experiment has issues that affect content visibility.",
ValidationMessageType.error StudioValidationMessage.ERROR
) )
"""
Test xblock/validation.py
"""
import unittest
from xblock.test.tools import assert_raises
from xmodule.validation import StudioValidationMessage, StudioValidation
from xblock.validation import Validation, ValidationMessage
class StudioValidationMessageTest(unittest.TestCase):
"""
Tests for `ValidationMessage`
"""
def test_bad_parameters(self):
"""
Test that `TypeError`s are thrown for bad input parameters.
"""
with assert_raises(TypeError):
StudioValidationMessage("unknown type", u"Unknown type info")
with assert_raises(TypeError):
StudioValidationMessage(StudioValidationMessage.WARNING, u"bad warning", action_class=0)
with assert_raises(TypeError):
StudioValidationMessage(StudioValidationMessage.WARNING, u"bad warning", action_runtime_event=0)
with assert_raises(TypeError):
StudioValidationMessage(StudioValidationMessage.WARNING, u"bad warning", action_label="Non-unicode string")
def test_to_json(self):
"""
Test the `to_json` method.
"""
self.assertEqual(
{
"type": StudioValidationMessage.NOT_CONFIGURED,
"text": u"Not Configured message",
"action_label": u"Action label"
},
StudioValidationMessage(
StudioValidationMessage.NOT_CONFIGURED, u"Not Configured message", action_label=u"Action label"
).to_json()
)
self.assertEqual(
{
"type": StudioValidationMessage.WARNING,
"text": u"Warning message",
"action_class": "class-for-action"
},
StudioValidationMessage(
StudioValidationMessage.WARNING, u"Warning message", action_class="class-for-action"
).to_json()
)
self.assertEqual(
{
"type": StudioValidationMessage.ERROR,
"text": u"Error message",
"action_runtime_event": "do-fix-up"
},
StudioValidationMessage(
StudioValidationMessage.ERROR, u"Error message", action_runtime_event="do-fix-up"
).to_json()
)
class StudioValidationTest(unittest.TestCase):
"""
Tests for `StudioValidation` class.
"""
def test_copy(self):
validation = Validation("id")
validation.add(ValidationMessage(ValidationMessage.ERROR, u"Error message"))
studio_validation = StudioValidation.copy(validation)
self.assertIsInstance(studio_validation, StudioValidation)
self.assertFalse(studio_validation)
self.assertEqual(1, len(studio_validation.messages))
expected = {
"type": StudioValidationMessage.ERROR,
"text": u"Error message"
}
self.assertEqual(expected, studio_validation.messages[0].to_json())
self.assertIsNone(studio_validation.summary)
def test_copy_studio_validation(self):
validation = StudioValidation("id")
validation.add(
StudioValidationMessage(StudioValidationMessage.WARNING, u"Warning message", action_label=u"Action Label")
)
validation_copy = StudioValidation.copy(validation)
self.assertFalse(validation_copy)
self.assertEqual(1, len(validation_copy.messages))
expected = {
"type": StudioValidationMessage.WARNING,
"text": u"Warning message",
"action_label": u"Action Label"
}
self.assertEqual(expected, validation_copy.messages[0].to_json())
def test_copy_errors(self):
with assert_raises(TypeError):
StudioValidation.copy("foo")
def test_empty(self):
"""
Test that `empty` return True iff there are no messages and no summary.
Also test the "bool" property of `Validation`.
"""
validation = StudioValidation("id")
self.assertTrue(validation.empty)
self.assertTrue(validation)
validation.add(StudioValidationMessage(StudioValidationMessage.ERROR, u"Error message"))
self.assertFalse(validation.empty)
self.assertFalse(validation)
validation_with_summary = StudioValidation("id")
validation_with_summary.set_summary(
StudioValidationMessage(StudioValidationMessage.NOT_CONFIGURED, u"Summary message")
)
self.assertFalse(validation.empty)
self.assertFalse(validation)
def test_add_messages(self):
"""
Test the behavior of calling `add_messages` with combination of `StudioValidation` instances.
"""
validation_1 = StudioValidation("id")
validation_1.set_summary(StudioValidationMessage(StudioValidationMessage.WARNING, u"Summary message"))
validation_1.add(StudioValidationMessage(StudioValidationMessage.ERROR, u"Error message"))
validation_2 = StudioValidation("id")
validation_2.set_summary(StudioValidationMessage(StudioValidationMessage.ERROR, u"Summary 2 message"))
validation_2.add(StudioValidationMessage(StudioValidationMessage.NOT_CONFIGURED, u"Not configured"))
validation_1.add_messages(validation_2)
self.assertEqual(2, len(validation_1.messages))
self.assertEqual(StudioValidationMessage.ERROR, validation_1.messages[0].type)
self.assertEqual(u"Error message", validation_1.messages[0].text)
self.assertEqual(StudioValidationMessage.NOT_CONFIGURED, validation_1.messages[1].type)
self.assertEqual(u"Not configured", validation_1.messages[1].text)
self.assertEqual(StudioValidationMessage.WARNING, validation_1.summary.type)
self.assertEqual(u"Summary message", validation_1.summary.text)
def test_set_summary_accepts_validation_message(self):
"""
Test that `set_summary` accepts a ValidationMessage.
"""
validation = StudioValidation("id")
validation.set_summary(ValidationMessage(ValidationMessage.WARNING, u"Summary message"))
self.assertEqual(ValidationMessage.WARNING, validation.summary.type)
self.assertEqual(u"Summary message", validation.summary.text)
def test_set_summary_errors(self):
"""
Test that `set_summary` errors if argument is not a ValidationMessage.
"""
with assert_raises(TypeError):
StudioValidation("id").set_summary("foo")
def test_to_json(self):
"""
Test the ability to serialize a `StudioValidation` instance.
"""
validation = StudioValidation("id")
expected = {
"xblock_id": "id",
"messages": [],
"empty": True
}
self.assertEqual(expected, validation.to_json())
validation.add(
StudioValidationMessage(
StudioValidationMessage.ERROR,
u"Error message",
action_label=u"Action label",
action_class="edit-button"
)
)
validation.add(
StudioValidationMessage(
StudioValidationMessage.NOT_CONFIGURED,
u"Not configured message",
action_label=u"Action label",
action_runtime_event="make groups"
)
)
validation.set_summary(
StudioValidationMessage(
StudioValidationMessage.WARNING,
u"Summary message",
action_label=u"Summary label",
action_runtime_event="fix everything"
)
)
# Note: it is important to test all the expected strings here because the client-side model depends on them
# (for instance, "warning" vs. using the xblock constant ValidationMessageTypes.WARNING).
expected = {
"xblock_id": "id",
"messages": [
{"type": "error", "text": u"Error message", "action_label": u"Action label", "action_class": "edit-button"},
{"type": "not-configured", "text": u"Not configured message", "action_label": u"Action label", "action_runtime_event": "make groups"}
],
"summary": {"type": "warning", "text": u"Summary message", "action_label": u"Summary label", "action_runtime_event": "fix everything"},
"empty": False
}
self.assertEqual(expected, validation.to_json())
"""
Extension of XBlock Validation class to include information for presentation in Studio.
"""
from xblock.validation import Validation, ValidationMessage
class StudioValidationMessage(ValidationMessage):
"""
A message containing validation information about an xblock, extended to provide Studio-specific fields.
"""
# A special message type indicating that the xblock is not yet configured. This message may be rendered
# in a different way within Studio.
NOT_CONFIGURED = "not-configured"
TYPES = [ValidationMessage.WARNING, ValidationMessage.ERROR, NOT_CONFIGURED]
def __init__(self, message_type, message_text, action_label=None, action_class=None, action_runtime_event=None):
"""
Create a new message.
Args:
message_type (str): The type associated with this message. Most be `WARNING` or `ERROR`.
message_text (unicode): The textual message.
action_label (unicode): Text to show on a "fix-up" action (optional). If present, either `action_class`
or `action_runtime_event` should be specified.
action_class (str): A class to link to the "fix-up" action (optional). A click handler must be added
for this class, unless it is "edit-button", "duplicate-button", or "delete-button" (which are all
handled in general for xblock instances.
action_runtime_event (str): An event name to be triggered on the xblock client-side runtime when
the "fix-up" action is clicked (optional).
"""
super(StudioValidationMessage, self).__init__(message_type, message_text)
if action_label is not None:
if not isinstance(action_label, unicode):
raise TypeError("Action label must be unicode.")
self.action_label = action_label
if action_class is not None:
if not isinstance(action_class, basestring):
raise TypeError("Action class must be a string.")
self.action_class = action_class
if action_runtime_event is not None:
if not isinstance(action_runtime_event, basestring):
raise TypeError("Action runtime event must be a string.")
self.action_runtime_event = action_runtime_event
def to_json(self):
"""
Convert to a json-serializable representation.
Returns:
dict: A dict representation that is json-serializable.
"""
serialized = super(StudioValidationMessage, self).to_json()
if hasattr(self, "action_label"):
serialized["action_label"] = self.action_label
if hasattr(self, "action_class"):
serialized["action_class"] = self.action_class
if hasattr(self, "action_runtime_event"):
serialized["action_runtime_event"] = self.action_runtime_event
return serialized
class StudioValidation(Validation):
"""
Extends `Validation` to add Studio-specific summary message.
"""
@classmethod
def copy(cls, validation):
"""
Copies the `Validation` object to a `StudioValidation` object. This is a shallow copy.
Args:
validation (Validation): A `Validation` object to be converted to a `StudioValidation` instance.
Returns:
StudioValidation: A `StudioValidation` instance populated with the messages from supplied
`Validation` object
"""
if not isinstance(validation, Validation):
raise TypeError("Copy must be called with a Validation instance")
studio_validation = cls(validation.xblock_id)
studio_validation.messages = validation.messages
return studio_validation
def __init__(self, xblock_id):
"""
Create a `StudioValidation` instance.
Args:
xblock_id (object): An identification object that must support conversion to unicode.
"""
super(StudioValidation, self).__init__(xblock_id)
self.summary = None
def set_summary(self, message):
"""
Sets a summary message on this instance. The summary is optional.
Args:
message (ValidationMessage): A validation message to set as this instance's summary.
"""
if not isinstance(message, ValidationMessage):
raise TypeError("Argument must of type ValidationMessage")
self.summary = message
@property
def empty(self):
"""
Is this object empty (contains no messages and no summary)?
Returns:
bool: True iff this instance has no validation issues and therefore has no messages or summary.
"""
return super(StudioValidation, self).empty and not self.summary
def to_json(self):
"""
Convert to a json-serializable representation.
Returns:
dict: A dict representation that is json-serializable.
"""
serialized = super(StudioValidation, self).to_json()
if self.summary:
serialized["summary"] = self.summary.to_json()
return serialized
...@@ -16,6 +16,7 @@ class ContainerPage(PageObject): ...@@ -16,6 +16,7 @@ class ContainerPage(PageObject):
NAME_SELECTOR = '.page-header-title' NAME_SELECTOR = '.page-header-title'
NAME_INPUT_SELECTOR = '.page-header .xblock-field-input' NAME_INPUT_SELECTOR = '.page-header .xblock-field-input'
NAME_FIELD_WRAPPER_SELECTOR = '.page-header .wrapper-xblock-field' NAME_FIELD_WRAPPER_SELECTOR = '.page-header .wrapper-xblock-field'
ADD_MISSING_GROUPS_SELECTOR = '.notification-action-button[data-notification-action="add-missing-groups"]'
def __init__(self, browser, locator): def __init__(self, browser, locator):
super(ContainerPage, self).__init__(browser) super(ContainerPage, self).__init__(browser)
...@@ -246,7 +247,7 @@ class ContainerPage(PageObject): ...@@ -246,7 +247,7 @@ class ContainerPage(PageObject):
Click the "add missing groups" link. Click the "add missing groups" link.
Note that this does an ajax call. Note that this does an ajax call.
""" """
self.q(css='.add-missing-groups-button').first.click() self.q(css=self.ADD_MISSING_GROUPS_SELECTOR).first.click()
self.wait_for_ajax() self.wait_for_ajax()
# Wait until all xblocks rendered. # Wait until all xblocks rendered.
...@@ -256,7 +257,7 @@ class ContainerPage(PageObject): ...@@ -256,7 +257,7 @@ class ContainerPage(PageObject):
""" """
Returns True if the "add missing groups" button is present. Returns True if the "add missing groups" button is present.
""" """
return self.q(css='.add-missing-groups-button').present return self.q(css=self.ADD_MISSING_GROUPS_SELECTOR).present
def get_xblock_information_message(self): def get_xblock_information_message(self):
""" """
......
...@@ -18,7 +18,7 @@ from courseware.tests.factories import StudentPrefsFactory, StudentInfoFactory ...@@ -18,7 +18,7 @@ from courseware.tests.factories import StudentPrefsFactory, StudentInfoFactory
from xblock.fields import Scope, BlockScope, ScopeIds from xblock.fields import Scope, BlockScope, ScopeIds
from django.test import TestCase from django.test import TestCase
from django.db import DatabaseError from django.db import DatabaseError
from xblock.core import KeyValueMultiSaveError from xblock.exceptions import KeyValueMultiSaveError
def mock_field(scope, name): def mock_field(scope, name):
......
<%! from django.utils.translation import ugettext as _ %> <%! from django.utils.translation import ugettext as _ %>
<%! from xmodule.split_test_module import ValidationMessageType %>
<% <%
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()
show_link = group_configuration_url is not None show_link = group_configuration_url is not None
%> %>
% if is_root and not is_configured: % if is_configured:
<div class="no-container-content"> <div class="wrapper-xblock-message">
% else:
<div class="wrapper-xblock-message">
% endif
% if user_partition:
<div class="xblock-message information"> <div class="xblock-message information">
<p> <p>
<span class="message-text"> <span class="message-text">
...@@ -24,56 +17,8 @@ show_link = group_configuration_url is not None ...@@ -24,56 +17,8 @@ show_link = group_configuration_url is not None
</span> </span>
</p> </p>
</div> </div>
% endif
% if len(messages) > 0:
<%
general_validation = split_test.descriptor.general_validation_message
def get_validation_icon(validation_type):
if validation_type == ValidationMessageType.error:
return 'icon-exclamation-sign'
elif validation_type == ValidationMessageType.warning:
return 'icon-warning-sign'
return None
aggregate_validation_class = 'has-errors' if general_validation['type']==ValidationMessageType.error else ' has-warnings'
%>
<div class="xblock-message validation ${aggregate_validation_class}">
% if is_configured:
<p class="${general_validation['type']}"><i class="${get_validation_icon(general_validation['type'])}"></i>
${general_validation['message']}
</p>
% endif
% if is_root or not is_configured:
<ul class="xblock-message-list">
% for message in messages:
<%
message_type = message.message_type
message_type_display_name = ValidationMessageType.display_name(message_type) if message_type else None
%>
<li class="xblock-message-item ${message_type}">
% if not is_configured:
<i class="${get_validation_icon(message_type)}"></i>
% endif
<span class="message-text">
% if message_type_display_name:
<span class="sr">${message_type_display_name}:</span>
% endif
${unicode(message)}
% if message.action_class:
<a href="#" class="button action-button ${message.action_class}">
<span class="action-button-text">${message.action_label}</span>
</a>
% endif
</span>
</li>
% endfor
</ul>
% endif
</div>
% endif
</div> </div>
% endif
% if is_root: % if is_root:
<div class="wrapper-groups is-active"> <div class="wrapper-groups is-active">
......
...@@ -20,7 +20,7 @@ ...@@ -20,7 +20,7 @@
-e git+https://github.com/pmitros/django-pyfs.git@d175715e0fe3367ec0f1ee429c242d603f6e8b10#egg=djpyfs -e git+https://github.com/pmitros/django-pyfs.git@d175715e0fe3367ec0f1ee429c242d603f6e8b10#egg=djpyfs
# Our libraries: # Our libraries:
-e git+https://github.com/edx/XBlock.git@246811773c67a84fdb17614a8e9f7ec7b1890574#egg=XBlock -e git+https://github.com/edx/XBlock.git@2029af2a4b524310847decfb34ef39da8a30dc4e#egg=XBlock
-e git+https://github.com/edx/codejail.git@66dd5a45e5072666ff9a70c768576e9ffd1daa4b#egg=codejail -e git+https://github.com/edx/codejail.git@66dd5a45e5072666ff9a70c768576e9ffd1daa4b#egg=codejail
-e git+https://github.com/edx/diff-cover.git@v0.7.1#egg=diff_cover -e git+https://github.com/edx/diff-cover.git@v0.7.1#egg=diff_cover
-e git+https://github.com/edx/js-test-tool.git@v0.1.5#egg=js_test_tool -e git+https://github.com/edx/js-test-tool.git@v0.1.5#egg=js_test_tool
......
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