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 _import_module(module): module.location = module.location._replace(revision='draft') # make sure our parent has us in its list of children # this is to make sure private only verticals show up in the list of children since # they would have been filtered out from the non-draft store export if module.location.category == 'vertical': module.location = module.location._replace(revision=None) sequential_url = module.xml_attributes['parent_sequential_url'] index = int(module.xml_attributes['index_in_children_list']) seq_location = Location(sequential_url) # IMPORTANT: Be sure to update the sequential in the NEW namespace seq_location = seq_location._replace(org=target_location_namespace.org, course=target_location_namespace.course ) sequential = store.get_item(seq_location) if module.location.url() not in sequential.children: sequential.children.insert(index, module.location.url()) store.update_children(sequential.location, sequential.children) del module.xml_attributes['parent_sequential_url'] del module.xml_attributes['index_in_children_list'] import_module(module, draft_store, course_data_path, static_content_store, allow_not_found=True) for child in module.get_children(): _import_module(child)
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 remap_namespace(module, target_location_namespace): if target_location_namespace is None: return module # This looks a bit wonky as we need to also change the 'name' of the imported course to be what # the caller passed in if module.location.category != 'course': module.location = module.location._replace(tag=target_location_namespace.tag, org=target_location_namespace.org, course=target_location_namespace.course) else: module.location = module.location._replace(tag=target_location_namespace.tag, org=target_location_namespace.org, course=target_location_namespace.course, name=target_location_namespace.name) # then remap children pointers since they too will be re-namespaced if hasattr(module, 'children'): children_locs = module.children if children_locs is not None and children_locs != []: new_locs = [] for child in children_locs: child_loc = Location(child) new_child_loc = child_loc._replace(tag=target_location_namespace.tag, org=target_location_namespace.org, course=target_location_namespace.course) new_locs.append(new_child_loc.url()) module.children = new_locs return 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 clone_item(request): parent_location = Location(request.POST['parent_location']) template = Location(request.POST['template']) display_name = request.POST.get('display_name') if not has_access(request.user, parent_location): raise PermissionDenied() parent = get_modulestore(template).get_item(parent_location) dest_location = parent_location._replace( category=template.category, name=uuid4().hex) new_item = get_modulestore(template).clone_item(template, dest_location) # replace the display name with an optional parameter passed in from the # caller if display_name is not None: new_item.display_name = display_name get_modulestore(template).update_metadata( new_item.location.url(), own_metadata(new_item)) if new_item.location.category not in DETACHED_CATEGORIES: get_modulestore(parent.location).update_children( parent_location, parent.children + [new_item.location.url()]) return HttpResponse(json.dumps({'id': dest_location.url()}))
def _query_children_for_cache_children(self, items): # first get non-draft in a round-trip to_process_non_drafts = super(DraftModuleStore, self)._query_children_for_cache_children(items) to_process_dict = {} for non_draft in to_process_non_drafts: to_process_dict[Location(non_draft["_id"])] = non_draft # now query all draft content in another round-trip query = { '_id': {'$in': [namedtuple_to_son(as_draft(Location(item))) for item in items]} } to_process_drafts = list(self.collection.find(query)) # now we have to go through all drafts and replace the non-draft # with the draft. This is because the semantics of the DraftStore is to # always return the draft - if available for draft in to_process_drafts: draft_loc = Location(draft["_id"]) draft_as_non_draft_loc = draft_loc.replace(revision=None) # does non-draft exist in the collection # if so, replace it if draft_as_non_draft_loc in to_process_dict: to_process_dict[draft_as_non_draft_loc] = draft # convert the dict - which is used for look ups - back into a list queried_children = to_process_dict.values() return queried_children
def import_module_from_xml(modulestore, static_content_store, course_data_path, module, target_location_namespace=None, verbose=False): # remap module to the new namespace if target_location_namespace is not None: # This looks a bit wonky as we need to also change the 'name' of the imported course to be what # the caller passed in if module.location.category != 'course': module.location = module.location._replace(tag=target_location_namespace.tag, org=target_location_namespace.org, course=target_location_namespace.course) else: module.location = module.location._replace(tag=target_location_namespace.tag, org=target_location_namespace.org, course=target_location_namespace.course, name=target_location_namespace.name) # then remap children pointers since they too will be re-namespaced if module.has_children: children_locs = module.children new_locs = [] for child in children_locs: child_loc = Location(child) new_child_loc = child_loc._replace(tag=target_location_namespace.tag, org=target_location_namespace.org, course=target_location_namespace.course) new_locs.append(new_child_loc.url()) module.children = new_locs if hasattr(module, 'data'): modulestore.update_item(module.location, module.data) if module.has_children: modulestore.update_children(module.location, module.children) 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 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 error_msg is None: # this string is not marked for translation because we don't have # access to the user context, and this will only be seen by staff error_msg = 'Error not available' 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, '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 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 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): 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 _import_module(module): module.location = module.location.replace(revision="draft") # make sure our parent has us in its list of children # this is to make sure private only verticals show up # in the list of children since they would have been # filtered out from the non-draft store export if module.location.category == "vertical": non_draft_location = module.location.replace(revision=None) sequential_url = module.xml_attributes["parent_sequential_url"] index = int(module.xml_attributes["index_in_children_list"]) seq_location = Location(sequential_url) # IMPORTANT: Be sure to update the sequential # in the NEW namespace seq_location = seq_location.replace( org=target_location_namespace.org, course=target_location_namespace.course ) sequential = store.get_item(seq_location, depth=0) if non_draft_location.url() not in sequential.children: sequential.children.insert(index, non_draft_location.url()) store.update_item(sequential, "**replace_user**") import_module( module, draft_store, course_data_path, static_content_store, source_location_namespace, target_location_namespace, allow_not_found=True, ) for child in module.get_children(): _import_module(child)
def remap_namespace(module, target_location_namespace): if target_location_namespace is None: return module # This looks a bit wonky as we need to also change the 'name' of the # imported course to be what the caller passed in if module.location.category != 'course': module.location = module.location._replace( tag=target_location_namespace.tag, org=target_location_namespace.org, course=target_location_namespace.course ) else: original_location = module.location # # module is a course module # module.location = module.location._replace( tag=target_location_namespace.tag, org=target_location_namespace.org, course=target_location_namespace.course, name=target_location_namespace.name ) # There is more re-namespacing work we have to do when # importing course modules # remap pdf_textbook urls for entry in module.pdf_textbooks: for chapter in entry.get('chapters', []): if StaticContent.is_c4x_path(chapter.get('url', '')): chapter['url'] = StaticContent.renamespace_c4x_path( chapter['url'], target_location_namespace ) # if there is a wiki_slug which is the same as the original location # (aka default value), then remap that so the wiki doesn't point to # the old Wiki. if module.wiki_slug == original_location.course: module.wiki_slug = target_location_namespace.course module.save() # then remap children pointers since they too will be re-namespaced if hasattr(module, 'children'): children_locs = module.children if children_locs is not None and children_locs != []: new_locs = [] for child in children_locs: child_loc = Location(child) new_child_loc = child_loc._replace( tag=target_location_namespace.tag, org=target_location_namespace.org, course=target_location_namespace.course ) new_locs.append(new_child_loc.url()) module.children = new_locs return module
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 create_item(request): parent_location = Location(request.POST["parent_location"]) category = request.POST["category"] display_name = request.POST.get("display_name") if not has_access(request.user, parent_location): raise PermissionDenied() parent = get_modulestore(category).get_item(parent_location) dest_location = parent_location.replace(category=category, name=uuid4().hex) # get the metadata, display_name, and definition from the request metadata = {} data = None template_id = request.POST.get("boilerplate") if template_id is not None: clz = XModuleDescriptor.load_class(category) if clz is not None: template = clz.get_template(template_id) if template is not None: metadata = template.get("metadata", {}) data = template.get("data") if display_name is not None: metadata["display_name"] = display_name get_modulestore(category).create_and_save_xmodule( dest_location, definition_data=data, metadata=metadata, system=parent.system ) if category not in DETACHED_CATEGORIES: get_modulestore(parent.location).update_children(parent_location, parent.children + [dest_location.url()]) return JsonResponse({"id": dest_location.url()})
def 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 create_new_course(request): if settings.MITX_FEATURES.get('DISABLE_COURSE_CREATION', False) and not request.user.is_staff: raise PermissionDenied() # This logic is repeated in xmodule/modulestore/tests/factories.py # so if you change anything here, you need to also change it there. # TODO: write a test that creates two courses, one with the factory and # the other with this method, then compare them to make sure they are # equivalent. template = Location(request.POST['template']) org = request.POST.get('org') number = request.POST.get('number') display_name = request.POST.get('display_name') try: dest_location = Location('i4x', org, number, 'course', Location.clean(display_name)) except InvalidLocationError as error: return HttpResponse(json.dumps({'ErrMsg': "Unable to create course '" + display_name + "'.\n\n" + error.message})) # see if the course already exists existing_course = None try: existing_course = modulestore('direct').get_item(dest_location) except ItemNotFoundError: pass if existing_course is not None: return HttpResponse(json.dumps({'ErrMsg': 'There is already a course defined with this name.'})) course_search_location = ['i4x', dest_location.org, dest_location.course, 'course', None] courses = modulestore().get_items(course_search_location) if len(courses) > 0: return HttpResponse(json.dumps({'ErrMsg': 'There is already a course defined with the same organization and course number.'})) new_course = modulestore('direct').clone_item(template, dest_location) # clone a default 'about' module as well about_template_location = Location(['i4x', 'edx', 'templates', 'about', 'overview']) dest_about_location = dest_location._replace(category='about', name='overview') modulestore('direct').clone_item(about_template_location, dest_about_location) if display_name is not None: new_course.display_name = display_name # set a default start date to now new_course.start = datetime.datetime.now(UTC()) initialize_course_tabs(new_course) create_all_course_groups(request.user, new_course.location) # seed the forums seed_permissions_roles(new_course.location.course_id) return HttpResponse(json.dumps({'id': new_course.location.url()}))
def test_post_course_update(self): """ Test that a user can successfully post on course updates and handouts of a course whose location in not in loc_mapper """ # create a course via the view handler course_location = Location(['i4x', 'Org_1', 'Course_1', 'course', 'Run_1']) course_locator = loc_mapper().translate_location( course_location.course_id, course_location, False, True ) self.client.ajax_post( course_locator.url_reverse('course'), { 'org': course_location.org, 'number': course_location.course, 'display_name': 'test course', 'run': course_location.name, } ) branch = u'draft' version = None block = u'updates' updates_locator = BlockUsageLocator( package_id=course_location.course_id.replace('/', '.'), branch=branch, version_guid=version, block_id=block ) content = u"Sample update" payload = {'content': content, 'date': 'January 8, 2013'} course_update_url = updates_locator.url_reverse('course_info_update') resp = self.client.ajax_post(course_update_url, payload) # check that response status is 200 not 400 self.assertEqual(resp.status_code, 200) payload = json.loads(resp.content) self.assertHTMLEqual(payload['content'], content) # now test that calling translate_location returns a locator whose block_id is 'updates' updates_location = course_location.replace(category='course_info', name=block) updates_locator = loc_mapper().translate_location(course_location.course_id, updates_location) self.assertTrue(isinstance(updates_locator, BlockUsageLocator)) self.assertEqual(updates_locator.block_id, block) # check posting on handouts block = u'handouts' handouts_locator = BlockUsageLocator( package_id=updates_locator.package_id, branch=updates_locator.branch, version_guid=version, block_id=block ) course_handouts_url = handouts_locator.url_reverse('xblock') content = u"Sample handout" payload = {"data": content} resp = self.client.ajax_post(course_handouts_url, payload) # check that response status is 200 not 500 self.assertEqual(resp.status_code, 200) payload = json.loads(resp.content) self.assertHTMLEqual(payload['data'], content)
def create_new_course(request): """ Create a new course """ if not is_user_in_creator_group(request.user): raise PermissionDenied() org = request.POST.get("org") number = request.POST.get("number") display_name = request.POST.get("display_name") try: dest_location = Location("i4x", org, number, "course", Location.clean(display_name)) except InvalidLocationError as error: return JsonResponse( {"ErrMsg": "Unable to create course '{name}'.\n\n{err}".format(name=display_name, err=error.message)} ) # see if the course already exists existing_course = None try: existing_course = modulestore("direct").get_item(dest_location) except ItemNotFoundError: pass if existing_course is not None: return JsonResponse({"ErrMsg": "There is already a course defined with this name."}) course_search_location = ["i4x", dest_location.org, dest_location.course, "course", None] courses = modulestore().get_items(course_search_location) if len(courses) > 0: return JsonResponse( {"ErrMsg": "There is already a course defined with the same organization and course number."} ) # instantiate the CourseDescriptor and then persist it # note: no system to pass if display_name is None: metadata = {} else: metadata = {"display_name": display_name} modulestore("direct").create_and_save_xmodule(dest_location, metadata=metadata) new_course = modulestore("direct").get_item(dest_location) # clone a default 'about' overview module as well dest_about_location = dest_location.replace(category="about", name="overview") overview_template = AboutDescriptor.get_template("overview.yaml") modulestore("direct").create_and_save_xmodule( dest_about_location, system=new_course.system, definition_data=overview_template.get("data") ) initialize_course_tabs(new_course) create_all_course_groups(request.user, new_course.location) # seed the forums seed_permissions_roles(new_course.location.course_id) return JsonResponse({"id": new_course.location.url()})
def import_module_from_xml(modulestore, static_content_store, course_data_path, module, target_location_namespace=None, verbose=False): # remap module to the new namespace if target_location_namespace is not None: # This looks a bit wonky as we need to also change the 'name' of the imported course to be what # the caller passed in if module.location.category != 'course': module.location = module.location._replace( tag=target_location_namespace.tag, org=target_location_namespace.org, course=target_location_namespace.course) else: module.location = module.location._replace( tag=target_location_namespace.tag, org=target_location_namespace.org, course=target_location_namespace.course, name=target_location_namespace.name) # then remap children pointers since they too will be re-namespaced if module.has_children: children_locs = module.children new_locs = [] for child in children_locs: child_loc = Location(child) new_child_loc = child_loc._replace( tag=target_location_namespace.tag, org=target_location_namespace.org, course=target_location_namespace.course) new_locs.append(new_child_loc.url()) module.children = new_locs if hasattr(module, 'data'): # cdodge: now go through any link references to '/static/' and make sure we've imported # it as a StaticContent asset try: remap_dict = {} # use the rewrite_links as a utility means to enumerate through all links # in the module data. We use that to load that reference into our asset store # IMPORTANT: There appears to be a bug in lxml.rewrite_link which makes us not be able to # do the rewrites natively in that code. # For example, what I'm seeing is <img src='foo.jpg' /> -> <img src='bar.jpg'> # Note the dropped element closing tag. This causes the LMS to fail when rendering modules - that's # no good, so we have to do this kludge if isinstance(module.data, str) or isinstance(module.data, unicode): # some module 'data' fields are non strings which blows up the link traversal code lxml_rewrite_links(module.data, lambda link: verify_content_links( module, course_data_path, static_content_store, link, remap_dict)) for key in remap_dict.keys(): module.data = module.data.replace(key, remap_dict[key]) except Exception: logging.exception( "failed to rewrite links on {0}. Continuing...".format(module.location)) modulestore.update_item(module.location, module.data) if module.has_children: modulestore.update_children(module.location, module.children) modulestore.update_metadata(module.location, own_metadata(module))
def load_item(self, location): """ Return an XModule instance for the specified location """ location = Location(location) json_data = self.module_data.get(location) if json_data is None: module = self.modulestore.get_item(location) if module is not None: # update our own cache after going to the DB to get cache miss self.module_data.update(module.runtime.module_data) return module else: # load the module and apply the inherited metadata try: category = json_data['location']['category'] class_ = XModuleDescriptor.load_class( category, self.default_class ) definition = json_data.get('definition', {}) metadata = json_data.get('metadata', {}) for old_name, new_name in getattr(class_, 'metadata_translations', {}).items(): if old_name in metadata: metadata[new_name] = metadata[old_name] del metadata[old_name] kvs = MongoKeyValueStore( definition.get('data', {}), definition.get('children', []), metadata, ) field_data = DbModel(kvs) scope_ids = ScopeIds(None, category, location, location) module = self.construct_xblock_from_class(class_, field_data, scope_ids) if self.cached_metadata is not None: # parent container pointers don't differentiate between draft and non-draft # so when we do the lookup, we should do so with a non-draft location non_draft_loc = location.replace(revision=None) # Convert the serialized fields values in self.cached_metadata # to python values metadata_to_inherit = self.cached_metadata.get(non_draft_loc.url(), {}) inherit_metadata(module, metadata_to_inherit) # decache any computed pending field settings module.save() return module except: log.warning("Failed to load descriptor", exc_info=True) return ErrorDescriptor.from_json( json_data, self, json_data['location'], error_msg=exc_info_to_str(sys.exc_info()) )
def load_item(self, location): """ Return an XModule instance for the specified location """ location = Location(location) json_data = self.module_data.get(location) if json_data is None: module = self.modulestore.get_item(location) if module is not None: # update our own cache after going to the DB to get cache miss self.module_data.update(module.system.module_data) return module else: # load the module and apply the inherited metadata try: category = json_data['location']['category'] class_ = XModuleDescriptor.load_class( category, self.default_class ) definition = json_data.get('definition', {}) metadata = json_data.get('metadata', {}) for old_name, new_name in class_.metadata_translations.items(): if old_name in metadata: metadata[new_name] = metadata[old_name] del metadata[old_name] kvs = MongoKeyValueStore( definition.get('data', {}), definition.get('children', []), metadata, location, category ) model_data = DbModel(kvs, class_, None, MongoUsage(self.course_id, location)) model_data['category'] = category model_data['location'] = location module = class_(self, model_data) if self.cached_metadata is not None: # parent container pointers don't differentiate between draft and non-draft # so when we do the lookup, we should do so with a non-draft location non_draft_loc = location.replace(revision=None) metadata_to_inherit = self.cached_metadata.get(non_draft_loc.url(), {}) inherit_metadata(module, metadata_to_inherit) return module except: log.warning("Failed to load descriptor", exc_info=True) return ErrorDescriptor.from_json( json_data, self, json_data['location'], error_msg=exc_info_to_str(sys.exc_info()) )
def test_parse_course_id(self): """ Test the parse_course_id class method """ source_string = "myorg/mycourse/myrun" parsed = Location.parse_course_id(source_string) self.assertEqual(parsed["org"], "myorg") self.assertEqual(parsed["course"], "mycourse") self.assertEqual(parsed["name"], "myrun") with self.assertRaises(ValueError): Location.parse_course_id("notlegit.id/foo")
def _infer_course_id_try(self, location): """ Create, Update, Delete operations don't require a fully-specified course_id, but there's no complete & sound general way to compute the course_id except via the proper modulestore. This method attempts several sound but not complete methods. :param location: an old style Location """ if isinstance(location, CourseLocator): return location.package_id elif isinstance(location, basestring): try: location = Location(location) except InvalidLocationError: # try to parse as a course_id try: Location.parse_course_id(location) # it's already a course_id return location except ValueError: # cannot interpret the location return None # location is a Location at this point if location.category == 'course': # easiest case return location.course_id # try finding in loc_mapper try: # see if the loc mapper knows the course id (requires double translation) locator = loc_mapper().translate_location_to_course_locator(None, location) location = loc_mapper().translate_locator_to_location(locator, get_course=True) return location.course_id except ItemNotFoundError: pass # expensive query against all location-based modulestores to look for location. for store in self.modulestores.itervalues(): if isinstance(location, store.reference_type): try: xblock = store.get_item(location) course_id = self._get_course_id_from_block(xblock, store) if course_id is not None: return course_id except NotImplementedError: blocks = store.get_items(location) if len(blocks) == 1: block = blocks[0] try: return block.course_id except UndefinedContext: pass except ItemNotFoundError: pass # if we get here, it must be in a Locator based store, but we won't be able to find # it. return None
def fetch(cls, course_location): """ Fetch the course details for the given course from persistence and return a CourseDetails model. """ if not isinstance(course_location, Location): course_location = Location(course_location) course = cls(course_location) descriptor = get_modulestore(course_location).get_item(course_location) course.start_date = descriptor.start course.end_date = descriptor.end course.enrollment_start = descriptor.enrollment_start course.enrollment_end = descriptor.enrollment_end course.course_image_name = descriptor.course_image course.course_image_asset_path = course_image_url(descriptor) temploc = course_location.replace(category='about', name='syllabus') try: course.syllabus = get_modulestore(temploc).get_item(temploc).data except ItemNotFoundError: pass temploc = temploc.replace(name='overview') try: course.overview = get_modulestore(temploc).get_item(temploc).data except ItemNotFoundError: pass temploc = temploc.replace(name='tags') try: course.tags = get_modulestore(temploc).get_item(temploc).data except ItemNotFoundError: pass temploc = temploc.replace(name='effort') try: course.effort = get_modulestore(temploc).get_item(temploc).data except ItemNotFoundError: pass temploc = temploc.replace(name='video') try: raw_video = get_modulestore(temploc).get_item(temploc).data course.intro_video = CourseDetails.parse_video_tag(raw_video) except ItemNotFoundError: pass return course
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 get_items(self, location, course_id=None, depth=0, qualifiers=None): """ Returns a list of XModuleDescriptor instances for the items that match location. Any element of location that is None is treated as a wildcard that matches any value. NOTE: don't use this to look for courses as the course_id is required. Use get_courses. location: either a Location possibly w/ None as wildcards for category or name or a Locator with at least a package_id and branch but possibly no block_id. depth: An argument that some module stores may use to prefetch descendents of the queried modules for more efficient results later in the request. The depth is counted in the number of calls to get_children() to cache. None indicates to cache all descendents """ if not (course_id or hasattr(location, 'package_id')): raise Exception("Must pass in a course_id when calling get_items()") store = self._get_modulestore_for_courseid(course_id or getattr(location, 'package_id')) # translate won't work w/ missing fields so work around it if store.reference_type == Location: if not self.use_locations: if getattr(location, 'block_id', False): location = self._incoming_reference_adaptor(store, course_id, location) else: # get the course's location location = loc_mapper().translate_locator_to_location(location, get_course=True) # now remove the unknowns location = location.replace( category=qualifiers.get('category', None), name=None ) else: if self.use_locations: if not isinstance(location, Location): location = Location(location) try: location.ensure_fully_specified() location = loc_mapper().translate_location( course_id, location, location.revision == 'published', True ) except InsufficientSpecificationError: # construct the Locator by hand if location.category is not None and qualifiers.get('category', False): qualifiers['category'] = location.category location = loc_mapper().translate_location_to_course_locator( course_id, location, location.revision == 'published' ) xblocks = store.get_items(location, course_id, depth, qualifiers) xblocks = [self._outgoing_xblock_adaptor(store, course_id, xblock) for xblock in xblocks] return xblocks
class TestOrphan(unittest.TestCase): """ Test the orphan finding code """ # Snippet of what would be in the django settings envs file db_config = { 'host': 'localhost', 'db': 'test_xmodule', } modulestore_options = { 'default_class': 'xmodule.raw_module.RawDescriptor', 'fs_root': '', 'render_template': mock.Mock(return_value=""), 'xblock_mixins': (InheritanceMixin, ) } split_course_id = 'test_org.test_course.runid' def setUp(self): self.db_config['collection'] = 'modulestore{0}'.format( uuid.uuid4().hex) self.userid = random.getrandbits(32) super(TestOrphan, self).setUp() self.split_mongo = SplitMongoModuleStore(self.db_config, **self.modulestore_options) self.addCleanup(self.tear_down_split) self.old_mongo = MongoModuleStore(self.db_config, **self.modulestore_options) self.addCleanup(self.tear_down_mongo) self.course_location = None self._create_course() def tear_down_split(self): """ Remove the test collections, close the db connection """ split_db = self.split_mongo.db split_db.drop_collection(split_db.course_index) split_db.drop_collection(split_db.structures) split_db.drop_collection(split_db.definitions) split_db.connection.close() def tear_down_mongo(self): """ Remove the test collections, close the db connection """ split_db = self.split_mongo.db # old_mongo doesn't give a db attr, but all of the dbs are the same split_db.drop_collection(self.old_mongo.collection) 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 _create_course(self): """ * some detached items * some attached children * some orphans """ date_proxy = Date() metadata = { 'start': date_proxy.to_json(datetime.datetime(2000, 3, 13, 4)), 'display_name': 'Migration test course', } data = {'wiki_slug': 'test_course_slug'} fields = metadata.copy() fields.update(data) # split requires the course to be created separately from creating items self.split_mongo.create_course('test_org', 'my course', self.userid, self.split_course_id, fields=fields, root_usage_id='runid') self.course_location = Location('i4x', 'test_org', 'test_course', 'course', 'runid') self.old_mongo.create_and_save_xmodule(self.course_location, data, metadata) runtime = self.old_mongo.get_item(self.course_location).runtime self._create_item('chapter', 'Chapter1', {}, {'display_name': 'Chapter 1'}, 'course', 'runid', runtime) self._create_item('chapter', 'Chapter2', {}, {'display_name': 'Chapter 2'}, 'course', 'runid', runtime) self._create_item('chapter', 'OrphanChapter', {}, {'display_name': 'Orphan Chapter'}, None, None, runtime) self._create_item('vertical', 'Vert1', {}, {'display_name': 'Vertical 1'}, 'chapter', 'Chapter1', runtime) self._create_item('vertical', 'OrphanVert', {}, {'display_name': 'Orphan Vertical'}, None, None, runtime) self._create_item('html', 'Html1', "<p>Goodbye</p>", {'display_name': 'Parented Html'}, 'vertical', 'Vert1', runtime) self._create_item('html', 'OrphanHtml', "<p>Hello</p>", {'display_name': 'Orphan html'}, None, None, runtime) self._create_item('static_tab', 'staticuno', "<p>tab</p>", {'display_name': 'Tab uno'}, None, None, runtime) self._create_item('about', 'overview', "<p>overview</p>", {}, None, None, runtime) self._create_item('course_info', 'updates', "<ol><li><h2>Sep 22</h2><p>test</p></li></ol>", {}, None, None, runtime) def test_mongo_orphan(self): """ Test that old mongo finds the orphans """ orphans = self.old_mongo.get_orphans( self.course_location, ['static_tab', 'about', 'course_info'], None) self.assertEqual(len(orphans), 3, "Wrong # {}".format(orphans)) location = self.course_location.replace(category='chapter', name='OrphanChapter') self.assertIn(location.url(), orphans) location = self.course_location.replace(category='vertical', name='OrphanVert') self.assertIn(location.url(), orphans) location = self.course_location.replace(category='html', name='OrphanHtml') self.assertIn(location.url(), orphans) def test_split_orphan(self): """ Test that old mongo finds the orphans """ orphans = self.split_mongo.get_orphans( self.split_course_id, ['static_tab', 'about', 'course_info'], 'draft') self.assertEqual(len(orphans), 3, "Wrong # {}".format(orphans)) location = BlockUsageLocator(course_id=self.split_course_id, branch='draft', usage_id='OrphanChapter') self.assertIn(location, orphans) location = BlockUsageLocator(course_id=self.split_course_id, branch='draft', usage_id='OrphanVert') self.assertIn(location, orphans) location = BlockUsageLocator(course_id=self.split_course_id, branch='draft', usage_id='OrphanHtml') self.assertIn(location, orphans)
def get_about_page_link(self): """ create mock course and return the about page link """ location = Location('i4x', 'mitX', '101', 'course', 'test') return utils.get_lms_link_for_about_page(location)
def test_delete_block(self): """ test delete_block_location_translator(location, old_course_id=None) """ org = 'foo_org' course = 'bar_course' new_style_course_id = '{}.geek_dept.{}.baz_run'.format(org, course) loc_mapper().create_map_entry(Location('i4x', org, course, 'course', 'baz_run'), new_style_course_id, block_map={ 'abc123': { 'problem': 'problem2' }, '48f23a10395384929234': { 'chapter': 'chapter48f' }, '1': { 'chapter': 'chapter1', 'problem': 'problem1' }, }) new_style_course_id2 = '{}.geek_dept.{}.delta_run'.format(org, course) loc_mapper().create_map_entry(Location('i4x', org, course, 'course', 'delta_run'), new_style_course_id2, block_map={ 'abc123': { 'problem': 'problem3' }, '48f23a10395384929234': { 'chapter': 'chapter48b' }, '1': { 'chapter': 'chapter2', 'problem': 'problem2' }, }) location = Location('i4x', org, course, 'problem', '1') # delete from all courses loc_mapper().delete_block_location_translator(location) self.assertIsNone(loc_mapper().translate_locator_to_location( BlockUsageLocator(course_id=new_style_course_id, usage_id='problem1'))) self.assertIsNone(loc_mapper().translate_locator_to_location( BlockUsageLocator(course_id=new_style_course_id2, usage_id='problem2'))) # delete from one course location = location.replace(name='abc123') loc_mapper().delete_block_location_translator( location, '{}/{}/{}'.format(org, course, 'baz_run')) with self.assertRaises(ItemNotFoundError): loc_mapper().translate_location('{}/{}/{}'.format( org, course, 'baz_run'), location, add_entry_if_missing=False) locator = loc_mapper().translate_location('{}/{}/{}'.format( org, course, 'delta_run'), location, add_entry_if_missing=False) self.assertEqual(locator.usage_id, 'problem3')
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_draft_metadata(self): ''' This verifies a bug we had where inherited metadata was getting written to the module as 'own-metadata' when publishing. Also verifies the metadata inheritance is properly computed ''' store = modulestore('direct') draft_store = modulestore('draft') import_from_xml(store, 'common/test/data/', ['simple']) course = draft_store.get_item(Location( ['i4x', 'edX', 'simple', 'course', '2012_Fall', None]), depth=None) html_module = draft_store.get_item( ['i4x', 'edX', 'simple', 'html', 'test_html', None]) self.assertEqual(html_module.lms.graceperiod, course.lms.graceperiod) self.assertNotIn('graceperiod', own_metadata(html_module)) draft_store.clone_item(html_module.location, html_module.location) # refetch to check metadata html_module = draft_store.get_item( ['i4x', 'edX', 'simple', 'html', 'test_html', None]) self.assertEqual(html_module.lms.graceperiod, course.lms.graceperiod) self.assertNotIn('graceperiod', own_metadata(html_module)) # publish module draft_store.publish(html_module.location, 0) # refetch to check metadata html_module = draft_store.get_item( ['i4x', 'edX', 'simple', 'html', 'test_html', None]) self.assertEqual(html_module.lms.graceperiod, course.lms.graceperiod) self.assertNotIn('graceperiod', own_metadata(html_module)) # put back in draft and change metadata and see if it's now marked as 'own_metadata' draft_store.clone_item(html_module.location, html_module.location) html_module = draft_store.get_item( ['i4x', 'edX', 'simple', 'html', 'test_html', None]) new_graceperiod = timedelta(**{'hours': 1}) self.assertNotIn('graceperiod', own_metadata(html_module)) html_module.lms.graceperiod = new_graceperiod self.assertIn('graceperiod', own_metadata(html_module)) self.assertEqual(html_module.lms.graceperiod, new_graceperiod) draft_store.update_metadata(html_module.location, own_metadata(html_module)) # read back to make sure it reads as 'own-metadata' html_module = draft_store.get_item( ['i4x', 'edX', 'simple', 'html', 'test_html', None]) self.assertIn('graceperiod', own_metadata(html_module)) self.assertEqual(html_module.lms.graceperiod, new_graceperiod) # republish draft_store.publish(html_module.location, 0) # and re-read and verify 'own-metadata' draft_store.clone_item(html_module.location, html_module.location) html_module = draft_store.get_item( ['i4x', 'edX', 'simple', 'html', 'test_html', None]) self.assertIn('graceperiod', own_metadata(html_module)) self.assertEqual(html_module.lms.graceperiod, new_graceperiod)
class OpenEndedModuleXmlAttemptTest(unittest.TestCase, DummyModulestore): """ Test if student is able to reset the problem """ problem_location = Location([ "i4x", "edX", "open_ended", "combinedopenended", "SampleQuestion1Attempt" ]) answer = "blah blah" assessment = [0, 1] hint = "blah" def setUp(self): self.test_system = get_test_system() self.test_system.xqueue['interface'] = Mock(send_to_queue=Mock( side_effect=[1, "queued"])) self.setup_modulestore(COURSE) def test_reset_fail(self): """ Test the flow of the module if we complete the self assessment step and then reset Since the problem only allows one attempt, should fail. @return: """ assessment = [0, 1] module = self.get_module_from_location(self.problem_location, COURSE) #Simulate a student saving an answer module.handle_ajax("save_answer", {"student_answer": self.answer}) status = module.handle_ajax("get_status", {}) self.assertTrue(isinstance(status, basestring)) #Mock a student submitting an assessment assessment_dict = MockQueryDict() assessment_dict.update({ 'assessment': sum(assessment), 'score_list[]': assessment }) module.handle_ajax("save_assessment", assessment_dict) task_one_json = json.loads(module.task_states[0]) self.assertEqual( json.loads(task_one_json['child_history'][0]['post_assessment']), assessment) status = module.handle_ajax("get_status", {}) self.assertTrue(isinstance(status, basestring)) #Move to the next step in the problem module.handle_ajax("next_problem", {}) self.assertEqual(module.current_task_number, 0) html = module.get_html() self.assertTrue(isinstance(html, basestring)) #Module should now be done rubric = module.handle_ajax("get_combined_rubric", {}) self.assertTrue(isinstance(rubric, basestring)) self.assertEqual(module.state, "done") #Try to reset, should fail because only 1 attempt is allowed reset_data = json.loads(module.handle_ajax("reset", {})) self.assertEqual(reset_data['success'], False)
class OpenEndedModuleTest(unittest.TestCase): """ Test the open ended module class """ location = Location( ["i4x", "edX", "sa_test", "selfassessment", "SampleQuestion"]) metadata = json.dumps({'attempts': '10'}) prompt = etree.XML("<prompt>This is a question prompt</prompt>") rubric = etree.XML('''<rubric> <category> <description>Response Quality</description> <option>The response is not a satisfactory answer to the question. It either fails to address the question or does so in a limited way, with no evidence of higher-order thinking.</option> </category> </rubric>''') max_score = 4 static_data = { 'max_attempts': 20, 'prompt': prompt, 'rubric': rubric, 'max_score': max_score, 'display_name': 'Name', 'accept_file_upload': False, 'close_date': None, 's3_interface': test_util_open_ended.S3_INTERFACE, 'open_ended_grading_interface': test_util_open_ended.OPEN_ENDED_GRADING_INTERFACE, 'skip_basic_checks': False, } oeparam = etree.XML(''' <openendedparam> <initial_display>Enter essay here.</initial_display> <answer_display>This is the answer.</answer_display> <grader_payload>{"grader_settings" : "ml_grading.conf", "problem_id" : "6.002x/Welcome/OETest"}</grader_payload> </openendedparam> ''') definition = {'oeparam': oeparam} descriptor = Mock() def setUp(self): self.test_system = get_test_system() self.test_system.location = self.location self.mock_xqueue = MagicMock() self.mock_xqueue.send_to_queue.return_value = (None, "Message") def constructed_callback(dispatch="score_update"): return dispatch self.test_system.xqueue = { 'interface': self.mock_xqueue, 'construct_callback': constructed_callback, 'default_queuename': 'testqueue', 'waittime': 1 } self.openendedmodule = OpenEndedModule(self.test_system, self.location, self.definition, self.descriptor, self.static_data, self.metadata) def test_message_post(self): get = { 'feedback': 'feedback text', 'submission_id': '1', 'grader_id': '1', 'score': 3 } qtime = datetime.strftime(datetime.now(UTC), xqueue_interface.dateformat) student_info = { 'anonymous_student_id': self.test_system.anonymous_student_id, 'submission_time': qtime } contents = { 'feedback': get['feedback'], 'submission_id': int(get['submission_id']), 'grader_id': int(get['grader_id']), 'score': get['score'], 'student_info': json.dumps(student_info) } result = self.openendedmodule.message_post(get, self.test_system) self.assertTrue(result['success']) # make sure it's actually sending something we want to the queue self.mock_xqueue.send_to_queue.assert_called_with( body=json.dumps(contents), header=ANY) state = json.loads(self.openendedmodule.get_instance_state()) self.assertIsNotNone(state['child_state'], OpenEndedModule.DONE) def test_send_to_grader(self): submission = "This is a student submission" qtime = datetime.strftime(datetime.now(UTC), xqueue_interface.dateformat) student_info = { 'anonymous_student_id': self.test_system.anonymous_student_id, 'submission_time': qtime } contents = self.openendedmodule.payload.copy() contents.update({ 'student_info': json.dumps(student_info), 'student_response': submission, 'max_score': self.max_score }) result = self.openendedmodule.send_to_grader(submission, self.test_system) self.assertTrue(result) self.mock_xqueue.send_to_queue.assert_called_with( body=json.dumps(contents), header=ANY) def update_score_single(self): self.openendedmodule.new_history_entry("New Entry") score_msg = { 'correct': True, 'score': 4, 'msg': 'Grader Message', 'feedback': "Grader Feedback" } get = {'queuekey': "abcd", 'xqueue_body': score_msg} self.openendedmodule.update_score(get, self.test_system) def update_score_single(self): self.openendedmodule.new_history_entry("New Entry") feedback = {"success": True, "feedback": "Grader Feedback"} score_msg = { 'correct': True, 'score': 4, 'msg': 'Grader Message', 'feedback': json.dumps(feedback), 'grader_type': 'IN', 'grader_id': '1', 'submission_id': '1', 'success': True, 'rubric_scores': [0], 'rubric_scores_complete': True, 'rubric_xml': etree.tostring(self.rubric) } get = {'queuekey': "abcd", 'xqueue_body': json.dumps(score_msg)} self.openendedmodule.update_score(get, self.test_system) def test_latest_post_assessment(self): self.update_score_single() assessment = self.openendedmodule.latest_post_assessment( self.test_system) self.assertFalse(assessment == '') # check for errors self.assertFalse('errors' in assessment) def test_update_score(self): self.update_score_single() score = self.openendedmodule.latest_score() self.assertEqual(score, 4)
class CombinedOpenEndedModuleTest(unittest.TestCase): """ Unit tests for the combined open ended xmodule """ location = Location( ["i4x", "edX", "open_ended", "combinedopenended", "SampleQuestion"]) definition_template = """ <combinedopenended attempts="10000"> {rubric} {prompt} <task> {task1} </task> <task> {task2} </task> </combinedopenended> """ prompt = "<prompt>This is a question prompt</prompt>" rubric = '''<rubric><rubric> <category> <description>Response Quality</description> <option>The response is not a satisfactory answer to the question. It either fails to address the question or does so in a limited way, with no evidence of higher-order thinking.</option> <option>Second option</option> </category> </rubric></rubric>''' max_score = 1 metadata = {'attempts': '10', 'max_score': max_score} static_data = { 'max_attempts': 20, 'prompt': prompt, 'rubric': rubric, 'max_score': max_score, 'display_name': 'Name', 'accept_file_upload': False, 'close_date': "", 's3_interface': test_util_open_ended.S3_INTERFACE, 'open_ended_grading_interface': test_util_open_ended.OPEN_ENDED_GRADING_INTERFACE, 'skip_basic_checks': False, 'graded': True, } oeparam = etree.XML(''' <openendedparam> <initial_display>Enter essay here.</initial_display> <answer_display>This is the answer.</answer_display> <grader_payload>{"grader_settings" : "ml_grading.conf", "problem_id" : "6.002x/Welcome/OETest"}</grader_payload> </openendedparam> ''') task_xml1 = ''' <selfassessment> <hintprompt> What hint about this problem would you give to someone? </hintprompt> <submitmessage> Save Succcesful. Thanks for participating! </submitmessage> </selfassessment> ''' task_xml2 = ''' <openended min_score_to_attempt="1" max_score_to_attempt="1"> <openendedparam> <initial_display>Enter essay here.</initial_display> <answer_display>This is the answer.</answer_display> <grader_payload>{"grader_settings" : "ml_grading.conf", "problem_id" : "6.002x/Welcome/OETest"}</grader_payload> </openendedparam> </openended>''' definition = { 'prompt': etree.XML(prompt), 'rubric': etree.XML(rubric), 'task_xml': [task_xml1, task_xml2] } full_definition = definition_template.format(prompt=prompt, rubric=rubric, task1=task_xml1, task2=task_xml2) descriptor = Mock(data=full_definition) test_system = get_test_system() test_system.open_ended_grading_interface = None combinedoe_container = CombinedOpenEndedModule( descriptor=descriptor, runtime=test_system, field_data=DictFieldData({ 'data': full_definition, 'weight': '1', }), scope_ids=ScopeIds(None, None, None, None), ) def setUp(self): self.combinedoe = CombinedOpenEndedV1Module( self.test_system, self.location, self.definition, self.descriptor, static_data=self.static_data, metadata=self.metadata, instance_state=self.static_data) def test_get_tag_name(self): """ Test to see if the xml tag name is correct """ name = self.combinedoe.get_tag_name("<t>Tag</t>") self.assertEqual(name, "t") def test_get_last_response(self): """ See if we can parse the last response """ response_dict = self.combinedoe.get_last_response(0) self.assertEqual(response_dict['type'], "selfassessment") self.assertEqual(response_dict['max_score'], self.max_score) self.assertEqual(response_dict['state'], CombinedOpenEndedV1Module.INITIAL) def test_update_task_states(self): """ See if we can update the task states properly """ changed = self.combinedoe.update_task_states() self.assertFalse(changed) current_task = self.combinedoe.current_task current_task.change_state(CombinedOpenEndedV1Module.DONE) changed = self.combinedoe.update_task_states() self.assertTrue(changed) def test_get_max_score(self): """ Try to get the max score of the problem """ self.combinedoe.update_task_states() self.combinedoe.state = "done" self.combinedoe.is_scored = True max_score = self.combinedoe.max_score() self.assertEqual(max_score, 1) def test_container_get_max_score(self): """ See if we can get the max score from the actual xmodule """ #The progress view requires that this function be exposed max_score = self.combinedoe_container.max_score() self.assertEqual(max_score, None) def test_container_get_progress(self): """ See if we can get the progress from the actual xmodule """ progress = self.combinedoe_container.max_score() self.assertEqual(progress, None) def test_get_progress(self): """ Test if we can get the correct progress from the combined open ended class """ self.combinedoe.update_task_states() self.combinedoe.state = "done" self.combinedoe.is_scored = True progress = self.combinedoe.get_progress() self.assertIsInstance(progress, Progress) # progress._a is the score of the xmodule, which is 0 right now. self.assertEqual(progress._a, 0) # progress._b is the max_score (which is 1), divided by the weight (which is 1). self.assertEqual(progress._b, 1) def test_container_weight(self): """ Check the problem weight in the container """ weight = self.combinedoe_container.weight self.assertEqual(weight, 1) def test_container_child_weight(self): """ Test the class to see if it picks up the right weight """ weight = self.combinedoe_container.child_module.weight self.assertEqual(weight, 1) def test_get_score(self): """ See if scoring works """ score_dict = self.combinedoe.get_score() self.assertEqual(score_dict['score'], 0) self.assertEqual(score_dict['total'], 1) def test_alternate_orderings(self): """ Try multiple ordering of definitions to see if the problem renders different steps correctly. """ t1 = self.task_xml1 t2 = self.task_xml2 xml_to_test = [[t1], [t2], [t1, t1], [t1, t2], [t2, t2], [t2, t1], [t1, t2, t1]] for xml in xml_to_test: definition = { 'prompt': etree.XML(self.prompt), 'rubric': etree.XML(self.rubric), 'task_xml': xml } descriptor = Mock(data=definition) combinedoe = CombinedOpenEndedV1Module( self.test_system, self.location, definition, descriptor, static_data=self.static_data, metadata=self.metadata, instance_state=self.static_data) changed = combinedoe.update_task_states() self.assertFalse(changed) combinedoe = CombinedOpenEndedV1Module( self.test_system, self.location, definition, descriptor, static_data=self.static_data, metadata=self.metadata, instance_state={'task_states': TEST_STATE_SA}) combinedoe = CombinedOpenEndedV1Module( self.test_system, self.location, definition, descriptor, static_data=self.static_data, metadata=self.metadata, instance_state={'task_states': TEST_STATE_SA_IN}) def test_get_score_realistic(self): """ Try to parse the correct score from a json instance state """ instance_state = json.loads(MOCK_INSTANCE_STATE) rubric = """ <rubric> <rubric> <category> <description>Response Quality</description> <option>The response is not a satisfactory answer to the question. It either fails to address the question or does so in a limited way, with no evidence of higher-order thinking.</option> <option>The response is a marginal answer to the question. It may contain some elements of a proficient response, but it is inaccurate or incomplete.</option> <option>The response is a proficient answer to the question. It is generally correct, although it may contain minor inaccuracies. There is limited evidence of higher-order thinking.</option> <option>The response is correct, complete, and contains evidence of higher-order thinking.</option> </category> </rubric> </rubric> """ definition = { 'prompt': etree.XML(self.prompt), 'rubric': etree.XML(rubric), 'task_xml': [self.task_xml1, self.task_xml2] } descriptor = Mock(data=definition) combinedoe = CombinedOpenEndedV1Module(self.test_system, self.location, definition, descriptor, static_data=self.static_data, metadata=self.metadata, instance_state=instance_state) score_dict = combinedoe.get_score() self.assertEqual(score_dict['score'], 15.0) self.assertEqual(score_dict['total'], 15.0) def generate_oe_module(self, task_state, task_number, task_xml): """ Return a combined open ended module with the specified parameters """ definition = { 'prompt': etree.XML(self.prompt), 'rubric': etree.XML(self.rubric), 'task_xml': task_xml } descriptor = Mock(data=definition) instance_state = {'task_states': task_state, 'graded': True} if task_number is not None: instance_state.update({'current_task_number': task_number}) combinedoe = CombinedOpenEndedV1Module(self.test_system, self.location, definition, descriptor, static_data=self.static_data, metadata=self.metadata, instance_state=instance_state) return combinedoe def ai_state_reset(self, task_state, task_number=None): """ See if state is properly reset """ combinedoe = self.generate_oe_module(task_state, task_number, [self.task_xml2]) html = combinedoe.get_html() self.assertIsInstance(html, basestring) score = combinedoe.get_score() if combinedoe.is_scored: self.assertEqual(score['score'], 0) else: self.assertEqual(score['score'], None) def ai_state_success(self, task_state, task_number=None, iscore=2, tasks=None): """ See if state stays the same """ if tasks is None: tasks = [self.task_xml1, self.task_xml2] combinedoe = self.generate_oe_module(task_state, task_number, tasks) html = combinedoe.get_html() self.assertIsInstance(html, basestring) score = combinedoe.get_score() self.assertEqual(int(score['score']), iscore) def test_ai_state_reset(self): self.ai_state_reset(TEST_STATE_AI) def test_ai_state2_reset(self): self.ai_state_reset(TEST_STATE_AI2) def test_ai_invalid_state(self): self.ai_state_reset(TEST_STATE_AI2_INVALID) def test_ai_state_rest_task_number(self): self.ai_state_reset(TEST_STATE_AI, task_number=2) self.ai_state_reset(TEST_STATE_AI, task_number=5) self.ai_state_reset(TEST_STATE_AI, task_number=1) self.ai_state_reset(TEST_STATE_AI, task_number=0) def test_ai_state_success(self): self.ai_state_success(TEST_STATE_AI) def test_state_single(self): self.ai_state_success(TEST_STATE_SINGLE, iscore=12) def test_state_pe_single(self): self.ai_state_success(TEST_STATE_PE_SINGLE, iscore=0, tasks=[self.task_xml2])
class OpenEndedChildTest(unittest.TestCase): """ Test the open ended child class """ location = Location( ["i4x", "edX", "sa_test", "selfassessment", "SampleQuestion"]) metadata = json.dumps({'attempts': '10'}) prompt = etree.XML("<prompt>This is a question prompt</prompt>") rubric = '''<rubric><rubric> <category> <description>Response Quality</description> <option>The response is not a satisfactory answer to the question. It either fails to address the question or does so in a limited way, with no evidence of higher-order thinking.</option> <option>Second option</option> </category> </rubric></rubric>''' max_score = 1 static_data = { 'max_attempts': 20, 'prompt': prompt, 'rubric': rubric, 'max_score': max_score, 'display_name': 'Name', 'accept_file_upload': False, 'close_date': None, 's3_interface': "", 'open_ended_grading_interface': {}, 'skip_basic_checks': False, 'control': { 'required_peer_grading': 1, 'peer_grader_count': 1, 'min_to_calibrate': 3, 'max_to_calibrate': 6, 'peer_grade_finished_submissions_when_none_pending': False, } } definition = Mock() descriptor = Mock() def setUp(self): self.test_system = get_test_system() self.test_system.open_ended_grading_interface = None self.openendedchild = OpenEndedChild(self.test_system, self.location, self.definition, self.descriptor, self.static_data, self.metadata) def test_latest_answer_empty(self): answer = self.openendedchild.latest_answer() self.assertEqual(answer, "") def test_latest_score_empty(self): answer = self.openendedchild.latest_score() self.assertEqual(answer, None) def test_latest_post_assessment_empty(self): answer = self.openendedchild.latest_post_assessment(self.test_system) self.assertEqual(answer, "") def test_new_history_entry(self): new_answer = "New Answer" self.openendedchild.new_history_entry(new_answer) answer = self.openendedchild.latest_answer() self.assertEqual(answer, new_answer) new_answer = "Newer Answer" self.openendedchild.new_history_entry(new_answer) answer = self.openendedchild.latest_answer() self.assertEqual(new_answer, answer) def test_record_latest_score(self): new_answer = "New Answer" self.openendedchild.new_history_entry(new_answer) new_score = 3 self.openendedchild.record_latest_score(new_score) score = self.openendedchild.latest_score() self.assertEqual(score, 3) new_score = 4 self.openendedchild.new_history_entry(new_answer) self.openendedchild.record_latest_score(new_score) score = self.openendedchild.latest_score() self.assertEqual(score, 4) def test_record_latest_post_assessment(self): new_answer = "New Answer" self.openendedchild.new_history_entry(new_answer) post_assessment = "Post assessment" self.openendedchild.record_latest_post_assessment(post_assessment) self.assertEqual( post_assessment, self.openendedchild.latest_post_assessment(self.test_system)) def test_get_score(self): new_answer = "New Answer" self.openendedchild.new_history_entry(new_answer) score = self.openendedchild.get_score() self.assertEqual(score['score'], 0) self.assertEqual(score['total'], self.static_data['max_score']) new_score = 4 self.openendedchild.new_history_entry(new_answer) self.openendedchild.record_latest_score(new_score) score = self.openendedchild.get_score() self.assertEqual(score['score'], new_score) self.assertEqual(score['total'], self.static_data['max_score']) def test_reset(self): self.openendedchild.reset(self.test_system) state = json.loads(self.openendedchild.get_instance_state()) self.assertEqual(state['child_state'], OpenEndedChild.INITIAL) def test_is_last_response_correct(self): new_answer = "New Answer" self.openendedchild.new_history_entry(new_answer) self.openendedchild.record_latest_score(self.static_data['max_score']) self.assertEqual(self.openendedchild.is_last_response_correct(), 'correct') self.openendedchild.new_history_entry(new_answer) self.openendedchild.record_latest_score(0) self.assertEqual(self.openendedchild.is_last_response_correct(), 'incorrect')
class SelfAssessmentTest(unittest.TestCase): rubric = '''<rubric><rubric> <category> <description>Response Quality</description> <option>The response is not a satisfactory answer to the question. It either fails to address the question or does so in a limited way, with no evidence of higher-order thinking.</option> </category> </rubric></rubric>''' prompt = etree.XML("<prompt>This is sample prompt text.</prompt>") definition = { 'rubric': rubric, 'prompt': prompt, 'submitmessage': 'Shall we submit now?', 'hintprompt': 'Consider this...', } location = Location( ["i4x", "edX", "sa_test", "selfassessment", "SampleQuestion"]) descriptor = Mock() def setUp(self): state = json.dumps({ 'student_answers': ["Answer 1", "answer 2", "answer 3"], 'scores': [0, 1], 'hints': ['o hai'], 'state': SelfAssessmentModule.INITIAL, 'attempts': 2 }) static_data = { 'max_attempts': 10, 'rubric': etree.XML(self.rubric), 'prompt': self.prompt, 'max_score': 1, 'display_name': "Name", 'accept_file_upload': False, 'close_date': None, 's3_interface': test_util_open_ended.S3_INTERFACE, 'open_ended_grading_interface': test_util_open_ended.OPEN_ENDED_GRADING_INTERFACE, 'skip_basic_checks': False, 'control': { 'required_peer_grading': 1, 'peer_grader_count': 1, 'min_to_calibrate': 3, 'max_to_calibrate': 6, } } self.module = SelfAssessmentModule(get_test_system(), self.location, self.definition, self.descriptor, static_data) def test_get_html(self): html = self.module.get_html(self.module.system) self.assertTrue("This is sample prompt text" in html) def test_self_assessment_flow(self): responses = {'assessment': '0', 'score_list[]': ['0', '0']} def get_fake_item(name): return responses[name] def get_data_for_location(self, location, student): return { 'count_graded': 0, 'count_required': 0, 'student_sub_count': 0, } mock_query_dict = MagicMock() mock_query_dict.__getitem__.side_effect = get_fake_item mock_query_dict.getlist = get_fake_item self.module.peer_gs.get_data_for_location = get_data_for_location self.assertEqual(self.module.get_score()['score'], 0) self.module.save_answer({'student_answer': "I am an answer"}, self.module.system) self.assertEqual(self.module.child_state, self.module.ASSESSING) self.module.save_assessment(mock_query_dict, self.module.system) self.assertEqual(self.module.child_state, self.module.DONE) d = self.module.reset({}) self.assertTrue(d['success']) self.assertEqual(self.module.child_state, self.module.INITIAL) # if we now assess as right, skip the REQUEST_HINT state self.module.save_answer({'student_answer': 'answer 4'}, self.module.system) responses['assessment'] = '1' self.module.save_assessment(mock_query_dict, self.module.system) self.assertEqual(self.module.child_state, self.module.DONE)
class OpenEndedModuleXmlTest(unittest.TestCase, DummyModulestore): """ Test the student flow in the combined open ended xmodule """ problem_location = Location( ["i4x", "edX", "open_ended", "combinedopenended", "SampleQuestion"]) answer = "blah blah" assessment = [0, 1] hint = "blah" def setUp(self): self.test_system = get_test_system() self.test_system.open_ended_grading_interface = None self.test_system.xqueue['interface'] = Mock(send_to_queue=Mock( side_effect=[1, "queued"])) self.setup_modulestore(COURSE) def test_open_ended_load_and_save(self): """ See if we can load the module and save an answer @return: """ # Load the module module = self.get_module_from_location(self.problem_location, COURSE) # Try saving an answer module.handle_ajax("save_answer", {"student_answer": self.answer}) # Save our modifications to the underlying KeyValueStore so they can be persisted module.save() task_one_json = json.loads(module.task_states[0]) self.assertEqual(task_one_json['child_history'][0]['answer'], self.answer) module = self.get_module_from_location(self.problem_location, COURSE) task_one_json = json.loads(module.task_states[0]) self.assertEqual(task_one_json['child_history'][0]['answer'], self.answer) def test_open_ended_flow_reset(self): """ Test the flow of the module if we complete the self assessment step and then reset @return: """ assessment = [0, 1] module = self.get_module_from_location(self.problem_location, COURSE) #Simulate a student saving an answer html = module.handle_ajax("get_html", {}) module.handle_ajax( "save_answer", { "student_answer": self.answer, "can_upload_files": False, "student_file": None }) html = module.handle_ajax("get_html", {}) #Mock a student submitting an assessment assessment_dict = MockQueryDict() assessment_dict.update({ 'assessment': sum(assessment), 'score_list[]': assessment }) module.handle_ajax("save_assessment", assessment_dict) task_one_json = json.loads(module.task_states[0]) self.assertEqual( json.loads(task_one_json['child_history'][0]['post_assessment']), assessment) rubric = module.handle_ajax("get_combined_rubric", {}) #Move to the next step in the problem module.handle_ajax("next_problem", {}) self.assertEqual(module.current_task_number, 0) html = module.get_html() self.assertTrue(isinstance(html, basestring)) rubric = module.handle_ajax("get_combined_rubric", {}) self.assertTrue(isinstance(rubric, basestring)) self.assertEqual(module.state, "assessing") module.handle_ajax("reset", {}) self.assertEqual(module.current_task_number, 0) def test_open_ended_flow_correct(self): """ Test a two step problem where the student first goes through the self assessment step, and then the open ended step. @return: """ assessment = [1, 1] #Load the module module = self.get_module_from_location(self.problem_location, COURSE) #Simulate a student saving an answer module.handle_ajax("save_answer", {"student_answer": self.answer}) status = module.handle_ajax("get_status", {}) self.assertTrue(isinstance(status, basestring)) #Mock a student submitting an assessment assessment_dict = MockQueryDict() assessment_dict.update({ 'assessment': sum(assessment), 'score_list[]': assessment }) module.handle_ajax("save_assessment", assessment_dict) task_one_json = json.loads(module.task_states[0]) self.assertEqual( json.loads(task_one_json['child_history'][0]['post_assessment']), assessment) #Move to the next step in the problem try: module.handle_ajax("next_problem", {}) except GradingServiceError: #This error is okay. We don't have a grading service to connect to! pass self.assertEqual(module.current_task_number, 1) try: module.get_html() except GradingServiceError: #This error is okay. We don't have a grading service to connect to! pass #Try to get the rubric from the module module.handle_ajax("get_combined_rubric", {}) #Make a fake reply from the queue queue_reply = { 'queuekey': "", 'xqueue_body': json.dumps({ 'score': 0, 'feedback': json.dumps({ "spelling": "Spelling: Ok.", "grammar": "Grammar: Ok.", "markup-text": " all of us can think of a book that we hope none of our children or any other children have taken off the shelf . but if i have the right to remove that book from the shelf that work i abhor then you also have exactly the same right and so does everyone else . and then we <bg>have no books left</bg> on the shelf for any of us . <bs>katherine</bs> <bs>paterson</bs> , author write a persuasive essay to a newspaper reflecting your vies on censorship <bg>in libraries . do</bg> you believe that certain materials , such as books , music , movies , magazines , <bg>etc . , should be</bg> removed from the shelves if they are found <bg>offensive ? support your</bg> position with convincing arguments from your own experience , observations <bg>, and or reading .</bg> " }), 'grader_type': "ML", 'success': True, 'grader_id': 1, 'submission_id': 1, 'rubric_xml': "<rubric><category><description>Writing Applications</description><score>0</score><option points='0'> The essay loses focus, has little information or supporting details, and the organization makes it difficult to follow.</option><option points='1'> The essay presents a mostly unified theme, includes sufficient information to convey the theme, and is generally organized well.</option></category><category><description> Language Conventions </description><score>0</score><option points='0'> The essay demonstrates a reasonable command of proper spelling and grammar. </option><option points='1'> The essay demonstrates superior command of proper spelling and grammar.</option></category></rubric>", 'rubric_scores_complete': True, }) } module.handle_ajax("check_for_score", {}) #Update the module with the fake queue reply module.handle_ajax("score_update", queue_reply) self.assertFalse(module.ready_to_reset) self.assertEqual(module.current_task_number, 1) #Get html and other data client will request module.get_html() module.handle_ajax("skip_post_assessment", {}) #Get all results module.handle_ajax("get_combined_rubric", {}) #reset the problem module.handle_ajax("reset", {}) self.assertEqual(module.state, "initial")
def test_translate_locator(self): """ tests translate_locator_to_location(BlockUsageLocator) """ # lookup for non-existent course org = 'foo_org' course = 'bar_course' new_style_course_id = '{}.geek_dept.{}.baz_run'.format(org, course) prob_locator = BlockUsageLocator(course_id=new_style_course_id, usage_id='problem2') prob_location = loc_mapper().translate_locator_to_location( prob_locator) self.assertIsNone(prob_location, 'found entry in empty map table') loc_mapper().create_map_entry(Location('i4x', org, course, 'course', 'baz_run'), new_style_course_id, block_map={ 'abc123': { 'problem': 'problem2' }, '48f23a10395384929234': { 'chapter': 'chapter48f' } }) # only one course matches prob_location = loc_mapper().translate_locator_to_location( prob_locator) # default branch self.assertEqual( prob_location, Location('i4x', org, course, 'problem', 'abc123', None)) # explicit branch prob_locator = BlockUsageLocator(prob_locator, branch='draft') prob_location = loc_mapper().translate_locator_to_location( prob_locator) self.assertEqual( prob_location, Location('i4x', org, course, 'problem', 'abc123', 'draft')) prob_locator = BlockUsageLocator(course_id=new_style_course_id, usage_id='problem2', branch='production') prob_location = loc_mapper().translate_locator_to_location( prob_locator) self.assertEqual( prob_location, Location('i4x', org, course, 'problem', 'abc123', None)) # same for chapter except chapter cannot be draft in old system chap_locator = BlockUsageLocator(course_id=new_style_course_id, usage_id='chapter48f') chap_location = loc_mapper().translate_locator_to_location( chap_locator) self.assertEqual( chap_location, Location('i4x', org, course, 'chapter', '48f23a10395384929234')) # explicit branch chap_locator = BlockUsageLocator(chap_locator, branch='draft') chap_location = loc_mapper().translate_locator_to_location( chap_locator) self.assertEqual( chap_location, Location('i4x', org, course, 'chapter', '48f23a10395384929234')) chap_locator = BlockUsageLocator(course_id=new_style_course_id, usage_id='chapter48f', branch='production') chap_location = loc_mapper().translate_locator_to_location( chap_locator) self.assertEqual( chap_location, Location('i4x', org, course, 'chapter', '48f23a10395384929234')) # look for non-existent problem prob_locator2 = BlockUsageLocator(course_id=new_style_course_id, branch='draft', usage_id='problem3') prob_location = loc_mapper().translate_locator_to_location( prob_locator2) self.assertIsNone(prob_location, 'Found non-existent problem') # add a distractor course new_style_course_id = '{}.geek_dept.{}.{}'.format( org, course, 'delta_run') loc_mapper().create_map_entry( Location('i4x', org, course, 'course', 'delta_run'), new_style_course_id, block_map={'abc123': { 'problem': 'problem3' }}) prob_location = loc_mapper().translate_locator_to_location( prob_locator) self.assertEqual( prob_location, Location('i4x', org, course, 'problem', 'abc123', None)) # add a default course pointing to the delta_run loc_mapper().create_map_entry( Location('i4x', org, course, 'problem', '789abc123efg456'), new_style_course_id, block_map={'abc123': { 'problem': 'problem3' }}) # now query delta (2 entries point to it) prob_locator = BlockUsageLocator(course_id=new_style_course_id, branch='production', usage_id='problem3') prob_location = loc_mapper().translate_locator_to_location( prob_locator) self.assertEqual(prob_location, Location('i4x', org, course, 'problem', 'abc123'))
def test_add_block(self): """ Test add_block_location_translator(location, old_course_id=None, usage_id=None) """ # call w/ no matching courses org = 'foo_org' course = 'bar_course' old_style_course_id = '{}/{}/{}'.format(org, course, 'baz_run') problem_name = 'abc123abc123abc123abc123abc123f9' location = Location('i4x', org, course, 'problem', problem_name) with self.assertRaises(ItemNotFoundError): loc_mapper().add_block_location_translator(location) with self.assertRaises(ItemNotFoundError): loc_mapper().add_block_location_translator(location, old_style_course_id) # w/ one matching course new_style_course_id = '{}.{}.{}'.format(org, course, 'baz_run') loc_mapper().create_map_entry( Location('i4x', org, course, 'course', 'baz_run'), new_style_course_id, ) new_usage_id = loc_mapper().add_block_location_translator(location) self.assertEqual(new_usage_id, 'problemabc') # look it up translated_loc = loc_mapper().translate_location( old_style_course_id, location, add_entry_if_missing=False) self.assertEqual(translated_loc.course_id, new_style_course_id) self.assertEqual(translated_loc.usage_id, new_usage_id) # w/ one distractor which has one entry already new_style_course_id = '{}.geek_dept.{}.{}'.format( org, course, 'delta_run') loc_mapper().create_map_entry( Location('i4x', org, course, 'course', 'delta_run'), new_style_course_id, block_map={'48f23a10395384929234': { 'chapter': 'chapter48f' }}) # try adding the one added before new_usage_id2 = loc_mapper().add_block_location_translator(location) self.assertEqual(new_usage_id, new_usage_id2) # it should be in the distractor now new_location = loc_mapper().translate_locator_to_location( BlockUsageLocator(course_id=new_style_course_id, usage_id=new_usage_id2)) self.assertEqual(new_location, location) # add one close to the existing chapter (cause name collision) location = Location('i4x', org, course, 'chapter', '48f23a103953849292341234567890ab') new_usage_id = loc_mapper().add_block_location_translator(location) self.assertRegexpMatches(new_usage_id, r'^chapter48f\d') # retrievable from both courses new_location = loc_mapper().translate_locator_to_location( BlockUsageLocator(course_id=new_style_course_id, usage_id=new_usage_id)) self.assertEqual(new_location, location) new_location = loc_mapper().translate_locator_to_location( BlockUsageLocator(course_id='{}.{}.{}'.format( org, course, 'baz_run'), usage_id=new_usage_id)) self.assertEqual(new_location, location) # provoke duplicate item errors location = location.replace(name='44f23a103953849292341234567890ab') with self.assertRaises(DuplicateItemError): loc_mapper().add_block_location_translator(location, usage_id=new_usage_id) new_usage_id = loc_mapper().add_block_location_translator( location, old_course_id=old_style_course_id) other_course_old_style = '{}/{}/{}'.format(org, course, 'delta_run') new_usage_id2 = loc_mapper().add_block_location_translator( location, old_course_id=other_course_old_style, usage_id='{}b'.format(new_usage_id)) with self.assertRaises(DuplicateItemError): loc_mapper().add_block_location_translator(location)
def test_update_block(self): """ test update_block_location_translator(location, usage_id, old_course_id=None) """ org = 'foo_org' course = 'bar_course' new_style_course_id = '{}.geek_dept.{}.baz_run'.format(org, course) loc_mapper().create_map_entry(Location('i4x', org, course, 'course', 'baz_run'), new_style_course_id, block_map={ 'abc123': { 'problem': 'problem2' }, '48f23a10395384929234': { 'chapter': 'chapter48f' }, '1': { 'chapter': 'chapter1', 'problem': 'problem1' }, }) new_style_course_id2 = '{}.geek_dept.{}.delta_run'.format(org, course) loc_mapper().create_map_entry(Location('i4x', org, course, 'course', 'delta_run'), new_style_course_id2, block_map={ 'abc123': { 'problem': 'problem3' }, '48f23a10395384929234': { 'chapter': 'chapter48b' }, '1': { 'chapter': 'chapter2', 'problem': 'problem2' }, }) location = Location('i4x', org, course, 'problem', '1') # change in all courses to same value loc_mapper().update_block_location_translator(location, 'problem1') trans_loc = loc_mapper().translate_locator_to_location( BlockUsageLocator(course_id=new_style_course_id, usage_id='problem1')) self.assertEqual(trans_loc, location) trans_loc = loc_mapper().translate_locator_to_location( BlockUsageLocator(course_id=new_style_course_id2, usage_id='problem1')) self.assertEqual(trans_loc, location) # try to change to overwrite used usage_id location = Location('i4x', org, course, 'chapter', '48f23a10395384929234') with self.assertRaises(DuplicateItemError): loc_mapper().update_block_location_translator(location, 'chapter2') # just change the one course loc_mapper().update_block_location_translator( location, 'chapter2', '{}/{}/{}'.format(org, course, 'baz_run')) trans_loc = loc_mapper().translate_locator_to_location( BlockUsageLocator(course_id=new_style_course_id, usage_id='chapter2')) self.assertEqual(trans_loc.name, '48f23a10395384929234') # but this still points to the old trans_loc = loc_mapper().translate_locator_to_location( BlockUsageLocator(course_id=new_style_course_id2, usage_id='chapter2')) self.assertEqual(trans_loc.name, '1')
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 OpenEndedModuleTest(unittest.TestCase): """ Test the open ended module class """ location = Location( ["i4x", "edX", "sa_test", "selfassessment", "SampleQuestion"]) metadata = json.dumps({'attempts': '10'}) prompt = etree.XML("<prompt>This is a question prompt</prompt>") rubric = etree.XML('''<rubric> <category> <description>Response Quality</description> <option>The response is not a satisfactory answer to the question. It either fails to address the question or does so in a limited way, with no evidence of higher-order thinking.</option> </category> </rubric>''') max_score = 4 static_data = { 'max_attempts': 20, 'prompt': prompt, 'rubric': rubric, 'max_score': max_score, 'display_name': 'Name', 'accept_file_upload': False, 'close_date': None, 's3_interface': test_util_open_ended.S3_INTERFACE, 'open_ended_grading_interface': test_util_open_ended.OPEN_ENDED_GRADING_INTERFACE, 'skip_basic_checks': False, 'control': { 'required_peer_grading': 1, 'peer_grader_count': 1, 'min_to_calibrate': 3, 'max_to_calibrate': 6, 'peer_grade_finished_submissions_when_none_pending': False, } } oeparam = etree.XML(''' <openendedparam> <initial_display>Enter essay here.</initial_display> <answer_display>This is the answer.</answer_display> <grader_payload>{"grader_settings" : "ml_grading.conf", "problem_id" : "6.002x/Welcome/OETest"}</grader_payload> </openendedparam> ''') definition = {'oeparam': oeparam} descriptor = Mock() def setUp(self): self.test_system = get_test_system() self.test_system.open_ended_grading_interface = None self.test_system.location = self.location self.mock_xqueue = MagicMock() self.mock_xqueue.send_to_queue.return_value = (None, "Message") def constructed_callback(dispatch="score_update"): return dispatch self.test_system.xqueue = { 'interface': self.mock_xqueue, 'construct_callback': constructed_callback, 'default_queuename': 'testqueue', 'waittime': 1 } self.openendedmodule = OpenEndedModule(self.test_system, self.location, self.definition, self.descriptor, self.static_data, self.metadata) def test_message_post(self): get = { 'feedback': 'feedback text', 'submission_id': '1', 'grader_id': '1', 'score': 3 } qtime = datetime.strftime(datetime.now(UTC), xqueue_interface.dateformat) student_info = { 'anonymous_student_id': self.test_system.anonymous_student_id, 'submission_time': qtime } contents = { 'feedback': get['feedback'], 'submission_id': int(get['submission_id']), 'grader_id': int(get['grader_id']), 'score': get['score'], 'student_info': json.dumps(student_info) } result = self.openendedmodule.message_post(get, self.test_system) self.assertTrue(result['success']) # make sure it's actually sending something we want to the queue self.mock_xqueue.send_to_queue.assert_called_with( body=json.dumps(contents), header=ANY) state = json.loads(self.openendedmodule.get_instance_state()) self.assertIsNotNone(state['child_state'], OpenEndedModule.DONE) def test_send_to_grader(self): submission = "This is a student submission" qtime = datetime.strftime(datetime.now(UTC), xqueue_interface.dateformat) student_info = { 'anonymous_student_id': self.test_system.anonymous_student_id, 'submission_time': qtime } contents = self.openendedmodule.payload.copy() contents.update({ 'student_info': json.dumps(student_info), 'student_response': submission, 'max_score': self.max_score }) result = self.openendedmodule.send_to_grader(submission, self.test_system) self.assertTrue(result) self.mock_xqueue.send_to_queue.assert_called_with( body=json.dumps(contents), header=ANY) def update_score_single(self): self.openendedmodule.new_history_entry("New Entry") score_msg = { 'correct': True, 'score': 4, 'msg': 'Grader Message', 'feedback': "Grader Feedback" } get = {'queuekey': "abcd", 'xqueue_body': score_msg} self.openendedmodule.update_score(get, self.test_system) def update_score_single(self): self.openendedmodule.new_history_entry("New Entry") feedback = {"success": True, "feedback": "Grader Feedback"} score_msg = { 'correct': True, 'score': 4, 'msg': 'Grader Message', 'feedback': json.dumps(feedback), 'grader_type': 'IN', 'grader_id': '1', 'submission_id': '1', 'success': True, 'rubric_scores': [0], 'rubric_scores_complete': True, 'rubric_xml': etree.tostring(self.rubric) } get = {'queuekey': "abcd", 'xqueue_body': json.dumps(score_msg)} self.openendedmodule.update_score(get, self.test_system) def update_score_multiple(self): self.openendedmodule.new_history_entry("New Entry") feedback = {"success": True, "feedback": "Grader Feedback"} score_msg = { 'correct': True, 'score': [0, 1], 'msg': 'Grader Message', 'feedback': [json.dumps(feedback), json.dumps(feedback)], 'grader_type': 'PE', 'grader_id': ['1', '2'], 'submission_id': '1', 'success': True, 'rubric_scores': [[0], [0]], 'rubric_scores_complete': [True, True], 'rubric_xml': [etree.tostring(self.rubric), etree.tostring(self.rubric)] } get = {'queuekey': "abcd", 'xqueue_body': json.dumps(score_msg)} self.openendedmodule.update_score(get, self.test_system) def test_latest_post_assessment(self): self.update_score_single() assessment = self.openendedmodule.latest_post_assessment( self.test_system) self.assertFalse(assessment == '') # check for errors self.assertFalse('errors' in assessment) def test_update_score_single(self): self.update_score_single() score = self.openendedmodule.latest_score() self.assertEqual(score, 4) def test_update_score_multiple(self): """ Tests that a score of [0, 1] gets aggregated to 1. A change in behavior added by @jbau """ self.update_score_multiple() score = self.openendedmodule.latest_score() self.assertEquals(score, 1) def test_open_ended_display(self): """ Test storing answer with the open ended module. """ # Create a module with no state yet. Important that this start off as a blank slate. test_module = OpenEndedModule(self.test_system, self.location, self.definition, self.descriptor, self.static_data, self.metadata) saved_response = "Saved response." submitted_response = "Submitted response." # Initially, there will be no stored answer. self.assertEqual(test_module.stored_answer, None) # And the initial answer to display will be an empty string. self.assertEqual(test_module.get_display_answer(), "") # Now, store an answer in the module. test_module.handle_ajax("store_answer", {'student_answer': saved_response}, get_test_system()) # The stored answer should now equal our response. self.assertEqual(test_module.stored_answer, saved_response) self.assertEqual(test_module.get_display_answer(), saved_response) # Mock out the send_to_grader function so it doesn't try to connect to the xqueue. test_module.send_to_grader = Mock(return_value=True) # Submit a student response to the question. test_module.handle_ajax( "save_answer", { "student_answer": submitted_response, "can_upload_files": False, "student_file": None }, get_test_system()) # Submitting an answer should clear the stored answer. self.assertEqual(test_module.stored_answer, None) # Confirm that the answer is stored properly. self.assertEqual(test_module.latest_answer(), submitted_response)
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_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 create_new_course(request): """ Create a new course. Returns the URL for the course overview page. """ if not is_user_in_creator_group(request.user): raise PermissionDenied() org = request.json.get('org') number = request.json.get('number') display_name = request.json.get('display_name') run = request.json.get('run') try: dest_location = Location('i4x', org, number, 'course', run) except InvalidLocationError as error: return JsonResponse({ "ErrMsg": _("Unable to create course '{name}'.\n\n{err}").format( name=display_name, err=error.message) }) # see if the course already exists existing_course = None try: existing_course = modulestore('direct').get_item(dest_location) except ItemNotFoundError: pass if existing_course is not None: return JsonResponse({ 'ErrMsg': _('There is already a course defined with the same ' 'organization, course number, and course run. Please ' 'change either organization or course number to be ' 'unique.'), 'OrgErrMsg': _('Please change either the organization or ' 'course number so that it is unique.'), 'CourseErrMsg': _('Please change either the organization or ' 'course number so that it is unique.'), }) # dhm: this query breaks the abstraction, but I'll fix it when I do my suspended refactoring of this # file for new locators. get_items should accept a query rather than requiring it be a legal location course_search_location = bson.son.SON({ '_id.tag': 'i4x', # cannot pass regex to Location constructor; thus this hack '_id.org': re.compile('^{}$'.format(dest_location.org), re.IGNORECASE), '_id.course': re.compile('^{}$'.format(dest_location.course), re.IGNORECASE), '_id.category': 'course', }) courses = modulestore().collection.find(course_search_location, fields=('_id')) if courses.count() > 0: return JsonResponse({ 'ErrMsg': _('There is already a course defined with the same ' 'organization and course number. Please ' 'change at least one field to be unique.'), 'OrgErrMsg': _('Please change either the organization or ' 'course number so that it is unique.'), 'CourseErrMsg': _('Please change either the organization or ' 'course number so that it is unique.'), }) # instantiate the CourseDescriptor and then persist it # note: no system to pass if display_name is None: metadata = {} else: metadata = {'display_name': display_name} modulestore('direct').create_and_save_xmodule(dest_location, metadata=metadata) new_course = modulestore('direct').get_item(dest_location) # clone a default 'about' overview module as well dest_about_location = dest_location.replace(category='about', name='overview') overview_template = AboutDescriptor.get_template('overview.yaml') modulestore('direct').create_and_save_xmodule( dest_about_location, system=new_course.system, definition_data=overview_template.get('data')) initialize_course_tabs(new_course) new_location = loc_mapper().translate_location( new_course.location.course_id, new_course.location, False, True) create_all_course_groups(request.user, new_location) # seed the forums seed_permissions_roles(new_course.location.course_id) # auto-enroll the course creator in the course so that "View Live" will # work. CourseEnrollment.enroll(request.user, new_course.location.course_id) return JsonResponse({'url': new_location.url_reverse("course/", "")})
class CombinedOpenEndedModuleTest(unittest.TestCase): """ Unit tests for the combined open ended xmodule """ location = Location( ["i4x", "edX", "open_ended", "combinedopenended", "SampleQuestion"]) definition_template = """ <combinedopenended attempts="10000"> {rubric} {prompt} <task> {task1} </task> <task> {task2} </task> </combinedopenended> """ prompt = "<prompt>This is a question prompt</prompt>" rubric = '''<rubric><rubric> <category> <description>Response Quality</description> <option>The response is not a satisfactory answer to the question. It either fails to address the question or does so in a limited way, with no evidence of higher-order thinking.</option> <option>Second option</option> </category> </rubric></rubric>''' max_score = 1 metadata = {'attempts': '10', 'max_score': max_score} static_data = { 'max_attempts': 20, 'prompt': prompt, 'rubric': rubric, 'max_score': max_score, 'display_name': 'Name', 'accept_file_upload': False, 'close_date': "", 's3_interface': test_util_open_ended.S3_INTERFACE, 'open_ended_grading_interface': test_util_open_ended.OPEN_ENDED_GRADING_INTERFACE, 'skip_basic_checks': False, 'graded': True, } oeparam = etree.XML(''' <openendedparam> <initial_display>Enter essay here.</initial_display> <answer_display>This is the answer.</answer_display> <grader_payload>{"grader_settings" : "ml_grading.conf", "problem_id" : "6.002x/Welcome/OETest"}</grader_payload> </openendedparam> ''') task_xml1 = ''' <selfassessment> <hintprompt> What hint about this problem would you give to someone? </hintprompt> <submitmessage> Save Succcesful. Thanks for participating! </submitmessage> </selfassessment> ''' task_xml2 = ''' <openended min_score_to_attempt="1" max_score_to_attempt="1"> <openendedparam> <initial_display>Enter essay here.</initial_display> <answer_display>This is the answer.</answer_display> <grader_payload>{"grader_settings" : "ml_grading.conf", "problem_id" : "6.002x/Welcome/OETest"}</grader_payload> </openendedparam> </openended>''' definition = { 'prompt': etree.XML(prompt), 'rubric': etree.XML(rubric), 'task_xml': [task_xml1, task_xml2] } full_definition = definition_template.format(prompt=prompt, rubric=rubric, task1=task_xml1, task2=task_xml2) descriptor = Mock(data=full_definition) test_system = get_test_system() combinedoe_container = CombinedOpenEndedModule(test_system, descriptor, model_data={ 'data': full_definition, 'weight': '1', 'location': location }) def setUp(self): # TODO: this constructor call is definitely wrong, but neither branch # of the merge matches the module constructor. Someone (Vik?) should fix this. self.combinedoe = CombinedOpenEndedV1Module( self.test_system, self.location, self.definition, self.descriptor, static_data=self.static_data, metadata=self.metadata, instance_state=self.static_data) def test_get_tag_name(self): name = self.combinedoe.get_tag_name("<t>Tag</t>") self.assertEqual(name, "t") def test_get_last_response(self): response_dict = self.combinedoe.get_last_response(0) self.assertEqual(response_dict['type'], "selfassessment") self.assertEqual(response_dict['max_score'], self.max_score) self.assertEqual(response_dict['state'], CombinedOpenEndedV1Module.INITIAL) def test_update_task_states(self): changed = self.combinedoe.update_task_states() self.assertFalse(changed) current_task = self.combinedoe.current_task current_task.change_state(CombinedOpenEndedV1Module.DONE) changed = self.combinedoe.update_task_states() self.assertTrue(changed) def test_get_max_score(self): self.combinedoe.update_task_states() self.combinedoe.state = "done" self.combinedoe.is_scored = True max_score = self.combinedoe.max_score() self.assertEqual(max_score, 1) def test_container_get_max_score(self): #The progress view requires that this function be exposed max_score = self.combinedoe_container.max_score() self.assertEqual(max_score, None) def test_container_weight(self): weight = self.combinedoe_container.weight self.assertEqual(weight, 1) def test_container_child_weight(self): weight = self.combinedoe_container.child_module.weight self.assertEqual(weight, 1) def test_get_score(self): score_dict = self.combinedoe.get_score() self.assertEqual(score_dict['score'], 0) self.assertEqual(score_dict['total'], 1) def test_alternate_orderings(self): t1 = self.task_xml1 t2 = self.task_xml2 xml_to_test = [[t1], [t2], [t1, t1], [t1, t2], [t2, t2], [t2, t1], [t1, t2, t1]] for xml in xml_to_test: definition = { 'prompt': etree.XML(self.prompt), 'rubric': etree.XML(self.rubric), 'task_xml': xml } descriptor = Mock(data=definition) combinedoe = CombinedOpenEndedV1Module( self.test_system, self.location, definition, descriptor, static_data=self.static_data, metadata=self.metadata, instance_state=self.static_data) changed = combinedoe.update_task_states() self.assertFalse(changed) def test_get_score_realistic(self): instance_state = r"""{"ready_to_reset": false, "skip_spelling_checks": true, "current_task_number": 1, "weight": 5.0, "graceperiod": "1 day 12 hours 59 minutes 59 seconds", "graded": "True", "task_states": ["{\"child_created\": false, \"child_attempts\": 4, \"version\": 1, \"child_history\": [{\"answer\": \"The students\\u2019 data are recorded in the table below.\\r\\n\\r\\nStarting Mass (g)\\tEnding Mass (g)\\tDifference in Mass (g)\\r\\nMarble\\t 9.8\\t 9.4\\t\\u20130.4\\r\\nLimestone\\t10.4\\t 9.1\\t\\u20131.3\\r\\nWood\\t11.2\\t11.2\\t 0.0\\r\\nPlastic\\t 7.2\\t 7.1\\t\\u20130.1\\r\\nAfter reading the group\\u2019s procedure, describe what additional information you would need in order to replicate the expe\", \"post_assessment\": \"{\\\"submission_id\\\": 3097, \\\"score\\\": 0, \\\"feedback\\\": \\\"{\\\\\\\"spelling\\\\\\\": \\\\\\\"Spelling: Ok.\\\\\\\", \\\\\\\"grammar\\\\\\\": \\\\\\\"Grammar: More grammar errors than average.\\\\\\\", \\\\\\\"markup-text\\\\\\\": \\\\\\\"the students data are recorded in the <bg>table below . starting mass</bg> g ending mass g difference in mass g marble . . . limestone . . . wood . . . plastic . . . after reading the groups <bg>procedure , describe what additional</bg> information you would need in order to replicate the <bs>expe</bs>\\\\\\\"}\\\", \\\"success\\\": true, \\\"grader_id\\\": 3233, \\\"grader_type\\\": \\\"ML\\\", \\\"rubric_scores_complete\\\": true, \\\"rubric_xml\\\": \\\"<rubric><category><description>Response Quality</description><score>0</score><option points='0'>The response is not a satisfactory answer to the question. It either fails to address the question or does so in a limited way, with no evidence of higher-order thinking.</option><option points='1'>The response is a marginal answer to the question. It may contain some elements of a proficient response, but it is inaccurate or incomplete.</option><option points='2'>The response is a proficient answer to the question. It is generally correct, although it may contain minor inaccuracies. There is limited evidence of higher-order thinking.</option><option points='3'>The response is correct, complete, and contains evidence of higher-order thinking.</option></category></rubric>\\\"}\", \"score\": 0}, {\"answer\": \"After 24 hours, remove the samples from the containers and rinse each sample with distilled water.\\r\\nAllow the samples to sit and dry for 30 minutes.\\r\\nDetermine the mass of each sample.\\r\\nThe students\\u2019 data are recorded in the table below.\\r\\n\\r\\nStarting Mass (g)\\tEnding Mass (g)\\tDifference in Mass (g)\\r\\nMarble\\t 9.8\\t 9.4\\t\\u20130.4\\r\\nLimestone\\t10.4\\t 9.1\\t\\u20131.3\\r\\nWood\\t11.2\\t11.2\\t 0.0\\r\\nPlastic\\t 7.2\\t 7.1\\t\\u20130.1\\r\\nAfter reading the\", \"post_assessment\": \"[3]\", \"score\": 3}, {\"answer\": \"To replicate the experiment, the procedure would require more detail. One piece of information that is omitted is the amount of vinegar used in the experiment. It is also important to know what temperature the experiment was kept at during the 24 hours. Finally, the procedure needs to include details about the experiment, for example if the whole sample must be submerged.\", \"post_assessment\": \"[3]\", \"score\": 3}, {\"answer\": \"e the mass of four different samples.\\r\\nPour vinegar in each of four separate, but identical, containers.\\r\\nPlace a sample of one material into one container and label. Repeat with remaining samples, placing a single sample into a single container.\\r\\nAfter 24 hours, remove the samples from the containers and rinse each sample with distilled water.\\r\\nAllow the samples to sit and dry for 30 minutes.\\r\\nDetermine the mass of each sample.\\r\\nThe students\\u2019 data are recorded in the table below.\\r\\n\", \"post_assessment\": \"[3]\", \"score\": 3}, {\"answer\": \"\", \"post_assessment\": \"[3]\", \"score\": 3}], \"max_score\": 3, \"child_state\": \"done\"}", "{\"child_created\": false, \"child_attempts\": 0, \"version\": 1, \"child_history\": [{\"answer\": \"The students\\u2019 data are recorded in the table below.\\r\\n\\r\\nStarting Mass (g)\\tEnding Mass (g)\\tDifference in Mass (g)\\r\\nMarble\\t 9.8\\t 9.4\\t\\u20130.4\\r\\nLimestone\\t10.4\\t 9.1\\t\\u20131.3\\r\\nWood\\t11.2\\t11.2\\t 0.0\\r\\nPlastic\\t 7.2\\t 7.1\\t\\u20130.1\\r\\nAfter reading the group\\u2019s procedure, describe what additional information you would need in order to replicate the expe\", \"post_assessment\": \"{\\\"submission_id\\\": 3097, \\\"score\\\": 0, \\\"feedback\\\": \\\"{\\\\\\\"spelling\\\\\\\": \\\\\\\"Spelling: Ok.\\\\\\\", \\\\\\\"grammar\\\\\\\": \\\\\\\"Grammar: More grammar errors than average.\\\\\\\", \\\\\\\"markup-text\\\\\\\": \\\\\\\"the students data are recorded in the <bg>table below . starting mass</bg> g ending mass g difference in mass g marble . . . limestone . . . wood . . . plastic . . . after reading the groups <bg>procedure , describe what additional</bg> information you would need in order to replicate the <bs>expe</bs>\\\\\\\"}\\\", \\\"success\\\": true, \\\"grader_id\\\": 3233, \\\"grader_type\\\": \\\"ML\\\", \\\"rubric_scores_complete\\\": true, \\\"rubric_xml\\\": \\\"<rubric><category><description>Response Quality</description><score>0</score><option points='0'>The response is not a satisfactory answer to the question. It either fails to address the question or does so in a limited way, with no evidence of higher-order thinking.</option><option points='1'>The response is a marginal answer to the question. It may contain some elements of a proficient response, but it is inaccurate or incomplete.</option><option points='2'>The response is a proficient answer to the question. It is generally correct, although it may contain minor inaccuracies. There is limited evidence of higher-order thinking.</option><option points='3'>The response is correct, complete, and contains evidence of higher-order thinking.</option></category></rubric>\\\"}\", \"score\": 0}, {\"answer\": \"After 24 hours, remove the samples from the containers and rinse each sample with distilled water.\\r\\nAllow the samples to sit and dry for 30 minutes.\\r\\nDetermine the mass of each sample.\\r\\nThe students\\u2019 data are recorded in the table below.\\r\\n\\r\\nStarting Mass (g)\\tEnding Mass (g)\\tDifference in Mass (g)\\r\\nMarble\\t 9.8\\t 9.4\\t\\u20130.4\\r\\nLimestone\\t10.4\\t 9.1\\t\\u20131.3\\r\\nWood\\t11.2\\t11.2\\t 0.0\\r\\nPlastic\\t 7.2\\t 7.1\\t\\u20130.1\\r\\nAfter reading the\", \"post_assessment\": \"{\\\"submission_id\\\": 3098, \\\"score\\\": 0, \\\"feedback\\\": \\\"{\\\\\\\"spelling\\\\\\\": \\\\\\\"Spelling: Ok.\\\\\\\", \\\\\\\"grammar\\\\\\\": \\\\\\\"Grammar: Ok.\\\\\\\", \\\\\\\"markup-text\\\\\\\": \\\\\\\"after hours , remove the samples from the containers and rinse each sample with distilled water . allow the samples to sit and dry for minutes . determine the mass of each sample . the students data are recorded in the <bg>table below . starting mass</bg> g ending mass g difference in mass g marble . . . limestone . . . wood . . . plastic . . . after reading the\\\\\\\"}\\\", \\\"success\\\": true, \\\"grader_id\\\": 3235, \\\"grader_type\\\": \\\"ML\\\", \\\"rubric_scores_complete\\\": true, \\\"rubric_xml\\\": \\\"<rubric><category><description>Response Quality</description><score>0</score><option points='0'>The response is not a satisfactory answer to the question. It either fails to address the question or does so in a limited way, with no evidence of higher-order thinking.</option><option points='1'>The response is a marginal answer to the question. It may contain some elements of a proficient response, but it is inaccurate or incomplete.</option><option points='2'>The response is a proficient answer to the question. It is generally correct, although it may contain minor inaccuracies. There is limited evidence of higher-order thinking.</option><option points='3'>The response is correct, complete, and contains evidence of higher-order thinking.</option></category></rubric>\\\"}\", \"score\": 0}, {\"answer\": \"To replicate the experiment, the procedure would require more detail. One piece of information that is omitted is the amount of vinegar used in the experiment. It is also important to know what temperature the experiment was kept at during the 24 hours. Finally, the procedure needs to include details about the experiment, for example if the whole sample must be submerged.\", \"post_assessment\": \"{\\\"submission_id\\\": 3099, \\\"score\\\": 3, \\\"feedback\\\": \\\"{\\\\\\\"spelling\\\\\\\": \\\\\\\"Spelling: Ok.\\\\\\\", \\\\\\\"grammar\\\\\\\": \\\\\\\"Grammar: Ok.\\\\\\\", \\\\\\\"markup-text\\\\\\\": \\\\\\\"to replicate the experiment , the procedure would require <bg>more detail . one</bg> piece of information <bg>that is omitted is the</bg> amount of vinegar used in the experiment . it is also important to know what temperature the experiment was kept at during the hours . finally , the procedure needs to include details about the experiment , for example if the whole sample must be submerged .\\\\\\\"}\\\", \\\"success\\\": true, \\\"grader_id\\\": 3237, \\\"grader_type\\\": \\\"ML\\\", \\\"rubric_scores_complete\\\": true, \\\"rubric_xml\\\": \\\"<rubric><category><description>Response Quality</description><score>3</score><option points='0'>The response is not a satisfactory answer to the question. It either fails to address the question or does so in a limited way, with no evidence of higher-order thinking.</option><option points='1'>The response is a marginal answer to the question. It may contain some elements of a proficient response, but it is inaccurate or incomplete.</option><option points='2'>The response is a proficient answer to the question. It is generally correct, although it may contain minor inaccuracies. There is limited evidence of higher-order thinking.</option><option points='3'>The response is correct, complete, and contains evidence of higher-order thinking.</option></category></rubric>\\\"}\", \"score\": 3}, {\"answer\": \"e the mass of four different samples.\\r\\nPour vinegar in each of four separate, but identical, containers.\\r\\nPlace a sample of one material into one container and label. Repeat with remaining samples, placing a single sample into a single container.\\r\\nAfter 24 hours, remove the samples from the containers and rinse each sample with distilled water.\\r\\nAllow the samples to sit and dry for 30 minutes.\\r\\nDetermine the mass of each sample.\\r\\nThe students\\u2019 data are recorded in the table below.\\r\\n\", \"post_assessment\": \"{\\\"submission_id\\\": 3100, \\\"score\\\": 0, \\\"feedback\\\": \\\"{\\\\\\\"spelling\\\\\\\": \\\\\\\"Spelling: Ok.\\\\\\\", \\\\\\\"grammar\\\\\\\": \\\\\\\"Grammar: Ok.\\\\\\\", \\\\\\\"markup-text\\\\\\\": \\\\\\\"e the mass of four different samples . pour vinegar in <bg>each of four separate</bg> , but identical , containers . place a sample of one material into one container and label . repeat with remaining samples , placing a single sample into a single container . after hours , remove the samples from the containers and rinse each sample with distilled water . allow the samples to sit and dry for minutes . determine the mass of each sample . the students data are recorded in the table below . \\\\\\\"}\\\", \\\"success\\\": true, \\\"grader_id\\\": 3239, \\\"grader_type\\\": \\\"ML\\\", \\\"rubric_scores_complete\\\": true, \\\"rubric_xml\\\": \\\"<rubric><category><description>Response Quality</description><score>0</score><option points='0'>The response is not a satisfactory answer to the question. It either fails to address the question or does so in a limited way, with no evidence of higher-order thinking.</option><option points='1'>The response is a marginal answer to the question. It may contain some elements of a proficient response, but it is inaccurate or incomplete.</option><option points='2'>The response is a proficient answer to the question. It is generally correct, although it may contain minor inaccuracies. There is limited evidence of higher-order thinking.</option><option points='3'>The response is correct, complete, and contains evidence of higher-order thinking.</option></category></rubric>\\\"}\", \"score\": 0}, {\"answer\": \"\", \"post_assessment\": \"{\\\"submission_id\\\": 3101, \\\"score\\\": 0, \\\"feedback\\\": \\\"{\\\\\\\"spelling\\\\\\\": \\\\\\\"Spelling: Ok.\\\\\\\", \\\\\\\"grammar\\\\\\\": \\\\\\\"Grammar: Ok.\\\\\\\", \\\\\\\"markup-text\\\\\\\": \\\\\\\"invalid essay .\\\\\\\"}\\\", \\\"success\\\": true, \\\"grader_id\\\": 3241, \\\"grader_type\\\": \\\"ML\\\", \\\"rubric_scores_complete\\\": true, \\\"rubric_xml\\\": \\\"<rubric><category><description>Response Quality</description><score>0</score><option points='0'>The response is not a satisfactory answer to the question. It either fails to address the question or does so in a limited way, with no evidence of higher-order thinking.</option><option points='1'>The response is a marginal answer to the question. It may contain some elements of a proficient response, but it is inaccurate or incomplete.</option><option points='2'>The response is a proficient answer to the question. It is generally correct, although it may contain minor inaccuracies. There is limited evidence of higher-order thinking.</option><option points='3'>The response is correct, complete, and contains evidence of higher-order thinking.</option></category></rubric>\\\"}\", \"score\": 0}], \"max_score\": 3, \"child_state\": \"done\"}"], "attempts": "10000", "student_attempts": 0, "due": null, "state": "done", "accept_file_upload": false, "display_name": "Science Question -- Machine Assessed"}""" instance_state = json.loads(instance_state) rubric = """ <rubric> <rubric> <category> <description>Response Quality</description> <option>The response is not a satisfactory answer to the question. It either fails to address the question or does so in a limited way, with no evidence of higher-order thinking.</option> <option>The response is a marginal answer to the question. It may contain some elements of a proficient response, but it is inaccurate or incomplete.</option> <option>The response is a proficient answer to the question. It is generally correct, although it may contain minor inaccuracies. There is limited evidence of higher-order thinking.</option> <option>The response is correct, complete, and contains evidence of higher-order thinking.</option> </category> </rubric> </rubric> """ definition = { 'prompt': etree.XML(self.prompt), 'rubric': etree.XML(rubric), 'task_xml': [self.task_xml1, self.task_xml2] } descriptor = Mock(data=definition) combinedoe = CombinedOpenEndedV1Module(self.test_system, self.location, definition, descriptor, static_data=self.static_data, metadata=self.metadata, instance_state=instance_state) score_dict = combinedoe.get_score() self.assertEqual(score_dict['score'], 15.0) self.assertEqual(score_dict['total'], 15.0)
def id_to_location(course_id): '''Convert the given course_id (org/course/name) to a location object. Throws ValueError if course_id is of the wrong format. ''' org, course, name = course_id.split('/') return Location('i4x', org, course, 'course', name)
def get_course_id(location): """ Returns the course_id from a given the location tuple. """ # TODO: These will need to be changed to point to the particular instance of this problem in the particular course return modulestore().get_containing_courses(Location(location))[0].id
def modx_dispatch(request, dispatch, location, course_id): ''' Generic view for extensions. This is where AJAX calls go. Arguments: - request -- the django request. - dispatch -- the command string to pass through to the module's handle_ajax call (e.g. 'problem_reset'). If this string contains '?', only pass through the part before the first '?'. - location -- the module location. Used to look up the XModule instance - course_id -- defines the course context for this request. Raises PermissionDenied if the user is not logged in. Raises Http404 if the location and course_id do not identify a valid module, the module is not accessible by the user, or the module raises NotFoundError. If the module raises any other error, it will escape this function. ''' # ''' (fix emacs broken parsing) # Check parameters and fail fast if there's a problem if not Location.is_valid(location): raise Http404("Invalid location") if not request.user.is_authenticated(): raise PermissionDenied # Get the submitted data data = request.POST.copy() # Get and check submitted files files = request.FILES or {} error_msg = _check_files_limits(files) if error_msg: return HttpResponse(json.dumps({'success': error_msg})) for key in files: # Merge files into to data dictionary data[key] = files.getlist(key) try: descriptor = modulestore().get_instance(course_id, location) except ItemNotFoundError: log.warn( "Invalid location for course id {course_id}: {location}".format( course_id=course_id, location=location)) raise Http404 field_data_cache = FieldDataCache.cache_for_descriptor_descendents( course_id, request.user, descriptor) instance = get_module(request.user, request, location, field_data_cache, course_id, grade_bucket_type='ajax') if instance is None: # Either permissions just changed, or someone is trying to be clever # and load something they shouldn't have access to. log.debug("No module {0} for user {1}--access denied?".format( location, request.user)) raise Http404 # Let the module handle the AJAX try: ajax_return = instance.handle_ajax(dispatch, data) # Save any fields that have changed to the underlying KeyValueStore instance.save() # If we can't find the module, respond with a 404 except NotFoundError: log.exception("Module indicating to user that request doesn't exist") raise Http404 # For XModule-specific errors, we log the error and respond with an error message except ProcessingError as err: log.warning("Module encountered an error while processing AJAX call", exc_info=True) return JsonResponse(object={'success': err.args[0]}, status=200) # If any other error occurred, re-raise it to trigger a 500 response except: log.exception("error processing ajax call") raise # Return whatever the module wanted to return to the client/caller return HttpResponse(ajax_return)
def test_export_course(self): module_store = modulestore('direct') draft_store = modulestore('draft') content_store = contentstore() import_from_xml(module_store, 'common/test/data/', ['full']) location = CourseDescriptor.id_to_location( 'edX/full/6.002_Spring_2012') # get a vertical (and components in it) to put into 'draft' vertical = module_store.get_item(Location( ['i4x', 'edX', 'full', 'vertical', 'vertical_66', None]), depth=1) draft_store.clone_item(vertical.location, vertical.location) # We had a bug where orphaned draft nodes caused export to fail. This is here to cover that case. draft_store.clone_item( vertical.location, Location( ['i4x', 'edX', 'full', 'vertical', 'no_references', 'draft'])) for child in vertical.get_children(): draft_store.clone_item(child.location, child.location) root_dir = path(mkdtemp_clean()) # now create a private vertical private_vertical = draft_store.clone_item( vertical.location, Location( ['i4x', 'edX', 'full', 'vertical', 'a_private_vertical', None])) # add private to list of children sequential = module_store.get_item( Location([ 'i4x', 'edX', 'full', 'sequential', 'Administrivia_and_Circuit_Elements', None ])) private_location_no_draft = private_vertical.location.replace( revision=None) module_store.update_children( sequential.location, sequential.children + [private_location_no_draft.url()]) # read back the sequential, to make sure we have a pointer to sequential = module_store.get_item( Location([ 'i4x', 'edX', 'full', 'sequential', 'Administrivia_and_Circuit_Elements', None ])) self.assertIn(private_location_no_draft.url(), sequential.children) print 'Exporting to tempdir = {0}'.format(root_dir) # export out to a tempdir export_to_xml(module_store, content_store, location, root_dir, 'test_export', draft_modulestore=draft_store) # check for static tabs self.verify_content_existence(module_store, root_dir, location, 'tabs', 'static_tab', '.html') # check for custom_tags self.verify_content_existence(module_store, root_dir, location, 'info', 'course_info', '.html') # check for custom_tags self.verify_content_existence(module_store, root_dir, location, 'custom_tags', 'custom_tag_template') # check for graiding_policy.json filesystem = OSFS(root_dir / 'test_export/policies/6.002_Spring_2012') self.assertTrue(filesystem.exists('grading_policy.json')) course = module_store.get_item(location) # compare what's on disk compared to what we have in our course with filesystem.open('grading_policy.json', 'r') as grading_policy: on_disk = loads(grading_policy.read()) self.assertEqual(on_disk, course.grading_policy) #check for policy.json self.assertTrue(filesystem.exists('policy.json')) # compare what's on disk to what we have in the course module with filesystem.open('policy.json', 'r') as course_policy: on_disk = loads(course_policy.read()) self.assertIn('course/6.002_Spring_2012', on_disk) self.assertEqual(on_disk['course/6.002_Spring_2012'], own_metadata(course)) # remove old course delete_course(module_store, content_store, location) # reimport import_from_xml(module_store, root_dir, ['test_export'], draft_store=draft_store) items = module_store.get_items( Location(['i4x', 'edX', 'full', 'vertical', None])) self.assertGreater(len(items), 0) for descriptor in items: # don't try to look at private verticals. Right now we're running # the service in non-draft aware if getattr(descriptor, 'is_draft', False): print "Checking {0}....".format(descriptor.location.url()) resp = self.client.get( reverse('edit_unit', kwargs={'location': descriptor.location.url()})) self.assertEqual(resp.status_code, 200) # verify that we have the content in the draft store as well vertical = draft_store.get_item(Location( ['i4x', 'edX', 'full', 'vertical', 'vertical_66', None]), depth=1) self.assertTrue(getattr(vertical, 'is_draft', False)) for child in vertical.get_children(): self.assertTrue(getattr(child, 'is_draft', False)) # make sure that we don't have a sequential that is in draft mode sequential = draft_store.get_item( Location([ 'i4x', 'edX', 'full', 'sequential', 'Administrivia_and_Circuit_Elements', None ])) self.assertFalse(getattr(sequential, 'is_draft', False)) # verify that we have the private vertical test_private_vertical = draft_store.get_item( Location(['i4x', 'edX', 'full', 'vertical', 'vertical_66', None])) self.assertTrue(getattr(test_private_vertical, 'is_draft', False)) # make sure the textbook survived the export/import course = module_store.get_item( Location( ['i4x', 'edX', 'full', 'course', '6.002_Spring_2012', None])) self.assertGreater(len(course.textbooks), 0) shutil.rmtree(root_dir)
def test_get_items_with_course_items(self): store = modulestore() # fix was to allow get_items() to take the course_id parameter store.get_items(Location(None, None, 'vertical', None, None), course_id='abc', depth=0)
def test_jumpto_invalid_location(self): location = Location('i4x', 'edX', 'toy', 'NoSuchPlace', None) jumpto_url = '{0}/{1}/jump_to/{2}'.format('/courses', self.course_name, location) response = self.client.get(jumpto_url) self.assertEqual(response.status_code, 404)
def _add_draft_modules_to_course(self, new_course_id, old_course_id, old_course_loc, user_id): """ update each draft. Create any which don't exist in published and attach to their parents. """ # each true update below will trigger a new version of the structure. We may want to just have one new version # but that's for a later date. new_draft_course_loc = CourseLocator(course_id=new_course_id, branch='draft') # to prevent race conditions of grandchilden being added before their parents and thus having no parent to # add to awaiting_adoption = {} for module in self.draft_modulestore.get_items( old_course_loc.replace(category=None, name=None, revision=draft.DRAFT), old_course_id): if getattr(module, 'is_draft', False): new_locator = self.loc_mapper.translate_location( old_course_id, module.location, False, add_entry_if_missing=True) if self.split_modulestore.has_item(new_course_id, new_locator): # was in 'direct' so draft is a new version split_module = self.split_modulestore.get_item(new_locator) # need to remove any no-longer-explicitly-set values and add/update any now set values. for name, field in split_module.fields.iteritems(): if field.is_set_on(split_module) and not module.fields[ name].is_set_on(module): field.delete_from(split_module) for name, field in module.fields.iteritems(): # draft children will insert themselves and the others are here already; so, don't do it 2x if name != 'children' and field.is_set_on(module): field.write_to(split_module, field.read_from(module)) _new_module = self.split_modulestore.update_item( split_module, user_id) else: # only a draft version (aka, 'private'). parent needs updated too. # create a new course version just in case the current head is also the prod head _new_module = self.split_modulestore.create_item( new_draft_course_loc, module.category, user_id, usage_id=new_locator.usage_id, fields=self._get_json_fields_translate_children( module, old_course_id, True)) awaiting_adoption[module.location] = new_locator.usage_id for draft_location, new_usage_id in awaiting_adoption.iteritems(): for parent_loc in self.draft_modulestore.get_parent_locations( draft_location, old_course_id): old_parent = self.draft_modulestore.get_item(parent_loc) new_parent = self.split_modulestore.get_item( self.loc_mapper.translate_location(old_course_id, old_parent.location, False)) # this only occurs if the parent was also awaiting adoption if new_usage_id in new_parent.children: break # find index for module: new_parent may be missing quite a few of old_parent's children new_parent_cursor = 0 draft_location = draft_location.url() # need as string for old_child_loc in old_parent.children: if old_child_loc == draft_location: break sibling_loc = self.loc_mapper.translate_location( old_course_id, Location(old_child_loc), False) # sibling may move cursor for idx in range(new_parent_cursor, len(new_parent.children)): if new_parent.children[idx] == sibling_loc.usage_id: new_parent_cursor = idx + 1 break new_parent.children.insert(new_parent_cursor, new_usage_id) new_parent = self.split_modulestore.update_item( new_parent, user_id)
def test_translate_location_read_only(self): """ Test the variants of translate_location which don't create entries, just decode """ # lookup before there are any maps org = 'foo_org' course = 'bar_course' old_style_course_id = '{}/{}/{}'.format(org, course, 'baz_run') with self.assertRaises(ItemNotFoundError): _ = loc_mapper().translate_location(old_style_course_id, Location( 'i4x', org, course, 'problem', 'abc123'), add_entry_if_missing=False) new_style_course_id = '{}.geek_dept.{}.baz_run'.format(org, course) block_map = {'abc123': {'problem': 'problem2'}} loc_mapper().create_map_entry(Location('i4x', org, course, 'course', 'baz_run'), new_style_course_id, block_map=block_map) # only one course matches prob_locator = loc_mapper().translate_location( old_style_course_id, Location('i4x', org, course, 'problem', 'abc123'), add_entry_if_missing=False) self.assertEqual(prob_locator.course_id, new_style_course_id) self.assertEqual(prob_locator.branch, 'published') self.assertEqual(prob_locator.usage_id, 'problem2') # look for w/ only the Location (works b/c there's only one possible course match) prob_locator = loc_mapper().translate_location( None, Location('i4x', org, course, 'problem', 'abc123'), add_entry_if_missing=False) self.assertEqual(prob_locator.course_id, new_style_course_id) # look for non-existent problem with self.assertRaises(ItemNotFoundError): prob_locator = loc_mapper().translate_location( None, Location('i4x', org, course, 'problem', '1def23'), add_entry_if_missing=False) # add a distractor course block_map = {'abc123': {'problem': 'problem3'}} loc_mapper().create_map_entry(Location('i4x', org, course, 'course', 'delta_run'), '{}.geek_dept.{}.{}'.format( org, course, 'delta_run'), block_map=block_map) prob_locator = loc_mapper().translate_location( old_style_course_id, Location('i4x', org, course, 'problem', 'abc123'), add_entry_if_missing=False) self.assertEqual(prob_locator.course_id, new_style_course_id) self.assertEqual(prob_locator.usage_id, 'problem2') # look for w/ only the Location (not unique; so, just verify it returns something) prob_locator = loc_mapper().translate_location( None, Location('i4x', org, course, 'problem', 'abc123'), add_entry_if_missing=False) self.assertIsNotNone(prob_locator, "couldn't find ambiguous location") # add a default course pointing to the delta_run loc_mapper().create_map_entry(Location('i4x', org, course, 'problem', '789abc123efg456'), '{}.geek_dept.{}.{}'.format( org, course, 'delta_run'), block_map=block_map) # now the ambiguous query should return delta prob_locator = loc_mapper().translate_location( None, Location('i4x', org, course, 'problem', 'abc123'), add_entry_if_missing=False) self.assertEqual(prob_locator.course_id, '{}.geek_dept.{}.{}'.format(org, course, 'delta_run')) self.assertEqual(prob_locator.usage_id, 'problem3') # get the draft one (I'm sorry this is getting long) prob_locator = loc_mapper().translate_location( None, Location('i4x', org, course, 'problem', 'abc123'), published=False, add_entry_if_missing=False) self.assertEqual(prob_locator.course_id, '{}.geek_dept.{}.{}'.format(org, course, 'delta_run')) self.assertEqual(prob_locator.usage_id, 'problem3') self.assertEqual(prob_locator.branch, 'draft')
def export_to_xml(modulestore, contentstore, course_location, root_dir, course_dir, draft_modulestore=None): """ Export all modules from `modulestore` and content from `contentstore` as xml to `root_dir`. `modulestore`: A `ModuleStore` object that is the source of the modules to export `contentstore`: A `ContentStore` object that is the source of the content to export, can be None `course_location`: The `Location` of the `CourseModuleDescriptor` to export `root_dir`: The directory to write the exported xml to `course_dir`: The name of the directory inside `root_dir` to write the course content to `draft_modulestore`: An optional `DraftModuleStore` that contains draft content, which will be exported alongside the public content in the course. """ course_id = course_location.course_id course = modulestore.get_course(course_id) fs = OSFS(root_dir) export_fs = fs.makeopendir(course_dir) xml = course.export_to_xml(export_fs) with export_fs.open('course.xml', 'w') as course_xml: course_xml.write(xml) # export the static assets policies_dir = export_fs.makeopendir('policies') if contentstore: contentstore.export_all_for_course( course_location, root_dir + '/' + course_dir + '/static/', root_dir + '/' + course_dir + '/policies/assets.json', ) # export the static tabs export_extra_content(export_fs, modulestore, course_id, course_location, 'static_tab', 'tabs', '.html') # export the custom tags export_extra_content(export_fs, modulestore, course_id, course_location, 'custom_tag_template', 'custom_tags') # export the course updates export_extra_content(export_fs, modulestore, course_id, course_location, 'course_info', 'info', '.html') # export the 'about' data (e.g. overview, etc.) export_extra_content(export_fs, modulestore, course_id, course_location, 'about', 'about', '.html') # export the grading policy course_run_policy_dir = policies_dir.makeopendir(course.location.name) with course_run_policy_dir.open('grading_policy.json', 'w') as grading_policy: grading_policy.write(dumps(course.grading_policy, cls=EdxJSONEncoder)) # export all of the course metadata in policy.json with course_run_policy_dir.open('policy.json', 'w') as course_policy: policy = {'course/' + course.location.name: own_metadata(course)} course_policy.write(dumps(policy, cls=EdxJSONEncoder)) # export draft content # NOTE: this code assumes that verticals are the top most draftable container # should we change the application, then this assumption will no longer # be valid if draft_modulestore is not None: draft_verticals = draft_modulestore.get_items([ None, course_location.org, course_location.course, 'vertical', None, 'draft' ]) if len(draft_verticals) > 0: draft_course_dir = export_fs.makeopendir('drafts') for draft_vertical in draft_verticals: parent_locs = draft_modulestore.get_parent_locations( draft_vertical.location, course.location.course_id) # Don't try to export orphaned items. if len(parent_locs) > 0: logging.debug('parent_locs = {0}'.format(parent_locs)) draft_vertical.xml_attributes[ 'parent_sequential_url'] = Location( parent_locs[0]).url() sequential = modulestore.get_item(Location(parent_locs[0])) index = sequential.children.index( draft_vertical.location.url()) draft_vertical.xml_attributes[ 'index_in_children_list'] = str(index) draft_vertical.export_to_xml(draft_course_dir)
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 setUp(self): self.location = Location("edX", 'course', 'run', "video", "SampleProblem1", None)