""" Test finding orphans via the view and django config """ import json import ddt from opaque_keys.edx.locator import BlockUsageLocator from contentstore.tests.utils import CourseTestCase from contentstore.utils import reverse_course_url from student.models import CourseEnrollment from xmodule.modulestore import ModuleStoreEnum from xmodule.modulestore.search import path_to_location from xmodule.modulestore.tests.factories import CourseFactory, check_mongo_calls_range class TestOrphanBase(CourseTestCase): """ Base class for Studio tests that require orphaned modules """ def create_course_with_orphans(self, default_store): """ Creates a course with 3 orphan modules, one of which has a child that's also in the course tree. """ course = CourseFactory.create(default_store=default_store) # create chapters and add them to course tree chapter1 = self.store.create_child(self.user.id, course.location, 'chapter', "Chapter1") self.store.publish(chapter1.location, self.user.id) chapter2 = self.store.create_child(self.user.id, course.location, 'chapter', "Chapter2") self.store.publish(chapter2.location, self.user.id) # orphan chapter orphan_chapter = self.store.create_item(self.user.id, course.id, 'chapter', "OrphanChapter") self.store.publish(orphan_chapter.location, self.user.id) # create vertical and add it as child to chapter1 vertical1 = self.store.create_child(self.user.id, chapter1.location, 'vertical', "Vertical1") self.store.publish(vertical1.location, self.user.id) # create orphan vertical orphan_vertical = self.store.create_item(self.user.id, course.id, 'vertical', "OrphanVert") self.store.publish(orphan_vertical.location, self.user.id) # create component and add it to vertical1 html1 = self.store.create_child(self.user.id, vertical1.location, 'html', "Html1") self.store.publish(html1.location, self.user.id) # create component and add it as a child to vertical1 and orphan_vertical multi_parent_html = self.store.create_child(self.user.id, vertical1.location, 'html', "multi_parent_html") self.store.publish(multi_parent_html.location, self.user.id) orphan_vertical.children.append(multi_parent_html.location) self.store.update_item(orphan_vertical, self.user.id) # create an orphaned html module orphan_html = self.store.create_item(self.user.id, course.id, 'html', "OrphanHtml") self.store.publish(orphan_html.location, self.user.id) self.store.create_child(self.user.id, course.location, 'static_tab', "staticuno") self.store.create_child(self.user.id, course.location, 'course_info', "updates") return course def assertOrphanCount(self, course_key, number): """ Asserts that we have the expected count of orphans for a given course_key """ self.assertEqual(len(self.store.get_orphans(course_key)), number) @ddt.ddt class TestOrphan(TestOrphanBase): """ Test finding orphans via view and django config """ @ddt.data(ModuleStoreEnum.Type.split, ModuleStoreEnum.Type.mongo) def test_get_orphans(self, default_store): """ Test that the orphan handler finds the orphans """ course = self.create_course_with_orphans(default_store) orphan_url = reverse_course_url('orphan_handler', course.id) orphans = json.loads( self.client.get( orphan_url, HTTP_ACCEPT='application/json' ).content ) self.assertEqual(len(orphans), 3, "Wrong # {}".format(orphans)) location = course.location.replace(category='chapter', name='OrphanChapter') self.assertIn(unicode(location), orphans) location = course.location.replace(category='vertical', name='OrphanVert') self.assertIn(unicode(location), orphans) location = course.location.replace(category='html', name='OrphanHtml') self.assertIn(unicode(location), orphans) @ddt.data( (ModuleStoreEnum.Type.split, 9, 5), (ModuleStoreEnum.Type.mongo, 34, 12), ) @ddt.unpack def test_delete_orphans(self, default_store, max_mongo_calls, min_mongo_calls): """ Test that the orphan handler deletes the orphans """ course = self.create_course_with_orphans(default_store) orphan_url = reverse_course_url('orphan_handler', course.id) with check_mongo_calls_range(max_mongo_calls, min_mongo_calls): self.client.delete(orphan_url) orphans = json.loads( self.client.get(orphan_url, HTTP_ACCEPT='application/json').content ) self.assertEqual(len(orphans), 0, "Orphans not deleted {}".format(orphans)) # make sure that any children with one orphan parent and one non-orphan # parent are not deleted self.assertTrue(self.store.has_item(course.id.make_usage_key('html', "multi_parent_html"))) @ddt.data(ModuleStoreEnum.Type.split, ModuleStoreEnum.Type.mongo) def test_not_permitted(self, default_store): """ Test that auth restricts get and delete appropriately """ course = self.create_course_with_orphans(default_store) orphan_url = reverse_course_url('orphan_handler', course.id) test_user_client, test_user = self.create_non_staff_authed_user_client() CourseEnrollment.enroll(test_user, course.id) response = test_user_client.get(orphan_url) self.assertEqual(response.status_code, 403) response = test_user_client.delete(orphan_url) self.assertEqual(response.status_code, 403) @ddt.data(ModuleStoreEnum.Type.split) def test_path_to_location_for_orphan_vertical(self, module_store): r""" Make sure that path_to_location works with a component having multiple vertical parents, from which one of them is orphan. course | chapter | vertical vertical \ / html """ # Get a course with orphan modules course = self.create_course_with_orphans(module_store) # Fetch the required course components. vertical1 = self.store.get_item(BlockUsageLocator(course.id, 'vertical', 'Vertical1')) chapter1 = self.store.get_item(BlockUsageLocator(course.id, 'chapter', 'Chapter1')) orphan_vertical = self.store.get_item(BlockUsageLocator(course.id, 'vertical', 'OrphanVert')) multi_parent_html = self.store.get_item(BlockUsageLocator(course.id, 'html', 'multi_parent_html')) # Verify `OrphanVert` is an orphan self.assertIn(orphan_vertical.location, self.store.get_orphans(course.id)) # Verify `multi_parent_html` is child of both `Vertical1` and `OrphanVert` self.assertIn(multi_parent_html.location, orphan_vertical.children) self.assertIn(multi_parent_html.location, vertical1.children) # HTML component has `vertical1` as its parent. html_parent = self.store.get_parent_location(multi_parent_html.location) self.assertNotEqual(unicode(html_parent), unicode(orphan_vertical.location)) self.assertEqual(unicode(html_parent), unicode(vertical1.location)) # Get path of the `multi_parent_html` & verify path_to_location returns a expected path path = path_to_location(self.store, multi_parent_html.location) expected_path = ( course.id, chapter1.location.block_id, vertical1.location.block_id, multi_parent_html.location.block_id, "", path[-1] ) self.assertIsNotNone(path) self.assertEqual(len(path), 6) self.assertEqual(path, expected_path) @ddt.data(ModuleStoreEnum.Type.split) def test_path_to_location_for_orphan_chapter(self, module_store): r""" Make sure that path_to_location works with a component having multiple chapter parents, from which one of them is orphan course | chapter chapter | | vertical vertical \ / html """ # Get a course with orphan modules course = self.create_course_with_orphans(module_store) orphan_chapter = self.store.get_item(BlockUsageLocator(course.id, 'chapter', 'OrphanChapter')) chapter1 = self.store.get_item(BlockUsageLocator(course.id, 'chapter', 'Chapter1')) vertical1 = self.store.get_item(BlockUsageLocator(course.id, 'vertical', 'Vertical1')) # Verify `OrhanChapter` is an orphan self.assertIn(orphan_chapter.location, self.store.get_orphans(course.id)) # Create a vertical (`Vertical0`) in orphan chapter (`OrphanChapter`). # OrphanChapter -> Vertical0 vertical0 = self.store.create_child(self.user.id, orphan_chapter.location, 'vertical', "Vertical0") self.store.publish(vertical0.location, self.user.id) # Create a component in `Vertical0` # OrphanChapter -> Vertical0 -> Html html = self.store.create_child(self.user.id, vertical0.location, 'html', "HTML0") self.store.publish(html.location, self.user.id) # Verify chapter1 is parent of vertical1. vertical1_parent = self.store.get_parent_location(vertical1.location) self.assertEqual(unicode(vertical1_parent), unicode(chapter1.location)) # Make `Vertical1` the parent of `HTML0`. So `HTML0` will have to parents (`Vertical0` & `Vertical1`) vertical1.children.append(html.location) self.store.update_item(vertical1, self.user.id) # Get parent location & verify its either of the two verticals. As both parents are non-orphan, # alphabetically least is returned html_parent = self.store.get_parent_location(html.location) self.assertEquals(unicode(html_parent), unicode(vertical1.location)) # verify path_to_location returns a expected path path = path_to_location(self.store, html.location) expected_path = ( course.id, chapter1.location.block_id, vertical1.location.block_id, html.location.block_id, "", path[-1] ) self.assertIsNotNone(path) self.assertEqual(len(path), 6) self.assertEqual(path, expected_path)