test_module_render.py 17.3 KB
Newer Older
1 2 3
"""
Test for lms courseware app, module render unit
"""
4
from mock import MagicMock, patch, Mock
5 6
import json

7
from django.http import Http404, HttpResponse
Brian Wilson committed
8
from django.core.urlresolvers import reverse
9
from django.conf import settings
10 11
from django.test import TestCase
from django.test.client import RequestFactory
12
from django.test.utils import override_settings
13

14
from xmodule.modulestore.django import modulestore
15 16
from xmodule.modulestore.tests.factories import ItemFactory, CourseFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
17
import courseware.module_render as render
18 19
from courseware.tests.tests import LoginEnrollmentTestCase
from courseware.tests.modulestore_config import TEST_DATA_MIXED_MODULESTORE
Calen Pennington committed
20
from courseware.model_data import FieldDataCache
21

22
from courseware.courses import get_course_with_access, course_image_url, get_course_info_section
23

24
from .factories import UserFactory
Jay Zoldak committed
25

Jay Zoldak committed
26

27 28
@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE)
class ModuleRenderTestCase(ModuleStoreTestCase, LoginEnrollmentTestCase):
29
    def setUp(self):
Deena Wang committed
30 31 32
        self.location = ['i4x', 'edX', 'toy', 'chapter', 'Overview']
        self.course_id = 'edX/toy/2012_Fall'
        self.toy_course = modulestore().get_course(self.course_id)
33 34 35 36 37 38 39 40 41 42 43 44 45 46
        self.mock_user = UserFactory()
        self.mock_user.id = 1
        self.request_factory = RequestFactory()

        # Construct a mock module for the modulestore to return
        self.mock_module = MagicMock()
        self.mock_module.id = 1
        self.dispatch = 'score_update'

        # Construct a 'standard' xqueue_callback url
        self.callback_url = reverse('xqueue_callback', kwargs=dict(course_id=self.course_id,
                                                                   userid=str(self.mock_user.id),
                                                                   mod_id=self.mock_module.id,
                                                                   dispatch=self.dispatch))
47 48

    def test_get_module(self):
49 50 51 52
        self.assertEqual(
            None,
            render.get_module('dummyuser', None, 'invalid location', None, None)
        )
53

54 55 56 57 58 59 60 61 62 63 64
    def test_module_render_with_jump_to_id(self):
        """
        This test validates that the /jump_to_id/<id> shorthand for intracourse linking works assertIn
        expected. Note there's a HTML element in the 'toy' course with the url_name 'toyjumpto' which
        defines this linkage
        """
        mock_request = MagicMock()
        mock_request.user = self.mock_user

        course = get_course_with_access(self.mock_user, self.course_id, 'load')

Calen Pennington committed
65
        field_data_cache = FieldDataCache.cache_for_descriptor_descendents(
66 67
            self.course_id, self.mock_user, course, depth=2)

Chris Dodge committed
68 69 70 71
        module = render.get_module(
            self.mock_user,
            mock_request,
            ['i4x', 'edX', 'toy', 'html', 'toyjumpto'],
Calen Pennington committed
72
            field_data_cache,
73
            self.course_id
Chris Dodge committed
74
        )
75 76 77 78 79 80

        # get the rendered HTML output which should have the rewritten link
        html = module.get_html()

        # See if the url got rewritten to the target link
        # note if the URL mapping changes then this assertion will break
81
        self.assertIn('/courses/' + self.course_id + '/jump_to_id/vertical_test', html)
82

83 84 85 86 87
    def test_modx_dispatch(self):
        self.assertRaises(Http404, render.modx_dispatch, 'dummy', 'dummy',
                          'invalid Location', 'dummy')
        mock_request = MagicMock()
        mock_request.FILES.keys.return_value = ['file_id']
Jay Zoldak committed
88
        mock_request.FILES.getlist.return_value = ['file'] * (settings.MAX_FILEUPLOADS_PER_INPUT + 1)
89 90 91
        self.assertEquals(render.modx_dispatch(mock_request, 'dummy', self.location, 'dummy').content,
                          json.dumps({'success': 'Submission aborted! Maximum %d files may be submitted at once' %
                                      settings.MAX_FILEUPLOADS_PER_INPUT}))
92 93
        mock_request_2 = MagicMock()
        mock_request_2.FILES.keys.return_value = ['file_id']
94
        inputfile = MagicMock()
