-
Notifications
You must be signed in to change notification settings - Fork 4.2k
Add ui in studio for conditional_module. #11710
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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"), | ||
| 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( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
| 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. | ||
|
|
||
|
|
@@ -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 | ||
|
|
@@ -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 | ||
|
|
@@ -147,18 +185,31 @@ def get_html(self): | |
| 'depends': ';'.join(self.required_html_ids) | ||
| }) | ||
|
|
||
| def author_view(self, context): | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do you need to also override
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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: | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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()] | ||
|
|
||
|
|
@@ -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' | ||
|
|
||
|
|
@@ -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: | ||
|
|
@@ -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) | ||
|
|
@@ -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) | ||
|
|
@@ -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."), | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @catong any suggestions for this message?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
There was a problem hiding this comment.
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.