utils.py 17.6 KB
Newer Older
1
# pylint: disable=no-member
2 3 4
'''
Utilities for contentstore tests
'''
5
import json
6 7
import textwrap
from mock import Mock
8

9
from django.conf import settings
10
from django.contrib.auth.models import User
11 12
from django.test.client import Client
from opaque_keys.edx.locations import SlashSeparatedCourseKey, AssetLocation
David Baumgold committed
13

14 15
from contentstore.utils import reverse_url  # pylint: disable=import-error
from student.models import Registration  # pylint: disable=import-error
16
from xmodule.modulestore.split_mongo.split import SplitMongoModuleStore
Don Mitchell committed
17
from xmodule.contentstore.django import contentstore
18
from xmodule.modulestore import ModuleStoreEnum
Don Mitchell committed
19
from xmodule.modulestore.inheritance import own_metadata
David Baumgold committed
20
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
21
from xmodule.modulestore.tests.factories import CourseFactory
22
from xmodule.modulestore.xml_importer import import_course_from_xml
23
from xmodule.modulestore.tests.utils import ProceduralCourseTestMixin
muhammad-ammar committed
24 25

TEST_DATA_DIR = settings.COMMON_TEST_DATA_ROOT
26

Calen Pennington committed
27

28 29 30 31
def parse_json(response):
    """Parse response, which is assumed to be json"""
    return json.loads(response.content)

Calen Pennington committed
32

33 34 35 36
def user(email):
    """look up a user by email"""
    return User.objects.get(email=email)

Calen Pennington committed
37

38 39 40
def registration(email):
    """look up registration object by email"""
    return Registration.objects.get(user__email=email)
David Baumgold committed
41 42


43
class AjaxEnabledTestClient(Client):
44 45 46
    """
    Convenience class to make testing easier.
    """
47
    def ajax_post(self, path, data=None, content_type="application/json", **kwargs):
48 49 50 51
        """
        Convenience method for client post which serializes the data into json and sets the accept type
        to json
        """
52 53 54
        if not isinstance(data, basestring):
            data = json.dumps(data or {})
        kwargs.setdefault("HTTP_X_REQUESTED_WITH", "XMLHttpRequest")
Don Mitchell committed
55
        kwargs.setdefault("HTTP_ACCEPT", "application/json")
56 57
        return self.post(path=path, data=data, content_type=content_type, **kwargs)

58 59 60 61 62
    def get_html(self, path, data=None, follow=False, **extra):
        """
        Convenience method for client.get which sets the accept type to html
        """
        return self.get(path, data or {}, follow, HTTP_ACCEPT="text/html", **extra)
63

64 65 66 67 68 69
    def get_json(self, path, data=None, follow=False, **extra):
        """
        Convenience method for client.get which sets the accept type to json
        """
        return self.get(path, data or {}, follow, HTTP_ACCEPT="application/json", **extra)

70

71
class CourseTestCase(ProceduralCourseTestMixin, ModuleStoreTestCase):
72 73 74 75
    """
    Base class for Studio tests that require a logged in user and a course.
    Also provides helper methods for manipulating and verifying the course.
    """
David Baumgold committed
76 77
    def setUp(self):
        """
78 79
        These tests need a user in the DB so that the django Test Client can log them in.
        The test user is created in the ModuleStoreTestCase setUp method.
David Baumgold committed
80 81 82 83
        They inherit from the ModuleStoreTestCase class so that the mongodb collection
        will be cleared out before each test case execution and deleted
        afterwards.
        """
84

Don Mitchell committed
85
        self.user_password = super(CourseTestCase, self).setUp()
David Baumgold committed
86

87
        self.client = AjaxEnabledTestClient()
Don Mitchell committed
88
        self.client.login(username=self.user.username, password=self.user_password)
David Baumgold committed
89

Don Mitchell committed
90
        self.course = CourseFactory.create()
91

