test_courses.py 17 KB
Newer Older
1
# -*- coding: utf-8 -*-
Don Mitchell committed
2 3 4
"""
Tests for course access
"""
5 6
import itertools

7
import datetime
8
import ddt
9
import mock
10
import pytz
11
from django.conf import settings
12 13
from django.core.urlresolvers import reverse
from django.test.client import RequestFactory
14
from django.test.utils import override_settings
15
from nose.plugins.attrib import attr
16

17
from courseware.courses import (
18
    course_open_for_self_enrollment,
19 20 21 22 23 24 25
    get_cms_block_link,
    get_cms_course_link,
    get_course_about_section,
    get_course_by_id,
    get_course_info_section,
    get_course_overview_with_access,
    get_course_with_access,
26 27
    get_courses,
    get_current_child
28
)
29
from courseware.model_data import FieldDataCache
30
from courseware.module_render import get_module_for_descriptor
31
from lms.djangoapps.courseware.courseware_access_exception import CoursewareAccessException
32
from openedx.core.djangolib.testing.utils import get_mock_request
33
from openedx.core.lib.courses import course_image_url
34
from student.tests.factories import UserFactory
35
from xmodule.modulestore import ModuleStoreEnum
36
from xmodule.modulestore.django import _get_modulestore_branch_setting, modulestore
37
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
38 39
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory, check_mongo_calls
from xmodule.modulestore.xml_importer import import_course_from_xml
40 41 42
from xmodule.tests.xml import factories as xml
from xmodule.tests.xml import XModuleXmlImportTest

43
CMS_BASE_TEST = 'testcms'
44
TEST_DATA_DIR = settings.COMMON_TEST_DATA_ROOT
45

46

47
@attr(shard=1)
48
@ddt.ddt
49
class CoursesTest(ModuleStoreTestCase):
50
    """Test methods related to fetching courses."""
51
    ENABLED_SIGNALS = ['course_published']
Jeremy Bowman committed
52 53 54 55 56 57
    GET_COURSE_WITH_ACCESS = 'get_course_with_access'
    GET_COURSE_OVERVIEW_WITH_ACCESS = 'get_course_overview_with_access'
    COURSE_ACCESS_FUNCS = {
        GET_COURSE_WITH_ACCESS: get_course_with_access,
        GET_COURSE_OVERVIEW_WITH_ACCESS: get_course_overview_with_access,
    }
58

59
    @override_settings(CMS_BASE=CMS_BASE_TEST)
60
    def test_get_cms_course_block_link(self):
61
        """
62
        Tests that get_cms_course_link_by_id and get_cms_block_link_by_id return the right thing
63
        """
64 65 66 67
        self.course = CourseFactory.create(
            org='org', number='num', display_name='name'
        )

68
        cms_url = u"//{}/course/{}".format(CMS_BASE_TEST, unicode(self.course.id))
69
        self.assertEqual(cms_url, get_cms_course_link(self.course))
70
        cms_url = u"//{}/course/{}".format(CMS_BASE_TEST, unicode(self.course.location))
71
        self.assertEqual(cms_url, get_cms_block_link(self.course, 'course'))
72

Jeremy Bowman committed
73 74 75
    @ddt.data(GET_COURSE_WITH_ACCESS, GET_COURSE_OVERVIEW_WITH_ACCESS)
    def test_get_course_func_with_access_error(self, course_access_func_name):
        course_access_func = self.COURSE_ACCESS_FUNCS[course_access_func_name]
76 77 78 79
        user = UserFactory.create()
        course = CourseFactory.create(visible_to_staff_only=True)

        with self.assertRaises(CoursewareAccessException) as error:
80
            course_access_func(user, 'load', course.id)
81 82 83 84
        self.assertEqual(error.exception.message, "Course not found.")
        self.assertEqual(error.exception.access_response.error_code, "not_visible_to_user")
        self.assertFalse(error.exception.access_response.has_access)

85
    @ddt.data(
Jeremy Bowman committed
86 87
        (GET_COURSE_WITH_ACCESS, 1),
        (GET_COURSE_OVERVIEW_WITH_ACCESS, 0),
88 89
    )
    @ddt.unpack
Jeremy Bowman committed
90 91
    def test_get_course_func_with_access(self, course_access_func_name, num_mongo_calls):
        course_access_func = self.COURSE_ACCESS_FUNCS[course_access_func_name]