95 96 97 98 99 100
        inputfile.size = 1 + settings.STUDENT_FILEUPLOAD_MAX_SIZE
        inputfile.name = 'name'
        filelist = [inputfile]
        mock_request_2.FILES.getlist.return_value = filelist
        self.assertEquals(render.modx_dispatch(mock_request_2, 'dummy', self.location,
                                               'dummy').content,
Jay Zoldak committed
101
                          json.dumps({'success': 'Submission aborted! Your file "%s" is too large (max size: %d MB)' %
102
                                      (inputfile.name, settings.STUDENT_FILEUPLOAD_MAX_SIZE / (1000 ** 2))}))
103
        mock_request_3 = MagicMock()
104
        mock_request_3.POST.copy.return_value = {'position': 1}
Deena Wang committed
105
        mock_request_3.FILES = False
106
        mock_request_3.user = self.mock_user
107
        inputfile_2 = MagicMock()
108 109
        inputfile_2.size = 1
        inputfile_2.name = 'name'
Deena Wang committed
110
        self.assertIsInstance(render.modx_dispatch(mock_request_3, 'goto_position',
111
                                                   self.location, self.course_id), HttpResponse)
112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135
        self.assertRaises(
            Http404,
            render.modx_dispatch,
            mock_request_3,
            'goto_position',
            self.location,
            'bad_course_id'
        )
        self.assertRaises(
            Http404,
            render.modx_dispatch,
            mock_request_3,
            'goto_position',
            ['i4x', 'edX', 'toy', 'chapter', 'bad_location'],
            self.course_id
        )
        self.assertRaises(
            Http404,
            render.modx_dispatch,
            mock_request_3,
            'bad_dispatch',
            self.location,
            self.course_id
        )
Jay Zoldak committed
136

137 138 139 140 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
    def test_xqueue_callback_success(self):
        """
        Test for happy-path xqueue_callback
        """
        fake_key = 'fake key'
        xqueue_header = json.dumps({'lms_key': fake_key})
        data = {
            'xqueue_header': xqueue_header,
            'xqueue_body': 'hello world',
        }

        # Patch getmodule to return our mock module
        with patch('courseware.module_render.find_target_student_module') as get_fake_module:
            get_fake_module.return_value = self.mock_module
            # call xqueue_callback with our mocked information
            request = self.request_factory.post(self.callback_url, data)
            render.xqueue_callback(request, self.course_id, self.mock_user.id, self.mock_module.id, self.dispatch)

        # Verify that handle ajax is called with the correct data
        request.POST['queuekey'] = fake_key
        self.mock_module.handle_ajax.assert_called_once_with(self.dispatch, request.POST)

    def test_xqueue_callback_missing_header_info(self):
        data = {
            'xqueue_header': '{}',
            'xqueue_body': 'hello world',
        }

        with patch('courseware.module_render.find_target_student_module') as get_fake_module:
            get_fake_module.return_value = self.mock_module
            # Test with missing xqueue data
            with self.assertRaises(Http404):
                request = self.request_factory.post(self.callback_url, {})
                render.xqueue_callback(request, self.course_id, self.mock_user.id, self.mock_module.id, self.dispatch)

            # Test with missing xqueue_header
            with self.assertRaises(Http404):
                request = self.request_factory.post(self.callback_url, data)
                render.xqueue_callback(request, self.course_id, self.mock_user.id, self.mock_module.id, self.dispatch)

177 178 179 180 181 182 183 184
    def test_get_score_bucket(self):
        self.assertEquals(render.get_score_bucket(0, 10), 'incorrect')
        self.assertEquals(render.get_score_bucket(1, 10), 'partial')
        self.assertEquals(render.get_score_bucket(10, 10), 'correct')
        # get_score_bucket calls error cases 'incorrect'
        self.assertEquals(render.get_score_bucket(11, 10), 'incorrect')
        self.assertEquals(render.get_score_bucket(-1, 10), 'incorrect')

185 186 187 188 189 190 191 192 193 194 195 196
    def test_anonymous_modx_dispatch(self):
        dispatch_url = reverse(
            'modx_dispatch',
            args=[
                'edX/toy/2012_Fall',
                'i4x://edX/toy/videosequence/Toy_Videos',
                'goto_position'
            ]
        )
        response = self.client.post(dispatch_url, {'position': 2})
        self.assertEquals(403, response.status_code)

Jay Zoldak committed
197

198
@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE)
Jay Zoldak committed
199 200 201 202 203 204 205 206 207 208 209 210 211 212
class TestTOC(TestCase):
    """Check the Table of Contents for a course"""
    def setUp(self):

        # Toy courses should be loaded
        self.course_name = 'edX/toy/2012_Fall'
        self.toy_course = modulestore().get_course(self.course_name)
        self.portal_user = UserFactory()

    def test_toc_toy_from_chapter(self):
        chapter = 'Overview'
        chapter_url = '%s/%s/%s' % ('/courses', self.course_name, chapter)
        factory = RequestFactory()
        request = factory.get(chapter_url)
