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): 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 _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 _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 _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 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 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 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 _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 _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 _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 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 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 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 _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 _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_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_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 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 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 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 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 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 _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 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 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