Files
edx-platform/lms/djangoapps/courseware/tests/test_courses.py
Feanil Patel 2df8b8226b Merge pull request #22643 from edx/feanil/2to3_asserts
Run `2to3 -f asserts . -w` on edx-platform.
2019-12-30 12:13:42 -05:00

457 lines
18 KiB
Python

# -*- coding: utf-8 -*-
"""
Tests for course access
"""
import datetime
import itertools
import ddt
import mock
import pytz
import six
from crum import set_current_request
from django.conf import settings
from django.test.client import RequestFactory
from django.test.utils import override_settings
from django.urls import reverse
from opaque_keys.edx.keys import CourseKey
from six import text_type
from six.moves import range
from lms.djangoapps.courseware.courses import (
course_open_for_self_enrollment,
get_cms_block_link,
get_cms_course_link,
get_course_about_section,
get_course_by_id,
get_course_chapter_ids,
get_course_info_section,
get_course_overview_with_access,
get_course_with_access,
get_courses,
get_current_child
)
from lms.djangoapps.courseware.model_data import FieldDataCache
from lms.djangoapps.courseware.module_render import get_module_for_descriptor
from lms.djangoapps.courseware.courseware_access_exception import CoursewareAccessException
from openedx.core.djangolib.testing.utils import get_mock_request
from openedx.core.lib.courses import course_image_url
from student.tests.factories import UserFactory
from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.django import _get_modulestore_branch_setting, modulestore
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory, check_mongo_calls
from xmodule.modulestore.xml_importer import import_course_from_xml
from xmodule.tests.xml import XModuleXmlImportTest
from xmodule.tests.xml import factories as xml
CMS_BASE_TEST = 'testcms'
TEST_DATA_DIR = settings.COMMON_TEST_DATA_ROOT
@ddt.ddt
class CoursesTest(ModuleStoreTestCase):
"""Test methods related to fetching courses."""
ENABLED_SIGNALS = ['course_published']
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,
}
@override_settings(CMS_BASE=CMS_BASE_TEST)
def test_get_cms_course_block_link(self):
"""
Tests that get_cms_course_link_by_id and get_cms_block_link_by_id return the right thing
"""
self.course = CourseFactory.create(
org='org', number='num', display_name='name'
)
cms_url = u"//{}/course/{}".format(CMS_BASE_TEST, six.text_type(self.course.id))
self.assertEqual(cms_url, get_cms_course_link(self.course))
cms_url = u"//{}/course/{}".format(CMS_BASE_TEST, six.text_type(self.course.location))
self.assertEqual(cms_url, get_cms_block_link(self.course, 'course'))
@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]
user = UserFactory.create()
course = CourseFactory.create(visible_to_staff_only=True)
with self.assertRaises(CoursewareAccessException) as error:
course_access_func(user, 'load', course.id)
self.assertEqual(text_type(error.exception), "Course not found.")
self.assertEqual(error.exception.access_response.error_code, "not_visible_to_user")
self.assertFalse(error.exception.access_response.has_access)
@ddt.data(
(GET_COURSE_WITH_ACCESS, 1),
(GET_COURSE_OVERVIEW_WITH_ACCESS, 0),
)
@ddt.unpack
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]
user = UserFactory.create()
course = CourseFactory.create(emit_signals=True)
with check_mongo_calls(num_mongo_calls):
course_access_func(user, 'load', course.id)
def test_get_courses_by_org(self):
"""
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
org.
"""
primary = 'primary'
alternate = 'alternate'
def _fake_get_value(value, default=None):
"""Used to stub out site_configuration.helpers.get_value()."""
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)
)
with mock.patch(
'openedx.core.djangoapps.site_configuration.helpers.get_value',
autospec=True,
) as mock_get_value:
mock_get_value.side_effect = _fake_get_value
# Request filtering for an org distinct from the designated org.
no_courses = get_courses(user, org=primary)
self.assertEqual(list(no_courses), [])
# Request filtering for an org matching the designated org.
site_courses = get_courses(user, org=alternate)
self.assertTrue(
all(course.org == alternate_course.org for course in site_courses)
)
def test_get_courses_with_filter(self):
"""
Verify that filtering performs as expected.
"""
user = UserFactory.create()
mobile_course = CourseFactory.create(emit_signals=True)
non_mobile_course =\
CourseFactory.create(mobile_available=False, 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,
u"testing get_courses with filter_={}".format(filter_),
)
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))
class ModuleStoreBranchSettingTest(ModuleStoreTestCase):
"""Test methods related to the modulestore branch setting."""
@mock.patch(
'xmodule.modulestore.django.get_current_request_hostname',
mock.Mock(return_value='preview.localhost')
)
@override_settings(
HOSTNAME_MODULESTORE_DEFAULT_MAPPINGS={r'preview\.': ModuleStoreEnum.Branch.draft_preferred},
MODULESTORE_BRANCH='fake_default_branch',
)
def test_default_modulestore_preview_mapping(self):
self.assertEqual(_get_modulestore_branch_setting(), ModuleStoreEnum.Branch.draft_preferred)
@mock.patch(
'xmodule.modulestore.django.get_current_request_hostname',
mock.Mock(return_value='localhost')
)
@override_settings(
HOSTNAME_MODULESTORE_DEFAULT_MAPPINGS={r'preview\.': ModuleStoreEnum.Branch.draft_preferred},
MODULESTORE_BRANCH='fake_default_branch',
)
def test_default_modulestore_branch_mapping(self):
self.assertEqual(_get_modulestore_branch_setting(), 'fake_default_branch')
@override_settings(CMS_BASE=CMS_BASE_TEST)
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.assertEqual(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.assertEqual(
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.assertEqual(
course_image_url(course),
'/c4x/{org}/{course}/asset/before_after.jpg'.format(
org=course.location.org,
course=course.location.course
)
)
def test_static_asset_path_course_image_default(self):
"""
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.assertEqual(
course_image_url(course),
'/static/foo/images/course_image.jpg'
)
def test_static_asset_path_course_image_set(self):
"""
Test that with course_image and static_asset_path both
being set, that we get the right course_image url.
"""
course = CourseFactory.create(course_image=u'things_stuff.jpg',
static_asset_path="foo")
self.assertEqual(
course_image_url(course),
'/static/foo/things_stuff.jpg'
)
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.assertEqual(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'))
self.assertEqual(course_image_url(course), u'/static/xml_test_course/before_\N{SNOWMAN}_after.jpg')
def test_spaces_in_image_name(self):
course = self.process_xml(xml.CourseFactory.build(course_image=u'before after.jpg'))
self.assertEqual(course_image_url(course), u'/static/xml_test_course/before after.jpg')
class CoursesRenderTest(ModuleStoreTestCase):
"""Test methods related to rendering courses content."""
# 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()
store = modulestore()
course_items = import_course_from_xml(store, self.user.id, TEST_DATA_DIR, ['toy'])
course_key = course_items[0].id
self.course = get_course_by_id(course_key)
self.addCleanup(set_current_request, None)
self.request = get_mock_request(UserFactory.create())
def test_get_course_info_section_render(self):
# Test render works okay
course_info = get_course_info_section(self.request, self.request.user, self.course, 'handouts')
self.assertEqual(course_info, u"<a href='/c4x/edX/toy/asset/handouts_sample_handout.txt'>Sample</a>")
# Test when render raises an exception
with mock.patch('lms.djangoapps.courseware.courses.get_module') as mock_module_render:
mock_module_render.return_value = mock.MagicMock(
render=mock.Mock(side_effect=Exception('Render failed!'))
)
course_info = get_course_info_section(self.request, self.request.user, self.course, 'handouts')
self.assertIn("this module is temporarily unavailable", course_info)
def test_get_course_about_section_render(self):
# Test render works okay
course_about = get_course_about_section(self.request, self.course, 'short_description')
self.assertEqual(course_about, "A course about toys.")
# Test when render raises an exception
with mock.patch('lms.djangoapps.courseware.courses.get_module') as mock_module_render:
mock_module_render.return_value = mock.MagicMock(
render=mock.Mock(side_effect=Exception('Render failed!'))
)
course_about = get_course_about_section(self.request, self.course, 'short_description')
self.assertIn("this module is temporarily unavailable", course_about)
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))
@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(range(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': six.text_type(course.id)})
)
course = modulestore().get_course(course.id, depth=course_depth)
for _ in range(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,
course.id,
course=course
)
for chapter in course_module.get_children():
for section in chapter.get_children():
for item in section.get_children():
self.assertTrue(item.graded)
class TestGetCourseChapters(ModuleStoreTestCase):
"""
Tests for the `get_course_chapter_ids` function.
"""
def test_get_non_existant_course(self):
"""
Test non-existant course returns empty list.
"""
self.assertEqual(get_course_chapter_ids(None), [])
# build a fake key
fake_course_key = CourseKey.from_string('course-v1:FakeOrg+CN1+CR-FALLNEVER1')
self.assertEqual(get_course_chapter_ids(fake_course_key), [])
def test_get_chapters(self):
"""
Test get_course_chapter_ids returns expected result.
"""
course = CourseFactory()
ItemFactory(parent=course, category='chapter')
ItemFactory(parent=course, category='chapter')
course_chapter_ids = get_course_chapter_ids(course.location.course_key)
self.assertEqual(len(course_chapter_ids), 2)
self.assertEqual(
course_chapter_ids,
[six.text_type(child) for child in course.children]
)