92
    def create_non_staff_authed_user_client(self, authenticate=True):
93
        """
94
        Create a non-staff user, log them in (if authenticate=True), and return the client, user to use for testing.
95
        """
96
        nonstaff, password = self.create_non_staff_user()
97

98
        client = AjaxEnabledTestClient()
99
        if authenticate:
100
            client.login(username=nonstaff.username, password=password)
101
        nonstaff.is_authenticated = lambda: authenticate
102
        return client, nonstaff
103

104 105 106 107
    def reload_course(self):
        """
        Reloads the course object from the database
        """
108
        self.course = self.store.get_course(self.course.id)
109 110 111 112 113 114 115

    def save_course(self):
        """
        Updates the course object in the database
        """
        self.course.save()
        self.store.update_item(self.course, self.user.id)
Don Mitchell committed
116 117

    TEST_VERTICAL = 'vertical_test'
118 119
    ORPHAN_DRAFT_VERTICAL = 'orphan_draft_vertical'
    ORPHAN_DRAFT_HTML = 'orphan_draft_html'
Don Mitchell committed
120 121 122
    PRIVATE_VERTICAL = 'a_private_vertical'
    PUBLISHED_VERTICAL = 'a_published_vertical'
    SEQUENTIAL = 'vertical_sequential'
123 124
    DRAFT_HTML = 'draft_html'
    DRAFT_VIDEO = 'draft_video'
Don Mitchell committed
125 126 127 128 129 130 131
    LOCKED_ASSET_KEY = AssetLocation.from_deprecated_string('/c4x/edX/toy/asset/sample_static.txt')

    def import_and_populate_course(self):
        """
        Imports the test toy course and populates it with additional test data
        """
        content_store = contentstore()
132
        import_course_from_xml(self.store, self.user.id, TEST_DATA_DIR, ['toy'], static_content_store=content_store)
Don Mitchell committed
133 134 135 136 137 138 139 140 141 142 143
        course_id = SlashSeparatedCourseKey('edX', 'toy', '2012_Fall')

        # create an Orphan
        # We had a bug where orphaned draft nodes caused export to fail. This is here to cover that case.
        vertical = self.store.get_item(course_id.make_usage_key('vertical', self.TEST_VERTICAL), depth=1)
        vertical.location = vertical.location.replace(name='no_references')
        self.store.update_item(vertical, self.user.id, allow_not_found=True)
        orphan_vertical = self.store.get_item(vertical.location)
        self.assertEqual(orphan_vertical.location.name, 'no_references')
        self.assertEqual(len(orphan_vertical.children), len(vertical.children))

144 145 146 147 148 149 150 151 152 153 154 155
        # create an orphan vertical and html; we already don't try to import
        # the orphaned vertical, but we should make sure we don't import
        # the orphaned vertical's child html, too
        orphan_draft_vertical = self.store.create_item(
            self.user.id, course_id, 'vertical', self.ORPHAN_DRAFT_VERTICAL
        )
        orphan_draft_html = self.store.create_item(
            self.user.id, course_id, 'html', self.ORPHAN_DRAFT_HTML
        )
        orphan_draft_vertical.children.append(orphan_draft_html.location)
        self.store.update_item(orphan_draft_vertical, self.user.id)

Don Mitchell committed
156 157 158
        # create a Draft vertical
        vertical = self.store.get_item(course_id.make_usage_key('vertical', self.TEST_VERTICAL), depth=1)
        draft_vertical = self.store.convert_to_draft(vertical.location, self.user.id)
159
        self.assertTrue(self.store.has_published_version(draft_vertical))
Don Mitchell committed
160 161

        # create a Private (draft only) vertical
162
        private_vertical = self.store.create_item(self.user.id, course_id, 'vertical', self.PRIVATE_VERTICAL)
163
        self.assertFalse(self.store.has_published_version(private_vertical))
