Commit a395c2fa by cahrens Committed by Andy Armstrong

Complete Studio support for handling group configuration changes

STUD-1658
parent 73e7ced6
define(["jquery", "underscore", "js/views/xblock", "js/utils/module", "gettext", "js/views/feedback_notification"], define(["jquery", "underscore", "js/views/xblock", "js/utils/module", "gettext", "js/views/feedback_notification",
"jquery.ui"], // The container view uses sortable, which is provided by jquery.ui.
function ($, _, XBlockView, ModuleUtils, gettext, NotificationView) { function ($, _, XBlockView, ModuleUtils, gettext, NotificationView) {
var reorderableClass = '.reorderable-container', var reorderableClass = '.reorderable-container',
sortableInitializedClass = '.ui-sortable', sortableInitializedClass = '.ui-sortable',
......
...@@ -168,10 +168,6 @@ define(["jquery", "underscore", "gettext", "js/views/modals/base_modal", ...@@ -168,10 +168,6 @@ define(["jquery", "underscore", "gettext", "js/views/modals/base_modal",
if (this.runtime) { if (this.runtime) {
this.runtime.notify('modal-hidden'); this.runtime.notify('modal-hidden');
} }
// Completely clear the contents of the modal
this.undelegateEvents();
this.$el.html("");
}, },
findXBlockInfo: function(xblockWrapperElement, defaultXBlockInfo) { findXBlockInfo: function(xblockWrapperElement, defaultXBlockInfo) {
......
...@@ -28,8 +28,11 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/contai ...@@ -28,8 +28,11 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/contai
// Hide both blocks until we know which one to show // Hide both blocks until we know which one to show
xblockView.$el.addClass('is-hidden'); xblockView.$el.addClass('is-hidden');
// Add actions to any top level buttons, e.g. "Edit" of the container itself if (!options || !options.refresh) {
self.addButtonActions(this.$el); // 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({
...@@ -192,7 +195,7 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/contai ...@@ -192,7 +195,7 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/contai
parentElement = xblockElement.parent(), parentElement = xblockElement.parent(),
rootLocator = this.xblockView.model.id; rootLocator = this.xblockView.model.id;
if (xblockElement.length === 0 || xblockElement.data('locator') === rootLocator) { if (xblockElement.length === 0 || xblockElement.data('locator') === rootLocator) {
this.render({ }); this.render({refresh: true});
} else if (parentElement.hasClass('reorderable-container')) { } else if (parentElement.hasClass('reorderable-container')) {
this.refreshChildXBlock(xblockElement); this.refreshChildXBlock(xblockElement);
} else { } else {
......
...@@ -152,47 +152,51 @@ ...@@ -152,47 +152,51 @@
padding: ($baseline/2) ($baseline*.75); padding: ($baseline/2) ($baseline*.75);
color: $white; color: $white;
.message-text {
display: inline-block;
width: 93%;
vertical-align: top;
}
[class^="icon-"] { [class^="icon-"] {
font-style: normal; font-style: normal;
} }
&.information { &.information {
@extend %t-copy-sub1;
background-color: $gray-l5; background-color: $gray-l5;
color: $gray-d2; color: $gray-d2;
} }
&.warning { &.validation {
background-color: $gray-d2; background-color: $gray-d2;
padding: ($baseline/2) $baseline;
color: $white; color: $white;
.icon-warning-sign { a {
margin-right: ($baseline/2); color: $blue-l2;
color: $orange;
} }
.message-text { &.has-warnings {
display: inline-block; border-bottom: 3px solid $orange;
width: 93%;
vertical-align: top; .icon-warning-sign {
margin-right: ($baseline/2);
color: $orange;
}
} }
}
&.error { &.has-errors {
background-color: $gray-d2; border-bottom: 3px solid $red-l2;
padding: ($baseline/2) $baseline;
color: $white;
.icon-exclamation-sign { .icon-exclamation-sign {
margin-right: ($baseline/2); margin-right: ($baseline/2);
color: $red-l2; color: $red-l2;
}
} }
} }
} }
.xblock-message-list {
margin-bottom: 0;
}
.xblock-message-actions {
@extend %actions-header;
padding: ($baseline/2) $baseline;
background-color: $gray-d1;
}
} }
...@@ -64,7 +64,7 @@ ...@@ -64,7 +64,7 @@
.no-container-content { .no-container-content {
@extend %ui-well; @extend %ui-well;
padding: ($baseline*2); padding: $baseline;
background-color: $gray-l4; background-color: $gray-l4;
text-align: center; text-align: center;
color: $gray; color: $gray;
...@@ -78,7 +78,7 @@ ...@@ -78,7 +78,7 @@
@extend %t-action4; @extend %t-action4;
padding: 8px 20px 10px; padding: 8px 20px 10px;
text-align: center; text-align: center;
margin-left: $baseline; margin: ($baseline/2) 0 ($baseline/2) $baseline;
[class^="icon-"] { [class^="icon-"] {
margin-right: ($baseline/2); margin-right: ($baseline/2);
...@@ -158,6 +158,8 @@ body.view-container .content-primary { ...@@ -158,6 +158,8 @@ body.view-container .content-primary {
// CASE: page level xblock rendering // CASE: page level xblock rendering
&.level-page { &.level-page {
margin: 0; margin: 0;
box-shadow: none;
border: 0;
.xblock-header { .xblock-header {
display: none; display: none;
...@@ -166,15 +168,36 @@ body.view-container .content-primary { ...@@ -166,15 +168,36 @@ body.view-container .content-primary {
.xblock-message { .xblock-message {
border-radius: 3px 3px 0 0; border-radius: 3px 3px 0 0;
&.validation {
padding-top: ($baseline*.75);
}
.xblock-message-list {
margin: ($baseline/5) ($baseline*2.5);
list-style-type: disc;
color: $gray-l3;
}
.xblock-message-item {
padding-bottom: ($baseline/4);
}
&.information { &.information {
@extend %t-copy-base; padding: 0 0 ($baseline/2) 0;
margin-bottom: $baseline;
border-bottom: 1px solid $gray-l4;
padding: ($baseline/2) ($baseline*.75);
background-color: $gray-l5; background-color: $gray-l5;
color: $gray-d1; color: $gray-d1;
} }
} }
.no-container-content {
.xblock-message-list {
margin: 0;
list-style-type: none;
color: $gray-d2;
}
}
} }
// CASE: nesting level xblock rendering // CASE: nesting level xblock rendering
...@@ -250,6 +273,34 @@ body.view-container .content-primary { ...@@ -250,6 +273,34 @@ body.view-container .content-primary {
display: none; display: none;
} }
} }
.wrapper-xblock-message {
.xblock-message {
border-radius: 0 0 3px 3px;
.xblock-message-list {
margin: 0;
list-style-type: none;
}
&.information {
@extend %t-copy-sub2;
padding: 0 0 ($baseline/2) $baseline;
color: $gray-l1;
}
&.validation.has-warnings {
border: 0;
border-top: 3px solid $orange;
}
&.validation.has-errors {
border: 0;
border-top: 3px solid $red-l2;
}
}
}
} }
} }
...@@ -273,9 +324,9 @@ body.view-container .content-primary { ...@@ -273,9 +324,9 @@ body.view-container .content-primary {
} }
&.is-inactive { &.is-inactive {
margin: $baseline 0 0 0; margin: ($baseline*1.5) 0 0 0;
border-top: 2px dotted $gray-l2; border-top: 2px dotted $gray-l2;
padding: ($baseline/2) 0; padding: ($baseline*.75) 0;
background-color: $gray-l4; background-color: $gray-l4;
.wrapper-xblock.level-nesting { .wrapper-xblock.level-nesting {
......
...@@ -203,6 +203,16 @@ body.course.unit, ...@@ -203,6 +203,16 @@ body.course.unit,
padding-top: 0; padding-top: 0;
color: $gray-l1; color: $gray-l1;
} }
&.has-warnings {
border: 0;
border-top: 3px solid $orange;
}
&.has-errors {
border: 0;
border-top: 3px solid $red-l2;
}
} }
} }
......
...@@ -55,11 +55,13 @@ class ValidationMessage(object): ...@@ -55,11 +55,13 @@ class ValidationMessage(object):
""" """
Represents a single validation message for an xblock. Represents a single validation message for an xblock.
""" """
def __init__(self, xblock, message_text, message_type): def __init__(self, xblock, message_text, message_type, action_class=None, action_label=None):
assert isinstance(message_text, unicode) assert isinstance(message_text, unicode)
self.xblock = xblock self.xblock = xblock
self.message_text = message_text self.message_text = message_text
self.message_type = message_type self.message_type = message_type
self.action_class = action_class
self.action_label = action_label
def __unicode__(self): def __unicode__(self):
return self.message_text return self.message_text
...@@ -76,12 +78,15 @@ class SplitTestFields(object): ...@@ -76,12 +78,15 @@ class SplitTestFields(object):
no_partition_selected = {'display_name': _("Not Selected"), 'value': -1} no_partition_selected = {'display_name': _("Not Selected"), 'value': -1}
@staticmethod @staticmethod
def build_partition_values(all_user_partitions): def build_partition_values(all_user_partitions, selected_user_partition):
""" """
This helper method builds up the user_partition values that will This helper method builds up the user_partition values that will
be passed to the Studio editor be passed to the Studio editor
""" """
SplitTestFields.user_partition_values = [SplitTestFields.no_partition_selected] SplitTestFields.user_partition_values = []
# Add "No selection" value if there is not a valid selected user partition.
if not selected_user_partition:
SplitTestFields.user_partition_values.append(SplitTestFields.no_partition_selected)
for user_partition in all_user_partitions: for user_partition in all_user_partitions:
SplitTestFields.user_partition_values.append({"display_name": user_partition.name, "value": user_partition.id}) SplitTestFields.user_partition_values.append({"display_name": user_partition.name, "value": user_partition.id})
return SplitTestFields.user_partition_values return SplitTestFields.user_partition_values
...@@ -122,7 +127,6 @@ class SplitTestFields(object): ...@@ -122,7 +127,6 @@ class SplitTestFields(object):
scope=Scope.content scope=Scope.content
) )
@XBlock.needs('user_tags') # pylint: disable=abstract-method @XBlock.needs('user_tags') # pylint: disable=abstract-method
@XBlock.wants('partitions') @XBlock.wants('partitions')
class SplitTestModule(SplitTestFields, XModule, StudioEditableModule): class SplitTestModule(SplitTestFields, XModule, StudioEditableModule):
...@@ -258,15 +262,12 @@ class SplitTestModule(SplitTestFields, XModule, StudioEditableModule): ...@@ -258,15 +262,12 @@ 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
# We don't show the "add missing groups" button on the unit page-- only when showing the container page.
is_missing_groups = False
if is_root: if is_root:
user_partition = self.descriptor.get_selected_partition()
[active_children, inactive_children] = self.descriptor.active_and_inactive_children() [active_children, inactive_children] = self.descriptor.active_and_inactive_children()
is_missing_groups = user_partition and len(active_children) < len(user_partition.groups)
active_groups_preview = self.studio_render_children( active_groups_preview = self.studio_render_children(
fragment, active_children, context fragment, active_children, context
) )
...@@ -277,9 +278,9 @@ class SplitTestModule(SplitTestFields, XModule, StudioEditableModule): ...@@ -277,9 +278,9 @@ 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,
'active_groups_preview': active_groups_preview, 'active_groups_preview': active_groups_preview,
'inactive_groups_preview': inactive_groups_preview, 'inactive_groups_preview': inactive_groups_preview,
'is_missing_groups': is_missing_groups
})) }))
fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/split_test_author_view.js')) fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/split_test_author_view.js'))
fragment.initialize_js('SplitTestAuthorView') fragment.initialize_js('SplitTestAuthorView')
...@@ -430,7 +431,7 @@ class SplitTestDescriptor(SplitTestFields, SequenceDescriptor, StudioEditableDes ...@@ -430,7 +431,7 @@ class SplitTestDescriptor(SplitTestFields, SequenceDescriptor, StudioEditableDes
@property @property
def editable_metadata_fields(self): def editable_metadata_fields(self):
# Update the list of partitions based on the currently available user_partitions. # Update the list of partitions based on the currently available user_partitions.
SplitTestFields.build_partition_values(self.user_partitions) SplitTestFields.build_partition_values(self.user_partitions, self.get_selected_partition())
editable_fields = super(SplitTestDescriptor, self).editable_metadata_fields editable_fields = super(SplitTestDescriptor, self).editable_metadata_fields
...@@ -508,15 +509,17 @@ class SplitTestDescriptor(SplitTestFields, SequenceDescriptor, StudioEditableDes ...@@ -508,15 +509,17 @@ class SplitTestDescriptor(SplitTestFields, SequenceDescriptor, StudioEditableDes
if self.user_partition_id < 0: if self.user_partition_id < 0:
messages.append(ValidationMessage( messages.append(ValidationMessage(
self, self,
_(u"You must select a group configuration for this content experiment."), _(u"The experiment is not associated with a group configuration."),
ValidationMessageType.warning ValidationMessageType.warning,
'edit-button',
_(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( messages.append(ValidationMessage(
self, self,
_(u"This content experiment will not be shown to students because it refers to a group configuration that has been deleted. You can delete this experiment or reinstate the group configuration to repair it."), \ _(u"The experiment uses a deleted group configuration. Select a valid group configuration or delete this experiment."),
ValidationMessageType.error ValidationMessageType.error
)) ))
else: else:
...@@ -524,13 +527,15 @@ class SplitTestDescriptor(SplitTestFields, SequenceDescriptor, StudioEditableDes ...@@ -524,13 +527,15 @@ class SplitTestDescriptor(SplitTestFields, SequenceDescriptor, StudioEditableDes
if len(active_children) < len(user_partition.groups): if len(active_children) < len(user_partition.groups):
messages.append(ValidationMessage( messages.append(ValidationMessage(
self, self,
_(u"This content experiment is missing groups that are defined in the current configuration. You can press the 'Create Missing Groups' button to create them."), _(u"The experiment does not contain all of the groups in the configuration."),
ValidationMessageType.error ValidationMessageType.error,
'add-missing-groups-button',
_(u"Add Missing Groups")
)) ))
if len(inactive_children) > 0: if len(inactive_children) > 0:
messages.append(ValidationMessage( messages.append(ValidationMessage(
self, self,
_(u"This content experiment has children that are not associated with the selected group configuration. You can move content into an active group or delete it if it is unneeded."), _(u"The experiment has an inactive group. Move content into active groups, then delete the inactive group."),
ValidationMessageType.warning ValidationMessageType.warning
)) ))
return messages return messages
...@@ -543,16 +548,17 @@ class SplitTestDescriptor(SplitTestFields, SequenceDescriptor, StudioEditableDes ...@@ -543,16 +548,17 @@ class SplitTestDescriptor(SplitTestFields, SequenceDescriptor, StudioEditableDes
Called from Studio view. Called from Studio view.
""" """
user_partition = self.get_selected_partition() user_partition = self.get_selected_partition()
changed = False
for group in user_partition.groups: for group in user_partition.groups:
str_group_id = unicode(group.id) str_group_id = unicode(group.id)
changed = False
if str_group_id not in self.group_id_to_child: if str_group_id not in self.group_id_to_child:
self._create_vertical_for_group(group) self._create_vertical_for_group(group)
changed = True changed = True
if changed: if changed:
# request does not have a user attribute, so pass None for user. # request does not have a user attribute, so pass None for user.
self.system.modulestore.update_item(self, None) self.system.modulestore.update_item(self, None)
return Response() return Response()
def _create_vertical_for_group(self, group): def _create_vertical_for_group(self, group):
......
...@@ -180,8 +180,6 @@ class SplitTestModuleStudioTest(SplitTestModuleTest): ...@@ -180,8 +180,6 @@ class SplitTestModuleStudioTest(SplitTestModuleTest):
html = self.module_system.render(self.split_test_module, AUTHOR_VIEW, context).content html = self.module_system.render(self.split_test_module, AUTHOR_VIEW, context).content
self.assertIn('HTML FOR GROUP 0', html) self.assertIn('HTML FOR GROUP 0', html)
self.assertIn('HTML FOR GROUP 1', html) self.assertIn('HTML FOR GROUP 1', html)
# Note that the mock xblock system doesn't render the template but the parameters instead
self.assertNotIn('\'is_missing_groups\': True', html)
# When rendering as a child, it shouldn't render either of its groups # When rendering as a child, it shouldn't render either of its groups
context = create_studio_context(self.course_sequence) context = create_studio_context(self.course_sequence)
...@@ -198,8 +196,6 @@ class SplitTestModuleStudioTest(SplitTestModuleTest): ...@@ -198,8 +196,6 @@ class SplitTestModuleStudioTest(SplitTestModuleTest):
html = self.module_system.render(self.split_test_module, AUTHOR_VIEW, context).content html = self.module_system.render(self.split_test_module, AUTHOR_VIEW, context).content
self.assertIn('HTML FOR GROUP 0', html) self.assertIn('HTML FOR GROUP 0', html)
self.assertIn('HTML FOR GROUP 1', html) self.assertIn('HTML FOR GROUP 1', html)
# Note that the mock xblock system doesn't render the template but the parameters instead
self.assertIn('\'is_missing_groups\': True', html)
def test_editable_settings(self): def test_editable_settings(self):
""" """
...@@ -230,6 +226,7 @@ class SplitTestModuleStudioTest(SplitTestModuleTest): ...@@ -230,6 +226,7 @@ class SplitTestModuleStudioTest(SplitTestModuleTest):
self.assertEqual([], SplitTestDescriptor.user_partition_id.values) self.assertEqual([], SplitTestDescriptor.user_partition_id.values)
# user_partitions is empty, only the "Not Selected" item will appear. # user_partitions is empty, only the "Not Selected" item will appear.
self.split_test_module.user_partition_id = SplitTestFields.no_partition_selected['value']
self.split_test_module.editable_metadata_fields # pylint: disable=pointless-statement self.split_test_module.editable_metadata_fields # pylint: disable=pointless-statement
partitions = SplitTestDescriptor.user_partition_id.values partitions = SplitTestDescriptor.user_partition_id.values
self.assertEqual(1, len(partitions)) self.assertEqual(1, len(partitions))
...@@ -246,6 +243,23 @@ class SplitTestModuleStudioTest(SplitTestModuleTest): ...@@ -246,6 +243,23 @@ class SplitTestModuleStudioTest(SplitTestModuleTest):
self.assertEqual(0, partitions[1]['value']) self.assertEqual(0, partitions[1]['value'])
self.assertEqual("first_partition", partitions[1]['display_name']) self.assertEqual("first_partition", partitions[1]['display_name'])
# Try again with a selected partition and verify that there is no option for "No Selection"
self.split_test_module.user_partition_id = 0
self.split_test_module.editable_metadata_fields # pylint: disable=pointless-statement
partitions = SplitTestDescriptor.user_partition_id.values
self.assertEqual(1, len(partitions))
self.assertEqual(0, partitions[0]['value'])
self.assertEqual("first_partition", partitions[0]['display_name'])
# Finally try again with an invalid selected partition and verify that "No Selection" is an option
self.split_test_module.user_partition_id = 999
self.split_test_module.editable_metadata_fields # pylint: disable=pointless-statement
partitions = SplitTestDescriptor.user_partition_id.values
self.assertEqual(2, len(partitions))
self.assertEqual(SplitTestFields.no_partition_selected['value'], partitions[0]['value'])
self.assertEqual(0, partitions[1]['value'])
self.assertEqual("first_partition", partitions[1]['display_name'])
def test_active_and_inactive_children(self): def test_active_and_inactive_children(self):
""" """
Tests the active and inactive children returned for different split test configurations. Tests the active and inactive children returned for different split test configurations.
...@@ -304,20 +318,27 @@ class SplitTestModuleStudioTest(SplitTestModuleTest): ...@@ -304,20 +318,27 @@ 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):
""" """
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(unicode(message), expected_message)
self.assertEqual(message.message_type, expected_message_type) self.assertEqual(message.message_type, expected_message_type)
self.assertEqual(message.action_class, expected_action_class)
self.assertEqual(message.action_label, expected_action_label)
# 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() messages = split_test_module.validation_messages()
self.assertEqual(len(messages), 1) self.assertEqual(len(messages), 1)
verify_validation_message(messages[0], verify_validation_message(
u"You must select a group configuration for this content experiment.", messages[0],
ValidationMessageType.warning) u"The experiment is not associated with a group configuration.",
ValidationMessageType.warning,
'edit-button',
u"Select a Group Configuration",
)
# 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
...@@ -334,11 +355,13 @@ class SplitTestModuleStudioTest(SplitTestModuleTest): ...@@ -334,11 +355,13 @@ class SplitTestModuleStudioTest(SplitTestModuleTest):
] ]
messages = split_test_module.validation_messages() messages = split_test_module.validation_messages()
self.assertEqual(len(messages), 1) self.assertEqual(len(messages), 1)
verify_validation_message(messages[0], verify_validation_message(
u"This content experiment is missing groups that are defined in " messages[0],
u"the current configuration. " u"The experiment does not contain all of the groups in the configuration.",
u"You can press the 'Create Missing Groups' button to create them.", ValidationMessageType.error,
ValidationMessageType.error) 'add-missing-groups-button',
u"Add Missing Groups"
)
# 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 = [
...@@ -347,11 +370,11 @@ class SplitTestModuleStudioTest(SplitTestModuleTest): ...@@ -347,11 +370,11 @@ class SplitTestModuleStudioTest(SplitTestModuleTest):
] ]
messages = split_test_module.validation_messages() messages = split_test_module.validation_messages()
self.assertEqual(len(messages), 1) self.assertEqual(len(messages), 1)
verify_validation_message(messages[0], verify_validation_message(
u"This content experiment has children that are not associated with the " messages[0],
u"selected group configuration. " u"The experiment has an inactive group. Move content into active groups, then delete the inactive group.",
u"You can move content into an active group or delete it if it is unneeded.", ValidationMessageType.warning
ValidationMessageType.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 = [
...@@ -360,23 +383,26 @@ class SplitTestModuleStudioTest(SplitTestModuleTest): ...@@ -360,23 +383,26 @@ class SplitTestModuleStudioTest(SplitTestModuleTest):
] ]
messages = split_test_module.validation_messages() messages = split_test_module.validation_messages()
self.assertEqual(len(messages), 2) self.assertEqual(len(messages), 2)
verify_validation_message(messages[0], verify_validation_message(
u"This content experiment is missing groups that are defined in " messages[0],
u"the current configuration. " u"The experiment does not contain all of the groups in the configuration.",
u"You can press the 'Create Missing Groups' button to create them.", ValidationMessageType.error,
ValidationMessageType.error) 'add-missing-groups-button',
verify_validation_message(messages[1], u"Add Missing Groups"
u"This content experiment has children that are not associated with the " )
u"selected group configuration. " verify_validation_message(
u"You can move content into an active group or delete it if it is unneeded.", messages[1],
ValidationMessageType.warning) u"The experiment has an inactive group. Move content into active groups, then delete the inactive group.",
ValidationMessageType.warning
)
# 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() messages = split_test_module.validation_messages()
self.assertEqual(len(messages), 1) self.assertEqual(len(messages), 1)
verify_validation_message(messages[0], verify_validation_message(
u"This content experiment will not be shown to students because it refers " messages[0],
u"to a group configuration that has been deleted. " u"The experiment uses a deleted group configuration. "
u"You can delete this experiment or reinstate the group configuration to repair it.", u"Select a valid group configuration or delete this experiment.",
ValidationMessageType.error) ValidationMessageType.error
)
...@@ -35,15 +35,17 @@ XMODULE_METRIC_NAME = 'edxapp.xmodule' ...@@ -35,15 +35,17 @@ XMODULE_METRIC_NAME = 'edxapp.xmodule'
# xblock view names # xblock view names
# This is the view that will be rendered to display the XBlock in the LMS. # This is the view that will be rendered to display the XBlock in the LMS.
# It will also be used to render the block in "preview" mode in Studio, unless
# the XBlock also implements author_view.
STUDENT_VIEW = 'student_view' STUDENT_VIEW = 'student_view'
# An optional view of the xblock similar to student_view, but with possible inline # An optional view of the XBlock similar to student_view, but with possible inline
# editing capabilities. This view differs from studio_view in that it should be as similar to student_view # editing capabilities. This view differs from studio_view in that it should be as similar to student_view
# as possible. When previewing xblocks within Studio, Studio will prefer author_view to student_view. # as possible. When previewing XBlocks within Studio, Studio will prefer author_view to student_view.
AUTHOR_VIEW = 'author_view' AUTHOR_VIEW = 'author_view'
# The view used to render an editor in Studio. The editor rendering can be completely different # The view used to render an editor in Studio. The editor rendering can be completely different
# from the LMS student_view, and it is only shown with the author selects "Edit". # from the LMS student_view, and it is only shown when the author selects "Edit".
STUDIO_VIEW = 'studio_view' STUDIO_VIEW = 'studio_view'
# Views that present a "preview" view of an xblock (as opposed to an editing view). # Views that present a "preview" view of an xblock (as opposed to an editing view).
......
...@@ -40,8 +40,9 @@ Class Features ...@@ -40,8 +40,9 @@ Class Features
These are class attributes or functions that can be provided by an XBlock to customize behaviour These are class attributes or functions that can be provided by an XBlock to customize behaviour
in the LMS. in the LMS.
* student_view (XBlock view): This is the view that will be rendered to display * student_view (XBlock view): This is the view that will be rendered to display the XBlock
the XBlock in the LMS. in the LMS. It will also be used to render the block in "preview" mode in Studio, unless
the XBlock also implements author_view.
* has_score (class property): True if this block should appear in the LMS progress page. * has_score (class property): True if this block should appear in the LMS progress page.
* get_progress (method): See documentation in x_module.py:XModuleMixin.get_progress. * get_progress (method): See documentation in x_module.py:XModuleMixin.get_progress.
* icon_class (class property): This can be one of (``other``, ``video``, or ``problem``), and * icon_class (class property): This can be one of (``other``, ``video``, or ``problem``), and
...@@ -78,10 +79,10 @@ Class Features ...@@ -78,10 +79,10 @@ Class Features
~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~
* studio_view (XBlock.view): The view used to render an editor in Studio. The editor rendering can * studio_view (XBlock.view): The view used to render an editor in Studio. The editor rendering can
be completely different from the LMS student_view, and it is only shown with the author selects "Edit". be completely different from the LMS student_view, and it is only shown when the author selects "Edit".
* author_view (XBlock.view): An optional view of the xblock similar to student_view, but with possible inline * author_view (XBlock.view): An optional view of the XBlock similar to student_view, but with possible inline
editing capabilities. This view differs from studio_view in that it should be as similar to student_view editing capabilities. This view differs from studio_view in that it should be as similar to student_view
as possible. When previewing xblocks within Studio, Studio will prefer author_view to student_view. as possible. When previewing XBlocks within Studio, Studio will prefer author_view to student_view.
* non_editable_metadata_fields (property): A list of :class:`~xblock.fields.Field` objects that * non_editable_metadata_fields (property): A list of :class:`~xblock.fields.Field` objects that
shouldn't be displayed in the default editing view for Studio. shouldn't be displayed in the default editing view for Studio.
......
...@@ -7,83 +7,73 @@ user_partition = split_test.descriptor.get_selected_partition() ...@@ -7,83 +7,73 @@ user_partition = split_test.descriptor.get_selected_partition()
messages = split_test.descriptor.validation_messages() messages = split_test.descriptor.validation_messages()
%> %>
% if is_root and not user_partition: % if is_root and not is_configured:
<div class="no-container-content"> <div class="no-container-content">
% else: % else:
<div class="wrapper-xblock-message"> <div class="wrapper-xblock-message">
% endif % endif
% if not user_partition: % if user_partition:
<div class="xblock-message error"> <div class="xblock-message information">
<p> <p>
<i class="icon-exclamation-sign"></i> <span class="message-text">
${_("You must select a group configuration for this content experiment.")} ${_("This content experiment uses group configuration '{experiment_name}'.").format(experiment_name=user_partition.name)}
<a href="#" class="button edit-button action-button"> </span>
<i class="icon-pencil"></i> <span class="action-button-text">${_("Select a Group Configuration")}</span>
</a>
</p> </p>
</div> </div>
% else: % endif
% if is_root or len(messages) == 0: % if len(messages) > 0:
<div class="xblock-message information"> <%
<p> def get_validation_icon(validation_type):
<span class="message-text"> if validation_type == 'error':
${_("This content experiment uses group configuration '{experiment_name}'.").format(experiment_name=user_partition.name)} return 'icon-exclamation-sign'
</span> elif validation_type == 'warning':
</p> return 'icon-warning-sign'
</div> return None
error_messages = (message for message in messages if message.message_type==ValidationMessageType.error)
has_errors = next(error_messages, False)
aggregate_validation_class = 'has-errors' if has_errors else 'has-warnings'
aggregate_validation_type = 'error' if has_errors else 'warning'
%>
<div class="xblock-message validation ${aggregate_validation_class}">
% if is_configured:
<p class="${aggregate_validation_type}"><i class="${get_validation_icon(aggregate_validation_type)}"></i>
${_("This content experiment has issues that affect content visibility.")}
</p>
% endif % endif
% if is_root: % if is_root or not is_configured:
<ul> <ul class="xblock-message-list">
% for message in messages: % for message in messages:
<% <%
message_type = message.message_type message_type = message.message_type
message_type_display_name = ValidationMessageType.display_name(message_type) if message_type else None message_type_display_name = ValidationMessageType.display_name(message_type) if message_type else None
%> %>
<li class="xblock-message ${message_type}"> <li class="xblock-message-item ${message_type}">
% if message_type == 'warning': % if not is_configured:
<i class="icon-warning-sign"></i> <i class="${get_validation_icon(message_type)}"></i>
% elif message_type == 'error':
<i class="icon-exclamation-sign"></i>
% endif % endif
<span class="message-text"> <span class="message-text">
% if message_type_display_name: % if message_type_display_name:
<span class="sr">${message_type_display_name}:</span> <span class="sr">${message_type_display_name}:</span>
% endif % endif
${unicode(message)} ${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> </span>
</li> </li>
% endfor % endfor
</ul> </ul>
% elif len(messages) > 0:
<%
error_messages = (message for message in messages if message.message_type==ValidationMessageType.error)
%>
% if next(error_messages, False):
<div class="xblock-message error">
<i class="icon-exclamation-sign"></i>
${_("This content experiment has errors that should be resolved.")}
</div>
% else:
<div class="xblock-message error">
<i class="icon-warning-sign"></i>
${_("This content experiment has warnings that might need to be investigated.")}
</div>
% endif
% endif % endif
% if is_missing_groups:
<a href="#" class="button add-missing-groups-button action-button">
<span class="action-button-text">${_("Create Missing Groups")}</span>
</a>
% endif
% endif
% if is_root and not user_partition:
</div>
% else:
</div> </div>
% endif % endif
</div>
% if is_root: % if is_root:
<div class="wrapper-groups is-active"> <div class="wrapper-groups is-active">
<h3 class="sr">${_("Active Groups")}</h3> <h3 class="sr">${_("Active Groups")}</h3>
......
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