From 08ce09bde73dcf2aef25e2a2cb334e59e8c8c22f Mon Sep 17 00:00:00 2001
From: cahrens
Date: Thu, 23 Oct 2014 17:21:48 -0400
Subject: [PATCH] Display validation messages for any xblock on the container
page.
TNL-683
---
CHANGELOG.rst | 2 +
cms/djangoapps/contentstore/views/course.py | 4 +-
.../views/tests/test_group_configurations.py | 114 ++++-----
.../contentstore/views/tests/test_item.py | 6 +-
cms/static/coffee/spec/main.coffee | 2 +
cms/static/js/models/xblock_validation.js | 46 ++++
.../js/spec/models/xblock_validation_spec.js | 152 ++++++++++++
.../js/spec/views/group_configuration_spec.js | 4 +-
cms/static/js/spec/views/xblock_spec.js | 8 +
.../js/spec/views/xblock_validation_spec.js | 132 +++++++++++
cms/static/js/views/pages/container.js | 46 ++--
cms/static/js/views/xblock.js | 12 +
cms/static/js/views/xblock_validation.js | 76 ++++++
.../js/group-configuration-details.underscore | 2 +-
cms/templates/js/mock/mock-xblock.underscore | 3 +
.../js/xblock-validation-messages.underscore | 49 ++++
cms/templates/studio_xblock_wrapper.html | 72 ++++--
.../public/js/split_test_author_view.js | 26 ++-
.../lib/xmodule/xmodule/split_test_module.py | 186 +++++++--------
.../xmodule/tests/test_split_test_module.py | 125 +++++-----
.../xmodule/xmodule/tests/test_validation.py | 218 ++++++++++++++++++
common/lib/xmodule/xmodule/validation.py | 128 ++++++++++
.../test/acceptance/pages/studio/container.py | 5 +-
.../courseware/tests/test_model_data.py | 2 +-
lms/templates/split_test_author_view.html | 61 +----
requirements/edx/github.txt | 2 +-
26 files changed, 1146 insertions(+), 337 deletions(-)
create mode 100644 cms/static/js/models/xblock_validation.js
create mode 100644 cms/static/js/spec/models/xblock_validation_spec.js
create mode 100644 cms/static/js/spec/views/xblock_validation_spec.js
create mode 100644 cms/static/js/views/xblock_validation.js
create mode 100644 cms/templates/js/xblock-validation-messages.underscore
create mode 100644 common/lib/xmodule/xmodule/tests/test_validation.py
create mode 100644 common/lib/xmodule/xmodule/validation.py
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index e2b7eb2983..fb5aa94e5c 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -7,6 +7,8 @@ the top. Include a label indicating the component affected.
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 students to a cohort via the instructor dashboard. TNL-163
diff --git a/cms/djangoapps/contentstore/views/course.py b/cms/djangoapps/contentstore/views/course.py
index 79056f749d..77f6c95b58 100644
--- a/cms/djangoapps/contentstore/views/course.py
+++ b/cms/djangoapps/contentstore/views/course.py
@@ -1290,10 +1290,12 @@ class GroupConfiguration(object):
'container_handler',
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({
'label': '{} / {}'.format(unit.display_name, split_test.display_name),
'url': unit_url,
- 'validation': split_test.general_validation_message,
+ 'validation': validation_summary.to_json() if validation_summary else None,
})
return usage_info
diff --git a/cms/djangoapps/contentstore/views/tests/test_group_configurations.py b/cms/djangoapps/contentstore/views/tests/test_group_configurations.py
index ca122a58f7..7d03c23c0f 100644
--- a/cms/djangoapps/contentstore/views/tests/test_group_configurations.py
+++ b/cms/djangoapps/contentstore/views/tests/test_group_configurations.py
@@ -9,7 +9,7 @@ from contentstore.views.course import GroupConfiguration
from contentstore.tests.utils import CourseTestCase
from xmodule.partitions.partitions import Group, UserPartition
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 import ModuleStoreEnum
@@ -541,87 +541,75 @@ class GroupConfigurationsValidationTestCase(CourseTestCase, HelperMethods):
def setUp(self):
super(GroupConfigurationsValidationTestCase, self).setUp()
- @patch('xmodule.split_test_module.SplitTestDescriptor.validation_messages')
- def test_error_message_present(self, mocked_validation_messages):
+ @patch('xmodule.split_test_module.SplitTestDescriptor.validate_split_test')
+ 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()
split_test = self._create_content_experiment(cid=0, name_suffix='0')[1]
- mocked_validation_messages.return_value = [
- ValidationMessage(
- split_test,
- u"Validation message",
- ValidationMessageType.error
- )
- ]
- group_configuration = GroupConfiguration.add_usage_info(self.course, self.store)[0]
- self.assertEqual(
- group_configuration['usage'][0]['validation'],
- {
- 'message': u'This content experiment has issues that affect content visibility.',
- 'type': 'error'
- }
- )
+ validation = StudioValidation(split_test.location)
+ validation.add(mocked_message)
+ mocked_validation_messages.return_value = validation
- @patch('xmodule.split_test_module.SplitTestDescriptor.validation_messages')
- def test_warning_message_present(self, mocked_validation_messages):
+ group_configuration = GroupConfiguration.add_usage_info(self.course, self.store)[0]
+ self.assertEqual(expected_result.to_json(), group_configuration['usage'][0]['validation'])
+
+ def test_error_message_present(self):
"""
- Tests if validation message is present.
+ 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
+
+ def test_warning_message_present(self):
+ """
+ Tests if validation message is present (warning case).
+ """
+ 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()
split_test = self._create_content_experiment(cid=0, name_suffix='0')[1]
- mocked_validation_messages.return_value = [
- ValidationMessage(
- split_test,
- u"Validation message",
- ValidationMessageType.warning
- )
- ]
- group_configuration = GroupConfiguration.add_usage_info(self.course, self.store)[0]
+ validation = StudioValidation(split_test.location)
+ if mocked_message is not None:
+ validation.add(mocked_message)
+ mocked_validation_messages.return_value = validation
+
+ 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'
- }
+ expected_result.to_json() if expected_result is not None else None,
+ group_configuration['usage'][0]['validation']
)
- @patch('xmodule.split_test_module.SplitTestDescriptor.validation_messages')
- def test_update_usage_info(self, mocked_validation_messages):
+ def test_update_usage_info(self):
"""
Tests if validation message is present when updating usage info.
"""
- self._add_user_partitions()
- split_test = self._create_content_experiment(cid=0, name_suffix='0')[1]
-
- 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'
- }
+ mocked_message = StudioValidationMessage(StudioValidationMessage.WARNING, u"Validation message")
+ expected_result = StudioValidationMessage(
+ StudioValidationMessage.WARNING, u"This content experiment has issues that affect content visibility."
)
+ # 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, mocked_validation_messages):
+ def test_update_usage_info_no_message(self):
"""
Tests if validation message is not present when updating usage info.
"""
- self._add_user_partitions()
- 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)
+ self.verify_validation_update_usage_info(None, None) # pylint: disable=no-value-for-parameter
diff --git a/cms/djangoapps/contentstore/views/tests/test_item.py b/cms/djangoapps/contentstore/views/tests/test_item.py
index c765f1da55..a1296a9495 100644
--- a/cms/djangoapps/contentstore/views/tests/test_item.py
+++ b/cms/djangoapps/contentstore/views/tests/test_item.py
@@ -130,8 +130,10 @@ class GetItemTest(ItemTest):
root_usage_key = self._create_vertical()
html, __ = self._get_container_preview(root_usage_key)
- # Verify that the Studio wrapper is not added
- self.assertNotIn('wrapper-xblock', html)
+ # XBlock messages are added by the Studio wrapper.
+ 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
self.assertIn('
<% } %>
diff --git a/cms/templates/js/mock/mock-xblock.underscore b/cms/templates/js/mock/mock-xblock.underscore
index 652aa17001..386b4b8bf8 100644
--- a/cms/templates/js/mock/mock-xblock.underscore
+++ b/cms/templates/js/mock/mock-xblock.underscore
@@ -9,6 +9,9 @@
+
+ Add Missing Groups
+
diff --git a/cms/templates/js/xblock-validation-messages.underscore b/cms/templates/js/xblock-validation-messages.underscore
new file mode 100644
index 0000000000..716f619312
--- /dev/null
+++ b/cms/templates/js/xblock-validation-messages.underscore
@@ -0,0 +1,49 @@
+<%
+var summaryMessage = validation.get("summary");
+var aggregateMessageType = summaryMessage.type;
+var aggregateValidationClass = aggregateMessageType === "error"? "has-errors" : "has-warnings";
+%>
+
+
+ <%- summaryMessage.text %>
+ <% if (summaryMessage.action_class) { %>
+
+ <%- summaryMessage.action_label %>
+
+ <% } else if (summaryMessage.action_runtime_event) {%>
+
+ <%- summaryMessage.action_label %>
+
+ <% } %>
+
+ <% var detailedMessages = validation.get("messages"); %>
+ <% if (detailedMessages.length > 0) { %>
+
+ <% for (var i = 0; i < detailedMessages.length; i++) { %>
+ <%
+ var message = detailedMessages[i];
+ var messageType = message.type
+ var messageTypeDisplayName = getDisplayName(messageType)
+ %>
+ -
+
+ <% if (messageTypeDisplayName) { %>
+ <%- messageTypeDisplayName %>:
+ <% } %>
+ <%- message.text %>
+
+ <% if (message.action_class) { %>
+
+ <%- message.action_label %>
+
+ <% } else if (message.action_runtime_event) {%>
+
+ <%- message.action_label %>
+
+ <% } %>
+
+
+ <% } %>
+
+ <% } %>
+
diff --git a/cms/templates/studio_xblock_wrapper.html b/cms/templates/studio_xblock_wrapper.html
index 1f578f405c..1573773561 100644
--- a/cms/templates/studio_xblock_wrapper.html
+++ b/cms/templates/studio_xblock_wrapper.html
@@ -1,16 +1,51 @@
+
<%!
from django.utils.translation import ugettext as _
from contentstore.views.helpers import xblock_studio_url
+import json
%>
-
<%
xblock_url = xblock_studio_url(xblock)
show_inline = xblock.has_children and not xblock_url
section_class = "level-nesting" if show_inline else "level-element"
collapsible_class = "is-collapsible" if xblock.has_children else ""
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">
+
+%block>
+
+
+
+
% if not is_root:
% if is_reorderable:
@@ -64,21 +99,29 @@ label = xblock.display_name or xblock.scope_ids.block_type
- % if xblock_url and not is_root:
-
+ % if not is_root:
+
+ % if xblock_url:
+
+ % endif
% endif
+
+% if is_root:
+
+% endif
+
% if is_root or not xblock_url:
${content}
@@ -86,7 +129,6 @@ label = xblock.display_name or xblock.scope_ids.block_type
% else:
${content}
-
% endif
% if not is_root:
diff --git a/common/lib/xmodule/xmodule/public/js/split_test_author_view.js b/common/lib/xmodule/xmodule/public/js/split_test_author_view.js
index 8c4cc866f3..5c229fb5dd 100644
--- a/common/lib/xmodule/xmodule/public/js/split_test_author_view.js
+++ b/common/lib/xmodule/xmodule/public/js/split_test_author_view.js
@@ -1,25 +1,27 @@
/* JavaScript for editing operations that can be done on the split test author view. */
window.SplitTestAuthorView = function (runtime, element) {
var $element = $(element);
+ var splitTestLocator = $element.closest('.studio-xblock-wrapper').data('locator');
- $element.find('.add-missing-groups-button').click(function () {
- runtime.notify('save', {
- state: 'start',
- element: element,
- message: gettext('Creating missing groups…')
- });
- $.post(runtime.handlerUrl(element, 'add_missing_groups')).done(function() {
+ runtime.listenTo("add-missing-groups", function (parentLocator) {
+ if (splitTestLocator === parentLocator) {
runtime.notify('save', {
- state: 'end',
- element: element
+ state: 'start',
+ element: element,
+ message: gettext('Creating missing groups…')
});
- });
+ $.post(runtime.handlerUrl(element, 'add_missing_groups')).done(function() {
+ runtime.notify('save', {
+ state: 'end',
+ element: element
+ });
+ });
+ }
});
// Listen to delete events so that the view can refresh when the last inactive group is removed.
runtime.listenTo('deleted-child', function(parentLocator) {
- var splitTestLocator = $element.closest('.studio-xblock-wrapper').data('locator'),
- inactiveGroups = $element.find('.is-inactive .studio-xblock-wrapper');
+ var inactiveGroups = $element.find('.is-inactive .studio-xblock-wrapper');
if (splitTestLocator === parentLocator && inactiveGroups.length === 0) {
runtime.refreshXBlock($element);
}
diff --git a/common/lib/xmodule/xmodule/split_test_module.py b/common/lib/xmodule/xmodule/split_test_module.py
index 63f23a824e..890afb75dc 100644
--- a/common/lib/xmodule/xmodule/split_test_module.py
+++ b/common/lib/xmodule/xmodule/split_test_module.py
@@ -12,6 +12,7 @@ from xmodule.progress import Progress
from xmodule.seq_module import SequenceDescriptor
from xmodule.studio_editable import StudioEditableModule, StudioEditableDescriptor
from xmodule.x_module import XModule, module_attr, STUDENT_VIEW
+from xmodule.validation import StudioValidation, StudioValidationMessage
from xmodule.modulestore.inheritance import UserPartitionList
from lxml import etree
@@ -28,48 +29,6 @@ _ = lambda text: text
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):
"""Fields needed for split test module"""
has_children = True
@@ -231,6 +190,13 @@ class SplitTestModule(SplitTestFields, XModule, StudioEditableModule):
return None
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):
"""
Render the staff view for a split test module.
@@ -283,7 +249,6 @@ class SplitTestModule(SplitTestFields, XModule, StudioEditableModule):
"""
fragment = Fragment()
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
active_groups_preview = None
inactive_groups_preview = None
@@ -300,7 +265,7 @@ class SplitTestModule(SplitTestFields, XModule, StudioEditableModule):
fragment.add_content(self.system.render_template('split_test_author_view.html', {
'split_test': self,
'is_root': is_root,
- 'is_configured': is_configured,
+ 'is_configured': self.is_configured,
'active_groups_preview': active_groups_preview,
'inactive_groups_preview': inactive_groups_preview,
'group_configuration_url': self.descriptor.group_configuration_url,
@@ -320,7 +285,7 @@ class SplitTestModule(SplitTestFields, XModule, StudioEditableModule):
active_child = self.system.get_module(active_child_descriptor)
rendered_child = active_child.render(StudioEditableModule.get_preview_view_name(active_child), context)
if active_child.category == 'vertical':
- group_name, group_id = self.get_data_for_vertical(active_child)
+ group_name, group_id = self.get_data_for_vertical(active_child)
if group_name:
rendered_child.content = rendered_child.content.replace(
DEFAULT_GROUP_NAME.format(group_id=group_id),
@@ -384,6 +349,13 @@ class SplitTestModule(SplitTestFields, XModule, StudioEditableModule):
return (group.name, group.id)
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.wants('partitions')
@@ -544,46 +516,94 @@ class SplitTestDescriptor(SplitTestFields, SequenceDescriptor, StudioEditableDes
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
- includes a message type indicating whether the message represents information, a warning or an error.
+ Returns true if the split_test instance is associated with a UserPartition.
+ """
+ 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
- messages = []
+ split_validation = StudioValidation(self.location)
if self.user_partition_id < 0:
- messages.append(ValidationMessage(
- self,
- _(u"The experiment is not associated with a group configuration."),
- ValidationMessageType.warning,
- 'edit-button',
- _(u"Select a Group Configuration")
- ))
+ split_validation.add(
+ StudioValidationMessage(
+ StudioValidationMessage.NOT_CONFIGURED,
+ _(u"The experiment is not associated with a group configuration."),
+ action_class='edit-button',
+ action_label=_(u"Select a Group Configuration")
+ )
+ )
else:
user_partition = self.get_selected_partition()
if not user_partition:
- messages.append(ValidationMessage(
- self,
- _(u"The experiment uses a deleted group configuration. Select a valid group configuration or delete this experiment."),
- ValidationMessageType.error
- ))
+ split_validation.add(
+ StudioValidationMessage(
+ StudioValidationMessage.ERROR,
+ _(u"The experiment uses a deleted group configuration. Select a valid group configuration or delete this experiment.")
+ )
+ )
else:
[active_children, inactive_children] = self.active_and_inactive_children()
if len(active_children) < len(user_partition.groups):
- messages.append(ValidationMessage(
- self,
- _(u"The experiment does not contain all of the groups in the configuration."),
- ValidationMessageType.error,
- 'add-missing-groups-button',
- _(u"Add Missing Groups")
- ))
+ split_validation.add(
+ StudioValidationMessage(
+ StudioValidationMessage.ERROR,
+ _(u"The experiment does not contain all of the groups in the configuration."),
+ action_runtime_event='add-missing-groups',
+ action_label=_(u"Add Missing Groups")
+ )
+ )
if len(inactive_children) > 0:
- messages.append(ValidationMessage(
- self,
- _(u"The experiment has an inactive group. Move content into active groups, then delete the inactive group."),
- ValidationMessageType.warning
- ))
- return messages
+ split_validation.add(
+ StudioValidationMessage(
+ StudioValidationMessage.WARNING,
+ _(u"The experiment has an inactive group. Move content into active groups, then delete the inactive group.")
+ )
+ )
+ 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
def add_missing_groups(self, request, suffix=''): # pylint: disable=unused-argument
@@ -603,7 +623,7 @@ class SplitTestDescriptor(SplitTestFields, SequenceDescriptor, StudioEditableDes
changed = True
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)
return Response()
@@ -648,19 +668,3 @@ class SplitTestDescriptor(SplitTestFields, SequenceDescriptor, StudioEditableDes
)
self.children.append(dest_usage_key) # pylint: disable=no-member
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
diff --git a/common/lib/xmodule/xmodule/tests/test_split_test_module.py b/common/lib/xmodule/xmodule/tests/test_split_test_module.py
index a7484d902e..b82310c845 100644
--- a/common/lib/xmodule/xmodule/tests/test_split_test_module.py
+++ b/common/lib/xmodule/xmodule/tests/test_split_test_module.py
@@ -10,7 +10,8 @@ from xmodule.tests.xml import factories as xml
from xmodule.tests.xml import XModuleXmlImportTest
from xmodule.tests import get_test_system
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.test_partitions import StaticPartitionService, MemoryUserTagsService
@@ -320,14 +321,6 @@ class SplitTestModuleStudioTest(SplitTestModuleTest):
self.assertEqual(active_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):
"""
Test the validation messages produced for different split test configurations.
@@ -335,122 +328,128 @@ class SplitTestModuleStudioTest(SplitTestModuleTest):
split_test_module = self.split_test_module
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.
"""
- self.assertEqual(unicode(message), expected_message)
- self.assertEqual(message.message_type, expected_message_type)
- self.assertEqual(message.action_class, expected_action_class)
- self.assertEqual(message.action_label, expected_action_label)
+ self.assertEqual(message.text, expected_message)
+ self.assertEqual(message.type, expected_message_type)
+ if 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)
+ else:
+ 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_general_validation_message(general_validation, expected_message, expected_message_type):
+ def verify_summary_message(general_validation, expected_message, expected_message_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['type'], expected_message_type)
+ self.assertEqual(general_validation.text, expected_message)
+ self.assertEqual(general_validation.type, expected_message_type)
# Verify the messages for an unconfigured user partition
split_test_module.user_partition_id = -1
- messages = split_test_module.validation_messages()
- self.assertEqual(len(messages), 1)
+ validation = split_test_module.validate()
+ self.assertEqual(len(validation.messages), 0)
verify_validation_message(
- messages[0],
+ validation.summary,
u"The experiment is not associated with a group configuration.",
- ValidationMessageType.warning,
+ StudioValidationMessage.NOT_CONFIGURED,
'edit-button',
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
split_test_module.user_partition_id = 0
split_test_module.user_partitions = [
UserPartition(0, 'first_partition', 'First Partition', [Group("0", 'alpha'), Group("1", 'beta')])
]
- messages = split_test_module.validation_messages()
- self.assertEqual(len(messages), 0)
- self.assertIsNone(split_test_module.general_validation_message, None)
+ validation = split_test_module.validate_split_test()
+ self.assertTrue(validation)
+ self.assertIsNone(split_test_module.general_validation_message(), None)
# Verify the messages for a split test with too few groups
split_test_module.user_partitions = [
UserPartition(0, 'first_partition', 'First Partition',
[Group("0", 'alpha'), Group("1", 'beta'), Group("2", 'gamma')])
]
- messages = split_test_module.validation_messages()
- self.assertEqual(len(messages), 1)
+ validation = split_test_module.validate()
+ self.assertEqual(len(validation.messages), 1)
verify_validation_message(
- messages[0],
+ validation.messages[0],
u"The experiment does not contain all of the groups in the configuration.",
- ValidationMessageType.error,
- 'add-missing-groups-button',
- u"Add Missing Groups"
+ StudioValidationMessage.ERROR,
+ expected_action_runtime_event='add-missing-groups',
+ expected_action_label=u"Add Missing Groups"
)
- verify_general_validation_message(
- split_test_module.general_validation_message,
+ verify_summary_message(
+ validation.summary,
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
split_test_module.user_partitions = [
UserPartition(0, 'first_partition', 'First Partition',
[Group("0", 'alpha')])
]
- messages = split_test_module.validation_messages()
- self.assertEqual(len(messages), 1)
+ validation = split_test_module.validate()
+ self.assertEqual(len(validation.messages), 1)
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.",
- ValidationMessageType.warning
+ StudioValidationMessage.WARNING
)
- verify_general_validation_message(
- split_test_module.general_validation_message,
+ verify_summary_message(
+ validation.summary,
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
split_test_module.user_partitions = [
UserPartition(0, 'first_partition', 'First Partition',
[Group("0", 'alpha'), Group("2", 'gamma')])
]
- messages = split_test_module.validation_messages()
- self.assertEqual(len(messages), 2)
+ validation = split_test_module.validate()
+ self.assertEqual(len(validation.messages), 2)
verify_validation_message(
- messages[0],
+ validation.messages[0],
u"The experiment does not contain all of the groups in the configuration.",
- ValidationMessageType.error,
- 'add-missing-groups-button',
- u"Add Missing Groups"
+ StudioValidationMessage.ERROR,
+ expected_action_runtime_event='add-missing-groups',
+ expected_action_label=u"Add Missing Groups"
)
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.",
- ValidationMessageType.warning
+ StudioValidationMessage.WARNING
)
# With two messages of type error and warning priority given to error.
- verify_general_validation_message(
- split_test_module.general_validation_message,
+ verify_summary_message(
+ validation.summary,
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
split_test_module.user_partition_id = 2
- messages = split_test_module.validation_messages()
- self.assertEqual(len(messages), 1)
+ validation = split_test_module.validate()
+ self.assertEqual(len(validation.messages), 1)
verify_validation_message(
- messages[0],
+ validation.messages[0],
u"The experiment uses a deleted group configuration. "
u"Select a valid group configuration or delete this experiment.",
- ValidationMessageType.error
+ StudioValidationMessage.ERROR
)
- verify_general_validation_message(
- split_test_module.general_validation_message,
+ verify_summary_message(
+ validation.summary,
u"This content experiment has issues that affect content visibility.",
- ValidationMessageType.error
+ StudioValidationMessage.ERROR
)
diff --git a/common/lib/xmodule/xmodule/tests/test_validation.py b/common/lib/xmodule/xmodule/tests/test_validation.py
new file mode 100644
index 0000000000..f3e6b95b50
--- /dev/null
+++ b/common/lib/xmodule/xmodule/tests/test_validation.py
@@ -0,0 +1,218 @@
+"""
+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())
diff --git a/common/lib/xmodule/xmodule/validation.py b/common/lib/xmodule/xmodule/validation.py
new file mode 100644
index 0000000000..2b6d4fce59
--- /dev/null
+++ b/common/lib/xmodule/xmodule/validation.py
@@ -0,0 +1,128 @@
+"""
+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
diff --git a/common/test/acceptance/pages/studio/container.py b/common/test/acceptance/pages/studio/container.py
index 6eac9f26d4..878a0e0bbf 100644
--- a/common/test/acceptance/pages/studio/container.py
+++ b/common/test/acceptance/pages/studio/container.py
@@ -16,6 +16,7 @@ class ContainerPage(PageObject):
NAME_SELECTOR = '.page-header-title'
NAME_INPUT_SELECTOR = '.page-header .xblock-field-input'
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):
super(ContainerPage, self).__init__(browser)
@@ -246,7 +247,7 @@ class ContainerPage(PageObject):
Click the "add missing groups" link.
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()
# Wait until all xblocks rendered.
@@ -256,7 +257,7 @@ class ContainerPage(PageObject):
"""
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):
"""
diff --git a/lms/djangoapps/courseware/tests/test_model_data.py b/lms/djangoapps/courseware/tests/test_model_data.py
index b9bc5b2f6a..185806411f 100644
--- a/lms/djangoapps/courseware/tests/test_model_data.py
+++ b/lms/djangoapps/courseware/tests/test_model_data.py
@@ -18,7 +18,7 @@ from courseware.tests.factories import StudentPrefsFactory, StudentInfoFactory
from xblock.fields import Scope, BlockScope, ScopeIds
from django.test import TestCase
from django.db import DatabaseError
-from xblock.core import KeyValueMultiSaveError
+from xblock.exceptions import KeyValueMultiSaveError
def mock_field(scope, name):
diff --git a/lms/templates/split_test_author_view.html b/lms/templates/split_test_author_view.html
index ece87663c8..ede487e199 100644
--- a/lms/templates/split_test_author_view.html
+++ b/lms/templates/split_test_author_view.html
@@ -1,20 +1,13 @@
<%! from django.utils.translation import ugettext as _ %>
-<%! from xmodule.split_test_module import ValidationMessageType %>
<%
split_test = context.get('split_test')
user_partition = split_test.descriptor.get_selected_partition()
-messages = split_test.descriptor.validation_messages()
show_link = group_configuration_url is not None
%>
-% if is_root and not is_configured:
-
-% else:
-
-% endif
-
-% if user_partition:
+% if is_configured:
+
-% 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'
- %>
-
- % if is_configured:
-
- ${general_validation['message']}
-
- % endif
- % if is_root or not is_configured:
-
- % for message in messages:
- <%
- message_type = message.message_type
- message_type_display_name = ValidationMessageType.display_name(message_type) if message_type else None
- %>
- -
- % if not is_configured:
-
- % endif
-
- % if message_type_display_name:
- ${message_type_display_name}:
- % endif
- ${unicode(message)}
-
- % if message.action_class:
-
- ${message.action_label}
-
- % endif
-
-
- % endfor
-
- % endif
-
-% endif
-
+% endif
% if is_root:
diff --git a/requirements/edx/github.txt b/requirements/edx/github.txt
index e86c3bbdee..e50060288a 100644
--- a/requirements/edx/github.txt
+++ b/requirements/edx/github.txt
@@ -20,7 +20,7 @@
-e git+https://github.com/pmitros/django-pyfs.git@d175715e0fe3367ec0f1ee429c242d603f6e8b10#egg=djpyfs
# 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/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