Don Mitchell committed
164 165

        # create a Published (no draft) vertical
166
        public_vertical = self.store.create_item(self.user.id, course_id, 'vertical', self.PUBLISHED_VERTICAL)
Don Mitchell committed
167
        public_vertical = self.store.publish(public_vertical.location, self.user.id)
168
        self.assertTrue(self.store.has_published_version(public_vertical))
Don Mitchell committed
169 170 171 172 173 174 175

        # add the new private and new public as children of the sequential
        sequential = self.store.get_item(course_id.make_usage_key('sequential', self.SEQUENTIAL))
        sequential.children.append(private_vertical.location)
        sequential.children.append(public_vertical.location)
        self.store.update_item(sequential, self.user.id)

176 177 178 179 180 181 182 183 184 185 186 187 188 189
        # create an html and video component to make drafts:
        draft_html = self.store.create_item(self.user.id, course_id, 'html', self.DRAFT_HTML)
        draft_video = self.store.create_item(self.user.id, course_id, 'video', self.DRAFT_VIDEO)

        # add them as children to the public_vertical
        public_vertical.children.append(draft_html.location)
        public_vertical.children.append(draft_video.location)
        self.store.update_item(public_vertical, self.user.id)
        # publish changes to vertical
        self.store.publish(public_vertical.location, self.user.id)
        # convert html/video to draft
        self.store.convert_to_draft(draft_html.location, self.user.id)
        self.store.convert_to_draft(draft_video.location, self.user.id)

Don Mitchell committed
190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211
        # lock an asset
        content_store.set_attr(self.LOCKED_ASSET_KEY, 'locked', True)

        # create a non-portable link - should be rewritten in new courses
        html_module = self.store.get_item(course_id.make_usage_key('html', 'nonportable'))
        new_data = html_module.data = html_module.data.replace(
            '/static/',
            '/c4x/{0}/{1}/asset/'.format(course_id.org, course_id.course)
        )
        self.store.update_item(html_module, self.user.id)

        html_module = self.store.get_item(html_module.location)
        self.assertEqual(new_data, html_module.data)

        return course_id

    def check_populated_course(self, course_id):
        """
        Verifies the content of the given course, per data that was populated in import_and_populate_course
        """
        items = self.store.get_items(
            course_id,
212
            qualifiers={'category': 'vertical'},
Don Mitchell committed
213 214 215 216 217 218
            revision=ModuleStoreEnum.RevisionOption.published_only
        )
        self.check_verticals(items)

        def verify_item_publish_state(item, publish_state):
            """Verifies the publish state of the item is as expected."""
219
            self.assertEqual(self.store.has_published_version(item), publish_state)
Don Mitchell committed
220 221

        def get_and_verify_publish_state(item_type, item_name, publish_state):
222 223 224 225
            """
            Gets the given item from the store and verifies the publish state
            of the item is as expected.
            """
Don Mitchell committed
226 227 228 229
            item = self.store.get_item(course_id.make_usage_key(item_type, item_name))
            verify_item_publish_state(item, publish_state)
            return item

230
        # verify draft vertical has a published version with published children
231
        vertical = get_and_verify_publish_state('vertical', self.TEST_VERTICAL, True)
Don Mitchell committed
232
        for child in vertical.get_children():
233
            verify_item_publish_state(child, True)
Don Mitchell committed
234

235 236 237 238
        # verify that it has a draft too
        self.assertTrue(getattr(vertical, "is_draft", False))

        # make sure that we don't have a sequential that is in draft mode
239
        sequential = get_and_verify_publish_state('sequential', self.SEQUENTIAL, True)
240
        self.assertFalse(getattr(sequential, "is_draft", False))
Don Mitchell committed
241 242

        # verify that we have the private vertical
243
        private_vertical = get_and_verify_publish_state('vertical', self.PRIVATE_VERTICAL, False)
Don Mitchell committed
244 245

        # verify that we have the public vertical
