Display validation messages for any xblock on the container page.

TNL-683
This commit is contained in:
cahrens
2014-10-23 17:21:48 -04:00
parent cb5e90fc08
commit 08ce09bde7
26 changed files with 1146 additions and 337 deletions

View File

@@ -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);
}

View File

@@ -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

View File

@@ -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
)

View File

@@ -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())

View File

@@ -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

View File

@@ -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):
"""