def test_old_location_helpers(self): """ Test the functions intended to help with the conversion from old locations to locators """ location_tuple = ('i4x', 'mit', 'eecs.6002x', 'course', 't3_2013') location = Location(location_tuple) self.assertEqual(location, Locator.to_locator_or_location(location)) self.assertEqual(location, Locator.to_locator_or_location(location_tuple)) self.assertEqual(location, Locator.to_locator_or_location(list(location_tuple))) self.assertEqual(location, Locator.to_locator_or_location(location.dict())) locator = BlockUsageLocator(package_id='foo.bar', branch='alpha', block_id='deep') self.assertEqual(locator, Locator.to_locator_or_location(locator)) self.assertEqual(locator.as_course_locator(), Locator.to_locator_or_location(locator.as_course_locator())) self.assertEqual(location, Locator.to_locator_or_location(location.url())) self.assertEqual(locator, Locator.to_locator_or_location(locator.url())) self.assertEqual(locator, Locator.to_locator_or_location(locator.__dict__)) asset_location = Location(['c4x', 'mit', 'eecs.6002x', 'asset', 'selfie.jpeg']) self.assertEqual(asset_location, Locator.to_locator_or_location(asset_location)) self.assertEqual(asset_location, Locator.to_locator_or_location(asset_location.url())) def_location_url = "defx://version/" + '{:024x}'.format(random.randrange(16 ** 24)) self.assertEqual(DefinitionLocator(def_location_url), Locator.to_locator_or_location(def_location_url)) with self.assertRaises(ValueError): Locator.to_locator_or_location(22) with self.assertRaises(ValueError): Locator.to_locator_or_location("hello.world.not.a.url") self.assertIsNone(Locator.parse_url("unknown://foo.bar/baz"))
def _construct(cls, system, contents, error_msg, location): if isinstance(location, dict) and 'course' in location: location = Location(location) if isinstance(location, Location) and location.name is None: location = location.replace( category='error', # Pick a unique url_name -- the sha1 hash of the contents. # NOTE: We could try to pull out the url_name of the errored descriptor, # but url_names aren't guaranteed to be unique between descriptor types, # and ErrorDescriptor can wrap any type. When the wrapped module is fixed, # it will be written out with the original url_name. name=hashlib.sha1(contents.encode('utf8')).hexdigest()) # real metadata stays in the content, but add a display name model_data = { 'error_msg': str(error_msg), 'contents': contents, 'display_name': 'Error: ' + location.url(), 'location': location, 'category': 'error' } return cls( system, model_data, )
def _construct(cls, system, contents, error_msg, location): location = Location(location) if location.category == "error": location = location.replace( # Pick a unique url_name -- the sha1 hash of the contents. # NOTE: We could try to pull out the url_name of the errored descriptor, # but url_names aren't guaranteed to be unique between descriptor types, # and ErrorDescriptor can wrap any type. When the wrapped module is fixed, # it will be written out with the original url_name. name=hashlib.sha1(contents.encode("utf8")).hexdigest() ) # real metadata stays in the content, but add a display name field_data = DictFieldData( { "error_msg": str(error_msg), "contents": contents, "display_name": "Error: " + location.url(), "location": location, "category": "error", } ) return system.construct_xblock_from_class( cls, # The error module doesn't use scoped data, and thus doesn't need # real scope keys ScopeIds("error", None, location, location), field_data, )
def _clone_modules(modulestore, modules, dest_location): for module in modules: original_loc = Location(module.location) if original_loc.category != 'course': module.location = module.location._replace( tag=dest_location.tag, org=dest_location.org, course=dest_location.course) else: # on the course module we also have to update the module name module.location = module.location._replace( tag=dest_location.tag, org=dest_location.org, course=dest_location.course, name=dest_location.name) print "Cloning module {0} to {1}....".format(original_loc, module.location) # NOTE: usage of the the internal module.xblock_kvs._data does not include any 'default' values for the fields modulestore.update_item(module.location, module.xblock_kvs._data) # repoint children if module.has_children: new_children = [] for child_loc_url in module.children: child_loc = Location(child_loc_url) child_loc = child_loc._replace( tag=dest_location.tag, org=dest_location.org, course=dest_location.course ) new_children.append(child_loc.url()) modulestore.update_children(module.location, new_children) # save metadata modulestore.update_metadata(module.location, own_metadata(module))
def _construct(cls, system, contents, error_msg, location): if isinstance(location, dict) and 'course' in location: location = Location(location) if isinstance(location, Location) and location.name is None: location = location.replace( category='error', # Pick a unique url_name -- the sha1 hash of the contents. # NOTE: We could try to pull out the url_name of the errored descriptor, # but url_names aren't guaranteed to be unique between descriptor types, # and ErrorDescriptor can wrap any type. When the wrapped module is fixed, # it will be written out with the original url_name. name=hashlib.sha1(contents.encode('utf8')).hexdigest()) # real metadata stays in the content, but add a display name field_data = DictFieldData({ 'error_msg': str(error_msg), 'contents': contents, 'display_name': 'Error: ' + location.url(), 'location': location, 'category': 'error' }) return system.construct_xblock_from_class( cls, field_data, # The error module doesn't use scoped data, and thus doesn't need # real scope keys ScopeIds('error', None, location, location))
def delete_item(request): item_location = request.POST['id'] item_location = Location(item_location) # check permissions for this user within this course if not has_access(request.user, item_location): raise PermissionDenied() # optional parameter to delete all children (default False) delete_children = request.POST.get('delete_children', False) delete_all_versions = request.POST.get('delete_all_versions', False) store = get_modulestore(item_location) item = store.get_item(item_location) if delete_children: _xmodule_recurse(item, lambda i: store.delete_item(i.location, delete_all_versions)) else: store.delete_item(item.location, delete_all_versions) # cdodge: we need to remove our parent's pointer to us so that it is no longer dangling if delete_all_versions: parent_locs = modulestore('direct').get_parent_locations(item_location, None) for parent_loc in parent_locs: parent = modulestore('direct').get_item(parent_loc) item_url = item_location.url() if item_url in parent.children: children = parent.children children.remove(item_url) parent.children = children modulestore('direct').update_children(parent.location, parent.children) return JsonResponse()
def _create_item(self, category, name, data, metadata, parent_category, parent_name, runtime): """ Create the item of the given category and block id in split and old mongo, add it to the optional parent. The parent category is only needed because old mongo requires it for the id. """ location = Location('i4x', 'test_org', 'test_course', category, name) self.old_mongo.create_and_save_xmodule(location, data, metadata, runtime) if isinstance(data, basestring): fields = {'data': data} else: fields = data.copy() fields.update(metadata) if parent_name: # add child to parent in mongo parent_location = Location('i4x', 'test_org', 'test_course', parent_category, parent_name) parent = self.old_mongo.get_item(parent_location) parent.children.append(location.url()) self.old_mongo.update_item(parent, self.userid) # create pointer for split course_or_parent_locator = BlockUsageLocator( package_id=self.split_package_id, branch='draft', block_id=parent_name ) else: course_or_parent_locator = CourseLocator( package_id='test_org.test_course.runid', branch='draft', ) self.split_mongo.create_item(course_or_parent_locator, category, self.userid, block_id=name, fields=fields)
def _construct(cls, system, contents, error_msg, location): if isinstance(location, dict) and 'course' in location: location = Location(location) if isinstance(location, Location) and location.name is None: location = location.replace( category='error', # Pick a unique url_name -- the sha1 hash of the contents. # NOTE: We could try to pull out the url_name of the errored descriptor, # but url_names aren't guaranteed to be unique between descriptor types, # and ErrorDescriptor can wrap any type. When the wrapped module is fixed, # it will be written out with the original url_name. name=hashlib.sha1(contents.encode('utf8')).hexdigest() ) # real metadata stays in the content, but add a display name field_data = DictFieldData({ 'error_msg': str(error_msg), 'contents': contents, 'display_name': 'Error: ' + location.url(), 'location': location, 'category': 'error' }) return system.construct_xblock_from_class( cls, field_data, # The error module doesn't use scoped data, and thus doesn't need # real scope keys ScopeIds('error', None, location, location) )
def _create_item(self, category, name, data, metadata, parent_category, parent_name, runtime): """ Create the item of the given category and block id in split and old mongo, add it to the optional parent. The parent category is only needed because old mongo requires it for the id. """ location = Location('i4x', 'test_org', 'test_course', category, name) self.old_mongo.create_and_save_xmodule(location, data, metadata, runtime) if isinstance(data, basestring): fields = {'data': data} else: fields = data.copy() fields.update(metadata) if parent_name: # add child to parent in mongo parent_location = Location('i4x', 'test_org', 'test_course', parent_category, parent_name) parent = self.old_mongo.get_item(parent_location) parent.children.append(location.url()) self.old_mongo.update_children(parent_location, parent.children) # create pointer for split course_or_parent_locator = BlockUsageLocator( course_id=self.split_course_id, branch='draft', usage_id=parent_name ) else: course_or_parent_locator = CourseLocator( course_id='test_org.test_course.runid', branch='draft', ) self.split_mongo.create_item(course_or_parent_locator, category, self.userid, usage_id=name, fields=fields)
def _clone_modules(modulestore, modules, source_location, dest_location): for module in modules: original_loc = Location(module.location) if original_loc.category != "course": module.location = module.location._replace( tag=dest_location.tag, org=dest_location.org, course=dest_location.course ) else: # on the course module we also have to update the module name module.location = module.location._replace( tag=dest_location.tag, org=dest_location.org, course=dest_location.course, name=dest_location.name ) print "Cloning module {0} to {1}....".format(original_loc, module.location) if "data" in module.fields and module.fields["data"].is_set_on(module) and isinstance(module.data, basestring): module.data = rewrite_nonportable_content_links( source_location.course_id, dest_location.course_id, module.data ) # repoint children if module.has_children: new_children = [] for child_loc_url in module.children: child_loc = Location(child_loc_url) child_loc = child_loc._replace( tag=dest_location.tag, org=dest_location.org, course=dest_location.course ) new_children.append(child_loc.url()) module.children = new_children modulestore.update_item(module, "**replace_user**")
def delete_item(request): item_location = request.POST['id'] item_loc = Location(item_location) # check permissions for this user within this course if not has_access(request.user, item_location): raise PermissionDenied() # optional parameter to delete all children (default False) delete_children = request.POST.get('delete_children', False) delete_all_versions = request.POST.get('delete_all_versions', False) store = get_modulestore(item_location) item = store.get_item(item_location) if delete_children: _xmodule_recurse(item, lambda i: store.delete_item(i.location, delete_all_versions)) else: store.delete_item(item.location, delete_all_versions) # cdodge: we need to remove our parent's pointer to us so that it is no longer dangling if delete_all_versions: parent_locs = modulestore('direct').get_parent_locations(item_loc, None) for parent_loc in parent_locs: parent = modulestore('direct').get_item(parent_loc) item_url = item_loc.url() if item_url in parent.children: children = parent.children children.remove(item_url) parent.children = children modulestore('direct').update_children(parent.location, parent.children) return HttpResponse()
def _construct(cls, system, contents, error_msg, location): if isinstance(location, dict) and 'course' in location: location = Location(location) if isinstance(location, Location) and location.name is None: location = location.replace( category='error', # Pick a unique url_name -- the sha1 hash of the contents. # NOTE: We could try to pull out the url_name of the errored descriptor, # but url_names aren't guaranteed to be unique between descriptor types, # and ErrorDescriptor can wrap any type. When the wrapped module is fixed, # it will be written out with the original url_name. name=hashlib.sha1(contents.encode('utf8')).hexdigest() ) # real metadata stays in the content, but add a display name model_data = { 'error_msg': str(error_msg), 'contents': contents, 'display_name': 'Error: ' + location.url(), 'location': location, 'category': 'error' } return cls( system, model_data, )
def export_all_for_course(self, course_location, output_directory, assets_policy_file): """ Export all of this course's assets to the output_directory. Export all of the assets' attributes to the policy file. :param course_location: the Location of type 'course' :param output_directory: the directory under which to put all the asset files :param assets_policy_file: the filename for the policy file which should be in the same directory as the other policy files. """ policy = {} assets = self.get_all_content_for_course(course_location) for asset in assets: asset_location = Location(asset['_id']) self.export(asset_location, output_directory) for attr, value in asset.iteritems(): if attr not in [ '_id', 'md5', 'uploadDate', 'length', 'chunkSize' ]: policy.setdefault(asset_location.url(), {})[attr] = value with open(assets_policy_file, 'w') as f: json.dump(policy, f)
def create_item(self, course_or_parent_loc, category, user_id=None, **kwargs): """ Create and return the item. If parent_loc is a specific location v a course id, it installs the new item as a child of the parent (if the parent_loc is a specific xblock reference). :param course_or_parent_loc: Can be a course_id (org/course/run), CourseLocator, Location, or BlockUsageLocator but must be what the persistence modulestore expects """ # find the store for the course course_id = self._infer_course_id_try(course_or_parent_loc) if course_id is None: raise ItemNotFoundError(u"Cannot find modulestore for %s" % course_or_parent_loc) store = self._get_modulestore_for_courseid(course_id) location = kwargs.pop('location', None) # invoke its create_item if isinstance(store, MongoModuleStore): block_id = kwargs.pop('block_id', getattr(location, 'name', uuid4().hex)) # convert parent loc if it's legit if isinstance(course_or_parent_loc, basestring): parent_loc = None if location is None: loc_dict = Location.parse_course_id(course_id) loc_dict['name'] = block_id location = Location(category=category, **loc_dict) else: parent_loc = course_or_parent_loc # must have a legitimate location, compute if appropriate if location is None: location = parent_loc.replace(category=category, name=block_id) # do the actual creation xblock = store.create_and_save_xmodule(location, **kwargs) # don't forget to attach to parent if parent_loc is not None and not 'detached' in xblock._class_tags: parent = store.get_item(parent_loc) parent.children.append(location.url()) store.update_item(parent) elif isinstance(store, SplitMongoModuleStore): if isinstance(course_or_parent_loc, basestring): # course_id course_or_parent_loc = loc_mapper().translate_location_to_course_locator( # hardcode draft version until we figure out how we're handling branches from app course_or_parent_loc, None, published=False ) elif not isinstance(course_or_parent_loc, CourseLocator): raise ValueError(u"Cannot create a child of {} in split. Wrong repr.".format(course_or_parent_loc)) # split handles all the fields in one dict not separated by scope fields = kwargs.get('fields', {}) fields.update(kwargs.pop('metadata', {})) fields.update(kwargs.pop('definition_data', {})) kwargs['fields'] = fields xblock = store.create_item(course_or_parent_loc, category, user_id, **kwargs) else: raise NotImplementedError(u"Cannot create an item on store %s" % store) return xblock
def test_old_location_helpers(self): """ Test the functions intended to help with the conversion from old locations to locators """ location_tuple = ('i4x', 'mit', 'eecs.6002x', 'course', 't3_2013') location = Location(location_tuple) self.assertEqual(location, Locator.to_locator_or_location(location)) self.assertEqual(location, Locator.to_locator_or_location(location_tuple)) self.assertEqual(location, Locator.to_locator_or_location(list(location_tuple))) self.assertEqual(location, Locator.to_locator_or_location(location.dict())) locator = BlockUsageLocator(course_id='foo.bar', branch='alpha', usage_id='deep') self.assertEqual(locator, Locator.to_locator_or_location(locator)) self.assertEqual( locator.as_course_locator(), Locator.to_locator_or_location(locator.as_course_locator())) self.assertEqual(location, Locator.to_locator_or_location(location.url())) self.assertEqual(locator, Locator.to_locator_or_location(locator.url())) self.assertEqual(locator, Locator.to_locator_or_location(locator.__dict__)) asset_location = Location( ['c4x', 'mit', 'eecs.6002x', 'asset', 'selfie.jpeg']) self.assertEqual(asset_location, Locator.to_locator_or_location(asset_location)) self.assertEqual(asset_location, Locator.to_locator_or_location(asset_location.url())) def_location_url = "defx://version/" + '{:024x}'.format( random.randrange(16**24)) self.assertEqual(DefinitionLocator(def_location_url), Locator.to_locator_or_location(def_location_url)) with self.assertRaises(ValueError): Locator.to_locator_or_location(22) with self.assertRaises(ValueError): Locator.to_locator_or_location("hello.world.not.a.url") self.assertIsNone(Locator.parse_url("unknown://foo.bar/baz"))
def _clone_modules(modulestore, modules, source_location, dest_location): for module in modules: original_loc = Location(module.location) if original_loc.category != 'course': new_location = module.location._replace( tag=dest_location.tag, org=dest_location.org, course=dest_location.course ) module.scope_ids = module.scope_ids._replace( def_id=new_location, usage_id=new_location ) else: # on the course module we also have to update the module name new_location = module.location._replace( tag=dest_location.tag, org=dest_location.org, course=dest_location.course, name=dest_location.name ) module.scope_ids = module.scope_ids._replace( def_id=new_location, usage_id=new_location ) print "Cloning module {0} to {1}....".format(original_loc, module.location) # NOTE: usage of the the internal module.xblock_kvs._data does not include any 'default' values for the fields data = module.xblock_kvs._data if isinstance(data, basestring): data = rewrite_nonportable_content_links( source_location.course_id, dest_location.course_id, data) modulestore.update_item(module.location, data) # repoint children if module.has_children: new_children = [] for child_loc_url in module.children: child_loc = Location(child_loc_url) child_loc = child_loc._replace( tag=dest_location.tag, org=dest_location.org, course=dest_location.course ) new_children.append(child_loc.url()) modulestore.update_children(module.location, new_children) # save metadata modulestore.update_metadata(module.location, own_metadata(module))
def test_metadata_inheritance(self): module_store = modulestore('direct') import_from_xml(module_store, 'common/test/data/', ['full']) course = module_store.get_item( Location( ['i4x', 'edX', 'full', 'course', '6.002_Spring_2012', None])) verticals = module_store.get_items( ['i4x', 'edX', 'full', 'vertical', None, None]) # let's assert on the metadata_inheritance on an existing vertical for vertical in verticals: self.assertEqual(course.lms.xqa_key, vertical.lms.xqa_key) self.assertGreater(len(verticals), 0) new_component_location = Location('i4x', 'edX', 'full', 'html', 'new_component') source_template_location = Location('i4x', 'edx', 'templates', 'html', 'Blank_HTML_Page') # crate a new module and add it as a child to a vertical module_store.clone_item(source_template_location, new_component_location) parent = verticals[0] module_store.update_children( parent.location, parent.children + [new_component_location.url()]) # flush the cache module_store.refresh_cached_metadata_inheritance_tree( new_component_location) new_module = module_store.get_item(new_component_location) # check for grace period definition which should be defined at the course level self.assertEqual(parent.lms.graceperiod, new_module.lms.graceperiod) self.assertEqual(course.lms.xqa_key, new_module.lms.xqa_key) # # now let's define an override at the leaf node level # new_module.lms.graceperiod = timedelta(1) module_store.update_metadata(new_module.location, own_metadata(new_module)) # flush the cache and refetch module_store.refresh_cached_metadata_inheritance_tree( new_component_location) new_module = module_store.get_item(new_component_location) self.assertEqual(timedelta(1), new_module.lms.graceperiod)
def test_metadata_inheritance(self): module_store = modulestore('direct') import_from_xml(module_store, 'common/test/data/', ['full']) course = module_store.get_item(Location( ['i4x', 'edX', 'full', 'course', '6.002_Spring_2012', None])) verticals = module_store.get_items( ['i4x', 'edX', 'full', 'vertical', None, None]) # let's assert on the metadata_inheritance on an existing vertical for vertical in verticals: self.assertEqual(course.lms.xqa_key, vertical.lms.xqa_key) self.assertGreater(len(verticals), 0) new_component_location = Location( 'i4x', 'edX', 'full', 'html', 'new_component') source_template_location = Location( 'i4x', 'edx', 'templates', 'html', 'Blank_HTML_Page') # crate a new module and add it as a child to a vertical module_store.clone_item( source_template_location, new_component_location) parent = verticals[0] module_store.update_children(parent.location, parent.children + [ new_component_location.url()]) # flush the cache module_store.refresh_cached_metadata_inheritance_tree( new_component_location) new_module = module_store.get_item(new_component_location) # check for grace period definition which should be defined at the # course level self.assertEqual(parent.lms.graceperiod, new_module.lms.graceperiod) self.assertEqual(course.lms.xqa_key, new_module.lms.xqa_key) # # now let's define an override at the leaf node level # new_module.lms.graceperiod = timedelta(1) module_store.update_metadata( new_module.location, own_metadata(new_module)) # flush the cache and refetch module_store.refresh_cached_metadata_inheritance_tree( new_component_location) new_module = module_store.get_item(new_component_location) self.assertEqual(timedelta(1), new_module.lms.graceperiod)
def _clone_modules(modulestore, modules, source_location, dest_location): for module in modules: original_loc = Location(module.location) if original_loc.category != 'course': new_location = module.location._replace( tag=dest_location.tag, org=dest_location.org, course=dest_location.course) module.scope_ids = module.scope_ids._replace(def_id=new_location, usage_id=new_location) else: # on the course module we also have to update the module name new_location = module.location._replace( tag=dest_location.tag, org=dest_location.org, course=dest_location.course, name=dest_location.name) module.scope_ids = module.scope_ids._replace(def_id=new_location, usage_id=new_location) print "Cloning module {0} to {1}....".format(original_loc, module.location) # NOTE: usage of the the internal module.xblock_kvs._data does not include any 'default' values for the fields data = module.xblock_kvs._data if isinstance(data, basestring): data = rewrite_nonportable_content_links(source_location.course_id, dest_location.course_id, data) modulestore.update_item(module.location, data) # repoint children if module.has_children: new_children = [] for child_loc_url in module.children: child_loc = Location(child_loc_url) child_loc = child_loc._replace(tag=dest_location.tag, org=dest_location.org, course=dest_location.course) new_children.append(child_loc.url()) modulestore.update_children(module.location, new_children) # save metadata modulestore.update_metadata(module.location, own_metadata(module))
def _clone_modules(modulestore, modules, source_location, dest_location): for module in modules: original_loc = Location(module.location) if original_loc.category != 'course': module.location = module.location._replace( tag=dest_location.tag, org=dest_location.org, course=dest_location.course ) else: # on the course module we also have to update the module name module.location = module.location._replace( tag=dest_location.tag, org=dest_location.org, course=dest_location.course, name=dest_location.name ) print "Cloning module {0} to {1}....".format(original_loc, module.location) if 'data' in module.fields and module.fields['data'].is_set_on(module) and isinstance(module.data, basestring): module.data = rewrite_nonportable_content_links( source_location.course_id, dest_location.course_id, module.data ) # repoint children if module.has_children: new_children = [] for child_loc_url in module.children: child_loc = Location(child_loc_url) child_loc = child_loc._replace( tag=dest_location.tag, org=dest_location.org, course=dest_location.course ) new_children.append(child_loc.url()) module.children = new_children modulestore.update_item(module, '**replace_user**')
def export_all_for_course(self, course_location, output_directory, assets_policy_file): """ Export all of this course's assets to the output_directory. Export all of the assets' attributes to the policy file. :param course_location: the Location of type 'course' :param output_directory: the directory under which to put all the asset files :param assets_policy_file: the filename for the policy file which should be in the same directory as the other policy files. """ policy = {} assets = self.get_all_content_for_course(course_location) for asset in assets: asset_location = Location(asset['_id']) self.export(asset_location, output_directory) for attr, value in asset.iteritems(): if attr not in ['_id', 'md5', 'uploadDate', 'length', 'chunkSize']: policy.setdefault(asset_location.url(), {})[attr] = value with open(assets_policy_file, 'w') as f: json.dump(policy, f)
def _construct(cls, system, contents, error_msg, location): if isinstance(location, dict) and "course" in location: location = Location(location) if isinstance(location, Location) and location.name is None: location = location.replace( category="error", # Pick a unique url_name -- the sha1 hash of the contents. # NOTE: We could try to pull out the url_name of the errored descriptor, # but url_names aren't guaranteed to be unique between descriptor types, # and ErrorDescriptor can wrap any type. When the wrapped module is fixed, # it will be written out with the original url_name. name=hashlib.sha1(contents.encode("utf8")).hexdigest(), ) # real metadata stays in the content, but add a display name model_data = { "error_msg": str(error_msg), "contents": contents, "display_name": "Error: " + location.url(), "location": location, "category": "error", } return cls(system, model_data)
def test_cms_imported_course_walkthrough(self): """ Import and walk through some common URL endpoints. This just verifies non-500 and no other correct behavior, so it is not a deep test """ import_from_xml(modulestore('direct'), 'common/test/data/', ['simple']) loc = Location(['i4x', 'edX', 'simple', 'course', '2012_Fall', None]) resp = self.client.get(reverse('course_index', kwargs={'org': loc.org, 'course': loc.course, 'name': loc.name})) self.assertEqual(200, resp.status_code) self.assertContains(resp, 'Chapter 2') # go to various pages # import page resp = self.client.get(reverse('import_course', kwargs={'org': loc.org, 'course': loc.course, 'name': loc.name})) self.assertEqual(200, resp.status_code) # export page resp = self.client.get(reverse('export_course', kwargs={'org': loc.org, 'course': loc.course, 'name': loc.name})) self.assertEqual(200, resp.status_code) # manage users resp = self.client.get(reverse('manage_users', kwargs={'location': loc.url()})) self.assertEqual(200, resp.status_code) # course info resp = self.client.get(reverse('course_info', kwargs={'org': loc.org, 'course': loc.course, 'name': loc.name})) self.assertEqual(200, resp.status_code) # settings_details resp = self.client.get(reverse('settings_details', kwargs={'org': loc.org, 'course': loc.course, 'name': loc.name})) self.assertEqual(200, resp.status_code) # settings_details resp = self.client.get(reverse('settings_grading', kwargs={'org': loc.org, 'course': loc.course, 'name': loc.name})) self.assertEqual(200, resp.status_code) # static_pages resp = self.client.get(reverse('static_pages', kwargs={'org': loc.org, 'course': loc.course, 'coursename': loc.name})) self.assertEqual(200, resp.status_code) # static_pages resp = self.client.get(reverse('asset_index', kwargs={'org': loc.org, 'course': loc.course, 'name': loc.name})) self.assertEqual(200, resp.status_code) # go look at a subsection page subsection_location = loc.replace(category='sequential', name='test_sequence') resp = self.client.get(reverse('edit_subsection', kwargs={'location': subsection_location.url()})) self.assertEqual(200, resp.status_code) # go look at the Edit page unit_location = loc.replace(category='vertical', name='test_vertical') resp = self.client.get(reverse('edit_unit', kwargs={'location': unit_location.url()})) self.assertEqual(200, resp.status_code) # delete a component del_loc = loc.replace(category='html', name='test_html') resp = self.client.post(reverse('delete_item'), json.dumps({'id': del_loc.url()}), "application/json") self.assertEqual(200, resp.status_code) # delete a unit del_loc = loc.replace(category='vertical', name='test_vertical') resp = self.client.post(reverse('delete_item'), json.dumps({'id': del_loc.url()}), "application/json") self.assertEqual(200, resp.status_code) # delete a unit del_loc = loc.replace(category='sequential', name='test_sequence') resp = self.client.post(reverse('delete_item'), json.dumps({'id': del_loc.url()}), "application/json") self.assertEqual(200, resp.status_code) # delete a chapter del_loc = loc.replace(category='chapter', name='chapter_2') resp = self.client.post(reverse('delete_item'), json.dumps({'id': del_loc.url()}), "application/json") self.assertEqual(200, resp.status_code)
def clone_course(modulestore, contentstore, source_location, dest_location, delete_original=False): # first check to see if the modulestore is Mongo backed if not isinstance(modulestore, MongoModuleStore): raise Exception("Expected a MongoModuleStore in the runtime. Aborting....") # check to see if the dest_location exists as an empty course # we need an empty course because the app layers manage the permissions and users if not modulestore.has_item(dest_location): raise Exception("An empty course at {0} must have already been created. Aborting...".format(dest_location)) # verify that the dest_location really is an empty course, which means only one with an optional 'overview' dest_modules = modulestore.get_items([dest_location.tag, dest_location.org, dest_location.course, None, None, None]) basically_empty = True for module in dest_modules: if module.location.category == 'course' or (module.location.category == 'about' and module.location.name == 'overview'): continue basically_empty = False break if not basically_empty: raise Exception("Course at destination {0} is not an empty course. You can only clone into an empty course. Aborting...".format(dest_location)) # check to see if the source course is actually there if not modulestore.has_item(source_location): raise Exception("Cannot find a course at {0}. Aborting".format(source_location)) # Get all modules under this namespace which is (tag, org, course) tuple modules = modulestore.get_items([source_location.tag, source_location.org, source_location.course, None, None, None]) for module in modules: original_loc = Location(module.location) if original_loc.category != 'course': module.location = module.location._replace(tag=dest_location.tag, org=dest_location.org, course=dest_location.course) else: # on the course module we also have to update the module name module.location = module.location._replace(tag=dest_location.tag, org=dest_location.org, course=dest_location.course, name=dest_location.name) print "Cloning module {0} to {1}....".format(original_loc, module.location) modulestore.update_item(module.location, module._model_data._kvs._data) # repoint children if module.has_children: new_children = [] for child_loc_url in module.children: child_loc = Location(child_loc_url) child_loc = child_loc._replace( tag=dest_location.tag, org=dest_location.org, course=dest_location.course ) new_children.append(child_loc.url()) modulestore.update_children(module.location, new_children) # save metadata modulestore.update_metadata(module.location, module._model_data._kvs._metadata) # now iterate through all of the assets and clone them # first the thumbnails thumbs = contentstore.get_all_content_thumbnails_for_course(source_location) for thumb in thumbs: thumb_loc = Location(thumb["_id"]) content = contentstore.find(thumb_loc) content.location = content.location._replace(org=dest_location.org, course=dest_location.course) print "Cloning thumbnail {0} to {1}".format(thumb_loc, content.location) contentstore.save(content) # now iterate through all of the assets, also updating the thumbnail pointer assets = contentstore.get_all_content_for_course(source_location) for asset in assets: asset_loc = Location(asset["_id"]) content = contentstore.find(asset_loc) content.location = content.location._replace(org=dest_location.org, course=dest_location.course) # be sure to update the pointer to the thumbnail if content.thumbnail_location is not None: content.thumbnail_location = content.thumbnail_location._replace(org=dest_location.org, course=dest_location.course) print "Cloning asset {0} to {1}".format(asset_loc, content.location) contentstore.save(content) return True
class RolesTestCase(TestCase): """ Tests of student.roles """ def setUp(self): self.course = Location('i4x://edX/toy/course/2012_Fall') self.anonymous_user = AnonymousUserFactory() self.student = UserFactory() self.global_staff = UserFactory(is_staff=True) self.course_staff = StaffFactory(course=self.course) self.course_instructor = InstructorFactory(course=self.course) def test_global_staff(self): self.assertFalse(GlobalStaff().has_user(self.student)) self.assertFalse(GlobalStaff().has_user(self.course_staff)) self.assertFalse(GlobalStaff().has_user(self.course_instructor)) self.assertTrue(GlobalStaff().has_user(self.global_staff)) def test_group_name_case_insensitive(self): uppercase_loc = "i4x://ORG/COURSE/course/NAME" lowercase_loc = uppercase_loc.lower() lowercase_group = "role_org/course/name" uppercase_group = lowercase_group.upper() lowercase_user = UserFactory(groups=lowercase_group) uppercase_user = UserFactory(groups=uppercase_group) self.assertTrue(CourseRole("role", lowercase_loc).has_user(lowercase_user)) self.assertTrue(CourseRole("role", uppercase_loc).has_user(lowercase_user)) self.assertTrue(CourseRole("role", lowercase_loc).has_user(uppercase_user)) self.assertTrue(CourseRole("role", uppercase_loc).has_user(uppercase_user)) def test_course_role(self): """ Test that giving a user a course role enables access appropriately """ course_locator = loc_mapper().translate_location( self.course.course_id, self.course, add_entry_if_missing=True ) self.assertFalse( CourseStaffRole(course_locator).has_user(self.student), "Student has premature access to {}".format(unicode(course_locator)) ) self.assertFalse( CourseStaffRole(self.course).has_user(self.student), "Student has premature access to {}".format(self.course.url()) ) CourseStaffRole(course_locator).add_users(self.student) self.assertTrue( CourseStaffRole(course_locator).has_user(self.student), "Student doesn't have access to {}".format(unicode(course_locator)) ) self.assertTrue( CourseStaffRole(self.course).has_user(self.student), "Student doesn't have access to {}".format(unicode(self.course.url())) ) # now try accessing something internal to the course vertical_locator = BlockUsageLocator( package_id=course_locator.package_id, branch='published', block_id='madeup' ) vertical_location = self.course.replace(category='vertical', name='madeuptoo') self.assertTrue( CourseStaffRole(vertical_locator).has_user(self.student), "Student doesn't have access to {}".format(unicode(vertical_locator)) ) self.assertTrue( CourseStaffRole(vertical_location, course_context=self.course.course_id).has_user(self.student), "Student doesn't have access to {}".format(unicode(vertical_location.url())) )
def clone_course(modulestore, contentstore, source_location, dest_location, delete_original=False): # first check to see if the modulestore is Mongo backed if not isinstance(modulestore, MongoModuleStore): raise Exception( "Expected a MongoModuleStore in the runtime. Aborting....") # check to see if the dest_location exists as an empty course # we need an empty course because the app layers manage the permissions and users if not modulestore.has_item(dest_location): raise Exception( "An empty course at {0} must have already been created. Aborting..." .format(dest_location)) # verify that the dest_location really is an empty course, which means only one with an optional 'overview' dest_modules = modulestore.get_items([ dest_location.tag, dest_location.org, dest_location.course, None, None, None ]) basically_empty = True for module in dest_modules: if module.location.category == 'course' or ( module.location.category == 'about' and module.location.name == 'overview'): continue basically_empty = False break if not basically_empty: raise Exception( "Course at destination {0} is not an empty course. You can only clone into an empty course. Aborting..." .format(dest_location)) # check to see if the source course is actually there if not modulestore.has_item(source_location): raise Exception( "Cannot find a course at {0}. Aborting".format(source_location)) # Get all modules under this namespace which is (tag, org, course) tuple modules = modulestore.get_items([ source_location.tag, source_location.org, source_location.course, None, None, None ]) for module in modules: original_loc = Location(module.location) if original_loc.category != 'course': module.location = module.location._replace( tag=dest_location.tag, org=dest_location.org, course=dest_location.course) else: # on the course module we also have to update the module name module.location = module.location._replace( tag=dest_location.tag, org=dest_location.org, course=dest_location.course, name=dest_location.name) print "Cloning module {0} to {1}....".format(original_loc, module.location) modulestore.update_item(module.location, module._model_data._kvs._data) # repoint children if module.has_children: new_children = [] for child_loc_url in module.children: child_loc = Location(child_loc_url) child_loc = child_loc._replace(tag=dest_location.tag, org=dest_location.org, course=dest_location.course) new_children.append(child_loc.url()) modulestore.update_children(module.location, new_children) # save metadata modulestore.update_metadata(module.location, module._model_data._kvs._metadata) # now iterate through all of the assets and clone them # first the thumbnails thumbs = contentstore.get_all_content_thumbnails_for_course( source_location) for thumb in thumbs: thumb_loc = Location(thumb["_id"]) content = contentstore.find(thumb_loc) content.location = content.location._replace( org=dest_location.org, course=dest_location.course) print "Cloning thumbnail {0} to {1}".format(thumb_loc, content.location) contentstore.save(content) # now iterate through all of the assets, also updating the thumbnail pointer assets = contentstore.get_all_content_for_course(source_location) for asset in assets: asset_loc = Location(asset["_id"]) content = contentstore.find(asset_loc) content.location = content.location._replace( org=dest_location.org, course=dest_location.course) # be sure to update the pointer to the thumbnail if content.thumbnail_location is not None: content.thumbnail_location = content.thumbnail_location._replace( org=dest_location.org, course=dest_location.course) print "Cloning asset {0} to {1}".format(asset_loc, content.location) contentstore.save(content) return True
def test_cms_imported_course_walkthrough(self): """ Import and walk through some common URL endpoints. This just verifies non-500 and no other correct behavior, so it is not a deep test """ import_from_xml(modulestore("direct"), "common/test/data/", ["simple"]) loc = Location(["i4x", "edX", "simple", "course", "2012_Fall", None]) resp = self.client.get(reverse("course_index", kwargs={"org": loc.org, "course": loc.course, "name": loc.name})) self.assertEqual(200, resp.status_code) self.assertContains(resp, "Chapter 2") # go to various pages # import page resp = self.client.get( reverse("import_course", kwargs={"org": loc.org, "course": loc.course, "name": loc.name}) ) self.assertEqual(200, resp.status_code) # export page resp = self.client.get( reverse("export_course", kwargs={"org": loc.org, "course": loc.course, "name": loc.name}) ) self.assertEqual(200, resp.status_code) # manage users resp = self.client.get(reverse("manage_users", kwargs={"location": loc.url()})) self.assertEqual(200, resp.status_code) # course info resp = self.client.get(reverse("course_info", kwargs={"org": loc.org, "course": loc.course, "name": loc.name})) self.assertEqual(200, resp.status_code) # settings_details resp = self.client.get( reverse("settings_details", kwargs={"org": loc.org, "course": loc.course, "name": loc.name}) ) self.assertEqual(200, resp.status_code) # settings_details resp = self.client.get( reverse("settings_grading", kwargs={"org": loc.org, "course": loc.course, "name": loc.name}) ) self.assertEqual(200, resp.status_code) # static_pages resp = self.client.get( reverse("static_pages", kwargs={"org": loc.org, "course": loc.course, "coursename": loc.name}) ) self.assertEqual(200, resp.status_code) # static_pages resp = self.client.get(reverse("asset_index", kwargs={"org": loc.org, "course": loc.course, "name": loc.name})) self.assertEqual(200, resp.status_code) # go look at a subsection page subsection_location = loc.replace(category="sequential", name="test_sequence") resp = self.client.get(reverse("edit_subsection", kwargs={"location": subsection_location.url()})) self.assertEqual(200, resp.status_code) # go look at the Edit page unit_location = loc.replace(category="vertical", name="test_vertical") resp = self.client.get(reverse("edit_unit", kwargs={"location": unit_location.url()})) self.assertEqual(200, resp.status_code) # delete a component del_loc = loc.replace(category="html", name="test_html") resp = self.client.post(reverse("delete_item"), json.dumps({"id": del_loc.url()}), "application/json") self.assertEqual(200, resp.status_code) # delete a unit del_loc = loc.replace(category="vertical", name="test_vertical") resp = self.client.post(reverse("delete_item"), json.dumps({"id": del_loc.url()}), "application/json") self.assertEqual(200, resp.status_code) # delete a unit del_loc = loc.replace(category="sequential", name="test_sequence") resp = self.client.post(reverse("delete_item"), json.dumps({"id": del_loc.url()}), "application/json") self.assertEqual(200, resp.status_code) # delete a chapter del_loc = loc.replace(category="chapter", name="chapter_2") resp = self.client.post(reverse("delete_item"), json.dumps({"id": del_loc.url()}), "application/json") self.assertEqual(200, resp.status_code)
def test_encode_location(self): loc = Location('i4x', 'org', 'course', 'category', 'name') self.assertEqual(loc.url(), self.encoder.default(loc)) loc = Location('i4x', 'org', 'course', 'category', 'name', 'version') self.assertEqual(loc.url(), self.encoder.default(loc))
def create_item(self, course_or_parent_loc, category, user_id=None, **kwargs): """ Create and return the item. If parent_loc is a specific location v a course id, it installs the new item as a child of the parent (if the parent_loc is a specific xblock reference). :param course_or_parent_loc: Can be a course_id (org/course/run), CourseLocator, Location, or BlockUsageLocator but must be what the persistence modulestore expects """ # find the store for the course course_id = self._infer_course_id_try(course_or_parent_loc) if course_id is None: raise ItemNotFoundError(u"Cannot find modulestore for %s" % course_or_parent_loc) store = self._get_modulestore_for_courseid(course_id) location = kwargs.pop('location', None) # invoke its create_item if isinstance(store, MongoModuleStore): block_id = kwargs.pop('block_id', getattr(location, 'name', uuid4().hex)) # convert parent loc if it's legit if isinstance(course_or_parent_loc, basestring): parent_loc = None if location is None: loc_dict = Location.parse_course_id(course_id) loc_dict['name'] = block_id location = Location(category=category, **loc_dict) else: parent_loc = course_or_parent_loc # must have a legitimate location, compute if appropriate if location is None: location = parent_loc.replace(category=category, name=block_id) # do the actual creation xblock = store.create_and_save_xmodule(location, **kwargs) # don't forget to attach to parent if parent_loc is not None and not 'detached' in xblock._class_tags: parent = store.get_item(parent_loc) parent.children.append(location.url()) store.update_item(parent) elif isinstance(store, SplitMongoModuleStore): if isinstance(course_or_parent_loc, basestring): # course_id course_or_parent_loc = loc_mapper( ).translate_location_to_course_locator( # hardcode draft version until we figure out how we're handling branches from app course_or_parent_loc, None, published=False) elif not isinstance(course_or_parent_loc, CourseLocator): raise ValueError( u"Cannot create a child of {} in split. Wrong repr.". format(course_or_parent_loc)) # split handles all the fields in one dict not separated by scope fields = kwargs.get('fields', {}) fields.update(kwargs.pop('metadata', {})) fields.update(kwargs.pop('definition_data', {})) kwargs['fields'] = fields xblock = store.create_item(course_or_parent_loc, category, user_id, **kwargs) else: raise NotImplementedError(u"Cannot create an item on store %s" % store) return xblock
def compute_metadata_inheritance_tree(self, location): ''' TODO (cdodge) This method can be deleted when the 'split module store' work has been completed ''' # get all collections in the course, this query should not return any leaf nodes # note this is a bit ugly as when we add new categories of containers, we have to add it here query = {'_id.org': location.org, '_id.course': location.course, '_id.category': {'$in': ['course', 'chapter', 'sequential', 'vertical', 'videosequence', 'wrapper', 'problemset', 'conditional', 'randomize']} } # we just want the Location, children, and inheritable metadata record_filter = {'_id': 1, 'definition.children': 1} # just get the inheritable metadata since that is all we need for the computation # this minimizes both data pushed over the wire for attr in INHERITABLE_METADATA: record_filter['metadata.{0}'.format(attr)] = 1 # call out to the DB resultset = self.collection.find(query, record_filter) results_by_url = {} root = None # now go through the results and order them by the location url for result in resultset: location = Location(result['_id']) # We need to collate between draft and non-draft # i.e. draft verticals can have children which are not in non-draft versions location = location.replace(revision=None) location_url = location.url() if location_url in results_by_url: existing_children = results_by_url[location_url].get('definition', {}).get('children', []) additional_children = result.get('definition', {}).get('children', []) total_children = existing_children + additional_children if 'definition' not in results_by_url[location_url]: results_by_url[location_url]['definition'] = {} results_by_url[location_url]['definition']['children'] = total_children results_by_url[location.url()] = result if location.category == 'course': root = location.url() # now traverse the tree and compute down the inherited metadata metadata_to_inherit = {} def _compute_inherited_metadata(url): """ Helper method for computing inherited metadata for a specific location url """ # check for presence of metadata key. Note that a given module may not yet be fully formed. # example: update_item -> update_children -> update_metadata sequence on new item create # if we get called here without update_metadata called first then 'metadata' hasn't been set # as we're not fully transactional at the DB layer. Same comment applies to below key name # check my_metadata = results_by_url[url].get('metadata', {}) # go through all the children and recurse, but only if we have # in the result set. Remember results will not contain leaf nodes for child in results_by_url[url].get('definition', {}).get('children', []): if child in results_by_url: new_child_metadata = copy.deepcopy(my_metadata) new_child_metadata.update(results_by_url[child].get('metadata', {})) results_by_url[child]['metadata'] = new_child_metadata metadata_to_inherit[child] = new_child_metadata _compute_inherited_metadata(child) else: # this is likely a leaf node, so let's record what metadata we need to inherit metadata_to_inherit[child] = my_metadata if root is not None: _compute_inherited_metadata(root) return metadata_to_inherit
class XModule(XModuleFields, HTMLSnippet, XBlock): ''' Implements a generic learning module. Subclasses must at a minimum provide a definition for get_html in order to be displayed to users. See the HTML module for a simple example. ''' # The default implementation of get_icon_class returns the icon_class # attribute of the class # # This attribute can be overridden by subclasses, and # the function can also be overridden if the icon class depends on the data # in the module icon_class = 'other' def __init__(self, system, location, descriptor, model_data): ''' Construct a new xmodule system: A ModuleSystem allowing access to external resources location: Something Location-like that identifies this xmodule descriptor: the XModuleDescriptor that this module is an instance of. TODO (vshnayder): remove the definition parameter and location--they can come from the descriptor. model_data: A dictionary-like object that maps field names to values for those fields. ''' self._model_data = model_data self.system = system self.location = Location(location) self.descriptor = descriptor self.url_name = self.location.name self.category = self.location.category self._loaded_children = None @property def id(self): return self.location.url() @property def display_name_with_default(self): ''' Return a display name for the module: use display_name if defined in metadata, otherwise convert the url name. ''' name = self.display_name if name is None: name = self.url_name.replace('_', ' ') return name def get_children(self): ''' Return module instances for all the children of this module. ''' if self._loaded_children is None: child_descriptors = self.get_child_descriptors() children = [self.system.get_module(descriptor) for descriptor in child_descriptors] # get_module returns None if the current user doesn't have access # to the location. self._loaded_children = [c for c in children if c is not None] return self._loaded_children def __unicode__(self): return '<x_module(id={0})>'.format(self.id) def get_child_descriptors(self): ''' Returns the descriptors of the child modules Overriding this changes the behavior of get_children and anything that uses get_children, such as get_display_items. This method will not instantiate the modules of the children unless absolutely necessary, so it is cheaper to call than get_children These children will be the same children returned by the descriptor unless descriptor.has_dynamic_children() is true. ''' return self.descriptor.get_children() def get_child_by(self, selector): """ Return a child XModuleDescriptor with the specified url_name, if it exists, and None otherwise. """ for child in self.get_children(): if selector(child): return child return None def get_display_items(self): ''' Returns a list of descendent module instances that will display immediately inside this module. ''' items = [] for child in self.get_children(): items.extend(child.displayable_items()) return items def displayable_items(self): ''' Returns list of displayable modules contained by this module. If this module is visible, should return [self]. ''' return [self] def get_icon_class(self): ''' Return a css class identifying this module in the context of an icon ''' return self.icon_class ### Functions used in the LMS def get_score(self): """ Score the student received on the problem, or None if there is no score. Returns: dictionary {'score': integer, from 0 to get_max_score(), 'total': get_max_score()} NOTE (vshnayder): not sure if this was the intended return value, but that's what it's doing now. I suspect that we really want it to just return a number. Would need to change (at least) capa and modx_dispatch to match if we did that. """ return None def max_score(self): ''' Maximum score. Two notes: * This is generic; in abstract, a problem could be 3/5 points on one randomization, and 5/7 on another * In practice, this is a Very Bad Idea, and (a) will break some code in place (although that code should get fixed), and (b) break some analytics we plan to put in place. ''' return None def get_progress(self): ''' Return a progress.Progress object that represents how far the student has gone in this module. Must be implemented to get correct progress tracking behavior in nesting modules like sequence and vertical. If this module has no notion of progress, return None. ''' return None def handle_ajax(self, dispatch, get): ''' dispatch is last part of the URL. get is a dictionary-like object ''' return "" # cdodge: added to support dynamic substitutions of # links for courseware assets (e.g. images). <link> is passed through from lxml.html parser def rewrite_content_links(self, link): # see if we start with our format, e.g. 'xasset:<filename>' if link.startswith(XASSET_SRCREF_PREFIX): # yes, then parse out the name name = link[len(XASSET_SRCREF_PREFIX):] loc = Location(self.location) # resolve the reference to our internal 'filepath' which link = StaticContent.compute_location_filename(loc.org, loc.course, name) return link
def compute_metadata_inheritance_tree(self, location): """ TODO (cdodge) This method can be deleted when the 'split module store' work has been completed """ # get all collections in the course, this query should not return any leaf nodes # note this is a bit ugly as when we add new categories of containers, we have to add it here query = { "_id.org": location.org, "_id.course": location.course, "_id.category": { "$in": [ "course", "chapter", "sequential", "vertical", "wrapper", "problemset", "conditional", "randomize", ] }, } # we just want the Location, children, and inheritable metadata record_filter = {"_id": 1, "definition.children": 1} # just get the inheritable metadata since that is all we need for the computation # this minimizes both data pushed over the wire for attr in INHERITABLE_METADATA: record_filter["metadata.{0}".format(attr)] = 1 # call out to the DB resultset = self.collection.find(query, record_filter) results_by_url = {} root = None # now go through the results and order them by the location url for result in resultset: location = Location(result["_id"]) # We need to collate between draft and non-draft # i.e. draft verticals can have children which are not in non-draft versions location = location.replace(revision=None) location_url = location.url() if location_url in results_by_url: existing_children = results_by_url[location_url].get("definition", {}).get("children", []) additional_children = result.get("definition", {}).get("children", []) total_children = existing_children + additional_children if "definition" not in results_by_url[location_url]: results_by_url[location_url]["definition"] = {} results_by_url[location_url]["definition"]["children"] = total_children results_by_url[location.url()] = result if location.category == "course": root = location.url() # now traverse the tree and compute down the inherited metadata metadata_to_inherit = {} def _compute_inherited_metadata(url): """ Helper method for computing inherited metadata for a specific location url """ # check for presence of metadata key. Note that a given module may not yet be fully formed. # example: update_item -> update_children -> update_metadata sequence on new item create # if we get called here without update_metadata called first then 'metadata' hasn't been set # as we're not fully transactional at the DB layer. Same comment applies to below key name # check my_metadata = results_by_url[url].get("metadata", {}) # go through all the children and recurse, but only if we have # in the result set. Remember results will not contain leaf nodes for child in results_by_url[url].get("definition", {}).get("children", []): if child in results_by_url: new_child_metadata = copy.deepcopy(my_metadata) new_child_metadata.update(results_by_url[child].get("metadata", {})) results_by_url[child]["metadata"] = new_child_metadata metadata_to_inherit[child] = new_child_metadata _compute_inherited_metadata(child) else: # this is likely a leaf node, so let's record what metadata we need to inherit metadata_to_inherit[child] = my_metadata if root is not None: _compute_inherited_metadata(root) return metadata_to_inherit
class RolesTestCase(TestCase): """ Tests of student.roles """ def setUp(self): self.course = Location('i4x://edX/toy/course/2012_Fall') self.anonymous_user = AnonymousUserFactory() self.student = UserFactory() self.global_staff = UserFactory(is_staff=True) self.course_staff = StaffFactory(course=self.course) self.course_instructor = InstructorFactory(course=self.course) def test_global_staff(self): self.assertFalse(GlobalStaff().has_user(self.student)) self.assertFalse(GlobalStaff().has_user(self.course_staff)) self.assertFalse(GlobalStaff().has_user(self.course_instructor)) self.assertTrue(GlobalStaff().has_user(self.global_staff)) def test_group_name_case_insensitive(self): uppercase_loc = "i4x://ORG/COURSE/course/NAME" lowercase_loc = uppercase_loc.lower() lowercase_group = "role_org/course/name" uppercase_group = lowercase_group.upper() lowercase_user = UserFactory(groups=lowercase_group) uppercase_user = UserFactory(groups=uppercase_group) self.assertTrue( CourseRole("role", lowercase_loc).has_user(lowercase_user)) self.assertTrue( CourseRole("role", uppercase_loc).has_user(lowercase_user)) self.assertTrue( CourseRole("role", lowercase_loc).has_user(uppercase_user)) self.assertTrue( CourseRole("role", uppercase_loc).has_user(uppercase_user)) def test_course_role(self): """ Test that giving a user a course role enables access appropriately """ course_locator = loc_mapper().translate_location( self.course.course_id, self.course, add_entry_if_missing=True) self.assertFalse( CourseStaffRole(course_locator).has_user(self.student), "Student has premature access to {}".format( unicode(course_locator))) self.assertFalse( CourseStaffRole(self.course).has_user(self.student), "Student has premature access to {}".format(self.course.url())) CourseStaffRole(course_locator).add_users(self.student) self.assertTrue( CourseStaffRole(course_locator).has_user(self.student), "Student doesn't have access to {}".format( unicode(course_locator))) self.assertTrue( CourseStaffRole(self.course).has_user(self.student), "Student doesn't have access to {}".format( unicode(self.course.url()))) # now try accessing something internal to the course vertical_locator = BlockUsageLocator( package_id=course_locator.package_id, branch='published', block_id='madeup') vertical_location = self.course.replace(category='vertical', name='madeuptoo') self.assertTrue( CourseStaffRole(vertical_locator).has_user(self.student), "Student doesn't have access to {}".format( unicode(vertical_locator))) self.assertTrue( CourseStaffRole(vertical_location, course_context=self.course.course_id).has_user( self.student), "Student doesn't have access to {}".format( unicode(vertical_location.url())))
class XModule(XModuleFields, HTMLSnippet, XBlock): ''' Implements a generic learning module. Subclasses must at a minimum provide a definition for get_html in order to be displayed to users. See the HTML module for a simple example. ''' # The default implementation of get_icon_class returns the icon_class # attribute of the class # # This attribute can be overridden by subclasses, and # the function can also be overridden if the icon class depends on the data # in the module icon_class = 'other' def __init__(self, system, location, descriptor, model_data): ''' Construct a new xmodule system: A ModuleSystem allowing access to external resources location: Something Location-like that identifies this xmodule descriptor: the XModuleDescriptor that this module is an instance of. TODO (vshnayder): remove the definition parameter and location--they can come from the descriptor. model_data: A dictionary-like object that maps field names to values for those fields. ''' self._model_data = model_data self.system = system self.location = Location(location) self.descriptor = descriptor self.url_name = self.location.name self.category = self.location.category self._loaded_children = None @property def id(self): return self.location.url() @property def display_name_with_default(self): ''' Return a display name for the module: use display_name if defined in metadata, otherwise convert the url name. ''' name = self.display_name if name is None: name = self.url_name.replace('_', ' ') return name def get_children(self): ''' Return module instances for all the children of this module. ''' if self._loaded_children is None: child_descriptors = self.get_child_descriptors() children = [ self.system.get_module(descriptor) for descriptor in child_descriptors ] # get_module returns None if the current user doesn't have access # to the location. self._loaded_children = [c for c in children if c is not None] return self._loaded_children def __unicode__(self): return '<x_module(id={0})>'.format(self.id) def get_child_descriptors(self): ''' Returns the descriptors of the child modules Overriding this changes the behavior of get_children and anything that uses get_children, such as get_display_items. This method will not instantiate the modules of the children unless absolutely necessary, so it is cheaper to call than get_children These children will be the same children returned by the descriptor unless descriptor.has_dynamic_children() is true. ''' return self.descriptor.get_children() def get_child_by(self, selector): """ Return a child XModuleDescriptor with the specified url_name, if it exists, and None otherwise. """ for child in self.get_children(): if selector(child): return child return None def get_display_items(self): ''' Returns a list of descendent module instances that will display immediately inside this module. ''' items = [] for child in self.get_children(): items.extend(child.displayable_items()) return items def displayable_items(self): ''' Returns list of displayable modules contained by this module. If this module is visible, should return [self]. ''' return [self] def get_icon_class(self): ''' Return a css class identifying this module in the context of an icon ''' return self.icon_class ### Functions used in the LMS def get_score(self): """ Score the student received on the problem, or None if there is no score. Returns: dictionary {'score': integer, from 0 to get_max_score(), 'total': get_max_score()} NOTE (vshnayder): not sure if this was the intended return value, but that's what it's doing now. I suspect that we really want it to just return a number. Would need to change (at least) capa and modx_dispatch to match if we did that. """ return None def max_score(self): ''' Maximum score. Two notes: * This is generic; in abstract, a problem could be 3/5 points on one randomization, and 5/7 on another * In practice, this is a Very Bad Idea, and (a) will break some code in place (although that code should get fixed), and (b) break some analytics we plan to put in place. ''' return None def get_progress(self): ''' Return a progress.Progress object that represents how far the student has gone in this module. Must be implemented to get correct progress tracking behavior in nesting modules like sequence and vertical. If this module has no notion of progress, return None. ''' return None def handle_ajax(self, dispatch, get): ''' dispatch is last part of the URL. get is a dictionary-like object ''' return "" # cdodge: added to support dynamic substitutions of # links for courseware assets (e.g. images). <link> is passed through from lxml.html parser def rewrite_content_links(self, link): # see if we start with our format, e.g. 'xasset:<filename>' if link.startswith(XASSET_SRCREF_PREFIX): # yes, then parse out the name name = link[len(XASSET_SRCREF_PREFIX):] loc = Location(self.location) # resolve the reference to our internal 'filepath' which link = StaticContent.compute_location_filename( loc.org, loc.course, name) return link
class XModuleDescriptor(XModuleFields, HTMLSnippet, ResourceTemplates, XBlock): """ An XModuleDescriptor is a specification for an element of a course. This could be a problem, an organizational element (a group of content), or a segment of video, for example. XModuleDescriptors are independent and agnostic to the current student state on a problem. They handle the editing interface used by instructors to create a problem, and can generate XModules (which do know about student state). """ entry_point = "xmodule.v1" module_class = XModule # Attributes for inspection of the descriptor # This indicates whether the xmodule is a problem-type. # It should respond to max_score() and grade(). It can be graded or ungraded # (like a practice problem). has_score = False # A list of descriptor attributes that must be equal for the descriptors to # be equal equality_attributes = ('_model_data', 'location') # Name of resource directory to load templates from template_dir_name = "default" # Class level variable always_recalculate_grades = False """ Return whether this descriptor always requires recalculation of grades, for example if the score can change via an extrnal service, not just when the student interacts with the module on the page. A specific example is FoldIt, which posts grade-changing updates through a separate API. """ # VS[compat]. Backwards compatibility code that can go away after # importing 2012 courses. # A set of metadata key conversions that we want to make metadata_translations = { 'slug': 'url_name', 'name': 'display_name', } # ============================= STRUCTURAL MANIPULATION =================== def __init__(self, system, location, model_data): """ Construct a new XModuleDescriptor. The only required arguments are the system, used for interaction with external resources, and the definition, which specifies all the data needed to edit and display the problem (but none of the associated metadata that handles recordkeeping around the problem). This allows for maximal flexibility to add to the interface while preserving backwards compatibility. system: A DescriptorSystem for interacting with external resources location: Something Location-like that identifies this xmodule model_data: A dictionary-like object that maps field names to values for those fields. """ self.system = system self.location = Location(location) self.url_name = self.location.name self.category = self.location.category self._model_data = model_data self._child_instances = None @property def id(self): return self.location.url() @property def display_name_with_default(self): ''' Return a display name for the module: use display_name if defined in metadata, otherwise convert the url name. ''' name = self.display_name if name is None: name = self.url_name.replace('_', ' ') return name def get_required_module_descriptors(self): """Returns a list of XModuleDescritpor instances upon which this module depends, but are not children of this module""" return [] def get_children(self): """Returns a list of XModuleDescriptor instances for the children of this module""" if not self.has_children: return [] if self._child_instances is None: self._child_instances = [] for child_loc in self.children: try: child = self.system.load_item(child_loc) except ItemNotFoundError: log.exception('Unable to load item {loc}, skipping'.format( loc=child_loc)) continue self._child_instances.append(child) return self._child_instances def get_child_by(self, selector): """ Return a child XModuleDescriptor with the specified url_name, if it exists, and None otherwise. """ for child in self.get_children(): if selector(child): return child return None def xmodule(self, system): """ Returns an XModule. system: Module system """ return self.module_class( system, self.location, self, system.xblock_model_data(self), ) def has_dynamic_children(self): """ Returns True if this descriptor has dynamic children for a given student when the module is created. Returns False if the children of this descriptor are the same children that the module will return for any student. """ return False # ================================= JSON PARSING =========================== @staticmethod def load_from_json(json_data, system, default_class=None): """ This method instantiates the correct subclass of XModuleDescriptor based on the contents of json_data. json_data must contain a 'location' element, and must be suitable to be passed into the subclasses `from_json` method as model_data """ class_ = XModuleDescriptor.load_class( json_data['location']['category'], default_class) return class_.from_json(json_data, system) @classmethod def from_json(cls, json_data, system): """ Creates an instance of this descriptor from the supplied json_data. This may be overridden by subclasses json_data: A json object with the keys 'definition' and 'metadata', definition: A json object with the keys 'data' and 'children' data: A json value children: A list of edX Location urls metadata: A json object with any keys This json_data is transformed to model_data using the following rules: 1) The model data contains all of the fields from metadata 2) The model data contains the 'children' array 3) If 'definition.data' is a json object, model data contains all of its fields Otherwise, it contains the single field 'data' 4) Any value later in this list overrides a value earlier in this list system: A DescriptorSystem for interacting with external resources """ model_data = {} for key, value in json_data.get('metadata', {}).items(): model_data[cls._translate(key)] = value model_data.update(json_data.get('metadata', {})) definition = json_data.get('definition', {}) if 'children' in definition: model_data['children'] = definition['children'] if 'data' in definition: if isinstance(definition['data'], dict): model_data.update(definition['data']) else: model_data['data'] = definition['data'] return cls(system=system, location=json_data['location'], model_data=model_data) @classmethod def _translate(cls, key): 'VS[compat]' return cls.metadata_translations.get(key, key) # ================================= XML PARSING ============================ @staticmethod def load_from_xml(xml_data, system, org=None, course=None, default_class=None): """ This method instantiates the correct subclass of XModuleDescriptor based on the contents of xml_data. xml_data must be a string containing valid xml system is an XMLParsingSystem org and course are optional strings that will be used in the generated module's url identifiers """ class_ = XModuleDescriptor.load_class( etree.fromstring(xml_data).tag, default_class) # leave next line, commented out - useful for low-level debugging # log.debug('[XModuleDescriptor.load_from_xml] tag=%s, class_=%s' % ( # etree.fromstring(xml_data).tag,class_)) return class_.from_xml(xml_data, system, org, course) @classmethod def from_xml(cls, xml_data, system, org=None, course=None): """ Creates an instance of this descriptor from the supplied xml_data. This may be overridden by subclasses xml_data: A string of xml that will be translated into data and children for this module system is an XMLParsingSystem org and course are optional strings that will be used in the generated module's url identifiers """ raise NotImplementedError( 'Modules must implement from_xml to be parsable from xml') def export_to_xml(self, resource_fs): """ Returns an xml string representing this module, and all modules underneath it. May also write required resources out to resource_fs Assumes that modules have single parentage (that no module appears twice in the same course), and that it is thus safe to nest modules as xml children as appropriate. The returned XML should be able to be parsed back into an identical XModuleDescriptor using the from_xml method with the same system, org, and course """ raise NotImplementedError( 'Modules must implement export_to_xml to enable xml export') # =============================== Testing ================================== def get_sample_state(self): """ Return a list of tuples of instance_state, shared_state. Each tuple defines a sample case for this module """ return [('{}', '{}')] # =============================== BUILTIN METHODS ========================== def __eq__(self, other): eq = (self.__class__ == other.__class__ and all( getattr(self, attr, None) == getattr(other, attr, None) for attr in self.equality_attributes)) return eq def __repr__(self): return ("{class_}({system!r}, location={location!r}," " model_data={model_data!r})".format( class_=self.__class__.__name__, system=self.system, location=self.location, model_data=self._model_data, )) @property def non_editable_metadata_fields(self): """ Return the list of fields that should not be editable in Studio. When overriding, be sure to append to the superclasses' list. """ # We are not allowing editing of xblock tag and name fields at this time (for any component). return [XBlock.tags, XBlock.name] @property def editable_metadata_fields(self): """ Returns the metadata fields to be edited in Studio. These are fields with scope `Scope.settings`. Can be limited by extending `non_editable_metadata_fields`. """ inherited_metadata = getattr(self, '_inherited_metadata', {}) inheritable_metadata = getattr(self, '_inheritable_metadata', {}) metadata_fields = {} for field in self.fields: if field.scope != Scope.settings or field in self.non_editable_metadata_fields: continue inheritable = False value = getattr(self, field.name) default_value = field.default explicitly_set = field.name in self._model_data if field.name in inheritable_metadata: inheritable = True default_value = field.from_json( inheritable_metadata.get(field.name)) if field.name in inherited_metadata: explicitly_set = False # We support the following editors: # 1. A select editor for fields with a list of possible values (includes Booleans). # 2. Number editors for integers and floats. # 3. A generic string editor for anything else (editing JSON representation of the value). type = "Generic" values = [] if field.values is None else copy.deepcopy( field.values) if isinstance(values, tuple): values = list(values) if isinstance(values, list): if len(values) > 0: type = "Select" for index, choice in enumerate(values): json_choice = copy.deepcopy(choice) if isinstance(json_choice, dict) and 'value' in json_choice: json_choice['value'] = field.to_json( json_choice['value']) else: json_choice = field.to_json(json_choice) values[index] = json_choice elif isinstance(field, Integer): type = "Integer" elif isinstance(field, Float): type = "Float" metadata_fields[field.name] = { 'field_name': field.name, 'type': type, 'display_name': field.display_name, 'value': field.to_json(value), 'options': values, 'default_value': field.to_json(default_value), 'inheritable': inheritable, 'explicitly_set': explicitly_set, 'help': field.help } return metadata_fields
def compute_metadata_inheritance_tree(self, location): ''' TODO (cdodge) This method can be deleted when the 'split module store' work has been completed ''' # get all collections in the course, this query should not return any leaf nodes # note this is a bit ugly as when we add new categories of containers, we have to add it here query = {'_id.org': location.org, '_id.course': location.course, '_id.category': {'$in': ['course', 'chapter', 'sequential', 'vertical', 'videosequence', 'wrapper', 'problemset', 'conditional', 'randomize']} } # we just want the Location, children, and inheritable metadata record_filter = {'_id': 1, 'definition.children': 1} # just get the inheritable metadata since that is all we need for the computation # this minimizes both data pushed over the wire for field_name in InheritanceMixin.fields: record_filter['metadata.{0}'.format(field_name)] = 1 # call out to the DB resultset = self.collection.find(query, record_filter) results_by_url = {} root = None # now go through the results and order them by the location url for result in resultset: location = Location(result['_id']) # We need to collate between draft and non-draft # i.e. draft verticals will have draft children but will have non-draft parents currently location = location.replace(revision=None) location_url = location.url() if location_url in results_by_url: existing_children = results_by_url[location_url].get('definition', {}).get('children', []) additional_children = result.get('definition', {}).get('children', []) total_children = existing_children + additional_children results_by_url[location_url].setdefault('definition', {})['children'] = total_children results_by_url[location.url()] = result if location.category == 'course': root = location.url() # now traverse the tree and compute down the inherited metadata metadata_to_inherit = {} def _compute_inherited_metadata(url): """ Helper method for computing inherited metadata for a specific location url """ # check for presence of metadata key. Note that a given module may not yet be fully formed. # example: update_item -> update_children -> update_metadata sequence on new item create # if we get called here without update_metadata called first then 'metadata' hasn't been set # as we're not fully transactional at the DB layer. Same comment applies to below key name # check my_metadata = results_by_url[url].get('metadata', {}) # go through all the children and recurse, but only if we have # in the result set. Remember results will not contain leaf nodes for child in results_by_url[url].get('definition', {}).get('children', []): if child in results_by_url: new_child_metadata = copy.deepcopy(my_metadata) new_child_metadata.update(results_by_url[child].get('metadata', {})) results_by_url[child]['metadata'] = new_child_metadata metadata_to_inherit[child] = new_child_metadata _compute_inherited_metadata(child) else: # this is likely a leaf node, so let's record what metadata we need to inherit metadata_to_inherit[child] = my_metadata if root is not None: _compute_inherited_metadata(root) return metadata_to_inherit
class XModuleDescriptor(XModuleFields, HTMLSnippet, ResourceTemplates, XBlock): """ An XModuleDescriptor is a specification for an element of a course. This could be a problem, an organizational element (a group of content), or a segment of video, for example. XModuleDescriptors are independent and agnostic to the current student state on a problem. They handle the editing interface used by instructors to create a problem, and can generate XModules (which do know about student state). """ entry_point = "xmodule.v1" module_class = XModule # Attributes for inspection of the descriptor # Indicates whether the xmodule state should be # stored in a database (independent of shared state) stores_state = False # This indicates whether the xmodule is a problem-type. # It should respond to max_score() and grade(). It can be graded or ungraded # (like a practice problem). has_score = False # A list of descriptor attributes that must be equal for the descriptors to # be equal equality_attributes = ('_model_data', 'location') # Name of resource directory to load templates from template_dir_name = "default" # Class level variable always_recalculate_grades = False """ Return whether this descriptor always requires recalculation of grades, for example if the score can change via an extrnal service, not just when the student interacts with the module on the page. A specific example is FoldIt, which posts grade-changing updates through a separate API. """ # VS[compat]. Backwards compatibility code that can go away after # importing 2012 courses. # A set of metadata key conversions that we want to make metadata_translations = { 'slug': 'url_name', 'name': 'display_name', } # ============================= STRUCTURAL MANIPULATION =================== def __init__(self, system, location, model_data): """ Construct a new XModuleDescriptor. The only required arguments are the system, used for interaction with external resources, and the definition, which specifies all the data needed to edit and display the problem (but none of the associated metadata that handles recordkeeping around the problem). This allows for maximal flexibility to add to the interface while preserving backwards compatibility. system: A DescriptorSystem for interacting with external resources location: Something Location-like that identifies this xmodule model_data: A dictionary-like object that maps field names to values for those fields. """ self.system = system self.location = Location(location) self.url_name = self.location.name self.category = self.location.category self._model_data = model_data self._child_instances = None @property def id(self): return self.location.url() @property def display_name_with_default(self): ''' Return a display name for the module: use display_name if defined in metadata, otherwise convert the url name. ''' name = self.display_name if name is None: name = self.url_name.replace('_', ' ') return name def get_required_module_descriptors(self): """Returns a list of XModuleDescritpor instances upon which this module depends, but are not children of this module""" return [] def get_children(self): """Returns a list of XModuleDescriptor instances for the children of this module""" if not self.has_children: return [] if self._child_instances is None: self._child_instances = [] for child_loc in self.children: try: child = self.system.load_item(child_loc) except ItemNotFoundError: log.exception('Unable to load item {loc}, skipping'.format(loc=child_loc)) continue self._child_instances.append(child) return self._child_instances def get_child_by(self, selector): """ Return a child XModuleDescriptor with the specified url_name, if it exists, and None otherwise. """ for child in self.get_children(): if selector(child): return child return None def xmodule(self, system): """ Returns an XModule. system: Module system """ return self.module_class( system, self.location, self, system.xblock_model_data(self), ) def has_dynamic_children(self): """ Returns True if this descriptor has dynamic children for a given student when the module is created. Returns False if the children of this descriptor are the same children that the module will return for any student. """ return False # ================================= JSON PARSING =========================== @staticmethod def load_from_json(json_data, system, default_class=None): """ This method instantiates the correct subclass of XModuleDescriptor based on the contents of json_data. json_data must contain a 'location' element, and must be suitable to be passed into the subclasses `from_json` method as model_data """ class_ = XModuleDescriptor.load_class( json_data['location']['category'], default_class ) return class_.from_json(json_data, system) @classmethod def from_json(cls, json_data, system): """ Creates an instance of this descriptor from the supplied json_data. This may be overridden by subclasses json_data: A json object with the keys 'definition' and 'metadata', definition: A json object with the keys 'data' and 'children' data: A json value children: A list of edX Location urls metadata: A json object with any keys This json_data is transformed to model_data using the following rules: 1) The model data contains all of the fields from metadata 2) The model data contains the 'children' array 3) If 'definition.data' is a json object, model data contains all of its fields Otherwise, it contains the single field 'data' 4) Any value later in this list overrides a value earlier in this list system: A DescriptorSystem for interacting with external resources """ model_data = {} for key, value in json_data.get('metadata', {}).items(): model_data[cls._translate(key)] = value model_data.update(json_data.get('metadata', {})) definition = json_data.get('definition', {}) if 'children' in definition: model_data['children'] = definition['children'] if 'data' in definition: if isinstance(definition['data'], dict): model_data.update(definition['data']) else: model_data['data'] = definition['data'] return cls(system=system, location=json_data['location'], model_data=model_data) @classmethod def _translate(cls, key): 'VS[compat]' return cls.metadata_translations.get(key, key) # ================================= XML PARSING ============================ @staticmethod def load_from_xml(xml_data, system, org=None, course=None, default_class=None): """ This method instantiates the correct subclass of XModuleDescriptor based on the contents of xml_data. xml_data must be a string containing valid xml system is an XMLParsingSystem org and course are optional strings that will be used in the generated module's url identifiers """ class_ = XModuleDescriptor.load_class( etree.fromstring(xml_data).tag, default_class ) # leave next line, commented out - useful for low-level debugging # log.debug('[XModuleDescriptor.load_from_xml] tag=%s, class_=%s' % ( # etree.fromstring(xml_data).tag,class_)) return class_.from_xml(xml_data, system, org, course) @classmethod def from_xml(cls, xml_data, system, org=None, course=None): """ Creates an instance of this descriptor from the supplied xml_data. This may be overridden by subclasses xml_data: A string of xml that will be translated into data and children for this module system is an XMLParsingSystem org and course are optional strings that will be used in the generated module's url identifiers """ raise NotImplementedError( 'Modules must implement from_xml to be parsable from xml') def export_to_xml(self, resource_fs): """ Returns an xml string representing this module, and all modules underneath it. May also write required resources out to resource_fs Assumes that modules have single parentage (that no module appears twice in the same course), and that it is thus safe to nest modules as xml children as appropriate. The returned XML should be able to be parsed back into an identical XModuleDescriptor using the from_xml method with the same system, org, and course """ raise NotImplementedError( 'Modules must implement export_to_xml to enable xml export') # =============================== Testing ================================== def get_sample_state(self): """ Return a list of tuples of instance_state, shared_state. Each tuple defines a sample case for this module """ return [('{}', '{}')] # =============================== BUILTIN METHODS ========================== def __eq__(self, other): eq = (self.__class__ == other.__class__ and all(getattr(self, attr, None) == getattr(other, attr, None) for attr in self.equality_attributes)) return eq def __repr__(self): return ("{class_}({system!r}, location={location!r}," " model_data={model_data!r})".format( class_=self.__class__.__name__, system=self.system, location=self.location, model_data=self._model_data, )) @property def non_editable_metadata_fields(self): """ Return the list of fields that should not be editable in Studio. When overriding, be sure to append to the superclasses' list. """ # We are not allowing editing of xblock tag and name fields at this time (for any component). return [XBlock.tags, XBlock.name] @property def editable_metadata_fields(self): """ Returns the metadata fields to be edited in Studio. These are fields with scope `Scope.settings`. Can be limited by extending `non_editable_metadata_fields`. """ inherited_metadata = getattr(self, '_inherited_metadata', {}) inheritable_metadata = getattr(self, '_inheritable_metadata', {}) metadata = {} for field in self.fields: if field.scope != Scope.settings or field in self.non_editable_metadata_fields: continue inheritable = False value = getattr(self, field.name) default_value = field.default explicitly_set = field.name in self._model_data if field.name in inheritable_metadata: inheritable = True default_value = field.from_json(inheritable_metadata.get(field.name)) if field.name in inherited_metadata: explicitly_set = False metadata[field.name] = {'field': field, 'value': value, 'default_value': default_value, 'inheritable': inheritable, 'explicitly_set': explicitly_set } return metadata
def compute_metadata_inheritance_tree(self, location): ''' TODO (cdodge) This method can be deleted when the 'split module store' work has been completed ''' # get all collections in the course, this query should not return any leaf nodes # note this is a bit ugly as when we add new categories of containers, we have to add it here block_types_with_children = set(name for name, class_ in XBlock.load_classes() if getattr(class_, 'has_children', False)) query = {'_id.org': location.org, '_id.course': location.course, '_id.category': {'$in': list(block_types_with_children)} } # we just want the Location, children, and inheritable metadata record_filter = {'_id': 1, 'definition.children': 1} # just get the inheritable metadata since that is all we need for the computation # this minimizes both data pushed over the wire for field_name in InheritanceMixin.fields: record_filter['metadata.{0}'.format(field_name)] = 1 # call out to the DB resultset = self.collection.find(query, record_filter) results_by_url = {} root = None # now go through the results and order them by the location url for result in resultset: location = Location(result['_id']) # We need to collate between draft and non-draft # i.e. draft verticals will have draft children but will have non-draft parents currently location = location.replace(revision=None) location_url = location.url() if location_url in results_by_url: existing_children = results_by_url[location_url].get('definition', {}).get('children', []) additional_children = result.get('definition', {}).get('children', []) total_children = existing_children + additional_children results_by_url[location_url].setdefault('definition', {})['children'] = total_children results_by_url[location.url()] = result if location.category == 'course': root = location.url() # now traverse the tree and compute down the inherited metadata metadata_to_inherit = {} def _compute_inherited_metadata(url): """ Helper method for computing inherited metadata for a specific location url """ my_metadata = results_by_url[url].get('metadata', {}) # go through all the children and recurse, but only if we have # in the result set. Remember results will not contain leaf nodes for child in results_by_url[url].get('definition', {}).get('children', []): if child in results_by_url: new_child_metadata = copy.deepcopy(my_metadata) new_child_metadata.update(results_by_url[child].get('metadata', {})) results_by_url[child]['metadata'] = new_child_metadata metadata_to_inherit[child] = new_child_metadata _compute_inherited_metadata(child) else: # this is likely a leaf node, so let's record what metadata we need to inherit metadata_to_inherit[child] = my_metadata if root is not None: _compute_inherited_metadata(root) return metadata_to_inherit
def compute_metadata_inheritance_tree(self, location): ''' TODO (cdodge) This method can be deleted when the 'split module store' work has been completed ''' # get all collections in the course, this query should not return any leaf nodes # note this is a bit ugly as when we add new categories of containers, we have to add it here query = { '_id.org': location.org, '_id.course': location.course, '_id.category': { '$in': [ 'course', 'chapter', 'sequential', 'vertical', 'videosequence', 'wrapper', 'problemset', 'conditional', 'randomize' ] } } # we just want the Location, children, and inheritable metadata record_filter = {'_id': 1, 'definition.children': 1} # just get the inheritable metadata since that is all we need for the computation # this minimizes both data pushed over the wire for field_name in InheritanceMixin.fields: record_filter['metadata.{0}'.format(field_name)] = 1 # call out to the DB resultset = self.collection.find(query, record_filter) results_by_url = {} root = None # now go through the results and order them by the location url for result in resultset: location = Location(result['_id']) # We need to collate between draft and non-draft # i.e. draft verticals will have draft children but will have non-draft parents currently location = location.replace(revision=None) location_url = location.url() if location_url in results_by_url: existing_children = results_by_url[location_url].get( 'definition', {}).get('children', []) additional_children = result.get('definition', {}).get('children', []) total_children = existing_children + additional_children results_by_url[location_url].setdefault( 'definition', {})['children'] = total_children results_by_url[location.url()] = result if location.category == 'course': root = location.url() # now traverse the tree and compute down the inherited metadata metadata_to_inherit = {} def _compute_inherited_metadata(url): """ Helper method for computing inherited metadata for a specific location url """ my_metadata = results_by_url[url].get('metadata', {}) # go through all the children and recurse, but only if we have # in the result set. Remember results will not contain leaf nodes for child in results_by_url[url].get('definition', {}).get('children', []): if child in results_by_url: new_child_metadata = copy.deepcopy(my_metadata) new_child_metadata.update(results_by_url[child].get( 'metadata', {})) results_by_url[child]['metadata'] = new_child_metadata metadata_to_inherit[child] = new_child_metadata _compute_inherited_metadata(child) else: # this is likely a leaf node, so let's record what metadata we need to inherit metadata_to_inherit[child] = my_metadata if root is not None: _compute_inherited_metadata(root) return metadata_to_inherit