246
        public_vertical = get_and_verify_publish_state('vertical', self.PUBLISHED_VERTICAL, True)
Don Mitchell committed
247

248 249 250 251 252 253 254 255
        # verify that we have the draft html
        draft_html = self.store.get_item(course_id.make_usage_key('html', self.DRAFT_HTML))
        self.assertTrue(getattr(draft_html, 'is_draft', False))

        # verify that we have the draft video
        draft_video = self.store.get_item(course_id.make_usage_key('video', self.DRAFT_VIDEO))
        self.assertTrue(getattr(draft_video, 'is_draft', False))

Don Mitchell committed
256 257 258 259
        # verify verticals are children of sequential
        for vert in [vertical, private_vertical, public_vertical]:
            self.assertIn(vert.location, sequential.children)

260 261 262 263 264 265
        # verify draft html is the child of the public vertical
        self.assertIn(draft_html.location, public_vertical.children)

        # verify draft video is the child of the public vertical
        self.assertIn(draft_video.location, public_vertical.children)

Don Mitchell committed
266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285
        # verify textbook exists
        course = self.store.get_course(course_id)
        self.assertGreater(len(course.textbooks), 0)

        # verify asset attributes of locked asset key
        self.assertAssetsEqual(self.LOCKED_ASSET_KEY, self.LOCKED_ASSET_KEY.course_key, course_id)

        # verify non-portable links are rewritten
        html_module = self.store.get_item(course_id.make_usage_key('html', 'nonportable'))
        self.assertIn('/static/foo.jpg', html_module.data)

        return course

    def assertCoursesEqual(self, course1_id, course2_id):
        """
        Verifies the content of the two given courses are equal
        """
        course1_items = self.store.get_items(course1_id)
        course2_items = self.store.get_items(course2_id)
        self.assertGreater(len(course1_items), 0)  # ensure it found content instead of [] == []
286 287 288 289 290 291 292 293
        if len(course1_items) != len(course2_items):
            course1_block_ids = set([item.location.block_id for item in course1_items])
            course2_block_ids = set([item.location.block_id for item in course2_items])
            raise AssertionError(
                u"Course1 extra blocks: {}; course2 extra blocks: {}".format(
                    course1_block_ids - course2_block_ids, course2_block_ids - course1_block_ids
                )
            )
Don Mitchell committed
294 295

        for course1_item in course1_items:
Don Mitchell committed
296 297 298
            course1_item_loc = course1_item.location
            course2_item_loc = course2_id.make_usage_key(course1_item_loc.block_type, course1_item_loc.block_id)
            if course1_item_loc.block_type == 'course':
Don Mitchell committed
299
                # mongo uses the run as the name, split uses 'course'
300
                store = self.store._get_modulestore_for_courselike(course2_id)  # pylint: disable=protected-access
Don Mitchell committed
301 302 303
                new_name = 'course' if isinstance(store, SplitMongoModuleStore) else course2_item_loc.run
                course2_item_loc = course2_item_loc.replace(name=new_name)
            course2_item = self.store.get_item(course2_item_loc)
Don Mitchell committed
304

305 306 307 308 309
            # compare published state
            self.assertEqual(
                self.store.has_published_version(course1_item),
                self.store.has_published_version(course2_item)
            )
Don Mitchell committed
310 311 312 313 314 315 316 317 318 319 320 321 322 323 324

            # compare data
            self.assertEqual(hasattr(course1_item, 'data'), hasattr(course2_item, 'data'))
            if hasattr(course1_item, 'data'):
                self.assertEqual(course1_item.data, course2_item.data)

            # compare meta-data
            self.assertEqual(own_metadata(course1_item), own_metadata(course2_item))

            # compare children
            self.assertEqual(course1_item.has_children, course2_item.has_children)
            if course1_item.has_children:
                expected_children = []
                for course1_item_child in course1_item.children:
                    expected_children.append(
Don Mitchell committed
325
                        course2_id.make_usage_key(course1_item_child.block_type, course1_item_child.block_id)
Don Mitchell committed
326
                    )
