Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
158 changes: 131 additions & 27 deletions common/lib/xmodule/xmodule/conditional_module.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,70 @@

from xmodule.x_module import XModule, STUDENT_VIEW
from xmodule.seq_module import SequenceDescriptor
from xblock.fields import Scope, ReferenceList
from xmodule.studio_editable import StudioEditableModule, StudioEditableDescriptor
from xmodule.modulestore.exceptions import ItemNotFoundError
from xmodule.validation import StudioValidation, StudioValidationMessage
from xblock.fields import Scope, ReferenceList, String
from xblock.fragment import Fragment


log = logging.getLogger('edx.' + __name__)

# Make '_' a no-op so we can scrape strings
_ = lambda text: text


class ConditionalFields(object):
has_children = True
show_tag_list = ReferenceList(help="List of urls of children that are references to external modules", scope=Scope.content)
sources_list = ReferenceList(help="List of sources upon which this module is conditional", scope=Scope.content)


class ConditionalModule(ConditionalFields, XModule):
display_name = String(
display_name=_("Display Name"),
help=_("This name appears in the horizontal navigation at the top of the page."),
scope=Scope.settings,
default=_('Conditional')
)

show_tag_list = ReferenceList(
help=_("List of urls of children that are references to external modules"),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@catong could you review the help strings here when you have a chance. Thanks.

scope=Scope.content
)

sources_list = ReferenceList(
display_name=_("Source Components"),
help=_("The component location IDs of all source components that are used to determine whether a learner is "
"shown the content of this conditional module. Copy the component location ID of a component from its "
"Settings dialog in Studio."),
scope=Scope.content
)

conditional_attr = String(
display_name=_("Conditional Attribute"),
help=_("The attribute of the source components that determines whether a learner is shown the content of this "
"conditional module."),
scope=Scope.content,
default='correct',
values=lambda: [{'display_name': xml_attr, 'value': xml_attr}
for xml_attr in ConditionalModule.conditions_map.keys()]
)

conditional_value = String(
display_name=_("Conditional Value"),
help=_("The value that the conditional attribute of the source components must match before a learner is shown "
"the content of this conditional module."),
scope=Scope.content,
default='True'
)

conditional_message = String(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to support a way to localize the messages? It seems odd that the default message is localized, but the author's message is not.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure about a general way of solving this issue. According to the source code of Open edX (https://github.com/edx/edx-platform/blob/master/common/lib/xmodule/xmodule/course_module.py#L421), this string is localised.
Could you please give an advice what should I do in my case.

display_name=_("Blocked Content Message"),
help=_("The message that is shown to learners when not all conditions are met to show the content of this "
"conditional module. Include {link} in the text of your message to give learners a direct link to "
"required units. For example, 'You must complete {link} before you can access this unit'."),
scope=Scope.content,
default=_('You must complete {link} before you can access this unit.')
)


class ConditionalModule(ConditionalFields, XModule, StudioEditableModule):
"""
Blocks child module from showing unless certain conditions are met.

Expand Down Expand Up @@ -95,27 +145,15 @@ class ConditionalModule(ConditionalFields, XModule):
'voted': 'voted' # poll_question attr
}

def _get_condition(self):
# Get first valid condition.
for xml_attr, attr_name in self.conditions_map.iteritems():
xml_value = self.descriptor.xml_attributes.get(xml_attr)
if xml_value:
return xml_value, attr_name
raise Exception(
'Error in conditional module: no known conditional found in {!r}'.format(
self.descriptor.xml_attributes.keys()
)
)

@lazy
def required_modules(self):
return [self.system.get_module(descriptor) for
descriptor in self.descriptor.get_required_module_descriptors()]

def is_condition_satisfied(self):
xml_value, attr_name = self._get_condition()
attr_name = self.conditions_map[self.conditional_attr]

if xml_value and self.required_modules:
if self.conditional_value and self.required_modules:
for module in self.required_modules:
if not hasattr(module, attr_name):
# We don't throw an exception here because it is possible for
Expand All @@ -130,7 +168,7 @@ def is_condition_satisfied(self):
if callable(attr):
attr = attr()

if xml_value != str(attr):
if self.conditional_value != str(attr):
break
else:
return True
Expand All @@ -147,18 +185,31 @@ def get_html(self):
'depends': ';'.join(self.required_html_ids)
})

def author_view(self, context):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you need to also override has_author_view to have this take effect?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has_author_view has value = true by default because it is identified in class https://github.com/edx/edx-platform/blob/master/common/lib/xmodule/xmodule/studio_editable.py#L13

"""
Renders the Studio preview by rendering each child so that they can all be seen and edited.
"""
fragment = Fragment()
root_xblock = context.get('root_xblock')
is_root = root_xblock and root_xblock.location == self.location
if is_root:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this render something if the block is not a root? It seems odd that it would just return an empty fragment.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see that this is a common convention, so the block just shows its message. Maybe that warrants a comment to make this clear.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We will add comments.

# User has clicked the "View" link. Show a preview of all possible children:
self.render_children(context, fragment, can_reorder=True, can_add=True)
# else: When shown on a unit page, don't show any sort of preview -
# just the status of this block in the validation area.

return fragment

def handle_ajax(self, _dispatch, _data):
"""This is called by courseware.moduleodule_render, to handle
an AJAX call.
"""
if not self.is_condition_satisfied():
defmsg = "{link} must be attempted before this will become visible."
message = self.descriptor.xml_attributes.get('message', defmsg)
context = {'module': self,
'message': message}
'message': self.conditional_message}
html = self.system.render_template('conditional_module.html',
context)
return json.dumps({'html': [html], 'message': bool(message)})
return json.dumps({'html': [html], 'message': bool(self.conditional_message)})

html = [child.render(STUDENT_VIEW).content for child in self.get_display_items()]

Expand All @@ -177,8 +228,16 @@ def get_icon_class(self):
new_class = c
return new_class

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

class ConditionalDescriptor(ConditionalFields, SequenceDescriptor):

class ConditionalDescriptor(ConditionalFields, SequenceDescriptor, StudioEditableDescriptor):
"""Descriptor for conditional xmodule."""
_tag_name = 'conditional'

Expand All @@ -197,6 +256,7 @@ def __init__(self, *args, **kwargs):
Create an instance of the conditional module.
"""
super(ConditionalDescriptor, self).__init__(*args, **kwargs)

