def _xmodule(self): """ Returns the XModule corresponding to this descriptor. Expects that the system already supports all of the attributes needed by xmodules """ assert self.xmodule_runtime is not None assert self.xmodule_runtime.error_descriptor_class is not None if self.xmodule_runtime.xmodule_instance is None: try: self.xmodule_runtime.construct_xblock_from_class( self.module_class, descriptor=self, scope_ids=self.scope_ids, field_data=self._field_data, ) self.xmodule_runtime.xmodule_instance.save() except Exception: # pylint: disable=broad-except # xmodule_instance is set by the XModule.__init__. If we had an error after that, # we need to clean it out so that we can set up the ErrorModule instead self.xmodule_runtime.xmodule_instance = None if isinstance(self, self.xmodule_runtime.error_descriptor_class): log.exception( 'Error creating an ErrorModule from an ErrorDescriptor' ) raise log.exception('Error creating xmodule') descriptor = self.xmodule_runtime.error_descriptor_class.from_descriptor( self, error_msg=exc_info_to_str(sys.exc_info())) descriptor.xmodule_runtime = self.xmodule_runtime self.xmodule_runtime.xmodule_instance = descriptor._xmodule # pylint: disable=protected-access return self.xmodule_runtime.xmodule_instance
def from_xml(cls, xml_data, system, id_generator, # pylint: disable=arguments-differ error_msg=None): '''Create an instance of this descriptor from the supplied data. Does not require that xml_data be parseable--just stores it and exports as-is if not. Takes an extra, optional, parameter--the error that caused an issue. (should be a string, or convert usefully into one). ''' try: # If this is already an error tag, don't want to re-wrap it. xml_obj = etree.fromstring(xml_data) if xml_obj.tag == 'error': xml_data = xml_obj.text error_node = xml_obj.find('error_msg') if error_node is not None: error_msg = error_node.text else: error_msg = None except etree.XMLSyntaxError: # Save the error to display later--overrides other problems error_msg = exc_info_to_str(sys.exc_info()) return cls._construct(system, xml_data, error_msg, location=id_generator.create_definition('error'))
def _xmodule(self): """ Returns the XModule corresponding to this descriptor. Expects that the system already supports all of the attributes needed by xmodules """ assert self.xmodule_runtime is not None assert self.xmodule_runtime.error_descriptor_class is not None if self.xmodule_runtime.xmodule_instance is None: try: self.xmodule_runtime.construct_xblock_from_class( self.module_class, descriptor=self, scope_ids=self.scope_ids, field_data=self._field_data, ) self.xmodule_runtime.xmodule_instance.save() except Exception: # pylint: disable=broad-except # xmodule_instance is set by the XModule.__init__. If we had an error after that, # we need to clean it out so that we can set up the ErrorModule instead self.xmodule_runtime.xmodule_instance = None if isinstance(self, self.xmodule_runtime.error_descriptor_class): log.exception('Error creating an ErrorModule from an ErrorDescriptor') raise log.exception('Error creating xmodule') descriptor = self.xmodule_runtime.error_descriptor_class.from_descriptor( self, error_msg=exc_info_to_str(sys.exc_info()) ) descriptor.xmodule_runtime = self.xmodule_runtime self.xmodule_runtime.xmodule_instance = descriptor._xmodule # pylint: disable=protected-access return self.xmodule_runtime.xmodule_instance
def from_xml(cls, xml_data, system, org=None, course=None, error_msg="Error not available"): """Create an instance of this descriptor from the supplied data. Does not require that xml_data be parseable--just stores it and exports as-is if not. Takes an extra, optional, parameter--the error that caused an issue. (should be a string, or convert usefully into one). """ try: # If this is already an error tag, don't want to re-wrap it. xml_obj = etree.fromstring(xml_data) if xml_obj.tag == "error": xml_data = xml_obj.text error_node = xml_obj.find("error_msg") if error_node is not None: error_msg = error_node.text else: error_msg = "Error not available" except etree.XMLSyntaxError: # Save the error to display later--overrides other problems error_msg = exc_info_to_str(sys.exc_info()) return cls._construct(system, xml_data, error_msg, location=Location("i4x", org, course, None, None))
def from_xml(cls, xml_data, system, id_generator, error_msg='Error not available'): '''Create an instance of this descriptor from the supplied data. Does not require that xml_data be parseable--just stores it and exports as-is if not. Takes an extra, optional, parameter--the error that caused an issue. (should be a string, or convert usefully into one). ''' try: # If this is already an error tag, don't want to re-wrap it. xml_obj = etree.fromstring(xml_data) if xml_obj.tag == 'error': xml_data = xml_obj.text error_node = xml_obj.find('error_msg') if error_node is not None: error_msg = error_node.text else: error_msg = 'Error not available' except etree.XMLSyntaxError: # Save the error to display later--overrides other problems error_msg = exc_info_to_str(sys.exc_info()) return cls._construct(system, xml_data, error_msg, location=id_generator.create_definition('error'))
def load_preview_module(request, preview_id, descriptor): """ Return a preview XModule instantiated from the supplied descriptor. request: The active django request preview_id (str): An identifier specifying which preview this module is used for descriptor: An XModuleDescriptor """ system = preview_module_system(request, preview_id, descriptor) try: module = descriptor.xmodule(system) except: log.debug("Unable to load preview module", exc_info=True) module = ErrorDescriptor.from_descriptor(descriptor, error_msg=exc_info_to_str(sys.exc_info())).xmodule(system) # cdodge: Special case if module.location.category == "static_tab": module.get_html = wrap_xmodule(module.get_html, module, "xmodule_tab_display.html") else: module.get_html = wrap_xmodule(module.get_html, module, "xmodule_display.html") # we pass a partially bogus course_id as we don't have the RUN information passed yet # through the CMS. Also the contentstore is also not RUN-aware at this point in time. module.get_html = replace_static_urls( module.get_html, getattr(module, "data_dir", module.location.course), course_id=module.location.org + "/" + module.location.course + "/BOGUS_RUN_REPLACE_WHEN_AVAILABLE", ) module.get_html = save_module(module.get_html, module) return module
def xblock_from_json(self, class_, usage_id, json_data, course_entry_override=None): if course_entry_override is None: course_entry_override = self.course_entry # most likely a lazy loader or the id directly definition = json_data.get('definition', {}) definition_id = self.modulestore.definition_locator(definition) # If no usage id is provided, generate an in-memory id if usage_id is None: usage_id = LocalId() block_locator = BlockUsageLocator( version_guid=course_entry_override['_id'], usage_id=usage_id, course_id=course_entry_override.get('course_id'), branch=course_entry_override.get('branch')) kvs = SplitMongoKVS( definition, json_data.get('fields', {}), json_data.get('_inherited_settings'), ) field_data = DbModel(kvs) try: module = self.construct_xblock_from_class( class_, field_data, ScopeIds(None, json_data.get('category'), definition_id, block_locator)) except Exception: log.warning("Failed to load descriptor", exc_info=True) return ErrorDescriptor.from_json( json_data, self, BlockUsageLocator(version_guid=course_entry_override['_id'], usage_id=usage_id), error_msg=exc_info_to_str(sys.exc_info())) edit_info = json_data.get('edit_info', {}) module.edited_by = edit_info.get('edited_by') module.edited_on = edit_info.get('edited_on') module.previous_version = edit_info.get('previous_version') module.update_version = edit_info.get('update_version') module.definition_locator = self.modulestore.definition_locator( definition) # decache any pending field settings module.save() # If this is an in-memory block, store it in this system if isinstance(block_locator.usage_id, LocalId): self.local_modules[block_locator] = module return module
def xblock_from_json(self, class_, usage_id, json_data, course_entry_override=None): if course_entry_override is None: course_entry_override = self.course_entry # most likely a lazy loader or the id directly definition = json_data.get('definition', {}) definition_id = self.modulestore.definition_locator(definition) # If no usage id is provided, generate an in-memory id if usage_id is None: usage_id = LocalId() block_locator = BlockUsageLocator( version_guid=course_entry_override['_id'], usage_id=usage_id, course_id=course_entry_override.get('course_id'), branch=course_entry_override.get('branch') ) kvs = SplitMongoKVS( definition, json_data.get('fields', {}), json_data.get('_inherited_settings'), ) field_data = DbModel(kvs) try: module = self.construct_xblock_from_class( class_, field_data, ScopeIds(None, json_data.get('category'), definition_id, block_locator) ) except Exception: log.warning("Failed to load descriptor", exc_info=True) return ErrorDescriptor.from_json( json_data, self, BlockUsageLocator( version_guid=course_entry_override['_id'], usage_id=usage_id ), error_msg=exc_info_to_str(sys.exc_info()) ) edit_info = json_data.get('edit_info', {}) module.edited_by = edit_info.get('edited_by') module.edited_on = edit_info.get('edited_on') module.previous_version = edit_info.get('previous_version') module.update_version = edit_info.get('update_version') module.definition_locator = self.modulestore.definition_locator(definition) # decache any pending field settings module.save() # If this is an in-memory block, store it in this system if isinstance(block_locator.usage_id, LocalId): self.local_modules[block_locator] = module return module
def load_item(self, location): """ Return an XModule instance for the specified location """ location = Location(location) json_data = self.module_data.get(location) if json_data is None: module = self.modulestore.get_item(location) if module is not None: # update our own cache after going to the DB to get cache miss self.module_data.update(module.runtime.module_data) return module else: # load the module and apply the inherited metadata try: category = json_data['location']['category'] class_ = XModuleDescriptor.load_class(category, self.default_class) definition = json_data.get('definition', {}) metadata = json_data.get('metadata', {}) for old_name, new_name in getattr(class_, 'metadata_translations', {}).items(): if old_name in metadata: metadata[new_name] = metadata[old_name] del metadata[old_name] kvs = MongoKeyValueStore( definition.get('data', {}), definition.get('children', []), metadata, ) field_data = DbModel(kvs) scope_ids = ScopeIds(None, category, location, location) module = self.construct_xblock_from_class( class_, field_data, scope_ids) if self.cached_metadata is not None: # parent container pointers don't differentiate between draft and non-draft # so when we do the lookup, we should do so with a non-draft location non_draft_loc = location.replace(revision=None) # Convert the serialized fields values in self.cached_metadata # to python values metadata_to_inherit = self.cached_metadata.get( non_draft_loc.url(), {}) inherit_metadata(module, metadata_to_inherit) # decache any computed pending field settings module.save() return module except: log.warning("Failed to load descriptor", exc_info=True) return ErrorDescriptor.from_json(json_data, self, json_data['location'], error_msg=exc_info_to_str( sys.exc_info()))
def load_item(self, location): """ Return an XModule instance for the specified location """ location = Location(location) json_data = self.module_data.get(location) if json_data is None: module = self.modulestore.get_item(location) if module is not None: # update our own cache after going to the DB to get cache miss self.module_data.update(module.runtime.module_data) return module else: # load the module and apply the inherited metadata try: category = json_data['location']['category'] class_ = XModuleDescriptor.load_class( category, self.default_class ) definition = json_data.get('definition', {}) metadata = json_data.get('metadata', {}) for old_name, new_name in getattr(class_, 'metadata_translations', {}).items(): if old_name in metadata: metadata[new_name] = metadata[old_name] del metadata[old_name] kvs = MongoKeyValueStore( definition.get('data', {}), definition.get('children', []), metadata, ) field_data = DbModel(kvs) scope_ids = ScopeIds(None, category, location, location) module = self.construct_xblock_from_class(class_, field_data, scope_ids) if self.cached_metadata is not None: # parent container pointers don't differentiate between draft and non-draft # so when we do the lookup, we should do so with a non-draft location non_draft_loc = location.replace(revision=None) # Convert the serialized fields values in self.cached_metadata # to python values metadata_to_inherit = self.cached_metadata.get(non_draft_loc.url(), {}) inherit_metadata(module, metadata_to_inherit) # decache any computed pending field settings module.save() return module except: log.warning("Failed to load descriptor", exc_info=True) return ErrorDescriptor.from_json( json_data, self, json_data['location'], error_msg=exc_info_to_str(sys.exc_info()) )
def load_item(self, location): """ Return an XModule instance for the specified location """ location = Location(location) json_data = self.module_data.get(location) if json_data is None: module = self.modulestore.get_item(location) if module is not None: # update our own cache after going to the DB to get cache miss self.module_data.update(module.system.module_data) return module else: # load the module and apply the inherited metadata try: category = json_data['location']['category'] class_ = XModuleDescriptor.load_class( category, self.default_class ) definition = json_data.get('definition', {}) metadata = json_data.get('metadata', {}) for old_name, new_name in class_.metadata_translations.items(): if old_name in metadata: metadata[new_name] = metadata[old_name] del metadata[old_name] kvs = MongoKeyValueStore( definition.get('data', {}), definition.get('children', []), metadata, location, category ) model_data = DbModel(kvs, class_, None, MongoUsage(self.course_id, location)) model_data['category'] = category model_data['location'] = location module = class_(self, model_data) if self.cached_metadata is not None: # parent container pointers don't differentiate between draft and non-draft # so when we do the lookup, we should do so with a non-draft location non_draft_loc = location.replace(revision=None) metadata_to_inherit = self.cached_metadata.get(non_draft_loc.url(), {}) inherit_metadata(module, metadata_to_inherit) return module except: log.warning("Failed to load descriptor", exc_info=True) return ErrorDescriptor.from_json( json_data, self, json_data['location'], error_msg=exc_info_to_str(sys.exc_info()) )
def xblock_from_json(self, class_, usage_id, json_data, course_entry_override=None): if course_entry_override is None: course_entry_override = self.course_entry # most likely a lazy loader but not the id directly definition = json_data.get("definition", {}) metadata = json_data.get("metadata", {}) block_locator = BlockUsageLocator( version_guid=course_entry_override["_id"], usage_id=usage_id, course_id=course_entry_override.get("course_id"), branch=course_entry_override.get("branch"), ) kvs = SplitMongoKVS( definition, json_data.get("children", []), metadata, json_data.get("_inherited_metadata"), block_locator, json_data.get("category"), ) model_data = DbModel( kvs, class_, None, SplitMongoKVSid( # DbModel req's that these support .url() block_locator, self.modulestore.definition_locator(definition), ), ) try: module = class_(self, model_data) except Exception: log.warning("Failed to load descriptor", exc_info=True) if usage_id is None: usage_id = "MISSING" return ErrorDescriptor.from_json( json_data, self, BlockUsageLocator(version_guid=course_entry_override["_id"], usage_id=usage_id), error_msg=exc_info_to_str(sys.exc_info()), ) module.edited_by = json_data.get("edited_by") module.edited_on = json_data.get("edited_on") module.previous_version = json_data.get("previous_version") module.update_version = json_data.get("update_version") module.definition_locator = self.modulestore.definition_locator(definition) # decache any pending field settings module.save() return module
def xblock_from_json(self, class_, usage_id, json_data, course_entry_override=None): if course_entry_override is None: course_entry_override = self.course_entry else: # most recent retrieval is most likely the right one for next caller (see comment above fn) self.course_entry["branch"] = course_entry_override["branch"] self.course_entry["course_id"] = course_entry_override["course_id"] # most likely a lazy loader or the id directly definition = json_data.get("definition", {}) definition_id = self.modulestore.definition_locator(definition) # If no usage id is provided, generate an in-memory id if usage_id is None: usage_id = LocalId() block_locator = BlockUsageLocator( version_guid=course_entry_override["structure"]["_id"], usage_id=usage_id, course_id=course_entry_override.get("course_id"), branch=course_entry_override.get("branch"), ) kvs = SplitMongoKVS(definition, json_data.get("fields", {}), json_data.get("_inherited_settings")) field_data = DbModel(kvs) try: module = self.construct_xblock_from_class( class_, ScopeIds(None, json_data.get("category"), definition_id, block_locator), field_data ) except Exception: log.warning("Failed to load descriptor", exc_info=True) return ErrorDescriptor.from_json( json_data, self, BlockUsageLocator(version_guid=course_entry_override["structure"]["_id"], usage_id=usage_id), error_msg=exc_info_to_str(sys.exc_info()), ) edit_info = json_data.get("edit_info", {}) module.edited_by = edit_info.get("edited_by") module.edited_on = edit_info.get("edited_on") module.previous_version = edit_info.get("previous_version") module.update_version = edit_info.get("update_version") module.definition_locator = self.modulestore.definition_locator(definition) # decache any pending field settings module.save() # If this is an in-memory block, store it in this system if isinstance(block_locator.usage_id, LocalId): self.local_modules[block_locator] = module return module
def xblock_from_json(self, class_, usage_id, json_data, course_entry_override=None): if course_entry_override is None: course_entry_override = self.course_entry # most likely a lazy loader or the id directly definition = json_data.get('definition', {}) block_locator = BlockUsageLocator( version_guid=course_entry_override['_id'], usage_id=usage_id, course_id=course_entry_override.get('course_id'), branch=course_entry_override.get('branch')) kvs = SplitMongoKVS(definition, json_data.get('fields', {}), json_data.get('_inherited_settings'), block_locator, json_data.get('category')) model_data = DbModel( kvs, class_, None, SplitMongoKVSid( # DbModel req's that these support .url() block_locator, self.modulestore.definition_locator(definition))) try: module = class_(self, model_data) except Exception: log.warning("Failed to load descriptor", exc_info=True) if usage_id is None: usage_id = "MISSING" return ErrorDescriptor.from_json( json_data, self, BlockUsageLocator(version_guid=course_entry_override['_id'], usage_id=usage_id), error_msg=exc_info_to_str(sys.exc_info())) edit_info = json_data.get('edit_info', {}) module.edited_by = edit_info.get('edited_by') module.edited_on = edit_info.get('edited_on') module.previous_version = edit_info.get('previous_version') module.update_version = edit_info.get('update_version') module.definition_locator = self.modulestore.definition_locator( definition) # decache any pending field settings module.save() return module
def load_item(self, location): """ Return an XModule instance for the specified location """ location = Location(location) json_data = self.module_data.get(location) if json_data is None: module = self.modulestore.get_item(location) if module is not None: # update our own cache after going to the DB to get cache miss self.module_data.update(module.system.module_data) return module else: # load the module and apply the inherited metadata try: class_ = XModuleDescriptor.load_class( json_data['location']['category'], self.default_class ) definition = json_data.get('definition', {}) metadata = json_data.get('metadata', {}) for old_name, new_name in class_.metadata_translations.items(): if old_name in metadata: metadata[new_name] = metadata[old_name] del metadata[old_name] kvs = MongoKeyValueStore( definition.get('data', {}), definition.get('children', []), metadata, ) model_data = DbModel(kvs, class_, None, MongoUsage( self.course_id, location)) module = class_(self, location, model_data) if self.cached_metadata is not None: # parent container pointers don't differentiate between draft and non-draft # so when we do the lookup, we should do so with a non- # draft location non_draft_loc = location._replace(revision=None) metadata_to_inherit = self.cached_metadata.get( non_draft_loc.url(), {}) inherit_metadata(module, metadata_to_inherit) return module except: log.warning("Failed to load descriptor", exc_info=True) return ErrorDescriptor.from_json( json_data, self, error_msg=exc_info_to_str(sys.exc_info()) )
def xblock_from_json(self, class_, usage_id, json_data, course_entry_override=None): if course_entry_override is None: course_entry_override = self.course_entry # most likely a lazy loader or the id directly definition = json_data.get('definition', {}) block_locator = BlockUsageLocator( version_guid=course_entry_override['_id'], usage_id=usage_id, course_id=course_entry_override.get('course_id'), branch=course_entry_override.get('branch') ) kvs = SplitMongoKVS( definition, json_data.get('fields', {}), json_data.get('_inherited_settings'), block_locator, json_data.get('category')) model_data = DbModel(kvs, class_, None, SplitMongoKVSid( # DbModel req's that these support .url() block_locator, self.modulestore.definition_locator(definition))) try: module = class_(self, model_data) except Exception: log.warning("Failed to load descriptor", exc_info=True) if usage_id is None: usage_id = "MISSING" return ErrorDescriptor.from_json( json_data, self, BlockUsageLocator(version_guid=course_entry_override['_id'], usage_id=usage_id), error_msg=exc_info_to_str(sys.exc_info()) ) edit_info = json_data.get('edit_info', {}) module.edited_by = edit_info.get('edited_by') module.edited_on = edit_info.get('edited_on') module.previous_version = edit_info.get('previous_version') module.update_version = edit_info.get('update_version') module.definition_locator = self.modulestore.definition_locator(definition) # decache any pending field settings module.save() return module
def load_preview_module(request, preview_id, descriptor): """ Return a preview XModule instantiated from the supplied descriptor. request: The active django request preview_id (str): An identifier specifying which preview this module is used for descriptor: An XModuleDescriptor """ system = preview_module_system(request, preview_id, descriptor) try: module = descriptor.xmodule(system) except: log.debug("Unable to load preview module", exc_info=True) module = ErrorDescriptor.from_descriptor( descriptor, error_msg=exc_info_to_str(sys.exc_info()) ).xmodule(system) # cdodge: Special case if module.location.category == 'static_tab': module.get_html = wrap_xmodule( module.get_html, module, "xmodule_tab_display.html", ) else: module.get_html = wrap_xmodule( module.get_html, module, "xmodule_display.html", ) # we pass a partially bogus course_id as we don't have the RUN information passed yet # through the CMS. Also the contentstore is also not RUN-aware at this point in time. module.get_html = replace_static_urls( module.get_html, getattr(module, 'data_dir', module.location.course), course_id=module.location.org + '/' + module.location.course + '/BOGUS_RUN_REPLACE_WHEN_AVAILABLE' ) module.get_html = save_module( module.get_html, module ) return module
def load_preview_module(request, preview_id, descriptor): """ Return a preview XModule instantiated from the supplied descriptor. request: The active django request preview_id (str): An identifier specifying which preview this module is used for descriptor: An XModuleDescriptor """ system = preview_module_system(request, preview_id, descriptor) try: module = descriptor.xmodule(system) except: log.debug("Unable to load preview module", exc_info=True) module = ErrorDescriptor.from_descriptor( descriptor, error_msg=exc_info_to_str(sys.exc_info())).xmodule(system) return module
def load_preview_module(request, preview_id, descriptor): """ Return a preview XModule instantiated from the supplied descriptor. request: The active django request preview_id (str): An identifier specifying which preview this module is used for descriptor: An XModuleDescriptor """ system = preview_module_system(request, preview_id, descriptor) try: module = descriptor.xmodule(system) except: log.debug("Unable to load preview module", exc_info=True) module = ErrorDescriptor.from_descriptor( descriptor, error_msg=exc_info_to_str(sys.exc_info()) ).xmodule(system) # cdodge: Special case if module.location.category == 'static_tab': module.get_html = wrap_xmodule( module.get_html, module, "xmodule_tab_display.html", ) else: module.get_html = wrap_xmodule( module.get_html, module, "xmodule_display.html", ) module.get_html = replace_static_urls( module.get_html, getattr(module, 'data_dir', module.location.course), course_namespace=Location([module.location.tag, module.location.org, module.location.course, None, None]) ) module.get_html = save_module( module.get_html, module ) return module
def load_preview_module(request, preview_id, descriptor): """ Return a preview XModule instantiated from the supplied descriptor, instance_state, and shared_state request: The active django request preview_id (str): An identifier specifying which preview this module is used for descriptor: An XModuleDescriptor instance_state: An instance state string shared_state: A shared state string """ system = preview_module_system(request, preview_id, descriptor) try: module = descriptor.xmodule(system) except: log.debug("Unable to load preview module", exc_info=True) module = ErrorDescriptor.from_descriptor( descriptor, error_msg=exc_info_to_str(sys.exc_info())).xmodule(system) # cdodge: Special case if module.location.category == 'static_tab': module.get_html = wrap_xmodule( module.get_html, module, "xmodule_tab_display.html", ) else: module.get_html = wrap_xmodule( module.get_html, module, "xmodule_display.html", ) module.get_html = replace_static_urls(module.get_html, getattr(module, 'data_dir', module.location.course), course_namespace=Location([ module.location.tag, module.location.org, module.location.course, None, None ])) return module
def load_preview_module(request, preview_id, descriptor): """ Return a preview XModule instantiated from the supplied descriptor. request: The active django request preview_id (str): An identifier specifying which preview this module is used for descriptor: An XModuleDescriptor """ system = preview_module_system(request, preview_id, descriptor) try: module = descriptor.xmodule(system) except: log.debug("Unable to load preview module", exc_info=True) module = ErrorDescriptor.from_descriptor( descriptor, error_msg=exc_info_to_str(sys.exc_info()) ).xmodule(system) return module
def _xmodule(self): """ Returns the XModule corresponding to this descriptor. Expects that the system already supports all of the attributes needed by xmodules """ assert self.xmodule_runtime is not None assert self.xmodule_runtime.error_descriptor_class is not None if self.xmodule_runtime.xmodule_instance is None: try: self.xmodule_runtime.xmodule_instance = self.xmodule_runtime.construct_xblock_from_class( self.module_class, descriptor=self, scope_ids=self.scope_ids, field_data=self._field_data, ) self.xmodule_runtime.xmodule_instance.save() except Exception: # pylint: disable=broad-except log.exception('Error creating xmodule') descriptor = self.xmodule_runtime.error_descriptor_class.from_descriptor( self, error_msg=exc_info_to_str(sys.exc_info()) ) self.xmodule_runtime.xmodule_instance = descriptor._xmodule # pylint: disable=protected-access return self.xmodule_runtime.xmodule_instance
def get_module_for_descriptor_internal( user, descriptor, field_data_cache, course_id, track_function, xqueue_callback_url_prefix, position=None, wrap_xmodule_display=True, grade_bucket_type=None, static_asset_path="", ): """ Actually implement get_module, without requiring a request. See get_module() docstring for further details. """ # Short circuit--if the user shouldn't have access, bail without doing any work if not has_access(user, descriptor, "load", course_id): return None # Setup system context for module instance ajax_url = reverse( "modx_dispatch", kwargs=dict(course_id=course_id, location=descriptor.location.url(), dispatch="") ) # Intended use is as {ajax_url}/{dispatch_command}, so get rid of the trailing slash. ajax_url = ajax_url.rstrip("/") def make_xqueue_callback(dispatch="score_update"): # Fully qualified callback URL for external queueing system relative_xqueue_callback_url = reverse( "xqueue_callback", kwargs=dict(course_id=course_id, userid=str(user.id), mod_id=descriptor.location.url(), dispatch=dispatch), ) return xqueue_callback_url_prefix + relative_xqueue_callback_url # Default queuename is course-specific and is derived from the course that # contains the current module. # TODO: Queuename should be derived from 'course_settings.json' of each course xqueue_default_queuename = descriptor.location.org + "-" + descriptor.location.course xqueue = { "interface": xqueue_interface, "construct_callback": make_xqueue_callback, "default_queuename": xqueue_default_queuename.replace(" ", "_"), "waittime": settings.XQUEUE_WAITTIME_BETWEEN_REQUESTS, } # This is a hacky way to pass settings to the combined open ended xmodule # It needs an S3 interface to upload images to S3 # It needs the open ended grading interface in order to get peer grading to be done # this first checks to see if the descriptor is the correct one, and only sends settings if it is # Get descriptor metadata fields indicating needs for various settings needs_open_ended_interface = getattr(descriptor, "needs_open_ended_interface", False) needs_s3_interface = getattr(descriptor, "needs_s3_interface", False) # Initialize interfaces to None open_ended_grading_interface = None s3_interface = None # Create interfaces if needed if needs_open_ended_interface: open_ended_grading_interface = settings.OPEN_ENDED_GRADING_INTERFACE open_ended_grading_interface["mock_peer_grading"] = settings.MOCK_PEER_GRADING open_ended_grading_interface["mock_staff_grading"] = settings.MOCK_STAFF_GRADING if needs_s3_interface: s3_interface = { "access_key": getattr(settings, "AWS_ACCESS_KEY_ID", ""), "secret_access_key": getattr(settings, "AWS_SECRET_ACCESS_KEY", ""), "storage_bucket_name": getattr(settings, "AWS_STORAGE_BUCKET_NAME", ""), } def inner_get_module(descriptor): """ Delegate to get_module_for_descriptor_internal() with all values except `descriptor` set. Because it does an access check, it may return None. """ # TODO: fix this so that make_xqueue_callback uses the descriptor passed into # inner_get_module, not the parent's callback. Add it as an argument.... return get_module_for_descriptor_internal( user, descriptor, field_data_cache, course_id, track_function, make_xqueue_callback, position, wrap_xmodule_display, grade_bucket_type, static_asset_path, ) def xblock_field_data(descriptor): student_data = DbModel(DjangoKeyValueStore(field_data_cache)) return lms_field_data(descriptor._field_data, student_data) def publish(event): """A function that allows XModules to publish events. This only supports grade changes right now.""" if event.get("event_name") != "grade": return # Construct the key for the module key = KeyValueStore.Key( scope=Scope.user_state, student_id=user.id, block_scope_id=descriptor.location, field_name="grade" ) student_module = field_data_cache.find_or_create(key) # Update the grades student_module.grade = event.get("value") student_module.max_grade = event.get("max_value") # Save all changes to the underlying KeyValueStore student_module.save() # Bin score into range and increment stats score_bucket = get_score_bucket(student_module.grade, student_module.max_grade) org, course_num, run = course_id.split("/") tags = [ "org:{0}".format(org), "course:{0}".format(course_num), "run:{0}".format(run), "score_bucket:{0}".format(score_bucket), ] if grade_bucket_type is not None: tags.append("type:%s" % grade_bucket_type) statsd.increment("lms.courseware.question_answered", tags=tags) # TODO (cpennington): When modules are shared between courses, the static # prefix is going to have to be specific to the module, not the directory # that the xml was loaded from system = ModuleSystem( track_function=track_function, render_template=render_to_string, ajax_url=ajax_url, xqueue=xqueue, # TODO (cpennington): Figure out how to share info between systems filestore=descriptor.system.resources_fs, get_module=inner_get_module, user=user, # TODO (cpennington): This should be removed when all html from # a module is coming through get_html and is therefore covered # by the replace_static_urls code below replace_urls=partial( static_replace.replace_static_urls, data_directory=getattr(descriptor, "data_dir", None), course_id=course_id, static_asset_path=static_asset_path or descriptor.static_asset_path, ), replace_course_urls=partial(static_replace.replace_course_urls, course_id=course_id), replace_jump_to_id_urls=partial( static_replace.replace_jump_to_id_urls, course_id=course_id, jump_to_id_base_url=reverse("jump_to_id", kwargs={"course_id": course_id, "module_id": ""}), ), node_path=settings.NODE_PATH, xblock_field_data=xblock_field_data, publish=publish, anonymous_student_id=unique_id_for_user(user), course_id=course_id, open_ended_grading_interface=open_ended_grading_interface, s3_interface=s3_interface, cache=cache, can_execute_unsafe_code=(lambda: can_execute_unsafe_code(course_id)), # TODO: When we merge the descriptor and module systems, we can stop reaching into the mixologist (cpennington) mixins=descriptor.system.mixologist._mixins, ) # pass position specified in URL to module through ModuleSystem system.set("position", position) system.set("DEBUG", settings.DEBUG) if settings.MITX_FEATURES.get("ENABLE_PSYCHOMETRICS"): system.set( "psychometrics_handler", # set callback for updating PsychometricsData make_psychometrics_data_update_handler(course_id, user, descriptor.location.url()), ) try: module = descriptor.xmodule(system) except: log.exception("Error creating module from descriptor {0}".format(descriptor)) # make an ErrorDescriptor -- assuming that the descriptor's system is ok if has_access(user, descriptor.location, "staff", course_id): err_descriptor_class = ErrorDescriptor else: err_descriptor_class = NonStaffErrorDescriptor err_descriptor = err_descriptor_class.from_descriptor(descriptor, error_msg=exc_info_to_str(sys.exc_info())) # Make an error module return err_descriptor.xmodule(system) system.set("user_is_staff", has_access(user, descriptor.location, "staff", course_id)) _get_html = module.get_html if wrap_xmodule_display is True: _get_html = wrap_xmodule(module.get_html, module, "xmodule_display.html") module.get_html = replace_static_urls( _get_html, getattr(descriptor, "data_dir", None), course_id=course_id, static_asset_path=static_asset_path or descriptor.static_asset_path, ) # Allow URLs of the form '/course/' refer to the root of multicourse directory # hierarchy of this course module.get_html = replace_course_urls(module.get_html, course_id) # this will rewrite intra-courseware links # that use the shorthand /jump_to_id/<id>. This is very helpful # for studio authored courses (compared to the /course/... format) since it is # is durable with respect to moves and the author doesn't need to # know the hierarchy # NOTE: module_id is empty string here. The 'module_id' will get assigned in the replacement # function, we just need to specify something to get the reverse() to work module.get_html = replace_jump_to_id_urls( module.get_html, course_id, reverse("jump_to_id", kwargs={"course_id": course_id, "module_id": ""}) ) if settings.MITX_FEATURES.get("DISPLAY_HISTOGRAMS_TO_STAFF"): if has_access(user, module, "staff", course_id): module.get_html = add_histogram(module.get_html, module, user) # force the module to save after rendering module.get_html = save_module(module.get_html, module) return module
def get_module_for_descriptor(user, request, descriptor, model_data_cache, course_id, position=None, wrap_xmodule_display=True, grade_bucket_type=None): """ Actually implement get_module. See docstring there for details. """ # allow course staff to masquerade as student if has_access(user, descriptor, 'staff', course_id): setup_masquerade(request, True) # Short circuit--if the user shouldn't have access, bail without doing any work if not has_access(user, descriptor, 'load', course_id): return None # Setup system context for module instance ajax_url = reverse( 'modx_dispatch', kwargs=dict(course_id=course_id, location=descriptor.location.url(), dispatch=''), ) # Intended use is as {ajax_url}/{dispatch_command}, so get rid of the trailing slash. ajax_url = ajax_url.rstrip('/') def make_xqueue_callback(dispatch='score_update'): # Fully qualified callback URL for external queueing system xqueue_callback_url = '{proto}://{host}'.format( host=request.get_host(), proto=request.META.get('HTTP_X_FORWARDED_PROTO', 'https' if request.is_secure() else 'http')) xqueue_callback_url = settings.XQUEUE_INTERFACE.get( 'callback_url', xqueue_callback_url) # allow override xqueue_callback_url += reverse( 'xqueue_callback', kwargs=dict(course_id=course_id, userid=str(user.id), id=descriptor.location.url(), dispatch=dispatch), ) return xqueue_callback_url # Default queuename is course-specific and is derived from the course that # contains the current module. # TODO: Queuename should be derived from 'course_settings.json' of each course xqueue_default_queuename = descriptor.location.org + '-' + descriptor.location.course xqueue = { 'interface': xqueue_interface, 'construct_callback': make_xqueue_callback, 'default_queuename': xqueue_default_queuename.replace(' ', '_'), 'waittime': settings.XQUEUE_WAITTIME_BETWEEN_REQUESTS } #This is a hacky way to pass settings to the combined open ended xmodule #It needs an S3 interface to upload images to S3 #It needs the open ended grading interface in order to get peer grading to be done #this first checks to see if the descriptor is the correct one, and only sends settings if it is #Get descriptor metadata fields indicating needs for various settings needs_open_ended_interface = getattr(descriptor, "needs_open_ended_interface", False) needs_s3_interface = getattr(descriptor, "needs_s3_interface", False) #Initialize interfaces to None open_ended_grading_interface = None s3_interface = None #Create interfaces if needed if needs_open_ended_interface: open_ended_grading_interface = settings.OPEN_ENDED_GRADING_INTERFACE open_ended_grading_interface[ 'mock_peer_grading'] = settings.MOCK_PEER_GRADING open_ended_grading_interface[ 'mock_staff_grading'] = settings.MOCK_STAFF_GRADING if needs_s3_interface: s3_interface = { 'access_key': getattr(settings, 'AWS_ACCESS_KEY_ID', ''), 'secret_access_key': getattr(settings, 'AWS_SECRET_ACCESS_KEY', ''), 'storage_bucket_name': getattr(settings, 'AWS_STORAGE_BUCKET_NAME', 'openended') } def inner_get_module(descriptor): """ Delegate to get_module. It does an access check, so may return None """ return get_module_for_descriptor(user, request, descriptor, model_data_cache, course_id, position) def xblock_model_data(descriptor): return DbModel( LmsKeyValueStore(descriptor._model_data, model_data_cache), descriptor.module_class, user.id, LmsUsage(descriptor.location, descriptor.location)) def publish(event): if event.get('event_name') != 'grade': return student_module, created = StudentModule.objects.get_or_create( course_id=course_id, student=user, module_type=descriptor.location.category, module_state_key=descriptor.location.url(), defaults={'state': '{}'}, ) student_module.grade = event.get('value') student_module.max_grade = event.get('max_value') student_module.save() #Bin score into range and increment stats score_bucket = get_score_bucket(student_module.grade, student_module.max_grade) org, course_num, run = course_id.split("/") tags = [ "org:{0}".format(org), "course:{0}".format(course_num), "run:{0}".format(run), "score_bucket:{0}".format(score_bucket) ] if grade_bucket_type is not None: tags.append('type:%s' % grade_bucket_type) statsd.increment("lms.courseware.question_answered", tags=tags) def can_execute_unsafe_code(): # To decide if we can run unsafe code, we check the course id against # a list of regexes configured on the server. for regex in settings.COURSES_WITH_UNSAFE_CODE: if re.match(regex, course_id): return True return False # TODO (cpennington): When modules are shared between courses, the static # prefix is going to have to be specific to the module, not the directory # that the xml was loaded from system = ModuleSystem( track_function=make_track_function(request), render_template=render_to_string, ajax_url=ajax_url, xqueue=xqueue, # TODO (cpennington): Figure out how to share info between systems filestore=descriptor.system.resources_fs, get_module=inner_get_module, user=user, # TODO (cpennington): This should be removed when all html from # a module is coming through get_html and is therefore covered # by the replace_static_urls code below replace_urls=partial( static_replace.replace_static_urls, data_directory=getattr(descriptor, 'data_dir', None), course_namespace=descriptor.location._replace(category=None, name=None), ), node_path=settings.NODE_PATH, xblock_model_data=xblock_model_data, publish=publish, anonymous_student_id=unique_id_for_user(user), course_id=course_id, open_ended_grading_interface=open_ended_grading_interface, s3_interface=s3_interface, cache=cache, can_execute_unsafe_code=can_execute_unsafe_code, ) # pass position specified in URL to module through ModuleSystem system.set('position', position) system.set('DEBUG', settings.DEBUG) if settings.MITX_FEATURES.get('ENABLE_PSYCHOMETRICS'): system.set( 'psychometrics_handler', # set callback for updating PsychometricsData make_psychometrics_data_update_handler(course_id, user, descriptor.location.url())) try: module = descriptor.xmodule(system) except: log.exception( "Error creating module from descriptor {0}".format(descriptor)) # make an ErrorDescriptor -- assuming that the descriptor's system is ok if has_access(user, descriptor.location, 'staff', course_id): err_descriptor_class = ErrorDescriptor else: err_descriptor_class = NonStaffErrorDescriptor err_descriptor = err_descriptor_class.from_xml( str(descriptor), descriptor.system, org=descriptor.location.org, course=descriptor.location.course, error_msg=exc_info_to_str(sys.exc_info())) # Make an error module return err_descriptor.xmodule(system) system.set('user_is_staff', has_access(user, descriptor.location, 'staff', course_id)) _get_html = module.get_html if wrap_xmodule_display == True: _get_html = wrap_xmodule(module.get_html, module, 'xmodule_display.html') module.get_html = replace_static_urls( _get_html, getattr(descriptor, 'data_dir', None), course_namespace=module.location._replace(category=None, name=None)) # Allow URLs of the form '/course/' refer to the root of multicourse directory # hierarchy of this course module.get_html = replace_course_urls(module.get_html, course_id) if settings.MITX_FEATURES.get('DISPLAY_HISTOGRAMS_TO_STAFF'): if has_access(user, module, 'staff', course_id): module.get_html = add_histogram(module.get_html, module, user) return module
def load_item(self, location): """ Return an XModule instance for the specified location """ assert isinstance(location, UsageKey) json_data = self.module_data.get(location) if json_data is None: module = self.modulestore.get_item(location) if module is not None: # update our own cache after going to the DB to get cache miss self.module_data.update(module.runtime.module_data) return module else: # load the module and apply the inherited metadata try: category = json_data["location"]["category"] class_ = self.load_block_type(category) definition = json_data.get("definition", {}) metadata = json_data.get("metadata", {}) for old_name, new_name in getattr(class_, "metadata_translations", {}).items(): if old_name in metadata: metadata[new_name] = metadata[old_name] del metadata[old_name] children = [self._convert_reference_to_key(childloc) for childloc in definition.get("children", [])] data = definition.get("data", {}) if isinstance(data, basestring): data = {"data": data} mixed_class = self.mixologist.mix(class_) if data: # empty or None means no work data = self._convert_reference_fields_to_keys(mixed_class, location.course_key, data) metadata = self._convert_reference_fields_to_keys(mixed_class, location.course_key, metadata) kvs = MongoKeyValueStore(data, children, metadata) field_data = KvsFieldData(kvs) scope_ids = ScopeIds(None, category, location, location) module = self.construct_xblock_from_class(class_, scope_ids, field_data) if self.cached_metadata is not None: # parent container pointers don't differentiate between draft and non-draft # so when we do the lookup, we should do so with a non-draft location non_draft_loc = as_published(location) # Convert the serialized fields values in self.cached_metadata # to python values metadata_to_inherit = self.cached_metadata.get(non_draft_loc.to_deprecated_string(), {}) inherit_metadata(module, metadata_to_inherit) edit_info = json_data.get("edit_info") # migrate published_by and published_date if edit_info isn't present if not edit_info: module.edited_by = ( module.edited_on ) = module.subtree_edited_on = module.subtree_edited_by = module.published_date = None # published_date was previously stored as a list of time components instead of a datetime if metadata.get("published_date"): module.published_date = datetime(*metadata.get("published_date")[0:6]).replace(tzinfo=UTC) module.published_by = metadata.get("published_by") # otherwise restore the stored editing information else: module.edited_by = edit_info.get("edited_by") module.edited_on = edit_info.get("edited_on") module.subtree_edited_on = edit_info.get("subtree_edited_on") module.subtree_edited_by = edit_info.get("subtree_edited_by") module.published_date = edit_info.get("published_date") module.published_by = edit_info.get("published_by") # decache any computed pending field settings module.save() return module except: log.warning("Failed to load descriptor from %s", json_data, exc_info=True) return ErrorDescriptor.from_json(json_data, self, location, error_msg=exc_info_to_str(sys.exc_info()))
def xblock_from_json(self, class_, block_id, json_data, course_entry_override=None): if course_entry_override is None: course_entry_override = self.course_entry else: # most recent retrieval is most likely the right one for next caller (see comment above fn) self.course_entry['branch'] = course_entry_override['branch'] self.course_entry['org'] = course_entry_override['org'] self.course_entry['offering'] = course_entry_override['offering'] # most likely a lazy loader or the id directly definition = json_data.get('definition', {}) definition_id = self.modulestore.definition_locator(definition) # If no usage id is provided, generate an in-memory id if block_id is None: block_id = LocalId() block_locator = BlockUsageLocator( CourseLocator( version_guid=course_entry_override['structure']['_id'], org=course_entry_override.get('org'), offering=course_entry_override.get('offering'), branch=course_entry_override.get('branch'), ), block_type=json_data.get('category'), block_id=block_id, ) converted_fields = self.modulestore.convert_references_to_keys( block_locator.course_key, class_, json_data.get('fields', {}), self.course_entry['structure']['blocks'], ) kvs = SplitMongoKVS( definition, converted_fields, json_data.get('_inherited_settings'), ) field_data = KvsFieldData(kvs) try: module = self.construct_xblock_from_class( class_, ScopeIds(None, json_data.get('category'), definition_id, block_locator), field_data, ) except Exception: log.warning("Failed to load descriptor", exc_info=True) return ErrorDescriptor.from_json( json_data, self, BlockUsageLocator( CourseLocator(version_guid=course_entry_override['structure']['_id']), block_type='error', block_id=block_id ), error_msg=exc_info_to_str(sys.exc_info()) ) edit_info = json_data.get('edit_info', {}) module.edited_by = edit_info.get('edited_by') module.edited_on = edit_info.get('edited_on') module.previous_version = edit_info.get('previous_version') module.update_version = edit_info.get('update_version') module.definition_locator = definition_id # decache any pending field settings module.save() # If this is an in-memory block, store it in this system if isinstance(block_locator.block_id, LocalId): self.local_modules[block_locator] = module return module
def xblock_from_json(self, class_, course_key, block_id, json_data, course_entry_override=None, **kwargs): if course_entry_override is None: course_entry_override = self.course_entry else: # most recent retrieval is most likely the right one for next caller (see comment above fn) self.course_entry['branch'] = course_entry_override['branch'] self.course_entry['org'] = course_entry_override['org'] self.course_entry['course'] = course_entry_override['course'] self.course_entry['run'] = course_entry_override['run'] definition_id = json_data.get('definition') block_type = json_data['category'] if definition_id is not None and not json_data.get('definition_loaded', False): definition_loader = DefinitionLazyLoader( self.modulestore, block_type, definition_id, lambda fields: self.modulestore.convert_references_to_keys( course_key, self.load_block_type(block_type), fields, self.course_entry['structure']['blocks'], ) ) else: definition_loader = None # If no definition id is provide, generate an in-memory id if definition_id is None: definition_id = LocalId() # If no usage id is provided, generate an in-memory id if block_id is None: block_id = LocalId() block_locator = BlockUsageLocator( course_key, block_type=block_type, block_id=block_id, ) converted_fields = self.modulestore.convert_references_to_keys( block_locator.course_key, class_, json_data.get('fields', {}), self.course_entry['structure']['blocks'], ) kvs = SplitMongoKVS( definition_loader, converted_fields, json_data.get('_inherited_settings'), **kwargs ) field_data = KvsFieldData(kvs) try: module = self.construct_xblock_from_class( class_, ScopeIds(None, block_type, definition_id, block_locator), field_data, ) except Exception: log.warning("Failed to load descriptor", exc_info=True) return ErrorDescriptor.from_json( json_data, self, BlockUsageLocator( CourseLocator(version_guid=course_entry_override['structure']['_id']), block_type='error', block_id=block_id ), error_msg=exc_info_to_str(sys.exc_info()) ) edit_info = json_data.get('edit_info', {}) module.edited_by = edit_info.get('edited_by') module.edited_on = edit_info.get('edited_on') module.subtree_edited_by = None # TODO - addressed with LMS-11183 module.subtree_edited_on = None # TODO - addressed with LMS-11183 module.published_by = None # TODO - addressed with LMS-11184 module.published_date = None # TODO - addressed with LMS-11184 module.previous_version = edit_info.get('previous_version') module.update_version = edit_info.get('update_version') module.source_version = edit_info.get('source_version', None) module.definition_locator = DefinitionLocator(block_type, definition_id) # decache any pending field settings module.save() # If this is an in-memory block, store it in this system if isinstance(block_locator.block_id, LocalId): self.local_modules[block_locator] = module return module
def xblock_from_json(self, class_, block_id, json_data, course_entry_override=None, **kwargs): if course_entry_override is None: course_entry_override = self.course_entry else: # most recent retrieval is most likely the right one for next caller (see comment above fn) self.course_entry['branch'] = course_entry_override['branch'] self.course_entry['org'] = course_entry_override['org'] self.course_entry['course'] = course_entry_override['course'] self.course_entry['run'] = course_entry_override['run'] # most likely a lazy loader or the id directly definition = json_data.get('definition', {}) definition_id = self.modulestore.definition_locator(definition) # If no usage id is provided, generate an in-memory id if block_id is None: block_id = LocalId() block_locator = BlockUsageLocator( CourseLocator( version_guid=course_entry_override['structure']['_id'], org=course_entry_override.get('org'), course=course_entry_override.get('course'), run=course_entry_override.get('run'), branch=course_entry_override.get('branch'), ), block_type=json_data.get('category'), block_id=block_id, ) converted_fields = self.modulestore.convert_references_to_keys( block_locator.course_key, class_, json_data.get('fields', {}), self.course_entry['structure']['blocks'], ) kvs = SplitMongoKVS(definition, converted_fields, json_data.get('_inherited_settings'), **kwargs) field_data = KvsFieldData(kvs) try: module = self.construct_xblock_from_class( class_, ScopeIds(None, json_data.get('category'), definition_id, block_locator), field_data, ) except Exception: log.warning("Failed to load descriptor", exc_info=True) return ErrorDescriptor.from_json( json_data, self, BlockUsageLocator(CourseLocator( version_guid=course_entry_override['structure']['_id']), block_type='error', block_id=block_id), error_msg=exc_info_to_str(sys.exc_info())) edit_info = json_data.get('edit_info', {}) module.edited_by = edit_info.get('edited_by') module.edited_on = edit_info.get('edited_on') module.subtree_edited_by = None # TODO - addressed with LMS-11183 module.subtree_edited_on = None # TODO - addressed with LMS-11183 module.published_by = None # TODO - addressed with LMS-11184 module.published_date = None # TODO - addressed with LMS-11184 module.previous_version = edit_info.get('previous_version') module.update_version = edit_info.get('update_version') module.source_version = edit_info.get('source_version', None) module.definition_locator = definition_id # decache any pending field settings module.save() # If this is an in-memory block, store it in this system if isinstance(block_locator.block_id, LocalId): self.local_modules[block_locator] = module return module
def load_item(self, location): """ Return an XModule instance for the specified location """ assert isinstance(location, Location) json_data = self.module_data.get(location) if json_data is None: module = self.modulestore.get_item(location) if module is not None: # update our own cache after going to the DB to get cache miss self.module_data.update(module.runtime.module_data) return module else: # load the module and apply the inherited metadata try: category = json_data['location']['category'] class_ = self.load_block_type(category) definition = json_data.get('definition', {}) metadata = json_data.get('metadata', {}) for old_name, new_name in getattr(class_, 'metadata_translations', {}).items(): if old_name in metadata: metadata[new_name] = metadata[old_name] del metadata[old_name] children = [ location.course_key.make_usage_key_from_deprecated_string(childloc) for childloc in definition.get('children', []) ] data = definition.get('data', {}) if isinstance(data, basestring): data = {'data': data} mixed_class = self.mixologist.mix(class_) if data is not None: data = self._convert_reference_fields_to_keys(mixed_class, location.course_key, data) metadata = self._convert_reference_fields_to_keys(mixed_class, location.course_key, metadata) kvs = MongoKeyValueStore( data, children, metadata, ) field_data = KvsFieldData(kvs) scope_ids = ScopeIds(None, category, location, location) module = self.construct_xblock_from_class(class_, scope_ids, field_data) if self.cached_metadata is not None: # parent container pointers don't differentiate between draft and non-draft # so when we do the lookup, we should do so with a non-draft location non_draft_loc = location.replace(revision=None) # Convert the serialized fields values in self.cached_metadata # to python values metadata_to_inherit = self.cached_metadata.get(non_draft_loc.to_deprecated_string(), {}) inherit_metadata(module, metadata_to_inherit) # decache any computed pending field settings module.save() return module except: log.warning("Failed to load descriptor from %s", json_data, exc_info=True) return ErrorDescriptor.from_json( json_data, self, location, error_msg=exc_info_to_str(sys.exc_info()) )
def process_xml(xml): """Takes an xml string, and returns a XModuleDescriptor created from that xml. """ def make_name_unique(xml_data): """ Make sure that the url_name of xml_data is unique. If a previously loaded unnamed descriptor stole this element's url_name, create a new one. Removes 'slug' attribute if present, and adds or overwrites the 'url_name' attribute. """ # VS[compat]. Take this out once course conversion is done (perhaps leave the uniqueness check) # tags that really need unique names--they store (or should store) state. need_uniq_names = ('problem', 'sequential', 'video', 'course', 'chapter', 'videosequence', 'poll_question', 'timelimit') attr = xml_data.attrib tag = xml_data.tag id = lambda x: x # Things to try to get a name, in order (key, cleaning function, remove key after reading?) lookups = [('url_name', id, False), ('slug', id, True), ('name', Location.clean, False), ('display_name', Location.clean, False)] url_name = None for key, clean, remove in lookups: if key in attr: url_name = clean(attr[key]) if remove: del attr[key] break def looks_like_fallback(url_name): """Does this look like something that came from fallback_name()?""" return (url_name is not None and url_name.startswith(tag) and re.search('[0-9a-fA-F]{12}$', url_name)) def fallback_name(orig_name=None): """Return the fallback name for this module. This is a function instead of a variable because we want it to be lazy.""" if looks_like_fallback(orig_name): # We're about to re-hash, in case something changed, so get rid of the tag_ and hash orig_name = orig_name[len(tag) + 1:-12] # append the hash of the content--the first 12 bytes should be plenty. orig_name = "_" + orig_name if orig_name not in ( None, "") else "" return tag + orig_name + "_" + hashlib.sha1( xml).hexdigest()[:12] # Fallback if there was nothing we could use: if url_name is None or url_name == "": url_name = fallback_name() # Don't log a warning--we don't need this in the log. Do # put it in the error tracker--content folks need to see it. if tag in need_uniq_names: error_tracker( "PROBLEM: no name of any kind specified for {tag}. Student " "state will not be properly tracked for this module. Problem xml:" " '{xml}...'".format(tag=tag, xml=xml[:100])) else: # TODO (vshnayder): We may want to enable this once course repos are cleaned up. # (or we may want to give up on the requirement for non-state-relevant issues...) #error_tracker("WARNING: no name specified for module. xml='{0}...'".format(xml[:100])) pass # Make sure everything is unique if url_name in self.used_names[tag]: # Always complain about modules that store state. If it # doesn't store state, don't complain about things that are # hashed. if tag in need_uniq_names: msg = ( "Non-unique url_name in xml. This may break state tracking for content." " url_name={0}. Content={1}".format( url_name, xml[:100])) error_tracker("PROBLEM: " + msg) log.warning(msg) # Just set name to fallback_name--if there are multiple things with the same fallback name, # they are actually identical, so it's fragile, but not immediately broken. # TODO (vshnayder): if the tag is a pointer tag, this will # break the content because we won't have the right link. # That's also a legitimate attempt to reuse the same content # from multiple places. Once we actually allow that, we'll # need to update this to complain about non-unique names for # definitions, but allow multiple uses. url_name = fallback_name(url_name) self.used_names[tag].add(url_name) xml_data.set('url_name', url_name) try: # VS[compat] # TODO (cpennington): Remove this once all fall 2012 courses # have been imported into the cms from xml xml = clean_out_mako_templating(xml) xml_data = etree.fromstring(xml) make_name_unique(xml_data) descriptor = XModuleDescriptor.load_from_xml( etree.tostring(xml_data, encoding='unicode'), self, self.org, self.course, xmlstore.default_class) except Exception as err: if not self.load_error_modules: raise # Didn't load properly. Fall back on loading as an error # descriptor. This should never error due to formatting. msg = "Error loading from xml. " + str(err)[:200] log.warning(msg) # Normally, we don't want lots of exception traces in our logs from common # content problems. But if you're debugging the xml loading code itself, # uncomment the next line. log.exception(msg) self.error_tracker(msg) err_msg = msg + "\n" + exc_info_to_str(sys.exc_info()) descriptor = ErrorDescriptor.from_xml(xml, self, self.org, self.course, err_msg) setattr(descriptor, 'data_dir', course_dir) xmlstore.modules[course_id][descriptor.location] = descriptor if hasattr(descriptor, 'children'): for child in descriptor.get_children(): parent_tracker.add_parent(child.location, descriptor.location) return descriptor
def xblock_from_json(self, class_, course_key, block_key, block_data, course_entry_override=None, **kwargs): """ Load and return block info. """ if course_entry_override is None: course_entry_override = self.course_entry else: # most recent retrieval is most likely the right one for next caller (see comment above fn) self.course_entry = CourseEnvelope( course_entry_override.course_key, self.course_entry.structure) definition_id = block_data.definition # If no usage id is provided, generate an in-memory id if block_key is None: block_key = BlockKey(block_data.block_type, LocalId()) convert_fields = lambda field: self.modulestore.convert_references_to_keys( course_key, class_, field, self.course_entry.structure['blocks'], ) if definition_id is not None and not block_data.definition_loaded: definition_loader = DefinitionLazyLoader( self.modulestore, course_key, block_key.type, definition_id, convert_fields, ) else: definition_loader = None # If no definition id is provide, generate an in-memory id if definition_id is None: definition_id = LocalId() # Construct the Block Usage Locator: block_locator = course_key.make_usage_key( block_type=block_key.type, block_id=block_key.id, ) converted_fields = convert_fields(block_data.fields) converted_defaults = convert_fields(block_data.defaults) if block_key in self._parent_map: parent_key = self._parent_map[block_key] parent = course_key.make_usage_key(parent_key.type, parent_key.id) else: parent = None try: kvs = SplitMongoKVS(definition_loader, converted_fields, converted_defaults, parent=parent, field_decorator=kwargs.get('field_decorator')) if InheritanceMixin in self.modulestore.xblock_mixins: field_data = inheriting_field_data(kvs) else: field_data = KvsFieldData(kvs) module = self.construct_xblock_from_class( class_, ScopeIds(None, block_key.type, definition_id, block_locator), field_data, for_parent=kwargs.get('for_parent')) except Exception: # pylint: disable=broad-except log.warning("Failed to load descriptor", exc_info=True) return ErrorDescriptor.from_json( block_data, self, course_entry_override.course_key.make_usage_key( block_type='error', block_id=block_key.id), error_msg=exc_info_to_str(sys.exc_info())) edit_info = block_data.edit_info module._edited_by = edit_info.edited_by # pylint: disable=protected-access module._edited_on = edit_info.edited_on # pylint: disable=protected-access module.previous_version = edit_info.previous_version module.update_version = edit_info.update_version module.source_version = edit_info.source_version module.definition_locator = DefinitionLocator(block_key.type, definition_id) # decache any pending field settings module.save() # If this is an in-memory block, store it in this system if isinstance(block_locator.block_id, LocalId): self.local_modules[block_locator] = module return module
def xblock_from_json(self, class_, course_key, block_id, json_data, inherited_settings, course_entry_override=None, **kwargs): if course_entry_override is None: course_entry_override = self.course_entry else: # most recent retrieval is most likely the right one for next caller (see comment above fn) self.course_entry['branch'] = course_entry_override['branch'] self.course_entry['org'] = course_entry_override['org'] self.course_entry['course'] = course_entry_override['course'] self.course_entry['run'] = course_entry_override['run'] definition_id = json_data.get('definition') block_type = json_data['category'] if block_id is not None: if inherited_settings is None: # see if there's a value in course_entry if (block_type, block_id) in self.course_entry['inherited_settings']: inherited_settings = self.course_entry[ 'inherited_settings'][(block_type, block_id)] elif (block_type, block_id) not in self.course_entry['inherited_settings']: self.course_entry['inherited_settings'][( block_type, block_id)] = inherited_settings if definition_id is not None and not json_data.get( 'definition_loaded', False): definition_loader = DefinitionLazyLoader( self.modulestore, block_type, definition_id, lambda fields: self.modulestore.convert_references_to_keys( course_key, self.load_block_type(block_type), fields, self.course_entry['structure']['blocks'], )) else: definition_loader = None # If no definition id is provide, generate an in-memory id if definition_id is None: definition_id = LocalId() # If no usage id is provided, generate an in-memory id if block_id is None: block_id = LocalId() block_locator = BlockUsageLocator( course_key, block_type=block_type, block_id=block_id, ) converted_fields = self.modulestore.convert_references_to_keys( block_locator.course_key, class_, json_data.get('fields', {}), self.course_entry['structure']['blocks'], ) kvs = SplitMongoKVS(definition_loader, converted_fields, inherited_settings, **kwargs) field_data = KvsFieldData(kvs) try: module = self.construct_xblock_from_class( class_, ScopeIds(None, block_type, definition_id, block_locator), field_data, ) except Exception: log.warning("Failed to load descriptor", exc_info=True) return ErrorDescriptor.from_json( json_data, self, BlockUsageLocator(CourseLocator( version_guid=course_entry_override['structure']['_id']), block_type='error', block_id=block_id), error_msg=exc_info_to_str(sys.exc_info())) edit_info = json_data.get('edit_info', {}) module._edited_by = edit_info.get('edited_by') module._edited_on = edit_info.get('edited_on') module.previous_version = edit_info.get('previous_version') module.update_version = edit_info.get('update_version') module.source_version = edit_info.get('source_version', None) module.definition_locator = DefinitionLocator(block_type, definition_id) # decache any pending field settings module.save() # If this is an in-memory block, store it in this system if isinstance(block_locator.block_id, LocalId): self.local_modules[block_locator] = module return module
def xblock_from_json(self, class_, course_key, block_key, block_data, course_entry_override=None, **kwargs): """ Load and return block info. """ if course_entry_override is None: course_entry_override = self.course_entry else: # most recent retrieval is most likely the right one for next caller (see comment above fn) self.course_entry = CourseEnvelope(course_entry_override.course_key, self.course_entry.structure) definition_id = block_data.definition # If no usage id is provided, generate an in-memory id if block_key is None: block_key = BlockKey(block_data.block_type, LocalId()) convert_fields = lambda field: self.modulestore.convert_references_to_keys( course_key, class_, field, self.course_entry.structure['blocks'], ) if definition_id is not None and not block_data.definition_loaded: definition_loader = DefinitionLazyLoader( self.modulestore, course_key, block_key.type, definition_id, convert_fields, ) else: definition_loader = None # If no definition id is provide, generate an in-memory id if definition_id is None: definition_id = LocalId() # Construct the Block Usage Locator: block_locator = course_key.make_usage_key( block_type=block_key.type, block_id=block_key.id, ) converted_fields = convert_fields(block_data.fields) converted_defaults = convert_fields(block_data.defaults) if block_key in self._parent_map: parent_key = self._parent_map[block_key] parent = course_key.make_usage_key(parent_key.type, parent_key.id) else: parent = None aside_fields = None # for the situation if block_data has no asides attribute # (in case it was taken from memcache) try: if block_data.asides: aside_fields = {block_key.type: {}} for aside in block_data.asides: aside_fields[block_key.type].update(aside['fields']) except AttributeError: pass try: kvs = SplitMongoKVS( definition_loader, converted_fields, converted_defaults, parent=parent, aside_fields=aside_fields, field_decorator=kwargs.get('field_decorator') ) if InheritanceMixin in self.modulestore.xblock_mixins: field_data = inheriting_field_data(kvs) else: field_data = KvsFieldData(kvs) module = self.construct_xblock_from_class( class_, ScopeIds(None, block_key.type, definition_id, block_locator), field_data, for_parent=kwargs.get('for_parent') ) except Exception: # pylint: disable=broad-except log.warning("Failed to load descriptor", exc_info=True) return ErrorDescriptor.from_json( block_data, self, course_entry_override.course_key.make_usage_key( block_type='error', block_id=block_key.id ), error_msg=exc_info_to_str(sys.exc_info()) ) edit_info = block_data.edit_info module._edited_by = edit_info.edited_by # pylint: disable=protected-access module._edited_on = edit_info.edited_on # pylint: disable=protected-access module.previous_version = edit_info.previous_version module.update_version = edit_info.update_version module.source_version = edit_info.source_version module.definition_locator = DefinitionLocator(block_key.type, definition_id) for wrapper in self.modulestore.xblock_field_data_wrappers: module._field_data = wrapper(module, module._field_data) # pylint: disable=protected-access # decache any pending field settings module.save() # If this is an in-memory block, store it in this system if isinstance(block_locator.block_id, LocalId): self.local_modules[block_locator] = module return module
def xblock_from_json(self, class_, course_key, block_key, block_data, course_entry_override=None, **kwargs): """ Load and return block info. """ if course_entry_override is None: course_entry_override = self.course_entry else: # most recent retrieval is most likely the right one for next caller (see comment above fn) self.course_entry = CourseEnvelope(course_entry_override.course_key, self.course_entry.structure) definition_id = block_data.get("definition") # If no usage id is provided, generate an in-memory id if block_key is None: block_key = BlockKey(block_data["block_type"], LocalId()) convert_fields = lambda field: self.modulestore.convert_references_to_keys( course_key, class_, field, self.course_entry.structure["blocks"] ) if definition_id is not None and not block_data["definition_loaded"]: definition_loader = DefinitionLazyLoader( self.modulestore, course_key, block_key.type, definition_id, convert_fields ) else: definition_loader = None # If no definition id is provide, generate an in-memory id if definition_id is None: definition_id = LocalId() # Construct the Block Usage Locator: block_locator = course_key.make_usage_key(block_type=block_key.type, block_id=block_key.id) converted_fields = convert_fields(block_data.get("fields", {})) converted_defaults = convert_fields(block_data.get("defaults", {})) if block_key in self._parent_map: parent_key = self._parent_map[block_key] parent = course_key.make_usage_key(parent_key.type, parent_key.id) else: parent = None kvs = SplitMongoKVS( definition_loader, converted_fields, converted_defaults, parent=parent, field_decorator=kwargs.get("field_decorator"), ) if InheritanceMixin in self.modulestore.xblock_mixins: field_data = inheriting_field_data(kvs) else: field_data = KvsFieldData(kvs) try: module = self.construct_xblock_from_class( class_, ScopeIds(None, block_key.type, definition_id, block_locator), field_data ) except Exception: log.warning("Failed to load descriptor", exc_info=True) return ErrorDescriptor.from_json( block_data, self, course_entry_override.course_key.make_usage_key(block_type="error", block_id=block_key.id), error_msg=exc_info_to_str(sys.exc_info()), ) edit_info = block_data.get("edit_info", {}) module._edited_by = edit_info.get("edited_by") # pylint: disable=protected-access module._edited_on = edit_info.get("edited_on") # pylint: disable=protected-access module.previous_version = edit_info.get("previous_version") module.update_version = edit_info.get("update_version") module.source_version = edit_info.get("source_version", None) module.definition_locator = DefinitionLocator(block_key.type, definition_id) # decache any pending field settings module.save() # If this is an in-memory block, store it in this system if isinstance(block_locator.block_id, LocalId): self.local_modules[block_locator] = module return module
def process_xml(xml): # lint-amnesty, pylint: disable=too-many-statements """Takes an xml string, and returns a XBlock created from that xml. """ def make_name_unique(xml_data): """ Make sure that the url_name of xml_data is unique. If a previously loaded unnamed descriptor stole this element's url_name, create a new one. Removes 'slug' attribute if present, and adds or overwrites the 'url_name' attribute. """ # VS[compat]. Take this out once course conversion is done (perhaps leave the uniqueness check) # tags that really need unique names--they store (or should store) state. need_uniq_names = ('problem', 'sequential', 'video', 'course', 'chapter', 'videosequence', 'poll_question', 'vertical') attr = xml_data.attrib tag = xml_data.tag id = lambda x: x # lint-amnesty, pylint: disable=redefined-builtin # Things to try to get a name, in order (key, cleaning function, remove key after reading?) lookups = [('url_name', id, False), ('slug', id, True), ('name', BlockUsageLocator.clean, False), ('display_name', BlockUsageLocator.clean, False)] url_name = None for key, clean, remove in lookups: if key in attr: url_name = clean(attr[key]) if remove: del attr[key] break def looks_like_fallback(url_name): """Does this look like something that came from fallback_name()?""" return (url_name is not None and url_name.startswith(tag) and re.search('[0-9a-fA-F]{12}$', url_name)) def fallback_name(orig_name=None): """Return the fallback name for this module. This is a function instead of a variable because we want it to be lazy.""" if looks_like_fallback(orig_name): # We're about to re-hash, in case something changed, so get rid of the tag_ and hash orig_name = orig_name[len(tag) + 1:-12] # append the hash of the content--the first 12 bytes should be plenty. orig_name = "_" + orig_name if orig_name not in ( None, "") else "" xml_bytes = xml if isinstance( xml, bytes) else xml.encode('utf-8') return tag + orig_name + "_" + hashlib.sha1( xml_bytes).hexdigest()[:12] # Fallback if there was nothing we could use: if url_name is None or url_name == "": url_name = fallback_name() # Don't log a warning--we don't need this in the log. Do # put it in the error tracker--content folks need to see it. if tag in need_uniq_names: error_tracker( "PROBLEM: no name of any kind specified for {tag}. Student " "state will not be properly tracked for this module. Problem xml:" " '{xml}...'".format(tag=tag, xml=xml[:100])) else: # TODO (vshnayder): We may want to enable this once course repos are cleaned up. # (or we may want to give up on the requirement for non-state-relevant issues...) # error_tracker("WARNING: no name specified for module. xml='{0}...'".format(xml[:100])) pass # Make sure everything is unique if url_name in self.used_names[tag]: # Always complain about modules that store state. If it # doesn't store state, don't complain about things that are # hashed. if tag in need_uniq_names: msg = ( "Non-unique url_name in xml. This may break state tracking for content." " url_name={}. Content={}".format( url_name, xml[:100])) error_tracker("PROBLEM: " + msg) log.warning(msg) # Just set name to fallback_name--if there are multiple things with the same fallback name, # they are actually identical, so it's fragile, but not immediately broken. # TODO (vshnayder): if the tag is a pointer tag, this will # break the content because we won't have the right link. # That's also a legitimate attempt to reuse the same content # from multiple places. Once we actually allow that, we'll # need to update this to complain about non-unique names for # definitions, but allow multiple uses. url_name = fallback_name(url_name) self.used_names[tag].add(url_name) xml_data.set('url_name', url_name) try: xml_data = etree.fromstring(xml) make_name_unique(xml_data) descriptor = self.xblock_from_node( xml_data, None, # parent_id id_manager, ) except Exception as err: # pylint: disable=broad-except if not self.load_error_modules: raise # Didn't load properly. Fall back on loading as an error # descriptor. This should never error due to formatting. msg = "Error loading from xml. %s" log.warning( msg, str(err)[:200], # Normally, we don't want lots of exception traces in our logs from common # content problems. But if you're debugging the xml loading code itself, # uncomment the next line. # exc_info=True ) msg = msg % (str(err)[:200]) self.error_tracker(msg) err_msg = msg + "\n" + exc_info_to_str(sys.exc_info()) descriptor = ErrorBlock.from_xml(xml, self, id_manager, err_msg) descriptor.data_dir = course_dir if descriptor.scope_ids.usage_id in xmlstore.modules[course_id]: # keep the parent pointer if any but allow everything else to overwrite other_copy = xmlstore.modules[course_id][ descriptor.scope_ids.usage_id] descriptor.parent = other_copy.parent if descriptor != other_copy: log.warning("%s has more than one definition", descriptor.scope_ids.usage_id) xmlstore.modules[course_id][ descriptor.scope_ids.usage_id] = descriptor if descriptor.has_children: for child in descriptor.get_children(): # parent is alphabetically least if child.parent is None or child.parent > descriptor.scope_ids.usage_id: child.parent = descriptor.location child.save() # After setting up the descriptor, save any changes that we have # made to attributes on the descriptor to the underlying KeyValueStore. descriptor.save() return descriptor
def get_module_for_descriptor_internal(user, descriptor, field_data_cache, course_id, track_function, xqueue_callback_url_prefix, position=None, wrap_xmodule_display=True, grade_bucket_type=None, static_asset_path=''): """ Actually implement get_module, without requiring a request. See get_module() docstring for further details. """ # Short circuit--if the user shouldn't have access, bail without doing any work if not has_access(user, descriptor, 'load', course_id): return None # Setup system context for module instance ajax_url = reverse( 'modx_dispatch', kwargs=dict( course_id=course_id, location=descriptor.location.url(), dispatch='' ), ) # Intended use is as {ajax_url}/{dispatch_command}, so get rid of the trailing slash. ajax_url = ajax_url.rstrip('/') def make_xqueue_callback(dispatch='score_update'): # Fully qualified callback URL for external queueing system relative_xqueue_callback_url = reverse( 'xqueue_callback', kwargs=dict( course_id=course_id, userid=str(user.id), mod_id=descriptor.location.url(), dispatch=dispatch ), ) return xqueue_callback_url_prefix + relative_xqueue_callback_url # Default queuename is course-specific and is derived from the course that # contains the current module. # TODO: Queuename should be derived from 'course_settings.json' of each course xqueue_default_queuename = descriptor.location.org + '-' + descriptor.location.course xqueue = { 'interface': xqueue_interface, 'construct_callback': make_xqueue_callback, 'default_queuename': xqueue_default_queuename.replace(' ', '_'), 'waittime': settings.XQUEUE_WAITTIME_BETWEEN_REQUESTS } # This is a hacky way to pass settings to the combined open ended xmodule # It needs an S3 interface to upload images to S3 # It needs the open ended grading interface in order to get peer grading to be done # this first checks to see if the descriptor is the correct one, and only sends settings if it is # Get descriptor metadata fields indicating needs for various settings needs_open_ended_interface = getattr(descriptor, "needs_open_ended_interface", False) needs_s3_interface = getattr(descriptor, "needs_s3_interface", False) # Initialize interfaces to None open_ended_grading_interface = None s3_interface = None # Create interfaces if needed if needs_open_ended_interface: open_ended_grading_interface = settings.OPEN_ENDED_GRADING_INTERFACE open_ended_grading_interface['mock_peer_grading'] = settings.MOCK_PEER_GRADING open_ended_grading_interface['mock_staff_grading'] = settings.MOCK_STAFF_GRADING if needs_s3_interface: s3_interface = { 'access_key': getattr(settings, 'AWS_ACCESS_KEY_ID', ''), 'secret_access_key': getattr(settings, 'AWS_SECRET_ACCESS_KEY', ''), 'storage_bucket_name': getattr(settings, 'AWS_STORAGE_BUCKET_NAME', 'openended') } def inner_get_module(descriptor): """ Delegate to get_module_for_descriptor_internal() with all values except `descriptor` set. Because it does an access check, it may return None. """ # TODO: fix this so that make_xqueue_callback uses the descriptor passed into # inner_get_module, not the parent's callback. Add it as an argument.... return get_module_for_descriptor_internal(user, descriptor, field_data_cache, course_id, track_function, make_xqueue_callback, position, wrap_xmodule_display, grade_bucket_type, static_asset_path) def xmodule_field_data(descriptor): student_data = DbModel(DjangoKeyValueStore(field_data_cache)) return lms_field_data(descriptor._field_data, student_data) def publish(event): """A function that allows XModules to publish events. This only supports grade changes right now.""" if event.get('event_name') != 'grade': return # Construct the key for the module key = KeyValueStore.Key( scope=Scope.user_state, user_id=user.id, block_scope_id=descriptor.location, field_name='grade' ) student_module = field_data_cache.find_or_create(key) # Update the grades student_module.grade = event.get('value') student_module.max_grade = event.get('max_value') # Save all changes to the underlying KeyValueStore student_module.save() # Bin score into range and increment stats score_bucket = get_score_bucket(student_module.grade, student_module.max_grade) org, course_num, run = course_id.split("/") tags = [ "org:{0}".format(org), "course:{0}".format(course_num), "run:{0}".format(run), "score_bucket:{0}".format(score_bucket) ] if grade_bucket_type is not None: tags.append('type:%s' % grade_bucket_type) dog_stats_api.increment("lms.courseware.question_answered", tags=tags) # Build a list of wrapping functions that will be applied in order # to the Fragment content coming out of the xblocks that are about to be rendered. block_wrappers = [] # Wrap the output display in a single div to allow for the XModule # javascript to be bound correctly if wrap_xmodule_display is True: block_wrappers.append(partial(wrap_xmodule, 'xmodule_display.html')) # TODO (cpennington): When modules are shared between courses, the static # prefix is going to have to be specific to the module, not the directory # that the xml was loaded from # Rewrite urls beginning in /static to point to course-specific content block_wrappers.append(partial( replace_static_urls, getattr(descriptor, 'data_dir', None), course_id=course_id, static_asset_path=static_asset_path or descriptor.static_asset_path )) # Allow URLs of the form '/course/' refer to the root of multicourse directory # hierarchy of this course block_wrappers.append(partial(replace_course_urls, course_id)) # this will rewrite intra-courseware links (/jump_to_id/<id>). This format # is an improvement over the /course/... format for studio authored courses, # because it is agnostic to course-hierarchy. # NOTE: module_id is empty string here. The 'module_id' will get assigned in the replacement # function, we just need to specify something to get the reverse() to work. block_wrappers.append(partial( replace_jump_to_id_urls, course_id, reverse('jump_to_id', kwargs={'course_id': course_id, 'module_id': ''}), )) if settings.MITX_FEATURES.get('DISPLAY_HISTOGRAMS_TO_STAFF'): if has_access(user, descriptor, 'staff', course_id): block_wrappers.append(partial(add_histogram, user)) system = ModuleSystem( track_function=track_function, render_template=render_to_string, ajax_url=ajax_url, xqueue=xqueue, # TODO (cpennington): Figure out how to share info between systems filestore=descriptor.runtime.resources_fs, get_module=inner_get_module, user=user, debug=settings.DEBUG, hostname=settings.SITE_NAME, # TODO (cpennington): This should be removed when all html from # a module is coming through get_html and is therefore covered # by the replace_static_urls code below replace_urls=partial( static_replace.replace_static_urls, data_directory=getattr(descriptor, 'data_dir', None), course_id=course_id, static_asset_path=static_asset_path or descriptor.static_asset_path, ), replace_course_urls=partial( static_replace.replace_course_urls, course_id=course_id ), replace_jump_to_id_urls=partial( static_replace.replace_jump_to_id_urls, course_id=course_id, jump_to_id_base_url=reverse('jump_to_id', kwargs={'course_id': course_id, 'module_id': ''}) ), node_path=settings.NODE_PATH, xmodule_field_data=xmodule_field_data, publish=publish, anonymous_student_id=unique_id_for_user(user), course_id=course_id, open_ended_grading_interface=open_ended_grading_interface, s3_interface=s3_interface, cache=cache, can_execute_unsafe_code=(lambda: can_execute_unsafe_code(course_id)), # TODO: When we merge the descriptor and module systems, we can stop reaching into the mixologist (cpennington) mixins=descriptor.runtime.mixologist._mixins, # pylint: disable=protected-access wrappers=block_wrappers, ) # pass position specified in URL to module through ModuleSystem system.set('position', position) if settings.MITX_FEATURES.get('ENABLE_PSYCHOMETRICS'): system.set( 'psychometrics_handler', # set callback for updating PsychometricsData make_psychometrics_data_update_handler(course_id, user, descriptor.location.url()) ) try: module = descriptor.xmodule(system) except: log.exception("Error creating module from descriptor {0}".format(descriptor)) # make an ErrorDescriptor -- assuming that the descriptor's system is ok if has_access(user, descriptor.location, 'staff', course_id): err_descriptor_class = ErrorDescriptor else: err_descriptor_class = NonStaffErrorDescriptor err_descriptor = err_descriptor_class.from_descriptor( descriptor, error_msg=exc_info_to_str(sys.exc_info()) ) # Make an error module return err_descriptor.xmodule(system) system.set('user_is_staff', has_access(user, descriptor.location, 'staff', course_id)) return module
def xblock_from_json(self, class_, course_key, block_key, json_data, course_entry_override=None, **kwargs): if course_entry_override is None: course_entry_override = self.course_entry else: # most recent retrieval is most likely the right one for next caller (see comment above fn) self.course_entry = CourseEnvelope( course_entry_override.course_key, self.course_entry.structure) definition_id = json_data.get('definition') # If no usage id is provided, generate an in-memory id if block_key is None: block_key = BlockKey(json_data['block_type'], LocalId()) if definition_id is not None and not json_data.get( 'definition_loaded', False): definition_loader = DefinitionLazyLoader( self.modulestore, course_key, block_key.type, definition_id, lambda fields: self.modulestore.convert_references_to_keys( course_key, self.load_block_type(block_key.type), fields, self.course_entry.structure['blocks'], )) else: definition_loader = None # If no definition id is provide, generate an in-memory id if definition_id is None: definition_id = LocalId() block_locator = BlockUsageLocator( course_key, block_type=block_key.type, block_id=block_key.id, ) converted_fields = self.modulestore.convert_references_to_keys( block_locator.course_key, class_, json_data.get('fields', {}), self.course_entry.structure['blocks'], ) if block_key in self._parent_map: parent_key = self._parent_map[block_key] parent = course_key.make_usage_key(parent_key.type, parent_key.id) else: parent = None kvs = SplitMongoKVS(definition_loader, converted_fields, parent=parent, field_decorator=kwargs.get('field_decorator')) if InheritanceMixin in self.modulestore.xblock_mixins: field_data = inheriting_field_data(kvs) else: field_data = KvsFieldData(kvs) try: module = self.construct_xblock_from_class( class_, ScopeIds(None, block_key.type, definition_id, block_locator), field_data, ) except Exception: log.warning("Failed to load descriptor", exc_info=True) return ErrorDescriptor.from_json( json_data, self, course_entry_override.course_key.make_usage_key( block_type='error', block_id=block_key.id), error_msg=exc_info_to_str(sys.exc_info())) edit_info = json_data.get('edit_info', {}) module._edited_by = edit_info.get('edited_by') module._edited_on = edit_info.get('edited_on') module.previous_version = edit_info.get('previous_version') module.update_version = edit_info.get('update_version') module.source_version = edit_info.get('source_version', None) module.definition_locator = DefinitionLocator(block_key.type, definition_id) # decache any pending field settings module.save() # If this is an in-memory block, store it in this system if isinstance(block_locator.block_id, LocalId): self.local_modules[block_locator] = module return module
def process_xml(xml): """Takes an xml string, and returns a XBlock created from that xml. """ def make_name_unique(xml_data): """ Make sure that the url_name of xml_data is unique. If a previously loaded unnamed descriptor stole this element's url_name, create a new one. Removes 'slug' attribute if present, and adds or overwrites the 'url_name' attribute. """ # VS[compat]. Take this out once course conversion is done (perhaps leave the uniqueness check) # tags that really need unique names--they store (or should store) state. need_uniq_names = ( "problem", "sequential", "video", "course", "chapter", "videosequence", "poll_question", "vertical", ) attr = xml_data.attrib tag = xml_data.tag id = lambda x: x # Things to try to get a name, in order (key, cleaning function, remove key after reading?) lookups = [ ("url_name", id, False), ("slug", id, True), ("name", Location.clean, False), ("display_name", Location.clean, False), ] url_name = None for key, clean, remove in lookups: if key in attr: url_name = clean(attr[key]) if remove: del attr[key] break def looks_like_fallback(url_name): """Does this look like something that came from fallback_name()?""" return url_name is not None and url_name.startswith(tag) and re.search("[0-9a-fA-F]{12}$", url_name) def fallback_name(orig_name=None): """Return the fallback name for this module. This is a function instead of a variable because we want it to be lazy.""" if looks_like_fallback(orig_name): # We're about to re-hash, in case something changed, so get rid of the tag_ and hash orig_name = orig_name[len(tag) + 1 : -12] # append the hash of the content--the first 12 bytes should be plenty. orig_name = "_" + orig_name if orig_name not in (None, "") else "" xml_bytes = xml.encode("utf8") return tag + orig_name + "_" + hashlib.sha1(xml_bytes).hexdigest()[:12] # Fallback if there was nothing we could use: if url_name is None or url_name == "": url_name = fallback_name() # Don't log a warning--we don't need this in the log. Do # put it in the error tracker--content folks need to see it. if tag in need_uniq_names: error_tracker( "PROBLEM: no name of any kind specified for {tag}. Student " "state will not be properly tracked for this module. Problem xml:" " '{xml}...'".format(tag=tag, xml=xml[:100]) ) else: # TODO (vshnayder): We may want to enable this once course repos are cleaned up. # (or we may want to give up on the requirement for non-state-relevant issues...) # error_tracker("WARNING: no name specified for module. xml='{0}...'".format(xml[:100])) pass # Make sure everything is unique if url_name in self.used_names[tag]: # Always complain about modules that store state. If it # doesn't store state, don't complain about things that are # hashed. if tag in need_uniq_names: msg = ( "Non-unique url_name in xml. This may break state tracking for content." " url_name={0}. Content={1}".format(url_name, xml[:100]) ) error_tracker("PROBLEM: " + msg) log.warning(msg) # Just set name to fallback_name--if there are multiple things with the same fallback name, # they are actually identical, so it's fragile, but not immediately broken. # TODO (vshnayder): if the tag is a pointer tag, this will # break the content because we won't have the right link. # That's also a legitimate attempt to reuse the same content # from multiple places. Once we actually allow that, we'll # need to update this to complain about non-unique names for # definitions, but allow multiple uses. url_name = fallback_name(url_name) self.used_names[tag].add(url_name) xml_data.set("url_name", url_name) try: # VS[compat] # TODO (cpennington): Remove this once all fall 2012 courses # have been imported into the cms from xml xml = clean_out_mako_templating(xml) xml_data = etree.fromstring(xml) make_name_unique(xml_data) descriptor = create_block_from_xml(etree.tostring(xml_data, encoding="unicode"), self, id_generator) except Exception as err: # pylint: disable=broad-except if not self.load_error_modules: raise # Didn't load properly. Fall back on loading as an error # descriptor. This should never error due to formatting. msg = "Error loading from xml. %s" log.warning( msg, unicode(err)[:200], # Normally, we don't want lots of exception traces in our logs from common # content problems. But if you're debugging the xml loading code itself, # uncomment the next line. # exc_info=True ) msg = msg % (unicode(err)[:200]) self.error_tracker(msg) err_msg = msg + "\n" + exc_info_to_str(sys.exc_info()) descriptor = ErrorDescriptor.from_xml(xml, self, id_generator, err_msg) descriptor.data_dir = course_dir xmlstore.modules[course_id][descriptor.scope_ids.usage_id] = descriptor if descriptor.has_children: for child in descriptor.get_children(): parent_tracker.add_parent(child.scope_ids.usage_id, descriptor.scope_ids.usage_id) # After setting up the descriptor, save any changes that we have # made to attributes on the descriptor to the underlying KeyValueStore. descriptor.save() return descriptor
def get_module_for_descriptor_internal(user, descriptor, model_data_cache, course_id, track_function, xqueue_callback_url_prefix, position=None, wrap_xmodule_display=True, grade_bucket_type=None): """ Actually implement get_module, without requiring a request. See get_module() docstring for further details. """ # Short circuit--if the user shouldn't have access, bail without doing any work if not has_access(user, descriptor, 'load', course_id): return None # Setup system context for module instance ajax_url = reverse('modx_dispatch', kwargs=dict(course_id=course_id, location=descriptor.location.url(), dispatch=''), ) # Intended use is as {ajax_url}/{dispatch_command}, so get rid of the trailing slash. ajax_url = ajax_url.rstrip('/') def make_xqueue_callback(dispatch='score_update'): # Fully qualified callback URL for external queueing system relative_xqueue_callback_url = reverse('xqueue_callback', kwargs=dict(course_id=course_id, userid=str(user.id), mod_id=descriptor.location.url(), dispatch=dispatch), ) return xqueue_callback_url_prefix + relative_xqueue_callback_url # Default queuename is course-specific and is derived from the course that # contains the current module. # TODO: Queuename should be derived from 'course_settings.json' of each course xqueue_default_queuename = descriptor.location.org + '-' + descriptor.location.course xqueue = {'interface': xqueue_interface, 'construct_callback': make_xqueue_callback, 'default_queuename': xqueue_default_queuename.replace(' ', '_'), 'waittime': settings.XQUEUE_WAITTIME_BETWEEN_REQUESTS } # This is a hacky way to pass settings to the combined open ended xmodule # It needs an S3 interface to upload images to S3 # It needs the open ended grading interface in order to get peer grading to be done # this first checks to see if the descriptor is the correct one, and only sends settings if it is # Get descriptor metadata fields indicating needs for various settings needs_open_ended_interface = getattr(descriptor, "needs_open_ended_interface", False) needs_s3_interface = getattr(descriptor, "needs_s3_interface", False) # Initialize interfaces to None open_ended_grading_interface = None s3_interface = None # Create interfaces if needed if needs_open_ended_interface: open_ended_grading_interface = settings.OPEN_ENDED_GRADING_INTERFACE open_ended_grading_interface['mock_peer_grading'] = settings.MOCK_PEER_GRADING open_ended_grading_interface['mock_staff_grading'] = settings.MOCK_STAFF_GRADING if needs_s3_interface: s3_interface = { 'access_key': getattr(settings, 'AWS_ACCESS_KEY_ID', ''), 'secret_access_key': getattr(settings, 'AWS_SECRET_ACCESS_KEY', ''), 'storage_bucket_name': getattr(settings, 'AWS_STORAGE_BUCKET_NAME', 'openended') } def inner_get_module(descriptor): """ Delegate to get_module_for_descriptor_internal() with all values except `descriptor` set. Because it does an access check, it may return None. """ # TODO: fix this so that make_xqueue_callback uses the descriptor passed into # inner_get_module, not the parent's callback. Add it as an argument.... return get_module_for_descriptor_internal(user, descriptor, model_data_cache, course_id, track_function, make_xqueue_callback, position, wrap_xmodule_display, grade_bucket_type) def xblock_model_data(descriptor): return DbModel( LmsKeyValueStore(descriptor._model_data, model_data_cache), descriptor.module_class, user.id, LmsUsage(descriptor.location, descriptor.location) ) def publish(event): if event.get('event_name') != 'grade': return student_module, created = StudentModule.objects.get_or_create( course_id=course_id, student=user, module_type=descriptor.location.category, module_state_key=descriptor.location.url(), defaults={'state': '{}'}, ) student_module.grade = event.get('value') student_module.max_grade = event.get('max_value') student_module.save() # Bin score into range and increment stats score_bucket = get_score_bucket(student_module.grade, student_module.max_grade) org, course_num, run = course_id.split("/") tags = ["org:{0}".format(org), "course:{0}".format(course_num), "run:{0}".format(run), "score_bucket:{0}".format(score_bucket)] if grade_bucket_type is not None: tags.append('type:%s' % grade_bucket_type) statsd.increment("lms.courseware.question_answered", tags=tags) # TODO (cpennington): When modules are shared between courses, the static # prefix is going to have to be specific to the module, not the directory # that the xml was loaded from system = ModuleSystem(track_function=track_function, render_template=render_to_string, ajax_url=ajax_url, xqueue=xqueue, # TODO (cpennington): Figure out how to share info between systems filestore=descriptor.system.resources_fs, get_module=inner_get_module, user=user, # TODO (cpennington): This should be removed when all html from # a module is coming through get_html and is therefore covered # by the replace_static_urls code below replace_urls=partial( static_replace.replace_static_urls, data_directory=getattr(descriptor, 'data_dir', None), course_namespace=descriptor.location._replace(category=None, name=None), ), node_path=settings.NODE_PATH, xblock_model_data=xblock_model_data, publish=publish, anonymous_student_id=unique_id_for_user(user), course_id=course_id, open_ended_grading_interface=open_ended_grading_interface, s3_interface=s3_interface, cache=cache, can_execute_unsafe_code=(lambda: can_execute_unsafe_code(course_id)), ) # pass position specified in URL to module through ModuleSystem system.set('position', position) system.set('DEBUG', settings.DEBUG) if settings.MITX_FEATURES.get('ENABLE_PSYCHOMETRICS'): system.set('psychometrics_handler', # set callback for updating PsychometricsData make_psychometrics_data_update_handler(course_id, user, descriptor.location.url())) try: module = descriptor.xmodule(system) except: log.exception("Error creating module from descriptor {0}".format(descriptor)) # make an ErrorDescriptor -- assuming that the descriptor's system is ok if has_access(user, descriptor.location, 'staff', course_id): err_descriptor_class = ErrorDescriptor else: err_descriptor_class = NonStaffErrorDescriptor err_descriptor = err_descriptor_class.from_descriptor( descriptor, error_msg=exc_info_to_str(sys.exc_info()) ) # Make an error module return err_descriptor.xmodule(system) system.set('user_is_staff', has_access(user, descriptor.location, 'staff', course_id)) _get_html = module.get_html if wrap_xmodule_display == True: _get_html = wrap_xmodule(module.get_html, module, 'xmodule_display.html') module.get_html = replace_static_urls( _get_html, getattr(descriptor, 'data_dir', None), course_namespace=module.location._replace(category=None, name=None)) # Allow URLs of the form '/course/' refer to the root of multicourse directory # hierarchy of this course module.get_html = replace_course_urls(module.get_html, course_id) if settings.MITX_FEATURES.get('DISPLAY_HISTOGRAMS_TO_STAFF'): if has_access(user, module, 'staff', course_id): module.get_html = add_histogram(module.get_html, module, user) return module
def xblock_from_json( self, class_, course_key, block_key, json_data, course_entry_override=None, **kwargs ): if course_entry_override is None: course_entry_override = self.course_entry else: # most recent retrieval is most likely the right one for next caller (see comment above fn) self.course_entry = CourseEnvelope(course_entry_override.course_key, self.course_entry.structure) definition_id = json_data.get('definition') # If no usage id is provided, generate an in-memory id if block_key is None: block_key = BlockKey(json_data['block_type'], LocalId()) if definition_id is not None and not json_data.get('definition_loaded', False): definition_loader = DefinitionLazyLoader( self.modulestore, course_key, block_key.type, definition_id, lambda fields: self.modulestore.convert_references_to_keys( course_key, self.load_block_type(block_key.type), fields, self.course_entry.structure['blocks'], ) ) else: definition_loader = None # If no definition id is provide, generate an in-memory id if definition_id is None: definition_id = LocalId() # Construct the Block Usage Locator: block_locator = course_key.make_usage_key( block_type=block_key.type, block_id=block_key.id, ) converted_fields = self.modulestore.convert_references_to_keys( block_locator.course_key, class_, json_data.get('fields', {}), self.course_entry.structure['blocks'], ) if block_key in self._parent_map: parent_key = self._parent_map[block_key] parent = course_key.make_usage_key(parent_key.type, parent_key.id) else: parent = None kvs = SplitMongoKVS( definition_loader, converted_fields, parent=parent, field_decorator=kwargs.get('field_decorator') ) if InheritanceMixin in self.modulestore.xblock_mixins: field_data = inheriting_field_data(kvs) else: field_data = KvsFieldData(kvs) try: module = self.construct_xblock_from_class( class_, ScopeIds(None, block_key.type, definition_id, block_locator), field_data, ) except Exception: log.warning("Failed to load descriptor", exc_info=True) return ErrorDescriptor.from_json( json_data, self, course_entry_override.course_key.make_usage_key( block_type='error', block_id=block_key.id ), error_msg=exc_info_to_str(sys.exc_info()) ) edit_info = json_data.get('edit_info', {}) module._edited_by = edit_info.get('edited_by') module._edited_on = edit_info.get('edited_on') module.previous_version = edit_info.get('previous_version') module.update_version = edit_info.get('update_version') module.source_version = edit_info.get('source_version', None) module.definition_locator = DefinitionLocator(block_key.type, definition_id) # decache any pending field settings module.save() # If this is an in-memory block, store it in this system if isinstance(block_locator.block_id, LocalId): self.local_modules[block_locator] = module return module
def get_module_for_descriptor_internal(user, descriptor, field_data_cache, course_id, track_function, xqueue_callback_url_prefix, position=None, wrap_xmodule_display=True, grade_bucket_type=None, static_asset_path=''): """ Actually implement get_module, without requiring a request. See get_module() docstring for further details. """ # Short circuit--if the user shouldn't have access, bail without doing any work if not has_access(user, descriptor, 'load', course_id): return None # Setup system context for module instance ajax_url = reverse( 'modx_dispatch', kwargs=dict(course_id=course_id, location=descriptor.location.url(), dispatch=''), ) # Intended use is as {ajax_url}/{dispatch_command}, so get rid of the trailing slash. ajax_url = ajax_url.rstrip('/') def make_xqueue_callback(dispatch='score_update'): # Fully qualified callback URL for external queueing system relative_xqueue_callback_url = reverse( 'xqueue_callback', kwargs=dict(course_id=course_id, userid=str(user.id), mod_id=descriptor.location.url(), dispatch=dispatch), ) return xqueue_callback_url_prefix + relative_xqueue_callback_url # Default queuename is course-specific and is derived from the course that # contains the current module. # TODO: Queuename should be derived from 'course_settings.json' of each course xqueue_default_queuename = descriptor.location.org + '-' + descriptor.location.course xqueue = { 'interface': xqueue_interface, 'construct_callback': make_xqueue_callback, 'default_queuename': xqueue_default_queuename.replace(' ', '_'), 'waittime': settings.XQUEUE_WAITTIME_BETWEEN_REQUESTS } # This is a hacky way to pass settings to the combined open ended xmodule # It needs an S3 interface to upload images to S3 # It needs the open ended grading interface in order to get peer grading to be done # this first checks to see if the descriptor is the correct one, and only sends settings if it is # Get descriptor metadata fields indicating needs for various settings needs_open_ended_interface = getattr(descriptor, "needs_open_ended_interface", False) needs_s3_interface = getattr(descriptor, "needs_s3_interface", False) # Initialize interfaces to None open_ended_grading_interface = None s3_interface = None # Create interfaces if needed if needs_open_ended_interface: open_ended_grading_interface = settings.OPEN_ENDED_GRADING_INTERFACE open_ended_grading_interface[ 'mock_peer_grading'] = settings.MOCK_PEER_GRADING open_ended_grading_interface[ 'mock_staff_grading'] = settings.MOCK_STAFF_GRADING if needs_s3_interface: s3_interface = { 'access_key': getattr(settings, 'AWS_ACCESS_KEY_ID', ''), 'secret_access_key': getattr(settings, 'AWS_SECRET_ACCESS_KEY', ''), 'storage_bucket_name': getattr(settings, 'AWS_STORAGE_BUCKET_NAME', 'openended') } def inner_get_module(descriptor): """ Delegate to get_module_for_descriptor_internal() with all values except `descriptor` set. Because it does an access check, it may return None. """ # TODO: fix this so that make_xqueue_callback uses the descriptor passed into # inner_get_module, not the parent's callback. Add it as an argument.... return get_module_for_descriptor_internal( user, descriptor, field_data_cache, course_id, track_function, make_xqueue_callback, position, wrap_xmodule_display, grade_bucket_type, static_asset_path) def xblock_field_data(descriptor): student_data = DbModel(DjangoKeyValueStore(field_data_cache)) return lms_field_data(descriptor._field_data, student_data) def publish(event): """A function that allows XModules to publish events. This only supports grade changes right now.""" if event.get('event_name') != 'grade': return # Construct the key for the module key = KeyValueStore.Key(scope=Scope.user_state, student_id=user.id, block_scope_id=descriptor.location, field_name='grade') student_module = field_data_cache.find_or_create(key) # Update the grades student_module.grade = event.get('value') student_module.max_grade = event.get('max_value') # Save all changes to the underlying KeyValueStore student_module.save() # Bin score into range and increment stats score_bucket = get_score_bucket(student_module.grade, student_module.max_grade) org, course_num, run = course_id.split("/") tags = [ "org:{0}".format(org), "course:{0}".format(course_num), "run:{0}".format(run), "score_bucket:{0}".format(score_bucket) ] if grade_bucket_type is not None: tags.append('type:%s' % grade_bucket_type) statsd.increment("lms.courseware.question_answered", tags=tags) # TODO (cpennington): When modules are shared between courses, the static # prefix is going to have to be specific to the module, not the directory # that the xml was loaded from system = ModuleSystem( track_function=track_function, render_template=render_to_string, ajax_url=ajax_url, xqueue=xqueue, # TODO (cpennington): Figure out how to share info between systems filestore=descriptor.system.resources_fs, get_module=inner_get_module, user=user, # TODO (cpennington): This should be removed when all html from # a module is coming through get_html and is therefore covered # by the replace_static_urls code below replace_urls=partial( static_replace.replace_static_urls, data_directory=getattr(descriptor, 'data_dir', None), course_id=course_id, static_asset_path=static_asset_path or descriptor.static_asset_path, ), replace_course_urls=partial(static_replace.replace_course_urls, course_id=course_id), replace_jump_to_id_urls=partial(static_replace.replace_jump_to_id_urls, course_id=course_id, jump_to_id_base_url=reverse( 'jump_to_id', kwargs={ 'course_id': course_id, 'module_id': '' })), node_path=settings.NODE_PATH, xblock_field_data=xblock_field_data, publish=publish, anonymous_student_id=unique_id_for_user(user), course_id=course_id, open_ended_grading_interface=open_ended_grading_interface, s3_interface=s3_interface, cache=cache, can_execute_unsafe_code=(lambda: can_execute_unsafe_code(course_id)), # TODO: When we merge the descriptor and module systems, we can stop reaching into the mixologist (cpennington) mixins=descriptor.system.mixologist._mixins, ) # pass position specified in URL to module through ModuleSystem system.set('position', position) system.set('DEBUG', settings.DEBUG) if settings.MITX_FEATURES.get('ENABLE_PSYCHOMETRICS'): system.set( 'psychometrics_handler', # set callback for updating PsychometricsData make_psychometrics_data_update_handler(course_id, user, descriptor.location.url())) try: module = descriptor.xmodule(system) except: log.exception( "Error creating module from descriptor {0}".format(descriptor)) # make an ErrorDescriptor -- assuming that the descriptor's system is ok if has_access(user, descriptor.location, 'staff', course_id): err_descriptor_class = ErrorDescriptor else: err_descriptor_class = NonStaffErrorDescriptor err_descriptor = err_descriptor_class.from_descriptor( descriptor, error_msg=exc_info_to_str(sys.exc_info())) # Make an error module return err_descriptor.xmodule(system) system.set('user_is_staff', has_access(user, descriptor.location, 'staff', course_id)) _get_html = module.get_html if wrap_xmodule_display is True: _get_html = wrap_xmodule(module.get_html, module, 'xmodule_display.html') module.get_html = replace_static_urls(_get_html, getattr(descriptor, 'data_dir', None), course_id=course_id, static_asset_path=static_asset_path or descriptor.static_asset_path) # Allow URLs of the form '/course/' refer to the root of multicourse directory # hierarchy of this course module.get_html = replace_course_urls(module.get_html, course_id) # this will rewrite intra-courseware links # that use the shorthand /jump_to_id/<id>. This is very helpful # for studio authored courses (compared to the /course/... format) since it is # is durable with respect to moves and the author doesn't need to # know the hierarchy # NOTE: module_id is empty string here. The 'module_id' will get assigned in the replacement # function, we just need to specify something to get the reverse() to work module.get_html = replace_jump_to_id_urls( module.get_html, course_id, reverse('jump_to_id', kwargs={ 'course_id': course_id, 'module_id': '' })) if settings.MITX_FEATURES.get('DISPLAY_HISTOGRAMS_TO_STAFF'): if has_access(user, module, 'staff', course_id): module.get_html = add_histogram(module.get_html, module, user) # force the module to save after rendering module.get_html = save_module(module.get_html, module) return module