Don Mitchell committed
327
                self.assertEqual(expected_children, course2_item.children)
Don Mitchell committed
328 329

        # compare assets
Don Mitchell committed
330
        content_store = self.store.contentstore
Don Mitchell committed
331 332 333 334
        course1_assets, count_course1_assets = content_store.get_all_content_for_course(course1_id)
        _, count_course2_assets = content_store.get_all_content_for_course(course2_id)
        self.assertEqual(count_course1_assets, count_course2_assets)
        for asset in course1_assets:
Don Mitchell committed
335 336
            asset_son = asset.get('content_son', asset['_id'])
            self.assertAssetsEqual(asset_son, course1_id, course2_id)
Don Mitchell committed
337 338 339 340 341 342

    def check_verticals(self, items):
        """ Test getting the editing HTML for each vertical. """
        # assert is here to make sure that the course being tested actually has verticals (units) to check.
        self.assertGreater(len(items), 0, "Course has no verticals (units) to check")
        for descriptor in items:
343
            resp = self.client.get_html(get_url('container_handler', descriptor.location))
Don Mitchell committed
344 345
            self.assertEqual(resp.status_code, 200)

Don Mitchell committed
346
    def assertAssetsEqual(self, asset_son, course1_id, course2_id):
Don Mitchell committed
347 348
        """Verifies the asset of the given key has the same attributes in both given courses."""
        content_store = contentstore()
Don Mitchell committed
349 350 351 352
        category = asset_son.block_type if hasattr(asset_son, 'block_type') else asset_son['category']
        filename = asset_son.block_id if hasattr(asset_son, 'block_id') else asset_son['name']
        course1_asset_attrs = content_store.get_attrs(course1_id.make_asset_key(category, filename))
        course2_asset_attrs = content_store.get_attrs(course2_id.make_asset_key(category, filename))
Don Mitchell committed
353 354
        self.assertEqual(len(course1_asset_attrs), len(course2_asset_attrs))
        for key, value in course1_asset_attrs.iteritems():
Don Mitchell committed
355
            if key in ['_id', 'filename', 'uploadDate', 'content_son', 'thumbnail_location']:
Don Mitchell committed
356 357 358 359 360
                pass
            else:
                self.assertEqual(value, course2_asset_attrs[key])


361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389
def mock_requests_get(*args, **kwargs):
    """
    Returns mock responses for the youtube API.
    """
    # pylint: disable=unused-argument
    response_transcript_list = """
    <transcript_list>
        <track id="1" name="Custom" lang_code="en" />
        <track id="0" name="Custom1" lang_code="en-GB"/>
    </transcript_list>
    """
    response_transcript = textwrap.dedent("""
    <transcript>
        <text start="100" dur="100">subs #1</text>
        <text start="200" dur="40">subs #2</text>
        <text start="240" dur="140">subs #3</text>
    </transcript>
    """)

    if kwargs == {'params': {'lang': 'en', 'v': 'good_id_2'}}:
        return Mock(status_code=200, text='')
    elif kwargs == {'params': {'type': 'list', 'v': 'good_id_2'}}:
        return Mock(status_code=200, text=response_transcript_list, content=response_transcript_list)
    elif kwargs == {'params': {'lang': 'en', 'v': 'good_id_2', 'name': 'Custom'}}:
        return Mock(status_code=200, text=response_transcript, content=response_transcript)

    return Mock(status_code=404, text='')


Don Mitchell committed
390 391 392 393 394
def get_url(handler_name, key_value, key_name='usage_key_string', kwargs=None):
    """
    Helper function for getting HTML for a page in Studio and checking that it does not error.
    """
    return reverse_url(handler_name, key_name, key_value, kwargs)