# Convert sources xml_attribute to a ReferenceList field type so Location/Locator
# substitution can be done.
if not self.sources_list:
Expand Down Expand Up @@ -233,6 +293,14 @@ def get_required_module_descriptors(self):
def definition_from_xml(cls, xml_object, system):
children = []
show_tag_list = []
definition = {}
for conditional_attr in ConditionalModule.conditions_map.iterkeys():
conditional_value = xml_object.get(conditional_attr)
if conditional_value is not None:
definition.update({
'conditional_attr': conditional_attr,
'conditional_value': str(conditional_value),
})
for child in xml_object:
if child.tag == 'show':
locations = ConditionalDescriptor.parse_sources(child)
Expand All @@ -247,7 +315,11 @@ def definition_from_xml(cls, xml_object, system):
msg = "Unable to load child when parsing Conditional."
log.exception(msg)
system.error_tracker(msg)
return {'show_tag_list': show_tag_list}, children
definition.update({
'show_tag_list': show_tag_list,
'conditional_message': xml_object.get('message', '')
})
return definition, children

def definition_to_xml(self, resource_fs):
xml_object = etree.Element(self._tag_name)
Expand All @@ -264,4 +336,36 @@ def definition_to_xml(self, resource_fs):
# Locations may have been changed to Locators.
stringified_sources_list = map(lambda loc: loc.to_deprecated_string(), self.sources_list)
self.xml_attributes['sources'] = ';'.join(stringified_sources_list)
self.xml_attributes[self.conditional_attr] = self.conditional_value
self.xml_attributes['message'] = self.conditional_message
return xml_object

def validate(self):
validation = super(ConditionalDescriptor, self).validate()
if not self.sources_list:
conditional_validation = StudioValidation(self.location)
conditional_validation.add(
StudioValidationMessage(
StudioValidationMessage.NOT_CONFIGURED,
_(u"This component has no source components configured yet."),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@catong any suggestions for this message?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm fine with this existing message. I think it's consistent with others in the platform, e.g. content libraries.

action_class='edit-button',
action_label=_(u"Configure list of sources")
)
)
validation = StudioValidation.copy(validation)
validation.summary = conditional_validation.messages[0]
return validation

@property
def non_editable_metadata_fields(self):
non_editable_fields = super(ConditionalDescriptor, self).non_editable_metadata_fields
non_editable_fields.extend([
ConditionalDescriptor.due,
ConditionalDescriptor.is_practice_exam,
ConditionalDescriptor.is_proctored_enabled,
ConditionalDescriptor.is_time_limited,
ConditionalDescriptor.default_time_limit_minutes,
ConditionalDescriptor.show_tag_list,
ConditionalDescriptor.exam_review_rules,
])
return non_editable_fields
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ class @Conditional
return

@url = @el.data('url')
@render(element)
if @url
@render(element)

render: (element) ->
$.postWithPrefix "#{@url}/conditional_get", (response) =>
Expand Down
Loading