92 93 94 95 96
        user = UserFactory.create()
        course = CourseFactory.create(emit_signals=True)
        with check_mongo_calls(num_mongo_calls):
            course_access_func(user, 'load', course.id)

97
    def test_get_courses_by_org(self):
98 99 100
        """
        Verify that org filtering performs as expected, and that an empty result
        is returned if the org passed by the caller does not match the designated
101
        org.
102 103 104 105 106
        """
        primary = 'primary'
        alternate = 'alternate'

        def _fake_get_value(value, default=None):
107
            """Used to stub out site_configuration.helpers.get_value()."""
108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131
            if value == 'course_org_filter':
                return alternate

            return default

        user = UserFactory.create()

        # Pass `emit_signals=True` so that these courses are cached with CourseOverviews.
        primary_course = CourseFactory.create(org=primary, emit_signals=True)
        alternate_course = CourseFactory.create(org=alternate, emit_signals=True)

        self.assertNotEqual(primary_course.org, alternate_course.org)

        unfiltered_courses = get_courses(user)
        for org in [primary_course.org, alternate_course.org]:
            self.assertTrue(
                any(course.org == org for course in unfiltered_courses)
            )

        filtered_courses = get_courses(user, org=primary)
        self.assertTrue(
            all(course.org == primary_course.org for course in filtered_courses)
        )

132 133 134 135
        with mock.patch(
            'openedx.core.djangoapps.site_configuration.helpers.get_value',
            autospec=True,
        ) as mock_get_value:
136 137
            mock_get_value.side_effect = _fake_get_value

138
            # Request filtering for an org distinct from the designated org.
139 140 141
            no_courses = get_courses(user, org=primary)
            self.assertEqual(no_courses, [])

142 143
            # Request filtering for an org matching the designated org.
            site_courses = get_courses(user, org=alternate)
