426 lines
20 KiB
Python
426 lines
20 KiB
Python
""" Tests for utils. """
|
|
import collections
|
|
import copy
|
|
import mock
|
|
from datetime import datetime, timedelta
|
|
from pytz import UTC
|
|
|
|
from django.test import TestCase
|
|
from django.test.utils import override_settings
|
|
|
|
from contentstore import utils
|
|
from contentstore.tests.utils import CourseTestCase
|
|
from xmodule.modulestore import ModuleStoreEnum
|
|
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
|
|
from opaque_keys.edx.locations import SlashSeparatedCourseKey
|
|
|
|
from xmodule.modulestore.django import modulestore
|
|
from opaque_keys.edx.locator import CourseLocator
|
|
|
|
|
|
class LMSLinksTestCase(TestCase):
|
|
""" Tests for LMS links. """
|
|
def about_page_test(self):
|
|
""" Get URL for about page, no marketing site """
|
|
# default for ENABLE_MKTG_SITE is False.
|
|
self.assertEquals(self.get_about_page_link(), "//localhost:8000/courses/mitX/101/test/about")
|
|
|
|
@override_settings(MKTG_URLS={'ROOT': 'dummy-root'})
|
|
def about_page_marketing_site_test(self):
|
|
""" Get URL for about page, marketing root present. """
|
|
with mock.patch.dict('django.conf.settings.FEATURES', {'ENABLE_MKTG_SITE': True}):
|
|
self.assertEquals(self.get_about_page_link(), "//dummy-root/courses/mitX/101/test/about")
|
|
with mock.patch.dict('django.conf.settings.FEATURES', {'ENABLE_MKTG_SITE': False}):
|
|
self.assertEquals(self.get_about_page_link(), "//localhost:8000/courses/mitX/101/test/about")
|
|
|
|
@override_settings(MKTG_URLS={'ROOT': 'http://www.dummy'})
|
|
def about_page_marketing_site_remove_http_test(self):
|
|
""" Get URL for about page, marketing root present, remove http://. """
|
|
with mock.patch.dict('django.conf.settings.FEATURES', {'ENABLE_MKTG_SITE': True}):
|
|
self.assertEquals(self.get_about_page_link(), "//www.dummy/courses/mitX/101/test/about")
|
|
|
|
@override_settings(MKTG_URLS={'ROOT': 'https://www.dummy'})
|
|
def about_page_marketing_site_remove_https_test(self):
|
|
""" Get URL for about page, marketing root present, remove https://. """
|
|
with mock.patch.dict('django.conf.settings.FEATURES', {'ENABLE_MKTG_SITE': True}):
|
|
self.assertEquals(self.get_about_page_link(), "//www.dummy/courses/mitX/101/test/about")
|
|
|
|
@override_settings(MKTG_URLS={'ROOT': 'www.dummyhttps://x'})
|
|
def about_page_marketing_site_https__edge_test(self):
|
|
""" Get URL for about page, only remove https:// at the beginning of the string. """
|
|
with mock.patch.dict('django.conf.settings.FEATURES', {'ENABLE_MKTG_SITE': True}):
|
|
self.assertEquals(self.get_about_page_link(), "//www.dummyhttps://x/courses/mitX/101/test/about")
|
|
|
|
@override_settings(MKTG_URLS={})
|
|
def about_page_marketing_urls_not_set_test(self):
|
|
""" Error case. ENABLE_MKTG_SITE is True, but there is either no MKTG_URLS, or no MKTG_URLS Root property. """
|
|
with mock.patch.dict('django.conf.settings.FEATURES', {'ENABLE_MKTG_SITE': True}):
|
|
self.assertEquals(self.get_about_page_link(), None)
|
|
|
|
@override_settings(LMS_BASE=None)
|
|
def about_page_no_lms_base_test(self):
|
|
""" No LMS_BASE, nor is ENABLE_MKTG_SITE True """
|
|
self.assertEquals(self.get_about_page_link(), None)
|
|
|
|
def get_about_page_link(self):
|
|
""" create mock course and return the about page link """
|
|
course_key = SlashSeparatedCourseKey('mitX', '101', 'test')
|
|
return utils.get_lms_link_for_about_page(course_key)
|
|
|
|
def lms_link_test(self):
|
|
""" Tests get_lms_link_for_item. """
|
|
course_key = SlashSeparatedCourseKey('mitX', '101', 'test')
|
|
location = course_key.make_usage_key('vertical', 'contacting_us')
|
|
link = utils.get_lms_link_for_item(location, False)
|
|
self.assertEquals(link, "//localhost:8000/courses/mitX/101/test/jump_to/i4x://mitX/101/vertical/contacting_us")
|
|
|
|
# test preview
|
|
link = utils.get_lms_link_for_item(location, True)
|
|
self.assertEquals(
|
|
link,
|
|
"//preview/courses/mitX/101/test/jump_to/i4x://mitX/101/vertical/contacting_us"
|
|
)
|
|
|
|
# now test with the course' location
|
|
location = course_key.make_usage_key('course', 'test')
|
|
link = utils.get_lms_link_for_item(location)
|
|
self.assertEquals(link, "//localhost:8000/courses/mitX/101/test/jump_to/i4x://mitX/101/course/test")
|
|
|
|
|
|
class ExtraPanelTabTestCase(TestCase):
|
|
""" Tests adding and removing extra course tabs. """
|
|
|
|
def get_tab_type_dicts(self, tab_types):
|
|
""" Returns an array of tab dictionaries. """
|
|
if tab_types:
|
|
return [{'tab_type': tab_type} for tab_type in tab_types.split(',')]
|
|
else:
|
|
return []
|
|
|
|
def get_course_with_tabs(self, tabs=None):
|
|
""" Returns a mock course object with a tabs attribute. """
|
|
if tabs is None:
|
|
tabs = []
|
|
course = collections.namedtuple('MockCourse', ['tabs'])
|
|
if isinstance(tabs, basestring):
|
|
course.tabs = self.get_tab_type_dicts(tabs)
|
|
else:
|
|
course.tabs = tabs
|
|
return course
|
|
|
|
def test_add_extra_panel_tab(self):
|
|
""" Tests if a tab can be added to a course tab list. """
|
|
for tab_type in utils.EXTRA_TAB_PANELS.keys():
|
|
tab = utils.EXTRA_TAB_PANELS.get(tab_type)
|
|
|
|
# test adding with changed = True
|
|
for tab_setup in ['', 'x', 'x,y,z']:
|
|
course = self.get_course_with_tabs(tab_setup)
|
|
expected_tabs = copy.copy(course.tabs)
|
|
expected_tabs.append(tab)
|
|
changed, actual_tabs = utils.add_extra_panel_tab(tab_type, course)
|
|
self.assertTrue(changed)
|
|
self.assertEqual(actual_tabs, expected_tabs)
|
|
|
|
# test adding with changed = False
|
|
tab_test_setup = [
|
|
[tab],
|
|
[tab, self.get_tab_type_dicts('x,y,z')],
|
|
[self.get_tab_type_dicts('x,y'), tab, self.get_tab_type_dicts('z')],
|
|
[self.get_tab_type_dicts('x,y,z'), tab]]
|
|
|
|
for tab_setup in tab_test_setup:
|
|
course = self.get_course_with_tabs(tab_setup)
|
|
expected_tabs = copy.copy(course.tabs)
|
|
changed, actual_tabs = utils.add_extra_panel_tab(tab_type, course)
|
|
self.assertFalse(changed)
|
|
self.assertEqual(actual_tabs, expected_tabs)
|
|
|
|
def test_remove_extra_panel_tab(self):
|
|
""" Tests if a tab can be removed from a course tab list. """
|
|
for tab_type in utils.EXTRA_TAB_PANELS.keys():
|
|
tab = utils.EXTRA_TAB_PANELS.get(tab_type)
|
|
|
|
# test removing with changed = True
|
|
tab_test_setup = [
|
|
[tab],
|
|
[tab, self.get_tab_type_dicts('x,y,z')],
|
|
[self.get_tab_type_dicts('x,y'), tab, self.get_tab_type_dicts('z')],
|
|
[self.get_tab_type_dicts('x,y,z'), tab]]
|
|
|
|
for tab_setup in tab_test_setup:
|
|
course = self.get_course_with_tabs(tab_setup)
|
|
expected_tabs = [t for t in course.tabs if t != utils.EXTRA_TAB_PANELS.get(tab_type)]
|
|
changed, actual_tabs = utils.remove_extra_panel_tab(tab_type, course)
|
|
self.assertTrue(changed)
|
|
self.assertEqual(actual_tabs, expected_tabs)
|
|
|
|
# test removing with changed = False
|
|
for tab_setup in ['', 'x', 'x,y,z']:
|
|
course = self.get_course_with_tabs(tab_setup)
|
|
expected_tabs = copy.copy(course.tabs)
|
|
changed, actual_tabs = utils.remove_extra_panel_tab(tab_type, course)
|
|
self.assertFalse(changed)
|
|
self.assertEqual(actual_tabs, expected_tabs)
|
|
|
|
|
|
class CourseImageTestCase(TestCase):
|
|
"""Tests for course image URLs."""
|
|
|
|
def test_get_image_url(self):
|
|
"""Test image URL formatting."""
|
|
course = CourseFactory.create()
|
|
url = utils.course_image_url(course)
|
|
self.assertEquals(url, unicode(course.id.make_asset_key('asset', course.course_image)))
|
|
|
|
def test_non_ascii_image_name(self):
|
|
""" Verify that non-ascii image names are cleaned """
|
|
course_image = u'before_\N{SNOWMAN}_after.jpg'
|
|
course = CourseFactory.create(course_image=course_image)
|
|
self.assertEquals(
|
|
utils.course_image_url(course),
|
|
unicode(course.id.make_asset_key('asset', course_image.replace(u'\N{SNOWMAN}', '_')))
|
|
)
|
|
|
|
def test_spaces_in_image_name(self):
|
|
""" Verify that image names with spaces in them are cleaned """
|
|
course_image = u'before after.jpg'
|
|
course = CourseFactory.create(course_image=u'before after.jpg')
|
|
self.assertEquals(
|
|
utils.course_image_url(course),
|
|
unicode(course.id.make_asset_key('asset', course_image.replace(" ", "_")))
|
|
)
|
|
|
|
|
|
class XBlockVisibilityTestCase(TestCase):
|
|
"""Tests for xblock visibility for students."""
|
|
|
|
def setUp(self):
|
|
self.dummy_user = ModuleStoreEnum.UserID.test
|
|
self.past = datetime(1970, 1, 1)
|
|
self.future = datetime.now(UTC) + timedelta(days=1)
|
|
|
|
def test_private_unreleased_xblock(self):
|
|
"""Verifies that a private unreleased xblock is not visible"""
|
|
self._test_visible_to_students(False, 'private_unreleased', self.future)
|
|
|
|
def test_private_released_xblock(self):
|
|
"""Verifies that a private released xblock is not visible"""
|
|
self._test_visible_to_students(False, 'private_released', self.past)
|
|
|
|
def test_public_unreleased_xblock(self):
|
|
"""Verifies that a public (published) unreleased xblock is not visible"""
|
|
self._test_visible_to_students(False, 'public_unreleased', self.future, publish=True)
|
|
|
|
def test_public_released_xblock(self):
|
|
"""Verifies that public (published) released xblock is visible if staff lock is not enabled."""
|
|
self._test_visible_to_students(True, 'public_released', self.past, publish=True)
|
|
|
|
def test_private_no_start_xblock(self):
|
|
"""Verifies that a private xblock with no start date is not visible"""
|
|
self._test_visible_to_students(False, 'private_no_start', None)
|
|
|
|
def test_public_no_start_xblock(self):
|
|
"""Verifies that a public (published) xblock with no start date is visible unless staff lock is enabled"""
|
|
self._test_visible_to_students(True, 'public_no_start', None, publish=True)
|
|
|
|
def test_draft_released_xblock(self):
|
|
"""Verifies that a xblock with an unreleased draft and a released published version is visible"""
|
|
vertical = self._create_xblock_with_start_date('draft_released', self.past, publish=True)
|
|
|
|
# Create an unreleased draft version of the xblock
|
|
vertical.start = self.future
|
|
modulestore().update_item(vertical, self.dummy_user)
|
|
|
|
self.assertTrue(utils.is_currently_visible_to_students(vertical))
|
|
|
|
def _test_visible_to_students(self, expected_visible_without_lock, name, start_date, publish=False):
|
|
"""
|
|
Helper method that checks that is_xblock_visible_to_students returns the correct value both
|
|
with and without visible_to_staff_only set.
|
|
"""
|
|
no_staff_lock = self._create_xblock_with_start_date(name, start_date, publish, visible_to_staff_only=False)
|
|
self.assertEqual(expected_visible_without_lock, utils.is_currently_visible_to_students(no_staff_lock))
|
|
|
|
# any xblock with visible_to_staff_only set to True should not be visible to students.
|
|
staff_lock = self._create_xblock_with_start_date(
|
|
name + "_locked", start_date, publish, visible_to_staff_only=True
|
|
)
|
|
self.assertFalse(utils.is_currently_visible_to_students(staff_lock))
|
|
|
|
def _create_xblock_with_start_date(self, name, start_date, publish=False, visible_to_staff_only=False):
|
|
"""Helper to create an xblock with a start date, optionally publishing it"""
|
|
course_key = CourseLocator('edX', 'visibility', '2012_Fall')
|
|
|
|
vertical = modulestore().create_item(
|
|
self.dummy_user, course_key, 'vertical', name,
|
|
fields={'start': start_date, 'visible_to_staff_only': visible_to_staff_only}
|
|
)
|
|
|
|
if publish:
|
|
modulestore().publish(vertical.location, self.dummy_user)
|
|
|
|
return vertical
|
|
|
|
|
|
class ReleaseDateSourceTest(CourseTestCase):
|
|
"""Tests for finding the source of an xblock's release date."""
|
|
|
|
def setUp(self):
|
|
super(ReleaseDateSourceTest, self).setUp()
|
|
|
|
self.chapter = ItemFactory.create(category='chapter', parent_location=self.course.location)
|
|
self.sequential = ItemFactory.create(category='sequential', parent_location=self.chapter.location)
|
|
self.vertical = ItemFactory.create(category='vertical', parent_location=self.sequential.location)
|
|
|
|
# Read again so that children lists are accurate
|
|
self.chapter = self.store.get_item(self.chapter.location)
|
|
self.sequential = self.store.get_item(self.sequential.location)
|
|
self.vertical = self.store.get_item(self.vertical.location)
|
|
|
|
self.date_one = datetime(1980, 1, 1, tzinfo=UTC)
|
|
self.date_two = datetime(2020, 1, 1, tzinfo=UTC)
|
|
|
|
def _update_release_dates(self, chapter_start, sequential_start, vertical_start):
|
|
"""Sets the release dates of the chapter, sequential, and vertical"""
|
|
self.chapter.start = chapter_start
|
|
self.chapter = self.store.update_item(self.chapter, ModuleStoreEnum.UserID.test)
|
|
self.sequential.start = sequential_start
|
|
self.sequential = self.store.update_item(self.sequential, ModuleStoreEnum.UserID.test)
|
|
self.vertical.start = vertical_start
|
|
self.vertical = self.store.update_item(self.vertical, ModuleStoreEnum.UserID.test)
|
|
|
|
def _verify_release_date_source(self, item, expected_source):
|
|
"""Helper to verify that the release date source of a given item matches the expected source"""
|
|
source = utils.find_release_date_source(item)
|
|
self.assertEqual(source.location, expected_source.location)
|
|
self.assertEqual(source.start, expected_source.start)
|
|
|
|
def test_chapter_source_for_vertical(self):
|
|
"""Tests a vertical's release date being set by its chapter"""
|
|
self._update_release_dates(self.date_one, self.date_one, self.date_one)
|
|
self._verify_release_date_source(self.vertical, self.chapter)
|
|
|
|
def test_sequential_source_for_vertical(self):
|
|
"""Tests a vertical's release date being set by its sequential"""
|
|
self._update_release_dates(self.date_one, self.date_two, self.date_two)
|
|
self._verify_release_date_source(self.vertical, self.sequential)
|
|
|
|
def test_chapter_source_for_sequential(self):
|
|
"""Tests a sequential's release date being set by its chapter"""
|
|
self._update_release_dates(self.date_one, self.date_one, self.date_one)
|
|
self._verify_release_date_source(self.sequential, self.chapter)
|
|
|
|
def test_sequential_source_for_sequential(self):
|
|
"""Tests a sequential's release date being set by itself"""
|
|
self._update_release_dates(self.date_one, self.date_two, self.date_two)
|
|
self._verify_release_date_source(self.sequential, self.sequential)
|
|
|
|
|
|
class StaffLockTest(CourseTestCase):
|
|
"""Base class for testing staff lock functions."""
|
|
|
|
def setUp(self):
|
|
super(StaffLockTest, self).setUp()
|
|
|
|
self.chapter = ItemFactory.create(category='chapter', parent_location=self.course.location)
|
|
self.sequential = ItemFactory.create(category='sequential', parent_location=self.chapter.location)
|
|
self.vertical = ItemFactory.create(category='vertical', parent_location=self.sequential.location)
|
|
self.orphan = ItemFactory.create(category='vertical', parent_location=self.sequential.location)
|
|
|
|
# Read again so that children lists are accurate
|
|
self.chapter = self.store.get_item(self.chapter.location)
|
|
self.sequential = self.store.get_item(self.sequential.location)
|
|
self.vertical = self.store.get_item(self.vertical.location)
|
|
|
|
# Orphan the orphaned xblock
|
|
self.sequential.children = [self.vertical.location]
|
|
self.sequential = self.store.update_item(self.sequential, ModuleStoreEnum.UserID.test)
|
|
|
|
def _set_staff_lock(self, xblock, is_locked):
|
|
"""If is_locked is True, xblock is staff locked. Otherwise, the xblock staff lock field is removed."""
|
|
field = xblock.fields['visible_to_staff_only']
|
|
if is_locked:
|
|
field.write_to(xblock, True)
|
|
else:
|
|
field.delete_from(xblock)
|
|
return self.store.update_item(xblock, ModuleStoreEnum.UserID.test)
|
|
|
|
def _update_staff_locks(self, chapter_locked, sequential_locked, vertical_locked):
|
|
"""
|
|
Sets the staff lock on the chapter, sequential, and vertical
|
|
If the corresponding argument is False, then the field is deleted from the xblock
|
|
"""
|
|
self.chapter = self._set_staff_lock(self.chapter, chapter_locked)
|
|
self.sequential = self._set_staff_lock(self.sequential, sequential_locked)
|
|
self.vertical = self._set_staff_lock(self.vertical, vertical_locked)
|
|
|
|
|
|
class StaffLockSourceTest(StaffLockTest):
|
|
"""Tests for finding the source of an xblock's staff lock."""
|
|
|
|
def _verify_staff_lock_source(self, item, expected_source):
|
|
"""Helper to verify that the staff lock source of a given item matches the expected source"""
|
|
source = utils.find_staff_lock_source(item)
|
|
self.assertEqual(source.location, expected_source.location)
|
|
self.assertTrue(source.visible_to_staff_only)
|
|
|
|
def test_chapter_source_for_vertical(self):
|
|
"""Tests a vertical's staff lock being set by its chapter"""
|
|
self._update_staff_locks(True, False, False)
|
|
self._verify_staff_lock_source(self.vertical, self.chapter)
|
|
|
|
def test_sequential_source_for_vertical(self):
|
|
"""Tests a vertical's staff lock being set by its sequential"""
|
|
self._update_staff_locks(True, True, False)
|
|
self._verify_staff_lock_source(self.vertical, self.sequential)
|
|
self._update_staff_locks(False, True, False)
|
|
self._verify_staff_lock_source(self.vertical, self.sequential)
|
|
|
|
def test_vertical_source_for_vertical(self):
|
|
"""Tests a vertical's staff lock being set by itself"""
|
|
self._update_staff_locks(True, True, True)
|
|
self._verify_staff_lock_source(self.vertical, self.vertical)
|
|
self._update_staff_locks(False, True, True)
|
|
self._verify_staff_lock_source(self.vertical, self.vertical)
|
|
self._update_staff_locks(False, False, True)
|
|
self._verify_staff_lock_source(self.vertical, self.vertical)
|
|
|
|
def test_orphan_has_no_source(self):
|
|
"""Tests that a orphaned xblock has no staff lock source"""
|
|
self.assertIsNone(utils.find_staff_lock_source(self.orphan))
|
|
|
|
def test_no_source_for_vertical(self):
|
|
"""Tests a vertical with no staff lock set anywhere"""
|
|
self._update_staff_locks(False, False, False)
|
|
self.assertIsNone(utils.find_staff_lock_source(self.vertical))
|
|
|
|
|
|
class InheritedStaffLockTest(StaffLockTest):
|
|
"""Tests for determining if an xblock inherits a staff lock."""
|
|
|
|
def test_no_inheritance(self):
|
|
"""Tests that a locked or unlocked vertical with no locked ancestors does not have an inherited lock"""
|
|
self._update_staff_locks(False, False, False)
|
|
self.assertFalse(utils.ancestor_has_staff_lock(self.vertical))
|
|
self._update_staff_locks(False, False, True)
|
|
self.assertFalse(utils.ancestor_has_staff_lock(self.vertical))
|
|
|
|
def test_inheritance_in_locked_section(self):
|
|
"""Tests that a locked or unlocked vertical in a locked section has an inherited lock"""
|
|
self._update_staff_locks(True, False, False)
|
|
self.assertTrue(utils.ancestor_has_staff_lock(self.vertical))
|
|
self._update_staff_locks(True, False, True)
|
|
self.assertTrue(utils.ancestor_has_staff_lock(self.vertical))
|
|
|
|
def test_inheritance_in_locked_subsection(self):
|
|
"""Tests that a locked or unlocked vertical in a locked subsection has an inherited lock"""
|
|
self._update_staff_locks(False, True, False)
|
|
self.assertTrue(utils.ancestor_has_staff_lock(self.vertical))
|
|
self._update_staff_locks(False, True, True)
|
|
self.assertTrue(utils.ancestor_has_staff_lock(self.vertical))
|
|
|
|
def test_no_inheritance_for_orphan(self):
|
|
"""Tests that an orphaned xblock does not inherit staff lock"""
|
|
self.assertFalse(utils.ancestor_has_staff_lock(self.orphan))
|