Calen Pennington committed
213
        field_data_cache = FieldDataCache.cache_for_descriptor_descendents(
214
            self.toy_course.id, self.portal_user, self.toy_course, depth=2)
Jay Zoldak committed
215

Jay Zoldak committed
216
        expected = ([{'active': True, 'sections':
217
                      [{'url_name': 'Toy_Videos', 'display_name': u'Toy Videos', 'graded': True,
218
                        'format': u'Lecture Sequence', 'due': None, 'active': False},
219
                       {'url_name': 'Welcome', 'display_name': u'Welcome', 'graded': True,
220
                        'format': '', 'due': None, 'active': False},
221
                       {'url_name': 'video_123456789012', 'display_name': 'Test Video', 'graded': True,
222
                        'format': '', 'due': None, 'active': False},
223
                       {'url_name': 'video_4f66f493ac8f', 'display_name': 'Video', 'graded': True,
224
                        'format': '', 'due': None, 'active': False}],
225 226 227
                      'url_name': 'Overview', 'display_name': u'Overview'},
                     {'active': False, 'sections':
                      [{'url_name': 'toyvideo', 'display_name': 'toyvideo', 'graded': True,
228
                        'format': '', 'due': None, 'active': False}],
229
                      'url_name': 'secret:magic', 'display_name': 'secret:magic'}])
Jay Zoldak committed
230

Calen Pennington committed
231
        actual = render.toc_for_course(self.portal_user, request, self.toy_course, chapter, None, field_data_cache)
232 233
        for toc_section in expected:
            self.assertIn(toc_section, actual)
Jay Zoldak committed
234 235 236 237 238 239 240

    def test_toc_toy_from_section(self):
        chapter = 'Overview'
        chapter_url = '%s/%s/%s' % ('/courses', self.course_name, chapter)
        section = 'Welcome'
        factory = RequestFactory()
        request = factory.get(chapter_url)
Calen Pennington committed
241
        field_data_cache = FieldDataCache.cache_for_descriptor_descendents(
242
            self.toy_course.id, self.portal_user, self.toy_course, depth=2)
Jay Zoldak committed
243

Jay Zoldak committed
244
        expected = ([{'active': True, 'sections':
245
                      [{'url_name': 'Toy_Videos', 'display_name': u'Toy Videos', 'graded': True,
246
                        'format': u'Lecture Sequence', 'due': None, 'active': False},
247
                       {'url_name': 'Welcome', 'display_name': u'Welcome', 'graded': True,
248
                        'format': '', 'due': None, 'active': True},
249
                       {'url_name': 'video_123456789012', 'display_name': 'Test Video', 'graded': True,
250
                        'format': '', 'due': None, 'active': False},
251
                       {'url_name': 'video_4f66f493ac8f', 'display_name': 'Video', 'graded': True,
252
                        'format': '', 'due': None, 'active': False}],
253 254 255
                      'url_name': 'Overview', 'display_name': u'Overview'},
                     {'active': False, 'sections':
                      [{'url_name': 'toyvideo', 'display_name': 'toyvideo', 'graded': True,
256
                        'format': '', 'due': None, 'active': False}],
257
                      'url_name': 'secret:magic', 'display_name': 'secret:magic'}])
Jay Zoldak committed
258

Calen Pennington committed
259
        actual = render.toc_for_course(self.portal_user, request, self.toy_course, chapter, section, field_data_cache)
260 261 262 263
        for toc_section in expected:
            self.assertIn(toc_section, actual)


264
@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE)
265 266 267 268 269 270 271 272 273 274 275 276 277
class TestHtmlModifiers(ModuleStoreTestCase):
    """
    Tests to verify that standard modifications to the output of XModule/XBlock
    student_view are taking place
    """
    def setUp(self):
        self.user = UserFactory.create()
        self.request = RequestFactory().get('/')
        self.request.user = self.user
        self.request.session = {}
        self.course = CourseFactory.create()
        self.content_string = '<p>This is the content<p>'
        self.rewrite_link = '<a href="/static/foo/content">Test rewrite</a>'
278
        self.rewrite_bad_link = '<img src="/static//file.jpg" />'
279 280 281
        self.course_link = '<a href="/course/bar/content">Test course rewrite</a>'
        self.descriptor = ItemFactory.create(
            category='html',
282
            data=self.content_string + self.rewrite_link + self.rewrite_bad_link + self.course_link
283 284
        )
        self.location = self.descriptor.location
