def test_persist_dag(self): """ try saving temporary xblocks """ test_course = persistent_factories.PersistentCourseFactory.create(org='testx', prettyid='tempcourse', display_name='fun test course', user_id='testbot') test_chapter = XModuleDescriptor.load_from_json({'category': 'chapter', 'metadata': {'display_name': 'chapter n'}}, test_course.system, parent_xblock=test_course) test_def_content = '<problem>boo</problem>' test_problem = XModuleDescriptor.load_from_json({'category': 'problem', 'definition': {'data': test_def_content}}, test_course.system, parent_xblock=test_chapter) # better to pass in persisted parent over the subdag so # subdag gets the parent pointer (otherwise 2 ops, persist dag, update parent children, # persist parent persisted_course = modulestore('split').persist_xblock_dag(test_course, 'testbot') self.assertEqual(len(persisted_course.children), 1) persisted_chapter = persisted_course.get_children()[0] self.assertEqual(persisted_chapter.category, 'chapter') self.assertEqual(persisted_chapter.display_name, 'chapter n') self.assertEqual(len(persisted_chapter.children), 1) persisted_problem = persisted_chapter.get_children()[0] self.assertEqual(persisted_problem.category, 'problem') self.assertEqual(persisted_problem.data, test_def_content)
def create_xmodule(self, location, definition_data=None, metadata=None, system=None): """ Create the new xmodule but don't save it. Returns the new module. :param location: a Location--must have a category :param definition_data: can be empty. The initial definition_data for the kvs :param metadata: can be empty, the initial metadata for the kvs :param system: if you already have an xmodule from the course, the xmodule.system value """ if not isinstance(location, Location): location = Location(location) # differs from split mongo in that I believe most of this logic should be above the persistence # layer but added it here to enable quick conversion. I'll need to reconcile these. if metadata is None: metadata = {} if system is None: system = CachingDescriptorSystem( self, {}, self.default_class, None, self.error_tracker, self.render_template, {} ) xblock_class = XModuleDescriptor.load_class(location.category, self.default_class) if definition_data is None: if hasattr(xblock_class, 'data') and getattr(xblock_class, 'data').default is not None: definition_data = getattr(xblock_class, 'data').default else: definition_data = {} dbmodel = self._create_new_model_data(location.category, location, definition_data, metadata) xmodule = xblock_class(system, dbmodel) return xmodule
def _list_descriptors(): """Return a list of all registered XModuleDescriptor classes.""" return [ desc for desc in [ desc for (_, desc) in XModuleDescriptor.load_classes() ] ]
def create_item(request): parent_location = Location(request.POST["parent_location"]) category = request.POST["category"] display_name = request.POST.get("display_name") if not has_access(request.user, parent_location): raise PermissionDenied() parent = get_modulestore(category).get_item(parent_location) dest_location = parent_location.replace(category=category, name=uuid4().hex) # get the metadata, display_name, and definition from the request metadata = {} data = None template_id = request.POST.get("boilerplate") if template_id is not None: clz = XModuleDescriptor.load_class(category) if clz is not None: template = clz.get_template(template_id) if template is not None: metadata = template.get("metadata", {}) data = template.get("data") if display_name is not None: metadata["display_name"] = display_name get_modulestore(category).create_and_save_xmodule( dest_location, definition_data=data, metadata=metadata, system=parent.system ) if category not in DETACHED_CATEGORIES: get_modulestore(parent.location).update_children(parent_location, parent.children + [dest_location.url()]) return JsonResponse({"id": dest_location.url()})
def _list_modules(): """Return a list of all registered XModule classes.""" return [ desc.module_class for desc in [ desc for (_, desc) in XModuleDescriptor.load_classes() ] ] + XBLOCK_CLASSES
def load_mixed_class(category): """ Load an XBlock by category name, and apply all defined mixins """ component_class = XModuleDescriptor.load_class(category) mixologist = Mixologist(settings.XBLOCK_MIXINS) return mixologist.mix(component_class)
def load_from_json(json_data, system, default_class=None, parent_xblock=None): """ This method instantiates the correct subclass of XModuleDescriptor based on the contents of json_data. It does not persist it and can create one which has no usage id. parent_xblock is used to compute inherited metadata as well as to append the new xblock. json_data: - 'location' : must have this field - 'category': the xmodule category (required or location must be a Location) - 'metadata': a dict of locally set metadata (not inherited) - 'children': a list of children's usage_ids w/in this course - 'definition': - '_id' (optional): the usage_id of this. Will generate one if not given one. """ class_ = XModuleDescriptor.load_class( json_data.get('category', json_data.get('location', {}).get('category')), default_class ) usage_id = json_data.get('_id', None) if not '_inherited_settings' in json_data and parent_xblock is not None: json_data['_inherited_settings'] = parent_xblock.xblock_kvs.inherited_settings.copy() json_fields = json_data.get('fields', {}) for field_name in inheritance.InheritanceMixin.fields: if field_name in json_fields: json_data['_inherited_settings'][field_name] = json_fields[field_name] new_block = system.xblock_from_json(class_, usage_id, json_data) if parent_xblock is not None: parent_xblock.children.append(new_block.scope_ids.usage_id) # decache pending children field settings parent_xblock.save() return new_block
def create_block_from_xml(xml_data, system, org=None, course=None, default_class=None): """ Create an XBlock instance from XML data. `xml_data' is a string containing valid xml. `system` is an XMLParsingSystem. `org` and `course` are optional strings that will be used in the generated block's url identifiers. `default_class` is the class to instantiate of the XML indicates a class that can't be loaded. Returns the fully instantiated XBlock. """ node = etree.fromstring(xml_data) raw_class = XModuleDescriptor.load_class(node.tag, default_class) xblock_class = system.mixologist.mix(raw_class) # leave next line commented out - useful for low-level debugging # log.debug('[create_block_from_xml] tag=%s, class=%s' % (node.tag, xblock_class)) url_name = node.get('url_name', node.get('slug')) location = Location('i4x', org, course, node.tag, url_name) scope_ids = ScopeIds(None, location.category, location, location) xblock = xblock_class.parse_xml(node, system, scope_ids) return xblock
def _create(cls, target_class, **kwargs): """ Uses ``**kwargs``: :parent_location: (required): the location of the parent module (e.g. the parent course or section) :category: the category of the resulting item. :data: (optional): the data for the item (e.g. XML problem definition for a problem item) :display_name: (optional): the display name of the item :metadata: (optional): dictionary of metadata attributes :boilerplate: (optional) the boilerplate for overriding field values :target_class: is ignored """ DETACHED_CATEGORIES = ['about', 'static_tab', 'course_info'] # catch any old style users before they get into trouble assert not 'template' in kwargs data = kwargs.get('data') category = kwargs.get('category') display_name = kwargs.get('display_name') metadata = kwargs.get('metadata', {}) location = kwargs.get('location') if kwargs.get('boilerplate') is not None: template_id = kwargs.get('boilerplate') clz = XModuleDescriptor.load_class(category) template = clz.get_template(template_id) assert template is not None metadata.update(template.get('metadata', {})) if not isinstance(data, basestring): data.update(template.get('data')) store = kwargs.get('modulestore') # replace the display name with an optional parameter passed in from the caller if display_name is not None: metadata['display_name'] = display_name store.create_and_save_xmodule(location, metadata=metadata, definition_data=data) if location.category not in DETACHED_CATEGORIES: parent_location = Location(kwargs.get('parent_location')) assert location != parent_location # This code was based off that in cms/djangoapps/contentstore/views.py parent = kwargs.get('parent') or store.get_item(parent_location) parent.children.append(location.url()) store.update_children(parent_location, parent.children) return store.get_item(location)
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 _create_new_model_data(self, category, location, definition_data, metadata): """ To instantiate a new xmodule which will be saved latter, set up the dbModel and kvs """ kvs = MongoKeyValueStore(definition_data, [], metadata, location, category) class_ = XModuleDescriptor.load_class(category, self.default_class) model_data = DbModel(kvs, class_, None, MongoUsage(None, location)) model_data["category"] = category model_data["location"] = location return model_data
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 _load_item(self, usage_id, course_entry_override=None): # TODO ensure all callers of system.load_item pass just the id json_data = self.module_data.get(usage_id) if json_data is None: # deeper than initial descendant fetch or doesn't exist self.modulestore.cache_items(self, [usage_id], lazy=self.lazy) json_data = self.module_data.get(usage_id) if json_data is None: raise ItemNotFoundError class_ = XModuleDescriptor.load_class(json_data.get("category"), self.default_class) return self.xblock_from_json(class_, usage_id, json_data, course_entry_override)
def _load_item(self, usage_id, course_entry_override=None): # TODO ensure all callers of system.load_item pass just the id json_data = self.module_data.get(usage_id) if json_data is None: # deeper than initial descendant fetch or doesn't exist self.modulestore.cache_items(self, [usage_id], lazy=self.lazy) json_data = self.module_data.get(usage_id) if json_data is None: raise ItemNotFoundError class_ = XModuleDescriptor.load_class(json_data.get('category'), self.default_class) return self.xblock_from_json(class_, usage_id, json_data, course_entry_override)
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 test_temporary_xblocks(self): """ Test using load_from_json to create non persisted xblocks """ test_course = persistent_factories.PersistentCourseFactory.create(org='testx', prettyid='tempcourse', display_name='fun test course', user_id='testbot') test_chapter = XModuleDescriptor.load_from_json({'category': 'chapter', 'metadata': {'display_name': 'chapter n'}}, test_course.system, parent_xblock=test_course) self.assertIsInstance(test_chapter, SequenceDescriptor) self.assertEqual(test_chapter.display_name, 'chapter n') self.assertIn(test_chapter, test_course.get_children()) # test w/ a definition (e.g., a problem) test_def_content = '<problem>boo</problem>' test_problem = XModuleDescriptor.load_from_json({'category': 'problem', 'definition': {'data': test_def_content}}, test_course.system, parent_xblock=test_chapter) self.assertIsInstance(test_problem, CapaDescriptor) self.assertEqual(test_problem.data, test_def_content) self.assertIn(test_problem, test_chapter.get_children()) test_problem.display_name = 'test problem' self.assertEqual(test_problem.display_name, 'test problem')
def _create_item(request): """View for create items.""" parent_locator = BlockUsageLocator(request.json['parent_locator']) parent_location = loc_mapper().translate_locator_to_location( parent_locator) category = request.json['category'] display_name = request.json.get('display_name') if not has_access(request.user, parent_location): raise PermissionDenied() parent = get_modulestore(category).get_item(parent_location) dest_location = parent_location.replace(category=category, name=uuid4().hex) # get the metadata, display_name, and definition from the request metadata = {} data = None template_id = request.json.get('boilerplate') if template_id is not None: clz = XModuleDescriptor.load_class(category) if clz is not None: template = clz.get_template(template_id) if template is not None: metadata = template.get('metadata', {}) data = template.get('data') if display_name is not None: metadata['display_name'] = display_name get_modulestore(category).create_and_save_xmodule( dest_location, definition_data=data, metadata=metadata, system=parent.system, ) if category not in DETACHED_CATEGORIES: get_modulestore(parent.location).update_children( parent_location, parent.children + [dest_location.url()]) course_location = loc_mapper().translate_locator_to_location( parent_locator, get_course=True) locator = loc_mapper().translate_location(course_location.course_id, dest_location, False, True) return JsonResponse({"locator": unicode(locator)})
def _load_item(self, usage_id, course_entry_override=None): if isinstance(usage_id, BlockUsageLocator) and isinstance(usage_id.usage_id, LocalId): try: return self.local_modules[usage_id] except KeyError: raise ItemNotFoundError json_data = self.module_data.get(usage_id) if json_data is None: # deeper than initial descendant fetch or doesn't exist self.modulestore.cache_items(self, [usage_id], lazy=self.lazy) json_data = self.module_data.get(usage_id) if json_data is None: raise ItemNotFoundError(usage_id) class_ = XModuleDescriptor.load_class(json_data.get("category"), self.default_class) return self.xblock_from_json(class_, usage_id, json_data, course_entry_override)
def create_xmodule(self, location, definition_data=None, metadata=None, system=None): """ Create the new xmodule but don't save it. Returns the new module. :param location: a Location--must have a category :param definition_data: can be empty. The initial definition_data for the kvs :param metadata: can be empty, the initial metadata for the kvs :param system: if you already have an xblock from the course, the xblock.runtime value """ if not isinstance(location, Location): location = Location(location) # differs from split mongo in that I believe most of this logic should be above the persistence # layer but added it here to enable quick conversion. I'll need to reconcile these. if metadata is None: metadata = {} if system is None: system = CachingDescriptorSystem( modulestore=self, module_data={}, default_class=self.default_class, resources_fs=None, error_tracker=self.error_tracker, render_template=self.render_template, cached_metadata={}, mixins=self.xblock_mixins, ) xblock_class = XModuleDescriptor.load_class(location.category, self.default_class) if definition_data is None: if hasattr(xblock_class, 'data') and xblock_class.data.default is not None: definition_data = xblock_class.data.default else: definition_data = {} dbmodel = self._create_new_field_data(location.category, location, definition_data, metadata) xmodule = system.construct_xblock_from_class( xblock_class, dbmodel, # We're loading a descriptor, so student_id is meaningless # We also don't have separate notions of definition and usage ids yet, # so we use the location for both. ScopeIds(None, location.category, location, location) ) # decache any pending field settings from init xmodule.save() return xmodule
def _create_item(request): """View for create items.""" parent_locator = BlockUsageLocator(request.json['parent_locator']) parent_location = loc_mapper().translate_locator_to_location(parent_locator) try: category = request.json['category'] except KeyError: category = 'problem' display_name = request.json.get('display_name') if not has_access(request.user, parent_location): raise PermissionDenied() parent = get_modulestore(category).get_item(parent_location) dest_location = parent_location.replace(category=category, name=uuid4().hex) # get the metadata, display_name, and definition from the request metadata = {} data = None template_id = request.json.get('boilerplate') if template_id is not None: clz = XModuleDescriptor.load_class(category) if clz is not None: template = clz.get_template(template_id) if template is not None: metadata = template.get('metadata', {}) data = template.get('data') if display_name is not None: metadata['display_name'] = display_name get_modulestore(category).create_and_save_xmodule( dest_location, definition_data=data, metadata=metadata, system=parent.system, ) if category not in DETACHED_CATEGORIES: get_modulestore(parent.location).update_children(parent_location, parent.children + [dest_location.url()]) course_location = loc_mapper().translate_locator_to_location(parent_locator, get_course=True) locator = loc_mapper().translate_location(course_location.course_id, dest_location, False, True) return JsonResponse({"locator": unicode(locator)})
def _create_item(request): """View for create items.""" parent_locator = BlockUsageLocator(request.json['parent_locator']) parent_location = loc_mapper().translate_locator_to_location(parent_locator) category = request.json['category'] display_name = request.json.get('display_name') if not has_access(request.user, parent_location): raise PermissionDenied() parent = get_modulestore(category).get_item(parent_location) # Necessary to set revision=None or else metadata inheritance does not work # (the ID with @draft will be used as the key in the inherited metadata map, # and that is not expected by the code that later references it). dest_location = parent_location.replace(category=category, name=uuid4().hex, revision=None) # get the metadata, display_name, and definition from the request metadata = {} data = None template_id = request.json.get('boilerplate') if template_id is not None: clz = XModuleDescriptor.load_class(category) if clz is not None: template = clz.get_template(template_id) if template is not None: metadata = template.get('metadata', {}) data = template.get('data') if display_name is not None: metadata['display_name'] = display_name get_modulestore(category).create_and_save_xmodule( dest_location, definition_data=data, metadata=metadata, system=parent.system, ) if category not in DETACHED_CATEGORIES: get_modulestore(parent.location).update_children(parent_location, parent.children + [dest_location.url()]) course_location = loc_mapper().translate_locator_to_location(parent_locator, get_course=True) locator = loc_mapper().translate_location(course_location.course_id, dest_location, False, True) return JsonResponse({'id': dest_location.url(), "locator": unicode(locator)})
def load_from_json(json_data, system, default_class=None, parent_xblock=None): """ This method instantiates the correct subclass of XModuleDescriptor based on the contents of json_data. It does not persist it and can create one which has no usage id. parent_xblock is used to compute inherited metadata as well as to append the new xblock. json_data: - 'location' : must have this field - 'category': the xmodule category (required or location must be a Location) - 'metadata': a dict of locally set metadata (not inherited) - 'children': a list of children's usage_ids w/in this course - 'definition': - '_id' (optional): the usage_id of this. Will generate one if not given one. """ class_ = XModuleDescriptor.load_class( json_data.get('category', json_data.get('location', {}).get('category')), default_class) usage_id = json_data.get('_id', None) if not '_inherited_settings' in json_data and parent_xblock is not None: json_data[ '_inherited_settings'] = parent_xblock.xblock_kvs.get_inherited_settings( ).copy() json_fields = json_data.get('fields', {}) for field in inheritance.INHERITABLE_METADATA: if field in json_fields: json_data['_inherited_settings'][field] = json_fields[ field] new_block = system.xblock_from_json(class_, usage_id, json_data) if parent_xblock is not None: children = parent_xblock.children children.append(new_block) # trigger setter method by using top level field access parent_xblock.children = children # decache pending children field settings (Note, truly persisting at this point would break b/c # persistence assumes children is a list of ids not actual xblocks) parent_xblock.save() return new_block
def create_item(request): """View for create items.""" parent_location = Location(request.json['parent_location']) category = request.json['category'] display_name = request.json.get('display_name') if not has_access(request.user, parent_location): raise PermissionDenied() parent = get_modulestore(category).get_item(parent_location) dest_location = parent_location.replace(category=category, name=uuid4().hex) # get the metadata, display_name, and definition from the request metadata = {} data = None template_id = request.json.get('boilerplate') if template_id is not None: clz = XModuleDescriptor.load_class(category) if clz is not None: template = clz.get_template(template_id) if template is not None: metadata = template.get('metadata', {}) data = template.get('data') if display_name is not None: metadata['display_name'] = display_name get_modulestore(category).create_and_save_xmodule( dest_location, definition_data=data, metadata=metadata, system=parent.system, ) if category not in DETACHED_CATEGORIES: get_modulestore(parent.location).update_children(parent_location, parent.children + [dest_location.url()]) locator = loc_mapper().translate_location( get_course_for_item(parent_location).location.course_id, dest_location, False, True ) return JsonResponse({'id': dest_location.url(), "update_url": locator.url_reverse("xblock")})
def _load_item(self, usage_id, course_entry_override=None): if isinstance(usage_id, BlockUsageLocator) and isinstance( usage_id.usage_id, LocalId): try: return self.local_modules[usage_id] except KeyError: raise ItemNotFoundError json_data = self.module_data.get(usage_id) if json_data is None: # deeper than initial descendant fetch or doesn't exist self.modulestore.cache_items(self, [usage_id], lazy=self.lazy) json_data = self.module_data.get(usage_id) if json_data is None: raise ItemNotFoundError(usage_id) class_ = XModuleDescriptor.load_class(json_data.get('category'), self.default_class) return self.xblock_from_json(class_, usage_id, json_data, course_entry_override)
def _create_new_model_data(self, category, location, definition_data, metadata): """ To instantiate a new xmodule which will be saved latter, set up the dbModel and kvs """ kvs = MongoKeyValueStore( definition_data, [], metadata, location, category ) class_ = XModuleDescriptor.load_class( category, self.default_class ) model_data = DbModel(kvs, class_, None, MongoUsage(None, location)) model_data['category'] = category model_data['location'] = location return model_data
def create_item(request): parent_location = Location(request.POST['parent_location']) category = request.POST['category'] display_name = request.POST.get('display_name') if not has_access(request.user, parent_location): raise PermissionDenied() parent = get_modulestore(category).get_item(parent_location) dest_location = parent_location.replace(category=category, name=uuid4().hex) # get the metadata, display_name, and definition from the request metadata = {} data = None template_id = request.POST.get('boilerplate') if template_id is not None: clz = XModuleDescriptor.load_class(category) if clz is not None: template = clz.get_template(template_id) if template is not None: metadata = template.get('metadata', {}) data = template.get('data') if display_name is not None: metadata['display_name'] = display_name get_modulestore(category).create_and_save_xmodule( dest_location, definition_data=data, metadata=metadata, system=parent.system, ) if category not in DETACHED_CATEGORIES: get_modulestore(parent.location).update_children( parent_location, parent.children + [dest_location.url()]) return JsonResponse({'id': dest_location.url()})
def load_from_json(json_data, system, default_class=None, parent_xblock=None): """ This method instantiates the correct subclass of XModuleDescriptor based on the contents of json_data. It does not persist it and can create one which has no usage id. parent_xblock is used to compute inherited metadata as well as to append the new xblock. json_data: - 'location' : must have this field - 'category': the xmodule category (required or location must be a Location) - 'metadata': a dict of locally set metadata (not inherited) - 'children': a list of children's usage_ids w/in this course - 'definition': - '_id' (optional): the usage_id of this. Will generate one if not given one. """ class_ = XModuleDescriptor.load_class( json_data.get('category', json_data.get('location', {}).get('category')), default_class) usage_id = json_data.get('_id', None) if not '_inherited_settings' in json_data and parent_xblock is not None: json_data[ '_inherited_settings'] = parent_xblock.xblock_kvs.inherited_settings.copy( ) json_fields = json_data.get('fields', {}) for field_name in inheritance.InheritanceMixin.fields: if field_name in json_fields: json_data['_inherited_settings'][field_name] = json_fields[ field_name] new_block = system.xblock_from_json(class_, usage_id, json_data) if parent_xblock is not None: parent_xblock.children.append(new_block.scope_ids.usage_id) # decache pending children field settings parent_xblock.save() return new_block
def load_from_json(json_data, system, default_class=None, parent_xblock=None): """ This method instantiates the correct subclass of XModuleDescriptor based on the contents of json_data. It does not persist it and can create one which has no usage id. parent_xblock is used to compute inherited metadata as well as to append the new xblock. json_data: - 'location' : must have this field - 'category': the xmodule category (required or location must be a Location) - 'metadata': a dict of locally set metadata (not inherited) - 'children': a list of children's usage_ids w/in this course - 'definition': - '_id' (optional): the usage_id of this. Will generate one if not given one. """ class_ = XModuleDescriptor.load_class( json_data.get('category', json_data.get('location', {}).get('category')), default_class ) usage_id = json_data.get('_id', None) if not '_inherited_settings' in json_data and parent_xblock is not None: json_data['_inherited_settings'] = parent_xblock.xblock_kvs.get_inherited_settings().copy() json_fields = json_data.get('fields', {}) for field in inheritance.INHERITABLE_METADATA: if field in json_fields: json_data['_inherited_settings'][field] = json_fields[field] new_block = system.xblock_from_json(class_, usage_id, json_data) if parent_xblock is not None: children = parent_xblock.children children.append(new_block) # trigger setter method by using top level field access parent_xblock.children = children # decache pending children field settings (Note, truly persisting at this point would break b/c # persistence assumes children is a list of ids not actual xblocks) parent_xblock.save() return new_block
def create_xmodule(self, location, definition_data=None, metadata=None, system=None): """ Create the new xmodule but don't save it. Returns the new module. :param location: a Location--must have a category :param definition_data: can be empty. The initial definition_data for the kvs :param metadata: can be empty, the initial metadata for the kvs :param system: if you already have an xmodule from the course, the xmodule.system value """ if not isinstance(location, Location): location = Location(location) # differs from split mongo in that I believe most of this logic should be above the persistence # layer but added it here to enable quick conversion. I'll need to reconcile these. if metadata is None: metadata = {} if system is None: system = CachingDescriptorSystem(self, {}, self.default_class, None, self.error_tracker, self.render_template, {}) xblock_class = XModuleDescriptor.load_class(location.category, self.default_class) if definition_data is None: if hasattr(xblock_class, 'data') and getattr( xblock_class, 'data').default is not None: definition_data = getattr(xblock_class, 'data').default else: definition_data = {} dbmodel = self._create_new_model_data(location.category, location, definition_data, metadata) xmodule = xblock_class(system, dbmodel) # decache any pending field settings from init xmodule.save() return xmodule
mock_grade_histogram.return_value = [] module = render.get_module( self.user, self.request, self.location, self.field_data_cache, self.course.id, ) module.render('student_view') self.assertTrue(mock_grade_histogram.called) PER_COURSE_ANONYMIZED_DESCRIPTORS = (LTIDescriptor, ) PER_STUDENT_ANONYMIZED_DESCRIPTORS = [ class_ for (name, class_) in XModuleDescriptor.load_classes() if not issubclass(class_, PER_COURSE_ANONYMIZED_DESCRIPTORS) ] @ddt @override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE) class TestAnonymousStudentId(ModuleStoreTestCase, LoginEnrollmentTestCase): """ Test that anonymous_student_id is set correctly across a variety of XBlock types """ def setUp(self): self.user = UserFactory() @patch('courseware.module_render.has_access', Mock(return_value=True)) def _get_anonymous_id(self, course_id, xblock_class):
mock_grade_histogram.return_value = [] module = render.get_module( self.user, self.request, self.location, self.field_data_cache, self.course.id, ) module.render('student_view') self.assertTrue(mock_grade_histogram.called) PER_COURSE_ANONYMIZED_DESCRIPTORS = (LTIDescriptor, ) PER_STUDENT_ANONYMIZED_DESCRIPTORS = [ class_ for (name, class_) in XModuleDescriptor.load_classes() if not issubclass(class_, PER_COURSE_ANONYMIZED_DESCRIPTORS) ] @ddt @override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE) class TestAnonymousStudentId(ModuleStoreTestCase, LoginEnrollmentTestCase): """ Test that anonymous_student_id is set correctly across a variety of XBlock types """ def setUp(self): self.user = UserFactory() @patch('courseware.module_render.has_access', Mock(return_value=True))
def process_xml(self, xml): # pylint: disable=method-hidden """Parse `xml` as an XModuleDescriptor, and add it to `self._descriptors`""" descriptor = XModuleDescriptor.load_from_xml(xml, self, self.org, self.course, self.default_class) self._descriptors[descriptor.location.url()] = descriptor return descriptor
def _list_descriptors(): """Return a list of all registered XModuleDescriptor classes.""" return [ desc for desc in [desc for (_, desc) in XModuleDescriptor.load_classes()] ]
def test_load_class(self): vc = XModuleDescriptor.load_class('sequential') vc_str = "<class 'xmodule.seq_module.SequenceDescriptor'>" assert str(vc) == vc_str
def _list_descriptors(): return [ desc for desc in [ desc for (_, desc) in XModuleDescriptor.load_classes() ] ]
def _list_descriptors(): return [desc for desc in [desc for (_, desc) in XModuleDescriptor.load_classes()]]
def _list_descriptors(): """Return a list of all registered XModuleDescriptor classes.""" return sorted([desc for (_, desc) in XModuleDescriptor.load_classes()] + XBLOCK_CLASSES, key=str)
def test_load_class(self): vc = XModuleDescriptor.load_class('video') vc_str = "<class 'xmodule.video_module.VideoDescriptor'>" self.assertEqual(str(vc), vc_str)
def _list_modules(): """Return a list of all registered XModule classes.""" return [ desc.module_class for desc in [desc for (_, desc) in XModuleDescriptor.load_classes()] ] + XBLOCK_CLASSES
def edit_unit(request, location): """ Display an editing page for the specified module. Expects a GET request with the parameter `id`. id: A Location URL """ try: course = get_course_for_item(location) except InvalidLocationError: return HttpResponseBadRequest() if not has_access(request.user, course.location): raise PermissionDenied() try: item = modulestore().get_item(location, depth=1) except ItemNotFoundError: return HttpResponseBadRequest() lms_link = get_lms_link_for_item( item.location, course_id=course.location.course_id ) component_templates = defaultdict(list) for category in COMPONENT_TYPES: component_class = XModuleDescriptor.load_class(category) # add the default template component_templates[category].append(( component_class.display_name.default or 'Blank', category, False, # No defaults have markdown (hardcoded current default) None # no boilerplate for overrides )) # add boilerplates for template in component_class.templates(): component_templates[category].append(( template['metadata'].get('display_name'), category, template['metadata'].get('markdown') is not None, template.get('template_id') )) # Check if there are any advanced modules specified in the course policy. # These modules should be specified as a list of strings, where the strings # are the names of the modules in ADVANCED_COMPONENT_TYPES that should be # enabled for the course. course_advanced_keys = course.advanced_modules # Set component types according to course policy file if isinstance(course_advanced_keys, list): for category in course_advanced_keys: if category in ADVANCED_COMPONENT_TYPES: # Do I need to allow for boilerplates or just defaults on the # class? i.e., can an advanced have more than one entry in the # menu? one for default and others for prefilled boilerplates? try: component_class = XModuleDescriptor.load_class(category) component_templates['advanced'].append(( component_class.display_name.default or category, category, False, None # don't override default data )) except PluginMissingError: # dhm: I got this once but it can happen any time the # course author configures an advanced component which does # not exist on the server. This code here merely # prevents any authors from trying to instantiate the # non-existent component type by not showing it in the menu pass else: log.error( "Improper format for course advanced keys! %", course_advanced_keys ) components = [ component.location.url() for component in item.get_children() ] # TODO (cpennington): If we share units between courses, # this will need to change to check permissions correctly so as # to pick the correct parent subsection containing_subsection_locs = modulestore().get_parent_locations( location, None ) containing_subsection = modulestore().get_item(containing_subsection_locs[0]) containing_section_locs = modulestore().get_parent_locations( containing_subsection.location, None ) containing_section = modulestore().get_item(containing_section_locs[0]) # cdodge hack. We're having trouble previewing drafts via jump_to redirect # so let's generate the link url here # need to figure out where this item is in the list of children as the # preview will need this index = 1 for child in containing_subsection.get_children(): if child.location == item.location: break index = index + 1 preview_lms_base = settings.MITX_FEATURES.get('PREVIEW_LMS_BASE') preview_lms_link = ( '//{preview_lms_base}/courses/{org}/{course}/' '{course_name}/courseware/{section}/{subsection}/{index}' ).format( preview_lms_base=preview_lms_base, lms_base=settings.LMS_BASE, org=course.location.org, course=course.location.course, course_name=course.location.name, section=containing_section.location.name, subsection=containing_subsection.location.name, index=index ) unit_state = compute_unit_state(item) return render_to_response('unit.html', { 'context_course': course, 'unit': item, 'unit_location': location, 'components': components, 'component_templates': component_templates, 'draft_preview_link': preview_lms_link, 'published_preview_link': lms_link, 'subsection': containing_subsection, 'release_date': get_default_time_display(containing_subsection.lms.start) if containing_subsection.lms.start is not None else None, 'section': containing_section, 'new_unit_category': 'vertical', 'unit_state': unit_state, 'published_date': get_default_time_display(item.cms.published_date) if item.cms.published_date is not None else None })
def test_load_class(self): vc = XModuleDescriptor.load_class('sequential') vc_str = "<class 'xmodule.seq_module.SequenceDescriptor'>" self.assertEqual(str(vc), vc_str)
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 "" 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 = 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 _create(cls, target_class, **kwargs): """ Uses ``**kwargs``: :parent_location: (required): the location of the parent module (e.g. the parent course or section) :category: the category of the resulting item. :data: (optional): the data for the item (e.g. XML problem definition for a problem item) :display_name: (optional): the display name of the item :metadata: (optional): dictionary of metadata attributes :boilerplate: (optional) the boilerplate for overriding field values :target_class: is ignored """ # All class attributes (from this class and base classes) are # passed in via **kwargs. However, some of those aren't actual field values, # so pop those off for use separately DETACHED_CATEGORIES = ['about', 'static_tab', 'course_info'] # catch any old style users before they get into trouble assert 'template' not in kwargs parent_location = Location(kwargs.pop('parent_location', None)) data = kwargs.pop('data', None) category = kwargs.pop('category', None) display_name = kwargs.pop('display_name', None) metadata = kwargs.pop('metadata', {}) location = kwargs.pop('location') assert location != parent_location store = kwargs.pop('modulestore') # This code was based off that in cms/djangoapps/contentstore/views.py parent = kwargs.pop('parent', None) or store.get_item(parent_location) if 'boilerplate' in kwargs: template_id = kwargs.pop('boilerplate') clz = XModuleDescriptor.load_class(category) template = clz.get_template(template_id) assert template is not None metadata.update(template.get('metadata', {})) if not isinstance(data, basestring): data.update(template.get('data')) # replace the display name with an optional parameter passed in from the caller if display_name is not None: metadata['display_name'] = display_name module = store.create_and_save_xmodule(location, metadata=metadata, definition_data=data) module = store.get_item(location) for attr, val in kwargs.items(): setattr(module, attr, val) module.save() store.save_xmodule(module) if location.category not in DETACHED_CATEGORIES: parent.children.append(location.url()) store.update_children(parent_location, parent.children) return store.get_item(location)
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 "" 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 = 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 test_load_class(self): vc = XModuleDescriptor.load_class('video') vc_str = "<class 'xmodule.video_module.video_module.VideoDescriptor'>" self.assertEqual(str(vc), vc_str)
def _create(cls, target_class, **kwargs): """ Uses ``**kwargs``: :parent_location: (required): the location of the parent module (e.g. the parent course or section) :category: the category of the resulting item. :data: (optional): the data for the item (e.g. XML problem definition for a problem item) :display_name: (optional): the display name of the item :metadata: (optional): dictionary of metadata attributes :boilerplate: (optional) the boilerplate for overriding field values :target_class: is ignored """ # All class attributes (from this class and base classes) are # passed in via **kwargs. However, some of those aren't actual field values, # so pop those off for use separately DETACHED_CATEGORIES = ["about", "static_tab", "course_info"] # catch any old style users before they get into trouble assert "template" not in kwargs parent_location = Location(kwargs.pop("parent_location", None)) data = kwargs.pop("data", None) category = kwargs.pop("category", None) display_name = kwargs.pop("display_name", None) metadata = kwargs.pop("metadata", {}) location = kwargs.pop("location") assert location != parent_location store = kwargs.pop("modulestore") # This code was based off that in cms/djangoapps/contentstore/views.py parent = kwargs.pop("parent", None) or store.get_item(parent_location) if "boilerplate" in kwargs: template_id = kwargs.pop("boilerplate") clz = XModuleDescriptor.load_class(category) template = clz.get_template(template_id) assert template is not None metadata.update(template.get("metadata", {})) if not isinstance(data, basestring): data.update(template.get("data")) # replace the display name with an optional parameter passed in from the caller if display_name is not None: metadata["display_name"] = display_name module = store.create_and_save_xmodule(location, metadata=metadata, definition_data=data) module = store.get_item(location) for attr, val in kwargs.items(): setattr(module, attr, val) module.save() store.save_xmodule(module) if location.category not in DETACHED_CATEGORIES: parent.children.append(location.url()) store.update_children(parent_location, parent.children) return store.get_item(location)