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 ConditionalFields(object): has_children = True display_name = String(display_name=_("Display Name"), help=_("The display name for this component."), 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( 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 ReferenceTestXBlock(XBlock, XModuleMixin): """ Test xblock type to test the reference field types """ has_children = True reference_link = Reference(default=None, scope=Scope.content) reference_list = ReferenceList(scope=Scope.content) reference_dict = ReferenceValueDict(scope=Scope.settings)
def __new__(mcs, name, bases, attrs): if (attrs.get('has_children', False) or any(getattr(base, 'has_children', False) for base in bases)): attrs['children'] = ReferenceList( help='The ids of the children of this XBlock', scope=Scope.children) else: attrs['has_children'] = False return super(ChildrenModelMetaclass, mcs).__new__(mcs, name, bases, attrs)
class ConditionalFields(object): has_children = True display_name = String(display_name=_("Display Name"), help=_("Display name for this module"), 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( help=_("List of sources upon which this module is conditional"), scope=Scope.content) condional_attr = String( help=_("Tag attribute in xml"), 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(help=_("Value xml_attr in xml"), scope=Scope.content, default='True') conditional_message = String( display_name=_("Message"), help=_( "Message for case, where one or more are not passed. " "Here you can use variable {link}, which generate link to required module." ), scope=Scope.content, default=_('{link} must be attempted before this will become visible.'))
class ConditionalBlock( SequenceMixin, MakoTemplateBlockBase, XmlMixin, XModuleDescriptorToXBlockMixin, XModuleToXBlockMixin, HTMLSnippet, ResourceTemplates, XModuleMixin, StudioEditableBlock, ): """ Blocks child blocks from showing unless certain conditions are met. Example: <conditional sources="i4x://.../problem_1; i4x://.../problem_2" completed="True"> <show sources="i4x://.../test_6; i4x://.../Avi_resources"/> <video url_name="secret_video" /> </conditional> <conditional> tag attributes: sources - location id of required modules, separated by ';' submitted - map to `is_submitted` module method. (pressing RESET button makes this function to return False.) attempted - map to `is_attempted` module method correct - map to `is_correct` module method poll_answer - map to `poll_answer` module attribute voted - map to `voted` module attribute <show> tag attributes: sources - location id of required modules, separated by ';' You can add you own rules for <conditional> tag, like "completed", "attempted" etc. To do that yo must extend `ConditionalBlock.conditions_map` variable and add pair: my_attr: my_property/my_method After that you can use it: <conditional my_attr="some value" ...> ... </conditional> And my_property/my_method will be called for required modules. """ display_name = String(display_name=_("Display Name"), help=_("The display name for this component."), 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 ConditionalBlock.conditions_map]) 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( 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.')) has_children = True _tag_name = 'conditional' resources_dir = None filename_extension = "xml" has_score = False show_in_read_only_mode = True preview_view_js = { 'js': [ resource_string(__name__, 'js/src/conditional/display.js'), resource_string(__name__, 'js/src/javascript_loader.js'), resource_string(__name__, 'js/src/collapsible.js'), ], 'xmodule_js': resource_string(__name__, 'js/src/xmodule.js'), } preview_view_css = { 'scss': [], } mako_template = 'widgets/metadata-edit.html' studio_js_module_name = 'SequenceDescriptor' studio_view_js = { 'js': [resource_string(__name__, 'js/src/sequence/edit.js')], 'xmodule_js': resource_string(__name__, 'js/src/xmodule.js'), } studio_view_css = { 'scss': [], } # Map # key: <tag attribute in xml> # value: <name of module attribute> conditions_map = { 'poll_answer': 'poll_answer', # poll_question attr # problem was submitted (it can be wrong) # if student will press reset button after that, # state will be reverted 'submitted': 'is_submitted', # capa_problem attr # if student attempted problem 'attempted': 'is_attempted', # capa_problem attr # if problem is full points 'correct': 'is_correct', 'voted': 'voted' # poll_question attr } def __init__(self, *args, **kwargs): """ Create an instance of the Conditional XBlock. """ super(ConditionalBlock, 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: if 'sources' in self.xml_attributes and isinstance( self.xml_attributes['sources'], six.string_types): self.sources_list = [ # TODO: it is not clear why we are replacing the run here (which actually is a no-op # for old-style course locators. However, this is the implementation of # CourseLocator.make_usage_key_from_deprecated_string, which was previously # being called in this location. BlockUsageLocator.from_string(item).replace( run=self.location.course_key.run) for item in ConditionalBlock.parse_sources(self.xml_attributes) ] def is_condition_satisfied(self): attr_name = self.conditions_map[self.conditional_attr] if self.conditional_value and self.get_required_blocks: for module in self.get_required_blocks: if not hasattr(module, attr_name): # We don't throw an exception here because it is possible for # the descriptor of a required module to have a property but # for the resulting module to be a (flavor of) ErrorModule. # So just log and return false. if module is not None: # We do not want to log when module is None, and it is when requester # does not have access to the requested required module. log.warning('Error in conditional module: \ required module {module} has no {module_attr}'. format(module=module, module_attr=attr_name)) return False attr = getattr(module, attr_name) if callable(attr): attr = attr() if self.conditional_value != str(attr): break else: return True return False def student_view(self, _context): """ Renders the student view. """ fragment = Fragment() fragment.add_content(self.get_html()) add_webpack_to_fragment(fragment, 'ConditionalBlockPreview') shim_xmodule_js(fragment, 'Conditional') return fragment def get_html(self): required_html_ids = [ descriptor.location.html_id() for descriptor in self.get_required_blocks ] return self.system.render_template( 'conditional_ajax.html', { 'element_id': self.location.html_id(), 'ajax_url': self.ajax_url, 'depends': ';'.join(required_html_ids) }) def author_view(self, context): """ 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: # 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 studio_view(self, _context): """ Return the studio view. """ fragment = Fragment( self.system.render_template(self.mako_template, self.get_context())) add_webpack_to_fragment(fragment, 'ConditionalBlockStudio') shim_xmodule_js(fragment, self.studio_js_module_name) 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(): context = {'module': self, 'message': self.conditional_message} html = self.system.render_template('conditional_module.html', context) return json.dumps({ 'fragments': [{ 'content': html }], 'message': bool(self.conditional_message) }) fragments = [ child.render(STUDENT_VIEW).to_dict() for child in self.get_display_items() ] return json.dumps({'fragments': fragments}) def get_icon_class(self): new_class = 'other' # HACK: This shouldn't be hard-coded to two types # OBSOLETE: This obsoletes 'type' class_priority = ['video', 'problem'] child_classes = [ child_descriptor.get_icon_class() for child_descriptor in self.get_children() ] for c in class_priority: if c in child_classes: new_class = c return new_class @staticmethod def parse_sources(xml_element): """ Parse xml_element 'sources' attr and return a list of location strings. """ sources = xml_element.get('sources') if sources: return [location.strip() for location in sources.split(';')] @lazy def get_required_blocks(self): """ Returns a list of bound XBlocks instances upon which XBlock depends. """ return [ self.system.get_module(descriptor) for descriptor in self.get_required_module_descriptors() ] def get_required_module_descriptors(self): """ Returns a list of unbound XBlocks instances upon which this XBlock depends. """ descriptors = [] for location in self.sources_list: try: descriptor = self.system.load_item(location) descriptors.append(descriptor) except ItemNotFoundError: msg = "Invalid module by location." log.exception(msg) self.system.error_tracker(msg) return descriptors @classmethod def definition_from_xml(cls, xml_object, system): children = [] show_tag_list = [] definition = {} for conditional_attr in six.iterkeys(cls.conditions_map): 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 = cls.parse_sources(child) for location in locations: children.append(location) show_tag_list.append(location) else: try: descriptor = system.process_xml( etree.tostring(child, encoding='unicode')) children.append(descriptor.scope_ids.usage_id) except: msg = "Unable to load child when parsing Conditional." log.exception(msg) system.error_tracker(msg) 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) for child in self.get_children(): if child.location not in self.show_tag_list: self.runtime.add_block_as_child_node(child, xml_object) if self.show_tag_list: show_str = HTML(u'<show sources="{sources}" />').format( sources=Text(';'.join( text_type(location) for location in self.show_tag_list))) xml_object.append(etree.fromstring(show_str)) # Overwrite the original sources attribute with the value from sources_list, as # Locations may have been changed to Locators. stringified_sources_list = [ text_type(loc) for loc in 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(ConditionalBlock, 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." ), 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(ConditionalBlock, self).non_editable_metadata_fields non_editable_fields.extend([ ConditionalBlock.due, ConditionalBlock.show_tag_list, ]) return non_editable_fields