def test_course_constructor_course_id_no_branch(self): testurn = "mit.eecs.6002x" testobj = CourseLocator(course_id=testurn) self.check_course_locn_fields(testobj, "course_id", course_id=testurn) self.assertEqual(testobj.course_id, testurn) self.assertEqual(str(testobj), testurn) self.assertEqual(testobj.url(), "edx://" + testurn)
def test_course_keyword_setters(self): raise SkipTest() # arg list inits testobj = CourseLocator(version_guid='versionid') self.check_course_locn_fields(testobj, 'versionid arg', 'versionid') testobj = CourseLocator(course_id='courseid') self.check_course_locn_fields(testobj, 'courseid arg', course_id='courseid') testobj = CourseLocator(course_id='courseid', revision='rev') self.check_course_locn_fields(testobj, 'rev arg', course_id='courseid', revision='rev') # ignores garbage testobj = CourseLocator(course_id='courseid', revision='rev', potato='spud') self.check_course_locn_fields(testobj, 'extra keyword arg', course_id='courseid', revision='rev') # url w/ keyword override testurn = 'crx/courseid@revision/blockid' testobj = CourseLocator(testurn, revision='rev') self.check_course_locn_fields(testobj, 'rev override', course_id='courseid', revision='rev')
def translate_location_to_course_locator(self, old_style_course_id, location, published=True, lower_only=False): """ Used when you only need the CourseLocator and not a full BlockUsageLocator. Probably only useful for get_items which wildcards name or category. :param course_id: old style course id """ cached = self._get_course_locator_from_cache(old_style_course_id, published) if cached: return cached location_id = self._interpret_location_course_id( old_style_course_id, location, lower_only) entry = self._get_map_entry_form_location(location_id) if entry is None: raise ItemNotFoundError(location) published_course_locator = CourseLocator(package_id=entry['course_id'], branch=entry['prod_branch']) draft_course_locator = CourseLocator(package_id=entry['course_id'], branch=entry['draft_branch']) self._cache_course_locator(old_style_course_id, published_course_locator, draft_course_locator) if published: return published_course_locator else: return draft_course_locator
def test_course_constructor_course_id_no_revision(self): testurn = 'edu.mit.eecs.6002x' testobj = CourseLocator(course_id=testurn) self.check_course_locn_fields(testobj, 'course_id', course_id=testurn) self.assertEqual(testobj.course_id, testurn) self.assertEqual(str(testobj), testurn) self.assertEqual(testobj.url(), 'edx://' + testurn)
def test_delete_course(self): test_course = persistent_factories.PersistentCourseFactory.create( org='testx', prettyid='edu.harvard.history.doomed', display_name='doomed test course', user_id='testbot') persistent_factories.ItemFactory.create( display_name='chapter 1', parent_location=test_course.location) id_locator = CourseLocator(course_id=test_course.location.course_id, branch='draft') guid_locator = CourseLocator( version_guid=test_course.location.version_guid) # verify it can be retireved by id self.assertIsInstance( modulestore('split').get_course(id_locator), CourseDescriptor) # and by guid self.assertIsInstance( modulestore('split').get_course(guid_locator), CourseDescriptor) modulestore('split').delete_course(id_locator.course_id) # test can no longer retrieve by id self.assertRaises(ItemNotFoundError, modulestore('split').get_course, id_locator) # but can by guid self.assertIsInstance( modulestore('split').get_course(guid_locator), CourseDescriptor)
def translate_location_to_course_locator(self, course_key, published=True): """ Used when you only need the CourseLocator and not a full BlockUsageLocator. Probably only useful for get_items which wildcards name or category. :param course_key: a CourseKey :param published: a boolean representing whether or not we should return the published or draft version Returns a Courselocator """ cached = self._get_course_locator_from_cache(course_key, published) if cached: return cached course_son = self._interpret_location_course_id(course_key) entry = self.location_map.find_one(course_son) if entry is None: raise ItemNotFoundError(course_key) published_course_locator = CourseLocator(org=entry['org'], offering=entry['offering'], branch=entry['prod_branch']) draft_course_locator = CourseLocator(org=entry['org'], offering=entry['offering'], branch=entry['draft_branch']) self._cache_course_locator(course_key, published_course_locator, draft_course_locator) if published: return published_course_locator else: return draft_course_locator
def test_course_constructor_package_id_no_branch(self): org = 'mit.eecs' offering = '6002x' testurn = '{}+{}'.format(org, offering) testobj = CourseLocator(org=org, offering=offering) self.check_course_locn_fields(testobj, org=org, offering=offering) self.assertEqual(testobj._to_string(), testurn)
def translate_location_to_course_locator(self, old_style_course_id, location, published=True, lower_only=False): """ Used when you only need the CourseLocator and not a full BlockUsageLocator. Probably only useful for get_items which wildcards name or category. :param course_id: old style course id """ cached = self._get_course_locator_from_cache(old_style_course_id, published) if cached: return cached location_id = self._interpret_location_course_id(old_style_course_id, location, lower_only) maps = self.location_map.find(location_id) maps = list(maps) if len(maps) == 0: raise ItemNotFoundError(location) elif len(maps) == 1: entry = maps[0] else: # find entry w/o name, if any; otherwise, pick arbitrary entry = maps[0] for item in maps: if 'name' not in item['_id']: entry = item break published_course_locator = CourseLocator(package_id=entry['course_id'], branch=entry['prod_branch']) draft_course_locator = CourseLocator(package_id=entry['course_id'], branch=entry['draft_branch']) self._cache_course_locator(old_style_course_id, published_course_locator, draft_course_locator) if published: return published_course_locator else: return draft_course_locator
def test_course_constructor_package_id_no_branch(self): testurn = 'mit.eecs.6002x' testobj = CourseLocator(package_id=testurn) self.check_course_locn_fields(testobj, 'package_id', package_id=testurn) self.assertEqual(testobj.package_id, testurn) self.assertEqual(str(testobj), testurn) self.assertEqual(testobj.url(), 'edx://' + testurn)
def test_course_constructor_course_id_no_branch(self): testurn = 'mit.eecs.6002x' testobj = CourseLocator(course_id=testurn) self.check_course_locn_fields(testobj, 'course_id', course_id=testurn) self.assertEqual(testobj.course_id, testurn) self.assertEqual(str(testobj), testurn) self.assertEqual(testobj.url(), 'edx://' + testurn)
def test_get_course_negative(self): # Now negative testing self.assertRaises(InsufficientSpecificationError, modulestore().get_course, CourseLocator(course_id='edu.meh.blah')) self.assertRaises(ItemNotFoundError, modulestore().get_course, CourseLocator(course_id='nosuchthing', branch='draft')) self.assertRaises(ItemNotFoundError, modulestore().get_course, CourseLocator(course_id='GreekHero', branch='published'))
def test_course_constructor_course_id_separate_branch(self): test_id = "mit.eecs.6002x" test_branch = "published" expected_urn = "mit.eecs.6002x" + BRANCH_PREFIX + "published" testobj = CourseLocator(course_id=test_id, branch=test_branch) self.check_course_locn_fields(testobj, "course_id with separate branch", course_id=test_id, branch=test_branch) self.assertEqual(testobj.course_id, test_id) self.assertEqual(testobj.branch, test_branch) self.assertEqual(str(testobj), expected_urn) self.assertEqual(testobj.url(), "edx://" + expected_urn)
def test_cloned_course(self): """ Test making a course which points to an existing draft and published but not making any changes to either. """ pre_time = datetime.datetime.now(UTC) original_locator = CourseLocator(course_id="wonderful", branch='draft') original_index = modulestore().get_course_index_info(original_locator) new_draft = modulestore().create_course( 'leech', 'best_course', 'leech_master', id_root='best', versions_dict=original_index['versions']) new_draft_locator = new_draft.location self.assertRegexpMatches(new_draft_locator.course_id, r'best.*') # the edited_by and other meta fields on the new course will be the original author not this one self.assertEqual(new_draft.edited_by, '*****@*****.**') self.assertLess(new_draft.edited_on, pre_time) self.assertEqual(new_draft.location.version_guid, original_index['versions']['draft']) # however the edited_by and other meta fields on course_index will be this one new_index = modulestore().get_course_index_info(new_draft_locator) self.assertGreaterEqual(new_index["edited_on"], pre_time) self.assertLessEqual(new_index["edited_on"], datetime.datetime.now(UTC)) self.assertEqual(new_index['edited_by'], 'leech_master') new_published_locator = CourseLocator(course_id=new_draft_locator.course_id, branch='published') new_published = modulestore().get_course(new_published_locator) self.assertEqual(new_published.edited_by, '*****@*****.**') self.assertLess(new_published.edited_on, pre_time) self.assertEqual(new_published.location.version_guid, original_index['versions']['published']) # changing this course will not change the original course # using new_draft.location will insert the chapter under the course root new_item = modulestore().create_item( new_draft.location, 'chapter', 'leech_master', fields={'display_name': 'new chapter'} ) new_draft_locator.version_guid = None new_index = modulestore().get_course_index_info(new_draft_locator) self.assertNotEqual(new_index['versions']['draft'], original_index['versions']['draft']) new_draft = modulestore().get_course(new_draft_locator) self.assertEqual(new_item.edited_by, 'leech_master') self.assertGreaterEqual(new_item.edited_on, pre_time) self.assertNotEqual(new_item.location.version_guid, original_index['versions']['draft']) self.assertNotEqual(new_draft.location.version_guid, original_index['versions']['draft']) structure_info = modulestore().get_course_history_info(new_draft_locator) self.assertGreaterEqual(structure_info["edited_on"], pre_time) self.assertLessEqual(structure_info["edited_on"], datetime.datetime.now(UTC)) self.assertEqual(structure_info['edited_by'], 'leech_master') original_course = modulestore().get_course(original_locator) self.assertEqual(original_course.location.version_guid, original_index['versions']['draft']) self.assertFalse( modulestore().has_item(new_draft_locator.course_id, BlockUsageLocator( original_locator, usage_id=new_item.location.usage_id )) )
def test_course_constructor_bad_package_id(self, bad_id): """ Test all sorts of badly-formed package_ids (and urls with those package_ids) """ with self.assertRaises(InvalidKeyError): CourseLocator(org=bad_id, offering='test') with self.assertRaises(InvalidKeyError): CourseLocator(org='test', offering=bad_id) with self.assertRaises(InvalidKeyError): CourseKey.from_string('course-locator:test+{}'.format(bad_id))
def test_course_constructor_course_id_with_branch(self): testurn = 'mit.eecs.6002x/' + BRANCH_PREFIX + 'published' expected_id = 'mit.eecs.6002x' expected_branch = 'published' testobj = CourseLocator(course_id=testurn) self.check_course_locn_fields(testobj, 'course_id with branch', course_id=expected_id, branch=expected_branch, ) self.assertEqual(testobj.course_id, expected_id) self.assertEqual(testobj.branch, expected_branch) self.assertEqual(str(testobj), testurn) self.assertEqual(testobj.url(), 'edx://' + testurn)
def test_course_constructor_course_id_separate_revision(self): test_id = 'edu.mit.eecs.6002x' test_revision = 'published' expected_urn = 'edu.mit.eecs.6002x;published' testobj = CourseLocator(course_id=test_id, revision=test_revision) self.check_course_locn_fields(testobj, 'course_id with separate revision', course_id=test_id, revision=test_revision, ) self.assertEqual(testobj.course_id, test_id) self.assertEqual(testobj.revision, test_revision) self.assertEqual(str(testobj), expected_urn) self.assertEqual(testobj.url(), 'edx://' + expected_urn)
def test_course_constructor_package_id_separate_branch(self): test_id = 'mit.eecs.6002x' test_branch = 'published' expected_urn = 'mit.eecs.6002x/' + BRANCH_PREFIX + 'published' testobj = CourseLocator(package_id=test_id, branch=test_branch) self.check_course_locn_fields(testobj, 'package_id with separate branch', package_id=test_id, branch=test_branch, ) self.assertEqual(testobj.package_id, test_id) self.assertEqual(testobj.branch, test_branch) self.assertEqual(str(testobj), expected_urn) self.assertEqual(testobj.url(), 'edx://' + expected_urn)
def test_course_constructor_course_id_separate_branch(self): test_id = 'mit.eecs.6002x' test_branch = 'published' expected_urn = 'mit.eecs.6002x' + BRANCH_PREFIX + 'published' testobj = CourseLocator(course_id=test_id, branch=test_branch) self.check_course_locn_fields(testobj, 'course_id with separate branch', course_id=test_id, branch=test_branch, ) self.assertEqual(testobj.course_id, test_id) self.assertEqual(testobj.branch, test_branch) self.assertEqual(str(testobj), expected_urn) self.assertEqual(testobj.url(), 'edx://' + expected_urn)
def test_course_constructor_package_id_separate_branch(self): org = 'mit.eecs' offering = '6002x' test_branch = 'published' expected_urn = '{}+{}+{}+{}'.format(org, offering, CourseLocator.BRANCH_PREFIX, test_branch) testobj = CourseLocator(org=org, offering=offering, branch=test_branch) self.check_course_locn_fields( testobj, org=org, offering=offering, branch=test_branch, ) self.assertEqual(testobj.branch, test_branch) self.assertEqual(testobj._to_string(), expected_urn)
def test_get_parents(self): ''' get_parent_locations(locator, [usage_id], [branch]): [BlockUsageLocator] ''' locator = CourseLocator(course_id="GreekHero", branch='draft') parents = modulestore().get_parent_locations(locator, usage_id='chapter1') self.assertEqual(len(parents), 1) self.assertEqual(parents[0].usage_id, 'head12345') self.assertEqual(parents[0].course_id, "GreekHero") locator.usage_id = 'chapter2' parents = modulestore().get_parent_locations(locator) self.assertEqual(len(parents), 1) self.assertEqual(parents[0].usage_id, 'head12345') parents = modulestore().get_parent_locations(locator, usage_id='nosuchblock') self.assertEqual(len(parents), 0)
def test_course_constructor_course_id_separate_revision(self): test_id = 'edu.mit.eecs.6002x' test_revision = 'published' expected_urn = 'edu.mit.eecs.6002x;published' testobj = CourseLocator(course_id=test_id, revision=test_revision) self.check_course_locn_fields( testobj, 'course_id with separate revision', course_id=test_id, revision=test_revision, ) self.assertEqual(testobj.course_id, test_id) self.assertEqual(testobj.revision, test_revision) self.assertEqual(str(testobj), expected_urn) self.assertEqual(testobj.url(), 'edx://' + expected_urn)
def test_course_constructor_package_id_with_branch(self): testurn = 'mit.eecs.6002x/' + BRANCH_PREFIX + 'published' expected_id = 'mit.eecs.6002x' expected_branch = 'published' testobj = CourseLocator(package_id=testurn) self.check_course_locn_fields( testobj, 'package_id with branch', package_id=expected_id, branch=expected_branch, ) self.assertEqual(testobj.package_id, expected_id) self.assertEqual(testobj.branch, expected_branch) self.assertEqual(str(testobj), testurn) self.assertEqual(testobj.url(), 'edx://' + testurn)
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 _load_item(self, block_id, course_entry_override=None): if isinstance(block_id, BlockUsageLocator): if isinstance(block_id.block_id, LocalId): try: return self.local_modules[block_id] except KeyError: raise ItemNotFoundError else: block_id = block_id.block_id json_data = self.module_data.get(block_id) if json_data is None: # deeper than initial descendant fetch or doesn't exist course_info = course_entry_override or self.course_entry course_key = CourseLocator(course_info.get('org'), course_info.get('offering'), course_info.get('branch'), course_info['structure']['_id']) self.modulestore.cache_items(self, [block_id], course_key, lazy=self.lazy) json_data = self.module_data.get(block_id) if json_data is None: raise ItemNotFoundError(block_id) class_ = self.load_block_type(json_data.get('category')) return self.xblock_from_json(class_, block_id, json_data, course_entry_override)
def handle(self, *args, **options): if len(args) < 2: raise CommandError( "rollback_split_course requires 2 arguments (org offering)" ) try: locator = CourseLocator(org=args[0], offering=args[1]) except ValueError: raise CommandError("Invalid org or offering string {}, {}".format(*args)) location = loc_mapper().translate_locator_to_location(locator, get_course=True) if not location: raise CommandError( "This course does not exist in the old Mongo store. " "This command is designed to rollback a course, not delete " "it entirely." ) old_mongo_course = modulestore('direct').get_item(location) if not old_mongo_course: raise CommandError( "This course does not exist in the old Mongo store. " "This command is designed to rollback a course, not delete " "it entirely." ) try: modulestore('split').delete_course(locator) except ItemNotFoundError: raise CommandError("No course found with locator {}".format(locator)) print( 'Course rolled back successfully. To delete this course entirely, ' 'call the "delete_course" management command.' )
def setUp(self): """ Test case setup """ self.global_admin = AdminFactory() self.instructor = User.objects.create_user('testinstructor', '*****@*****.**', 'foo') self.staff = User.objects.create_user('teststaff', '*****@*****.**', 'foo') self.location = Location('i4x', 'mitX', '101', 'course', 'test') self.locator = CourseLocator(url='edx://mitX.101.test')
def test_course_constructor_url_course_id_branch_and_version_guid(self): test_id_loc = '519665f6223ebd6980884f2b' testobj = CourseLocator(url='edx://mit.eecs.6002x' + BRANCH_PREFIX + 'draft' + VERSION_PREFIX + test_id_loc) self.check_course_locn_fields(testobj, 'error parsing url with both course ID branch, and version GUID', course_id='mit.eecs.6002x', branch='draft', version_guid=ObjectId(test_id_loc))
def test_get_items(self): ''' get_items(locator, qualifiers, [branch]) ''' locator = CourseLocator(version_guid=self.GUID_D0) # get all modules matches = modulestore().get_items(locator) self.assertEqual(len(matches), 6) matches = modulestore().get_items(locator, qualifiers={}) self.assertEqual(len(matches), 6) matches = modulestore().get_items(locator, qualifiers={'category': 'chapter'}) self.assertEqual(len(matches), 3) matches = modulestore().get_items(locator, qualifiers={'category': 'garbage'}) self.assertEqual(len(matches), 0) matches = modulestore().get_items( locator, qualifiers= { 'category': 'chapter', 'fields': {'display_name': {'$regex': 'Hera'}} } ) self.assertEqual(len(matches), 2) matches = modulestore().get_items(locator, qualifiers={'fields': {'children': 'chapter2'}}) self.assertEqual(len(matches), 1) self.assertEqual(matches[0].location.usage_id, 'head12345')
def test_course_constructor_version_guid(self): # generate a random location test_id_1 = ObjectId() test_id_1_loc = str(test_id_1) testobj_1 = CourseLocator(version_guid=test_id_1) self.check_course_locn_fields(testobj_1, version_guid=test_id_1) self.assertEqual(str(testobj_1.version_guid), test_id_1_loc) self.assertEqual(testobj_1._to_string(), u'+'.join((testobj_1.VERSION_PREFIX, test_id_1_loc))) # Test using a given string test_id_2_loc = '519665f6223ebd6980884f2b' test_id_2 = ObjectId(test_id_2_loc) testobj_2 = CourseLocator(version_guid=test_id_2) self.check_course_locn_fields(testobj_2, version_guid=test_id_2) self.assertEqual(str(testobj_2.version_guid), test_id_2_loc) self.assertEqual(testobj_2._to_string(), u'+'.join((testobj_2.VERSION_PREFIX, test_id_2_loc)))
def test_course_constructor_course_id_repeated_branch(self): """ The same branch appears in the course_id and the branch field. """ test_id = "mit.eecs.6002x" + BRANCH_PREFIX + "published" test_branch = "published" expected_id = "mit.eecs.6002x" expected_urn = test_id testobj = CourseLocator(course_id=test_id, branch=test_branch) self.check_course_locn_fields( testobj, "course_id with repeated branch", course_id=expected_id, branch=test_branch ) self.assertEqual(testobj.course_id, expected_id) self.assertEqual(testobj.branch, test_branch) self.assertEqual(str(testobj), expected_urn) self.assertEqual(testobj.url(), "edx://" + expected_urn)
def create_map_entry(self, course_key, org=None, offering=None, draft_branch='draft', prod_branch='published', block_map=None): """ Add a new entry to map this SlashSeparatedCourseKey to the new style CourseLocator.org & offering. If org and offering are not provided, it defaults them based on course_key. WARNING: Exactly 1 CourseLocator key should index a given SlashSeparatedCourseKey. We provide no mechanism to enforce this assertion. NOTE: if there's already an entry w the given course_key, this may either overwrite that entry or throw an error depending on how mongo is configured. :param course_key (SlashSeparatedCourseKey): a SlashSeparatedCourseKey :param org (string): the CourseLocator style org :param offering (string): the CourseLocator offering :param draft_branch: the branch name to assign for drafts. This is hardcoded because old mongo had a fixed notion that there was 2 and only 2 versions for modules: draft and production. The old mongo did not, however, require that a draft version exist. The new one, however, does require a draft to exist. :param prod_branch: the branch name to assign for the production (live) copy. In old mongo, every course had to have a production version (whereas new split mongo does not require that until the author's ready to publish). :param block_map: an optional map to specify preferred names for blocks where the keys are the Location block names and the values are the BlockUsageLocator.block_id. Returns: :class:`CourseLocator` representing the new id for the course Raises: ValueError if one and only one of org and offering is provided. Provide either both or neither. """ if org is None and offering is None: assert (isinstance(course_key, SlashSeparatedCourseKey)) org = course_key.org offering = u"{0.course}.{0.run}".format(course_key) elif org is None or offering is None: raise ValueError( u"Either supply both org and offering or neither. Not just one: {}, {}" .format(org, offering)) # very like _interpret_location_id but using mongo subdoc lookup (more performant) course_son = self._construct_course_son(course_key) self.location_map.insert({ '_id': course_son, 'org': org, 'offering': offering, 'draft_branch': draft_branch, 'prod_branch': prod_branch, 'block_map': block_map or {}, 'schema': self.SCHEMA_VERSION, }) return CourseLocator(org, offering)
def test_course_constructor_course_id_repeated_branch(self): """ The same branch appears in the course_id and the branch field. """ test_id = 'mit.eecs.6002x' + BRANCH_PREFIX + 'published' test_branch = 'published' expected_id = 'mit.eecs.6002x' expected_urn = test_id testobj = CourseLocator(course_id=test_id, branch=test_branch) self.check_course_locn_fields(testobj, 'course_id with repeated branch', course_id=expected_id, branch=test_branch, ) self.assertEqual(testobj.course_id, expected_id) self.assertEqual(testobj.branch, test_branch) self.assertEqual(str(testobj), expected_urn) self.assertEqual(testobj.url(), 'edx://' + expected_urn)
def test_course_constructor_course_id_repeated_revision(self): """ The same revision appears in the course_id and the revision field. """ test_id = 'edu.mit.eecs.6002x;published' test_revision = 'published' expected_id = 'edu.mit.eecs.6002x' expected_urn = 'edu.mit.eecs.6002x;published' testobj = CourseLocator(course_id=test_id, revision=test_revision) self.check_course_locn_fields(testobj, 'course_id with repeated revision', course_id=expected_id, revision=test_revision, ) self.assertEqual(testobj.course_id, expected_id) self.assertEqual(testobj.revision, test_revision) self.assertEqual(str(testobj), expected_urn) self.assertEqual(testobj.url(), 'edx://' + expected_urn)
def test_course_constructor_redundant_002(self): testurn = 'mit.eecs.6002x' + BRANCH_PREFIX + 'published' expected_urn = 'mit.eecs.6002x' expected_rev = 'published' testobj = CourseLocator(course_id=testurn, url='edx://' + testurn) self.check_course_locn_fields(testobj, 'course_id', course_id=expected_urn, branch=expected_rev)
def test_course_constructor_package_id_repeated_branch(self): """ The same branch appears in the package_id and the branch field. """ test_id = 'mit.eecs.6002x/' + BRANCH_PREFIX + 'published' test_branch = 'published' expected_id = 'mit.eecs.6002x' expected_urn = test_id testobj = CourseLocator(package_id=test_id, branch=test_branch) self.check_course_locn_fields(testobj, 'package_id with repeated branch', package_id=expected_id, branch=test_branch, ) self.assertEqual(testobj.package_id, expected_id) self.assertEqual(testobj.branch, test_branch) self.assertEqual(str(testobj), expected_urn) self.assertEqual(testobj.url(), 'edx://' + expected_urn)
def course_info_update_handler(request, tag=None, package_id=None, branch=None, version_guid=None, block=None, provided_id=None): """ restful CRUD operations on course_info updates. provided_id should be none if it's new (create) and index otherwise. GET json: return the course info update models POST json: create an update PUT or DELETE json: change an existing update """ if 'application/json' not in request.META.get('HTTP_ACCEPT', 'application/json'): return HttpResponseBadRequest("Only supports json requests") course_location = loc_mapper().translate_locator_to_location( CourseLocator(package_id=package_id), get_course=True) updates_location = course_location.replace(category='course_info', name=block) if provided_id == '': provided_id = None # check that logged in user has permissions to this item (GET shouldn't require this level?) if not has_course_access(request.user, updates_location): raise PermissionDenied() if request.method == 'GET': course_updates = get_course_updates(updates_location, provided_id) if isinstance(course_updates, dict) and course_updates.get('error'): return JsonResponse( get_course_updates(updates_location, provided_id), course_updates.get('status', 400)) else: return JsonResponse( get_course_updates(updates_location, provided_id)) elif request.method == 'DELETE': try: return JsonResponse( delete_course_update(updates_location, request.json, provided_id, request.user)) except: return HttpResponseBadRequest("Failed to delete", content_type="text/plain") # can be either and sometimes django is rewriting one to the other: elif request.method in ('POST', 'PUT'): try: return JsonResponse( update_course_updates(updates_location, request.json, provided_id, request.user)) except: return HttpResponseBadRequest("Failed to save", content_type="text/plain")
def test_course_constructor_url(self): # Test parsing a url when it starts with a version ID and there is also a block ID. # This hits the parsers parse_guid method. test_id_loc = '519665f6223ebd6980884f2b' testobj = CourseLocator(url="edx://" + URL_VERSION_PREFIX + test_id_loc + BLOCK_PREFIX + "hw3") self.check_course_locn_fields(testobj, 'test_block constructor', version_guid=ObjectId(test_id_loc))
def test_course_constructor_url_package_id_and_version_guid(self): test_id_loc = '519665f6223ebd6980884f2b' testobj = CourseLocator(url='edx://mit.eecs-honors.6002x/' + VERSION_PREFIX + test_id_loc) self.check_course_locn_fields( testobj, 'error parsing url with both course ID and version GUID', package_id='mit.eecs-honors.6002x', version_guid=ObjectId(test_id_loc))
def test_course_constructor_redundant_002(self): testurn = 'edu.mit.eecs.6002x;published' expected_urn = 'edu.mit.eecs.6002x' expected_rev = 'published' testobj = CourseLocator(course_id=testurn, url='edx://' + testurn) self.check_course_locn_fields(testobj, 'course_id', course_id=expected_urn, revision=expected_rev)
def test_course_constructor_version_guid(self): # generate a random location test_id_1 = ObjectId() test_id_1_loc = str(test_id_1) testobj_1 = CourseLocator(version_guid=test_id_1) self.check_course_locn_fields(testobj_1, 'version_guid', version_guid=test_id_1) self.assertEqual(str(testobj_1.version_guid), test_id_1_loc) self.assertEqual(str(testobj_1), VERSION_PREFIX + test_id_1_loc) self.assertEqual(testobj_1.url(), 'edx://' + VERSION_PREFIX + test_id_1_loc) # Test using a given string test_id_2_loc = '519665f6223ebd6980884f2b' test_id_2 = ObjectId(test_id_2_loc) testobj_2 = CourseLocator(version_guid=test_id_2) self.check_course_locn_fields(testobj_2, 'version_guid', version_guid=test_id_2) self.assertEqual(str(testobj_2.version_guid), test_id_2_loc) self.assertEqual(str(testobj_2), VERSION_PREFIX + test_id_2_loc) self.assertEqual(testobj_2.url(), 'edx://' + VERSION_PREFIX + test_id_2_loc)
def test_course_constructor_course_id_repeated_revision(self): """ The same revision appears in the course_id and the revision field. """ test_id = 'edu.mit.eecs.6002x;published' test_revision = 'published' expected_id = 'edu.mit.eecs.6002x' expected_urn = 'edu.mit.eecs.6002x;published' testobj = CourseLocator(course_id=test_id, revision=test_revision) self.check_course_locn_fields( testobj, 'course_id with repeated revision', course_id=expected_id, revision=test_revision, ) self.assertEqual(testobj.course_id, expected_id) self.assertEqual(testobj.revision, test_revision) self.assertEqual(str(testobj), expected_urn) self.assertEqual(testobj.url(), 'edx://' + expected_urn)
def test_update_course_index(self): """ Test changing the org, pretty id, etc of a course. Test that it doesn't allow changing the id, etc. """ locator = CourseLocator(course_id="GreekHero", branch='draft') modulestore().update_course_index(locator, {'org': 'funkyU'}) course_info = modulestore().get_course_index_info(locator) self.assertEqual(course_info['org'], 'funkyU') modulestore().update_course_index(locator, {'org': 'moreFunky', 'prettyid': 'Ancient Greek Demagods'}) course_info = modulestore().get_course_index_info(locator) self.assertEqual(course_info['org'], 'moreFunky') self.assertEqual(course_info['prettyid'], 'Ancient Greek Demagods') self.assertRaises(ValueError, modulestore().update_course_index, locator, {'_id': 'funkygreeks'}) with self.assertRaises(ValueError): modulestore().update_course_index( locator, {'edited_on': datetime.datetime.now(UTC)} ) with self.assertRaises(ValueError): modulestore().update_course_index( locator, {'edited_by': 'sneak'} ) self.assertRaises(ValueError, modulestore().update_course_index, locator, {'versions': {'draft': self.GUID_D1}}) # an allowed but not necessarily recommended way to revert the draft version versions = course_info['versions'] versions['draft'] = self.GUID_D1 modulestore().update_course_index(locator, {'versions': versions}, update_versions=True) course = modulestore().get_course(locator) self.assertEqual(str(course.location.version_guid), self.GUID_D1) # an allowed but not recommended way to publish a course versions['published'] = self.GUID_D1 modulestore().update_course_index(locator, {'versions': versions}, update_versions=True) course = modulestore().get_course(CourseLocator(course_id=locator.course_id, branch="published")) self.assertEqual(str(course.location.version_guid), self.GUID_D1)
def test_course_urls(self): ''' Test constructor and property accessors. ''' raise SkipTest() self.assertRaises(TypeError, CourseLocator, 'empty constructor') # url inits testurn = 'edx://org/course/category/name' self.assertRaises(InvalidLocationError, CourseLocator, url=testurn) testurn = 'unknown/versionid/blockid' self.assertRaises(InvalidLocationError, CourseLocator, url=testurn) testurn = 'cvx/versionid' testobj = CourseLocator(testurn) self.check_course_locn_fields(testobj, testurn, 'versionid') self.assertEqual(testobj, CourseLocator(testobj), 'initialization from another instance') testurn = 'cvx/versionid/' testobj = CourseLocator(testurn) self.check_course_locn_fields(testobj, testurn, 'versionid') testurn = 'cvx/versionid/blockid' testobj = CourseLocator(testurn) self.check_course_locn_fields(testobj, testurn, 'versionid') testurn = 'cvx/versionid/blockid/extraneousstuff?including=args' testobj = CourseLocator(testurn) self.check_course_locn_fields(testobj, testurn, 'versionid') testurn = 'cvx://versionid/blockid' testobj = CourseLocator(testurn) self.check_course_locn_fields(testobj, testurn, 'versionid') testurn = 'crx/courseid/blockid' testobj = CourseLocator(testurn) self.check_course_locn_fields(testobj, testurn, course_id='courseid') testurn = 'crx/courseid@revision/blockid' testobj = CourseLocator(testurn) self.check_course_locn_fields(testobj, testurn, course_id='courseid', revision='revision') self.assertEqual(testobj, CourseLocator(testobj), 'run initialization from another instance')
def test_url_reverse(self): """ Test the url_reverse method """ locator = CourseLocator(package_id="a.fancy_course-id", branch="branch_1.2-3") self.assertEqual( '/expression/{}/format'.format(unicode(locator)), locator.url_reverse('expression', 'format') ) self.assertEqual( '/expression/{}/format'.format(unicode(locator)), locator.url_reverse('/expression', '/format') ) self.assertEqual( '/expression/{}'.format(unicode(locator)), locator.url_reverse('expression/', None) ) self.assertEqual( '/expression/{}'.format(unicode(locator)), locator.url_reverse('/expression/', '') )
def test_html(self): ''' Ensure CourseLocator generates expected urls. ''' raise SkipTest() testobj = CourseLocator(version_guid='versionid') self.assertEqual(testobj.html_id(), 'cvx/versionid', 'versionid') self.assertEqual(testobj, CourseLocator(testobj.html_id()), 'versionid conversion through html_id') testobj = CourseLocator(course_id='courseid') self.assertEqual(testobj.html_id(), 'crx/courseid', 'courseid') self.assertEqual(testobj, CourseLocator(testobj.html_id()), 'courseid conversion through html_id') testobj = CourseLocator(course_id='courseid', revision='rev') self.assertEqual(testobj.html_id(), 'crx/courseid%40rev', 'rev') self.assertEqual(testobj, CourseLocator(testobj.html_id()), 'rev conversion through html_id')
def translate_location(self, location, published=True, add_entry_if_missing=True, passed_block_id=None): """ Translate the given module location to a Locator. The rationale for auto adding entries was that there should be a reasonable default translation if the code just trips into this w/o creating translations. Will raise ItemNotFoundError if there's no mapping and add_entry_if_missing is False. :param location: a Location pointing to a module :param published: a boolean to indicate whether the caller wants the draft or published branch. :param add_entry_if_missing: a boolean as to whether to raise ItemNotFoundError or to create an entry if the course or block is not found in the map. :param passed_block_id: what block_id to assign and save if none is found (only if add_entry_if_missing) NOTE: unlike old mongo, draft branches contain the whole course; so, it applies to all category of locations including course. """ course_son = self._interpret_location_course_id(location.course_key) cached_value = self._get_locator_from_cache(location, published) if cached_value: return cached_value entry = self.location_map.find_one(course_son) if entry is None: if add_entry_if_missing: # create a new map self.create_map_entry(location.course_key) entry = self.location_map.find_one(course_son) else: raise ItemNotFoundError(location) else: entry = self._migrate_if_necessary([entry])[0] block_id = entry['block_map'].get(self.encode_key_for_mongo(location.name)) category = location.category if block_id is None: if add_entry_if_missing: block_id = self._add_to_block_map( location, course_son, entry['block_map'], passed_block_id ) else: raise ItemNotFoundError(location) else: # jump_to_id uses a None category. if category is None: if len(block_id) == 1: # unique match (most common case) category = block_id.keys()[0] block_id = block_id.values()[0] else: raise InvalidLocationError() elif category in block_id: block_id = block_id[category] elif add_entry_if_missing: block_id = self._add_to_block_map(location, course_son, entry['block_map']) else: raise ItemNotFoundError(location) prod_course_locator = CourseLocator( org=entry['org'], offering=entry['offering'], branch=entry['prod_branch'] ) published_usage = BlockUsageLocator( prod_course_locator, block_type=category, block_id=block_id ) draft_usage = BlockUsageLocator( prod_course_locator.for_branch(entry['draft_branch']), block_type=category, block_id=block_id ) if published: result = published_usage else: result = draft_usage self._cache_location_map_entry(location, published_usage, draft_usage) return result
def test_translate_locator(self): """ tests translate_locator_to_location(BlockUsageLocator) """ # lookup for non-existent course org = 'foo_org' course = 'bar_course' run = 'baz_run' new_style_org = '{}.geek_dept'.format(org) new_style_offering = '{}.{}'.format(course, run) prob_course_key = CourseLocator( org=new_style_org, offering=new_style_offering, branch='published', ) prob_locator = BlockUsageLocator( prob_course_key, block_type='problem', block_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( SlashSeparatedCourseKey(org, course, run), new_style_org, new_style_offering, block_map={ 'abc123': {'problem': 'problem2'}, '48f23a10395384929234': {'chapter': 'chapter48f'}, 'baz_run': {'course': 'root'}, } ) # only one course matches prob_location = loc_mapper().translate_locator_to_location(prob_locator) # default branch self.assertEqual(prob_location, Location(org, course, run, 'problem', 'abc123', None)) # test get_course keyword prob_location = loc_mapper().translate_locator_to_location(prob_locator, get_course=True) self.assertEqual(prob_location, SlashSeparatedCourseKey(org, course, run)) # explicit branch prob_locator = prob_locator.for_branch('draft') prob_location = loc_mapper().translate_locator_to_location(prob_locator) # Even though the problem was set as draft, we always return revision=None to work # with old mongo/draft modulestores. self.assertEqual(prob_location, Location(org, course, run, 'problem', 'abc123', None)) prob_locator = BlockUsageLocator( prob_course_key.for_branch('production'), block_type='problem', block_id='problem2' ) prob_location = loc_mapper().translate_locator_to_location(prob_locator) self.assertEqual(prob_location, Location(org, course, run, 'problem', 'abc123', None)) # same for chapter except chapter cannot be draft in old system chap_locator = BlockUsageLocator( prob_course_key.for_branch('production'), block_type='chapter', block_id='chapter48f', ) chap_location = loc_mapper().translate_locator_to_location(chap_locator) self.assertEqual(chap_location, Location(org, course, run, 'chapter', '48f23a10395384929234')) # explicit branch chap_locator = chap_locator.for_branch('draft') chap_location = loc_mapper().translate_locator_to_location(chap_locator) self.assertEqual(chap_location, Location(org, course, run, 'chapter', '48f23a10395384929234')) chap_locator = BlockUsageLocator( prob_course_key.for_branch('production'), block_type='chapter', block_id='chapter48f' ) chap_location = loc_mapper().translate_locator_to_location(chap_locator) self.assertEqual(chap_location, Location(org, course, run, 'chapter', '48f23a10395384929234')) # look for non-existent problem prob_locator2 = BlockUsageLocator( prob_course_key.for_branch('draft'), block_type='problem', block_id='problem3' ) prob_location = loc_mapper().translate_locator_to_location(prob_locator2) self.assertIsNone(prob_location, 'Found non-existent problem') # add a distractor course delta_run = 'delta_run' new_style_offering = '{}.{}'.format(course, delta_run) loc_mapper().create_map_entry( SlashSeparatedCourseKey(org, course, delta_run), new_style_org, new_style_offering, block_map={'abc123': {'problem': 'problem3'}} ) prob_location = loc_mapper().translate_locator_to_location(prob_locator) self.assertEqual(prob_location, Location(org, course, run, 'problem', 'abc123', None))