test_orphan.py 10.3 KB
Newer Older
1 2 3 4
"""
Test finding orphans via the view and django config
"""
import json
5

6
import ddt
7
from opaque_keys.edx.locator import BlockUsageLocator
8

9
from contentstore.tests.utils import CourseTestCase
10
from contentstore.utils import reverse_course_url
11
from student.models import CourseEnrollment
12
from xmodule.modulestore import ModuleStoreEnum
13
from xmodule.modulestore.search import path_to_location
14
from xmodule.modulestore.tests.factories import CourseFactory, check_mongo_calls_range
15

16

17
class TestOrphanBase(CourseTestCase):
18
    """
19
    Base class for Studio tests that require orphaned modules
20
    """
21 22 23 24 25 26
    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)
27

28
        # create chapters and add them to course tree
29
        chapter1 = self.store.create_child(self.user.id, course.location, 'chapter', "Chapter1")
30
        self.store.publish(chapter1.location, self.user.id)
31

32
        chapter2 = self.store.create_child(self.user.id, course.location, 'chapter', "Chapter2")
33
        self.store.publish(chapter2.location, self.user.id)
34

35
        # orphan chapter
36
        orphan_chapter = self.store.create_item(self.user.id, course.id, 'chapter', "OrphanChapter")
37
        self.store.publish(orphan_chapter.location, self.user.id)
38

39 40 41 42 43
        # 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
44
        orphan_vertical = self.store.create_item(self.user.id, course.id, 'vertical', "OrphanVert")
45 46 47 48 49 50 51 52 53 54 55 56 57 58
        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
59
        orphan_html = self.store.create_item(self.user.id, course.id, 'html', "OrphanHtml")
60 61
        self.store.publish(orphan_html.location, self.user.id)

62 63 64 65
        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
66

67 68 69 70 71 72 73
    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)

74

75
@ddt.ddt
76 77 78 79
class TestOrphan(TestOrphanBase):
    """
    Test finding orphans via view and django config
    """
80

81 82
    @ddt.data(ModuleStoreEnum.Type.split, ModuleStoreEnum.Type.mongo)
    def test_get_orphans(self, default_store):
83
        """
84
        Test that the orphan handler finds the orphans
85
        """
86 87 88
        course = self.create_course_with_orphans(default_store)
        orphan_url = reverse_course_url('orphan_handler', course.id)

89 90
        orphans = json.loads(
            self.client.get(
91
                orphan_url,
92 93 94 95
                HTTP_ACCEPT='application/json'
            ).content
        )
        self.assertEqual(len(orphans), 3, "Wrong # {}".format(orphans))
96
        location = course.location.replace(category='chapter', name='OrphanChapter')
97
        self.assertIn(unicode(location), orphans)
98
        location = course.location.replace(category='vertical', name='OrphanVert')
99
        self.assertIn(unicode(location), orphans)
100
        location = course.location.replace(category='html', name='OrphanHtml')
101 102 103
        self.assertIn(unicode(location), orphans)

    @ddt.data(
104 105
        (ModuleStoreEnum.Type.split, 9, 5),
        (ModuleStoreEnum.Type.mongo, 34, 12),
106 107 108
    )
    @ddt.unpack
    def test_delete_orphans(self, default_store, max_mongo_calls, min_mongo_calls):
109
        """
110
        Test that the orphan handler deletes the orphans
111
        """
112 113 114
        course = self.create_course_with_orphans(default_store)
        orphan_url = reverse_course_url('orphan_handler', course.id)

115 116 117
        with check_mongo_calls_range(max_mongo_calls, min_mongo_calls):
            self.client.delete(orphan_url)

118
        orphans = json.loads(
119
            self.client.get(orphan_url, HTTP_ACCEPT='application/json').content
120 121
        )
        self.assertEqual(len(orphans), 0, "Orphans not deleted {}".format(orphans))
122

123 124
        # make sure that any children with one orphan parent and one non-orphan
        # parent are not deleted
125
        self.assertTrue(self.store.has_item(course.id.make_usage_key('html', "multi_parent_html")))
126

127 128
    @ddt.data(ModuleStoreEnum.Type.split, ModuleStoreEnum.Type.mongo)
    def test_not_permitted(self, default_store):
129 130 131
        """
        Test that auth restricts get and delete appropriately
        """
132 133 134
        course = self.create_course_with_orphans(default_store)
        orphan_url = reverse_course_url('orphan_handler', course.id)

135
        test_user_client, test_user = self.create_non_staff_authed_user_client()
136 137
        CourseEnrollment.enroll(test_user, course.id)
        response = test_user_client.get(orphan_url)
138
        self.assertEqual(response.status_code, 403)
139
        response = test_user_client.delete(orphan_url)
140
        self.assertEqual(response.status_code, 403)
141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250

    @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)