Calen Pennington committed
285
        self.field_data_cache = FieldDataCache.cache_for_descriptor_descendents(
286 287 288 289 290 291 292 293 294 295
            self.course.id,
            self.user,
            self.descriptor
        )

    def test_xmodule_display_wrapper_enabled(self):
        module = render.get_module(
            self.user,
            self.request,
            self.location,
Calen Pennington committed
296
            self.field_data_cache,
297 298 299 300 301 302 303 304 305 306 307 308
            self.course.id,
            wrap_xmodule_display=True,
        )
        result_fragment = module.runtime.render(module, None, 'student_view')

        self.assertIn('section class="xmodule_display xmodule_HtmlModule"', result_fragment.content)

    def test_xmodule_display_wrapper_disabled(self):
        module = render.get_module(
            self.user,
            self.request,
            self.location,
Calen Pennington committed
309
            self.field_data_cache,
310 311 312 313 314 315 316 317 318 319 320 321
            self.course.id,
            wrap_xmodule_display=False,
        )
        result_fragment = module.runtime.render(module, None, 'student_view')

        self.assertNotIn('section class="xmodule_display xmodule_HtmlModule"', result_fragment.content)

    def test_static_link_rewrite(self):
        module = render.get_module(
            self.user,
            self.request,
            self.location,
Calen Pennington committed
322
            self.field_data_cache,
323 324 325 326 327 328 329 330 331 332 333 334
            self.course.id,
        )
        result_fragment = module.runtime.render(module, None, 'student_view')

        self.assertIn(
            '/c4x/{org}/{course}/asset/foo_content'.format(
                org=self.course.location.org,
                course=self.course.location.course,
            ),
            result_fragment.content
        )

335 336 337 338 339
    def test_static_badlink_rewrite(self):
        module = render.get_module(
            self.user,
            self.request,
            self.location,
Calen Pennington committed
340
            self.field_data_cache,
341 342 343 344 345 346 347 348
            self.course.id,
        )
        result_fragment = module.runtime.render(module, None, 'student_view')

        self.assertIn(
            '/c4x/{org}/{course}/asset/_file.jpg'.format(
                org=self.course.location.org,
                course=self.course.location.course,
349 350 351 352
            ),
            result_fragment.content
        )

353 354
    def test_static_asset_path_use(self):
        '''
355
        when a course is loaded with do_import_static=False (see xml_importer.py), then
356 357 358 359 360 361 362
        static_asset_path is set as an lms kv in course.  That should make static paths
        not be mangled (ie not changed to c4x://).
        '''
        module = render.get_module(
            self.user,
            self.request,
            self.location,
Calen Pennington committed
363
            self.field_data_cache,
364 365 366 367 368 369 370 371 372 373
            self.course.id,
            static_asset_path="toy_course_dir",
        )
        result_fragment = module.runtime.render(module, None, 'student_view')
        self.assertIn('href="/static/toy_course_dir', result_fragment.content)

    def test_course_image(self):
        url = course_image_url(self.course)
        self.assertTrue(url.startswith('/c4x/'))

Calen Pennington committed
374
        self.course.static_asset_path = "toy_course_dir"
375 376
        url = course_image_url(self.course)
        self.assertTrue(url.startswith('/static/toy_course_dir/'))
Calen Pennington committed
377
        self.course.static_asset_path = ""
378 379

    def test_get_course_info_section(self):
Calen Pennington committed
380
        self.course.static_asset_path = "toy_course_dir"
381
        get_course_info_section(self.request, self.course, "handouts")
Chris Dodge committed
382
        # NOTE: check handouts output...right now test course seems to have no such content
383 384
        # at least this makes sure get_course_info_section returns without exception

385 386 387 388 389
    def test_course_link_rewrite(self):
        module = render.get_module(
            self.user,
            self.request,
            self.location,
Calen Pennington committed
390
            self.field_data_cache,
391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407
            self.course.id,
        )
        result_fragment = module.runtime.render(module, None, 'student_view')

        self.assertIn(
            '/courses/{course_id}/bar/content'.format(
                course_id=self.course.id
            ),
            result_fragment.content
        )

    @patch('courseware.module_render.has_access', Mock(return_value=True))
    def test_histogram(self):
        module = render.get_module(
            self.user,
            self.request,
            self.location,
Calen Pennington committed
408
            self.field_data_cache,
409 410 411 412 413 414 415 416
            self.course.id,
        )
        result_fragment = module.runtime.render(module, None, 'student_view')

        self.assertIn(
            'Staff Debug',
            result_fragment.content
        )