144
            self.assertTrue(
145
                all(course.org == alternate_course.org for course in site_courses)
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
    def test_get_courses_with_filter(self):
        """
        Verify that filtering performs as expected.
        """
        user = UserFactory.create()
        non_mobile_course = CourseFactory.create(emit_signals=True)
        mobile_course = CourseFactory.create(mobile_available=True, emit_signals=True)

        test_cases = (
            (None, {non_mobile_course.id, mobile_course.id}),
            (dict(mobile_available=True), {mobile_course.id}),
            (dict(mobile_available=False), {non_mobile_course.id}),
        )
        for filter_, expected_courses in test_cases:
            self.assertEqual(
                {
                    course.id
                    for course in
                    get_courses(user, filter_=filter_)
                },
                expected_courses,
                "testing get_courses with filter_={}".format(filter_),
            )

172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188
    def test_get_current_child(self):
        mock_xmodule = mock.MagicMock()
        self.assertIsNone(get_current_child(mock_xmodule))

        mock_xmodule.position = -1
        mock_xmodule.get_display_items.return_value = ['one', 'two', 'three']
        self.assertEqual(get_current_child(mock_xmodule), 'one')

        mock_xmodule.position = 2
        self.assertEqual(get_current_child(mock_xmodule), 'two')
        self.assertEqual(get_current_child(mock_xmodule, requested_child='first'), 'one')
        self.assertEqual(get_current_child(mock_xmodule, requested_child='last'), 'three')

        mock_xmodule.position = 3
        mock_xmodule.get_display_items.return_value = []
        self.assertIsNone(get_current_child(mock_xmodule))

189

190
@attr(shard=1)
191 192
class ModuleStoreBranchSettingTest(ModuleStoreTestCase):
    """Test methods related to the modulestore branch setting."""
193 194 195 196 197
    @mock.patch(
        'xmodule.modulestore.django.get_current_request_hostname',
        mock.Mock(return_value='preview.localhost')
    )
    @override_settings(
198
        HOSTNAME_MODULESTORE_DEFAULT_MAPPINGS={r'preview\.': ModuleStoreEnum.Branch.draft_preferred},
199
        MODULESTORE_BRANCH='fake_default_branch',
200
    )
Don Mitchell committed
201
    def test_default_modulestore_preview_mapping(self):
202
        self.assertEqual(_get_modulestore_branch_setting(), ModuleStoreEnum.Branch.draft_preferred)
203

204 205 206 207 208
    @mock.patch(
        'xmodule.modulestore.django.get_current_request_hostname',
        mock.Mock(return_value='localhost')
    )
    @override_settings(
209
        HOSTNAME_MODULESTORE_DEFAULT_MAPPINGS={r'preview\.': ModuleStoreEnum.Branch.draft_preferred},
210
        MODULESTORE_BRANCH='fake_default_branch',
211
    )
212
    def test_default_modulestore_branch_mapping(self):
213
        self.assertEqual(_get_modulestore_branch_setting(), 'fake_default_branch')
214 215


216
@attr(shard=1)
217
@override_settings(CMS_BASE=CMS_BASE_TEST)
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
class MongoCourseImageTestCase(ModuleStoreTestCase):
    """Tests for course image URLs when using a mongo modulestore."""

    def test_get_image_url(self):
        """Test image URL formatting."""
        course = CourseFactory.create(org='edX', course='999')
        self.assertEquals(course_image_url(course), '/c4x/edX/999/asset/{0}'.format(course.course_image))

    def test_non_ascii_image_name(self):
        # Verify that non-ascii image names are cleaned
        course = CourseFactory.create(course_image=u'before_\N{SNOWMAN}_after.jpg')
        self.assertEquals(
            course_image_url(course),
            '/c4x/{org}/{course}/asset/before___after.jpg'.format(
                org=course.location.org,
                course=course.location.course
            )
        )

    def test_spaces_in_image_name(self):
        # Verify that image names with spaces in them are cleaned
        course = CourseFactory.create(course_image=u'before after.jpg')
        self.assertEquals(
            course_image_url(course),
            '/c4x/{org}/{course}/asset/before_after.jpg'.format(
                org=course.location.org,
                course=course.location.course
            )
        )

248
    def test_static_asset_path_course_image_default(self):
249 250 251 252 253 254 255 256 257 258
        """
        Test that without course_image being set, but static_asset_path
        being set that we get the right course_image url.
        """
        course = CourseFactory.create(static_asset_path="foo")
        self.assertEquals(
            course_image_url(course),
            '/static/foo/images/course_image.jpg'
        )

259
    def test_static_asset_path_course_image_set(self):
260
        """
261 262
        Test that with course_image and static_asset_path both
        being set, that we get the right course_image url.
263 264 265 266 267 268 269 270
        """
        course = CourseFactory.create(course_image=u'things_stuff.jpg',
                                      static_asset_path="foo")
        self.assertEquals(
            course_image_url(course),
            '/static/foo/things_stuff.jpg'
        )

271

272
@attr(shard=1)
273 274 275 276 277 278 279 280 281 282
class XmlCourseImageTestCase(XModuleXmlImportTest):
    """Tests for course image URLs when using an xml modulestore."""

    def test_get_image_url(self):
        """Test image URL formatting."""
        course = self.process_xml(xml.CourseFactory.build())
        self.assertEquals(course_image_url(course), '/static/xml_test_course/images/course_image.jpg')

    def test_non_ascii_image_name(self):
        course = self.process_xml(xml.CourseFactory.build(course_image=u'before_\N{SNOWMAN}_after.jpg'))
283
        self.assertEquals(course_image_url(course), u'/static/xml_test_course/before_\N{SNOWMAN}_after.jpg')
284 285 286

    def test_spaces_in_image_name(self):
        course = self.process_xml(xml.CourseFactory.build(course_image=u'before after.jpg'))
287
        self.assertEquals(course_image_url(course), u'/static/xml_test_course/before after.jpg')
288 289


290
@attr(shard=1)
291 292 293
class CoursesRenderTest(ModuleStoreTestCase):
    """Test methods related to rendering courses content."""

294 295 296 297 298 299 300 301
    # TODO: this test relies on the specific setup of the toy course.
    # It should be rewritten to build the course it needs and then test that.
    def setUp(self):
        """
        Set up the course and user context
        """
        super(CoursesRenderTest, self).setUp()

302
        store = modulestore()
303
        course_items = import_course_from_xml(store, self.user.id, TEST_DATA_DIR, ['toy'])
304 305
        course_key = course_items[0].id
        self.course = get_course_by_id(course_key)
306
        self.request = get_mock_request(UserFactory.create())
307

308
    def test_get_course_info_section_render(self):
309
        # Test render works okay
310
        course_info = get_course_info_section(self.request, self.request.user, self.course, 'handouts')
311
        self.assertEqual(course_info, u"<a href='/c4x/edX/toy/asset/handouts_sample_handout.txt'>Sample</a>")
312 313 314 315 316 317

        # Test when render raises an exception
        with mock.patch('courseware.courses.get_module') as mock_module_render:
            mock_module_render.return_value = mock.MagicMock(
                render=mock.Mock(side_effect=Exception('Render failed!'))
            )
318
            course_info = get_course_info_section(self.request, self.request.user, self.course, 'handouts')
319 320
            self.assertIn("this module is temporarily unavailable", course_info)

321
    def test_get_course_about_section_render(self):
322 323

        # Test render works okay
324
        course_about = get_course_about_section(self.request, self.course, 'short_description')
325 326 327 328 329 330 331
        self.assertEqual(course_about, "A course about toys.")

        # Test when render raises an exception
        with mock.patch('courseware.courses.get_module') as mock_module_render:
            mock_module_render.return_value = mock.MagicMock(
                render=mock.Mock(side_effect=Exception('Render failed!'))
            )
332
            course_about = get_course_about_section(self.request, self.course, 'short_description')
333
            self.assertIn("this module is temporarily unavailable", course_about)
334 335


336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381
class CourseEnrollmentOpenTests(ModuleStoreTestCase):
    def setUp(self):
        super(CourseEnrollmentOpenTests, self).setUp()
        self.now = datetime.datetime.now().replace(tzinfo=pytz.UTC)

    def test_course_enrollment_open(self):
        start = self.now - datetime.timedelta(days=1)
        end = self.now + datetime.timedelta(days=1)
        course = CourseFactory(enrollment_start=start, enrollment_end=end)
        self.assertTrue(course_open_for_self_enrollment(course.id))

    def test_course_enrollment_closed_future(self):
        start = self.now + datetime.timedelta(days=1)
        end = self.now + datetime.timedelta(days=2)
        course = CourseFactory(enrollment_start=start, enrollment_end=end)
        self.assertFalse(course_open_for_self_enrollment(course.id))

    def test_course_enrollment_closed_past(self):
        start = self.now - datetime.timedelta(days=2)
        end = self.now - datetime.timedelta(days=1)
        course = CourseFactory(enrollment_start=start, enrollment_end=end)
        self.assertFalse(course_open_for_self_enrollment(course.id))

    def test_course_enrollment_dates_missing(self):
        course = CourseFactory()
        self.assertTrue(course_open_for_self_enrollment(course.id))

    def test_course_enrollment_dates_missing_start(self):
        end = self.now + datetime.timedelta(days=1)
        course = CourseFactory(enrollment_end=end)
        self.assertTrue(course_open_for_self_enrollment(course.id))

        end = self.now - datetime.timedelta(days=1)
        course = CourseFactory(enrollment_end=end)
        self.assertFalse(course_open_for_self_enrollment(course.id))

    def test_course_enrollment_dates_missing_end(self):
        start = self.now - datetime.timedelta(days=1)
        course = CourseFactory(enrollment_start=start)
        self.assertTrue(course_open_for_self_enrollment(course.id))

        start = self.now + datetime.timedelta(days=1)
        course = CourseFactory(enrollment_start=start)
        self.assertFalse(course_open_for_self_enrollment(course.id))


382
@attr(shard=1)
383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417
@ddt.ddt
class CourseInstantiationTests(ModuleStoreTestCase):
    """
    Tests around instantiating a course multiple times in the same request.
    """
    def setUp(self):
        super(CourseInstantiationTests, self).setUp()

        self.factory = RequestFactory()

    @ddt.data(*itertools.product(xrange(5), [ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split], [None, 0, 5]))
    @ddt.unpack
    def test_repeated_course_module_instantiation(self, loops, default_store, course_depth):

        with modulestore().default_store(default_store):
            course = CourseFactory.create()
            chapter = ItemFactory(parent=course, category='chapter', graded=True)
            section = ItemFactory(parent=chapter, category='sequential')
            __ = ItemFactory(parent=section, category='problem')

        fake_request = self.factory.get(
            reverse('progress', kwargs={'course_id': unicode(course.id)})
        )

        course = modulestore().get_course(course.id, depth=course_depth)

        for _ in xrange(loops):
            field_data_cache = FieldDataCache.cache_for_descriptor_descendents(
                course.id, self.user, course, depth=course_depth
            )
            course_module = get_module_for_descriptor(
                self.user,
                fake_request,
                course,
                field_data_cache,
418 419
                course.id,
                course=course
420 421 422 423 424
            )
            for chapter in course_module.get_children():
                for section in chapter.get_children():
                    for item in section.get_children():
                        self.assertTrue(item.graded)