def test_error_block_xml_rendering(self): descriptor = ErrorBlock.from_xml(self.valid_xml, self.system, CourseLocationManager(self.course_id), self.error_msg) assert isinstance(descriptor, ErrorBlock) descriptor.xmodule_runtime = self.system context_repr = self.system.render(descriptor, STUDENT_VIEW).content assert self.error_msg in context_repr assert repr(self.valid_xml) in context_repr
def test_error_block_from_descriptor(self): descriptor = MagicMock( spec=XModuleDescriptor, runtime=self.system, location=self.location, ) error_descriptor = ErrorBlock.from_descriptor( descriptor, self.error_msg) self.assertIsInstance(error_descriptor, ErrorBlock) error_descriptor.xmodule_runtime = self.system context_repr = self.system.render(error_descriptor, STUDENT_VIEW).content self.assertIn(self.error_msg, context_repr) self.assertIn(repr(descriptor), context_repr)
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 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 ErrorBlock.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