Updates the discussions tab to return the learning MFE tab URL so the discussion content can be shown in the learning MFE itself.
935 lines
37 KiB
Python
935 lines
37 KiB
Python
"""
|
|
Test cases for tabs.
|
|
"""
|
|
|
|
from unittest.mock import MagicMock, Mock, patch
|
|
|
|
import ddt
|
|
import pytest
|
|
from crum import set_current_request
|
|
from django.contrib.auth.models import AnonymousUser
|
|
from django.http import Http404
|
|
from django.urls import reverse
|
|
from milestones.tests.utils import MilestonesTestCaseMixin
|
|
|
|
from edx_toggles.toggles.testutils import override_waffle_flag
|
|
from lms.djangoapps.courseware.tabs import (
|
|
CourseInfoTab,
|
|
CoursewareTab,
|
|
DatesTab,
|
|
ExternalDiscussionCourseTab,
|
|
ExternalLinkCourseTab,
|
|
ProgressTab,
|
|
get_course_tab_list
|
|
)
|
|
from lms.djangoapps.courseware.tests.helpers import LoginEnrollmentTestCase
|
|
from lms.djangoapps.courseware.views.views import StaticCourseTabView, get_static_tab_fragment
|
|
from openedx.core.djangoapps.discussions.models import DiscussionsConfiguration, Provider
|
|
from openedx.core.djangolib.testing.utils import get_mock_request
|
|
from openedx.core.lib.courses import get_course_by_id
|
|
from openedx.features.course_experience import DISABLE_UNIFIED_COURSE_TAB_FLAG
|
|
from common.djangoapps.student.models import CourseEnrollment
|
|
from common.djangoapps.student.tests.factories import InstructorFactory
|
|
from common.djangoapps.student.tests.factories import StaffFactory
|
|
from common.djangoapps.student.tests.factories import UserFactory
|
|
from common.djangoapps.util.milestones_helpers import (
|
|
add_course_content_milestone,
|
|
add_course_milestone,
|
|
add_milestone,
|
|
get_milestone_relationship_types
|
|
)
|
|
from openedx.features.course_experience.url_helpers import get_learning_mfe_home_url
|
|
from xmodule import tabs as xmodule_tabs # lint-amnesty, pylint: disable=wrong-import-order
|
|
from xmodule.modulestore.tests.django_utils import ( # lint-amnesty, pylint: disable=wrong-import-order
|
|
TEST_DATA_MIXED_MODULESTORE,
|
|
ModuleStoreTestCase,
|
|
SharedModuleStoreTestCase
|
|
)
|
|
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory # lint-amnesty, pylint: disable=wrong-import-order
|
|
from xmodule.modulestore.tests.utils import TEST_DATA_DIR # lint-amnesty, pylint: disable=wrong-import-order
|
|
from xmodule.modulestore.xml_importer import import_course_from_xml # lint-amnesty, pylint: disable=wrong-import-order
|
|
|
|
|
|
class TabTestCase(SharedModuleStoreTestCase):
|
|
"""Base class for Tab-related test cases."""
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
super().setUpClass()
|
|
|
|
cls.course = CourseFactory.create(org='edX', course='toy', run='2012_Fall')
|
|
cls.fake_dict_tab = {'fake_key': 'fake_value'}
|
|
cls.books = None
|
|
|
|
def setUp(self):
|
|
super().setUp()
|
|
self.reverse = lambda name, args: "name/{}/args/{}".format(name, ",".join(str(a) for a in args))
|
|
|
|
def create_mock_user(self, is_staff=True, is_enrolled=True):
|
|
"""
|
|
Creates a mock user with the specified properties.
|
|
"""
|
|
user = UserFactory(is_staff=is_staff)
|
|
user.is_enrolled = is_enrolled
|
|
return user
|
|
|
|
def is_tab_enabled(self, tab, course, user):
|
|
"""
|
|
Returns true if the specified tab is enabled.
|
|
"""
|
|
return tab is not None and tab.is_enabled(course, user=user)
|
|
|
|
def set_up_books(self, num_books):
|
|
"""Initializes the textbooks in the course and adds the given number of books to each textbook"""
|
|
self.books = [MagicMock() for _ in range(num_books)]
|
|
for book_index, book in enumerate(self.books):
|
|
book.title = f'Book{book_index}'
|
|
self.course.textbooks = self.books
|
|
self.course.pdf_textbooks = self.books
|
|
self.course.html_textbooks = self.books
|
|
|
|
def check_tab(
|
|
self,
|
|
tab_class,
|
|
dict_tab,
|
|
expected_link,
|
|
expected_tab_id,
|
|
expected_name='same',
|
|
invalid_dict_tab=None,
|
|
):
|
|
"""
|
|
Helper method to verify a tab class.
|
|
|
|
'tab_class' is the class of the tab that is being tested
|
|
'dict_tab' is the raw dictionary value of the tab
|
|
'expected_link' is the expected value for the hyperlink of the tab
|
|
'expected_tab_id' is the expected value for the unique id of the tab
|
|
'expected_name' is the expected value for the name of the tab
|
|
'invalid_dict_tab' is an invalid dictionary value for the tab.
|
|
Can be 'None' if the given tab class does not have any keys to validate.
|
|
"""
|
|
# create tab
|
|
tab = tab_class(tab_dict=dict_tab)
|
|
|
|
# name is as expected
|
|
assert tab.name == expected_name
|
|
|
|
# link is as expected
|
|
assert tab.link_func(self.course, self.reverse) == expected_link
|
|
|
|
# verify active page name
|
|
assert tab.tab_id == expected_tab_id
|
|
|
|
# validate tab
|
|
assert tab.validate(dict_tab)
|
|
if invalid_dict_tab:
|
|
with pytest.raises(xmodule_tabs.InvalidTabsException):
|
|
tab.validate(invalid_dict_tab)
|
|
|
|
# check get and set methods
|
|
self.check_get_and_set_methods(tab)
|
|
|
|
# check to_json and from_json methods
|
|
self.check_tab_json_methods(tab)
|
|
|
|
# check equality methods
|
|
self.check_tab_equality(tab, dict_tab)
|
|
|
|
# return tab for any additional tests
|
|
return tab
|
|
|
|
def check_tab_equality(self, tab, dict_tab):
|
|
"""Tests the equality methods on the given tab"""
|
|
assert tab == dict_tab
|
|
# test __eq__
|
|
ne_dict_tab = dict_tab
|
|
ne_dict_tab['type'] = 'fake_type'
|
|
assert tab != ne_dict_tab
|
|
# test __ne__: incorrect type
|
|
assert tab != {'fake_key': 'fake_value'}
|
|
# test __ne__: missing type
|
|
|
|
def check_tab_json_methods(self, tab):
|
|
"""Tests the json from and to methods on the given tab"""
|
|
serialized_tab = tab.to_json()
|
|
deserialized_tab = tab.from_json(serialized_tab)
|
|
assert serialized_tab == deserialized_tab
|
|
|
|
def check_can_display_results(
|
|
self,
|
|
tab,
|
|
expected_value=True,
|
|
for_authenticated_users_only=False,
|
|
for_staff_only=False,
|
|
for_enrolled_users_only=False
|
|
):
|
|
"""Checks can display results for various users"""
|
|
if for_staff_only:
|
|
user = self.create_mock_user(is_staff=True, is_enrolled=True)
|
|
assert expected_value == self.is_tab_enabled(tab, self.course, user)
|
|
if for_authenticated_users_only:
|
|
user = self.create_mock_user(is_staff=False, is_enrolled=False)
|
|
assert expected_value == self.is_tab_enabled(tab, self.course, user)
|
|
if not for_staff_only and not for_authenticated_users_only and not for_enrolled_users_only:
|
|
user = AnonymousUser()
|
|
assert expected_value == self.is_tab_enabled(tab, self.course, user)
|
|
if for_enrolled_users_only:
|
|
user = self.create_mock_user(is_staff=False, is_enrolled=True)
|
|
assert expected_value == self.is_tab_enabled(tab, self.course, user)
|
|
|
|
def check_get_and_set_methods(self, tab):
|
|
"""Test __getitem__ and __setitem__ calls"""
|
|
assert tab['type'] == tab.type
|
|
assert tab['tab_id'] == tab.tab_id
|
|
with pytest.raises(KeyError):
|
|
_ = tab['invalid_key']
|
|
|
|
self.check_get_and_set_method_for_key(tab, 'name')
|
|
self.check_get_and_set_method_for_key(tab, 'tab_id')
|
|
with pytest.raises(KeyError):
|
|
tab['invalid_key'] = 'New Value'
|
|
|
|
def check_get_and_set_method_for_key(self, tab, key):
|
|
"""Test __getitem__ and __setitem__ for the given key"""
|
|
old_value = tab[key]
|
|
new_value = 'New Value'
|
|
tab[key] = new_value
|
|
assert tab[key] == new_value
|
|
tab[key] = old_value
|
|
assert tab[key] == old_value
|
|
|
|
|
|
class TextbooksTestCase(TabTestCase):
|
|
"""Test cases for Textbook Tab."""
|
|
|
|
def setUp(self):
|
|
super().setUp()
|
|
|
|
self.set_up_books(2)
|
|
|
|
self.dict_tab = MagicMock()
|
|
self.course.tabs = [
|
|
xmodule_tabs.CourseTab.load('textbooks'),
|
|
xmodule_tabs.CourseTab.load('pdf_textbooks'),
|
|
xmodule_tabs.CourseTab.load('html_textbooks'),
|
|
]
|
|
self.num_textbook_tabs = sum(1 for tab in self.course.tabs if tab.type in [
|
|
'textbooks', 'pdf_textbooks', 'html_textbooks'
|
|
])
|
|
self.num_textbooks = self.num_textbook_tabs * len(self.books)
|
|
|
|
@patch.dict("django.conf.settings.FEATURES", {"ENABLE_TEXTBOOK": True})
|
|
def test_textbooks_enabled(self):
|
|
|
|
type_to_reverse_name = {'textbook': 'book', 'pdftextbook': 'pdf_book', 'htmltextbook': 'html_book'}
|
|
|
|
num_textbooks_found = 0
|
|
user = self.create_mock_user(is_staff=False, is_enrolled=True)
|
|
for tab in xmodule_tabs.CourseTabList.iterate_displayable(self.course, user=user):
|
|
# verify all textbook type tabs
|
|
if tab.type == 'single_textbook':
|
|
book_type, book_index = tab.tab_id.split("/", 1)
|
|
expected_link = self.reverse(
|
|
type_to_reverse_name[book_type],
|
|
args=[str(self.course.id), book_index]
|
|
)
|
|
assert tab.link_func(self.course, self.reverse) == expected_link
|
|
assert tab.name.startswith(f'Book{book_index}')
|
|
num_textbooks_found = num_textbooks_found + 1
|
|
assert num_textbooks_found == self.num_textbooks
|
|
|
|
|
|
class StaticTabDateTestCase(LoginEnrollmentTestCase, SharedModuleStoreTestCase):
|
|
"""Test cases for Static Tab Dates."""
|
|
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
super().setUpClass()
|
|
cls.course = CourseFactory.create()
|
|
cls.page = ItemFactory.create(
|
|
category="static_tab", parent_location=cls.course.location,
|
|
data="OOGIE BLOOGIE", display_name="new_tab"
|
|
)
|
|
cls.course.tabs.append(xmodule_tabs.CourseTab.load('static_tab', name='New Tab', url_slug='new_tab'))
|
|
cls.course.save()
|
|
|
|
def test_logged_in(self):
|
|
self.setup_user()
|
|
url = reverse('static_tab', args=[str(self.course.id), 'new_tab'])
|
|
resp = self.client.get(url)
|
|
self.assertContains(resp, "OOGIE BLOOGIE")
|
|
|
|
def test_anonymous_user(self):
|
|
url = reverse('static_tab', args=[str(self.course.id), 'new_tab'])
|
|
resp = self.client.get(url)
|
|
self.assertContains(resp, "OOGIE BLOOGIE")
|
|
|
|
def test_invalid_course_key(self):
|
|
self.setup_user()
|
|
self.addCleanup(set_current_request, None)
|
|
request = get_mock_request(self.user)
|
|
with pytest.raises(Http404):
|
|
StaticCourseTabView().get(request, course_id='edX/toy', tab_slug='new_tab')
|
|
|
|
def test_get_static_tab_fragment(self):
|
|
self.setup_user()
|
|
course = get_course_by_id(self.course.id)
|
|
self.addCleanup(set_current_request, None)
|
|
request = get_mock_request(self.user)
|
|
tab = xmodule_tabs.CourseTabList.get_tab_by_slug(course.tabs, 'new_tab')
|
|
|
|
# Test render works okay
|
|
tab_content = get_static_tab_fragment(request, course, tab).content
|
|
assert str(self.course.id) in tab_content
|
|
assert 'static_tab' in tab_content
|
|
|
|
# Test when render raises an exception
|
|
with patch('lms.djangoapps.courseware.views.views.get_module') as mock_module_render:
|
|
mock_module_render.return_value = MagicMock(
|
|
render=Mock(side_effect=Exception('Render failed!'))
|
|
)
|
|
static_tab_content = get_static_tab_fragment(request, course, tab).content
|
|
assert 'this module is temporarily unavailable' in static_tab_content
|
|
|
|
|
|
class StaticTabDateTestCaseXML(LoginEnrollmentTestCase, ModuleStoreTestCase):
|
|
"""
|
|
Tests for the static tab dates of an XML course
|
|
"""
|
|
|
|
def setUp(self):
|
|
"""
|
|
Set up the tests
|
|
"""
|
|
super().setUp()
|
|
|
|
# The following XML test course (which lives at common/test/data/2014)
|
|
# is closed; we're testing that tabs still appear when
|
|
# the course is already closed
|
|
self.xml_course_key = self.store.make_course_key('edX', 'detached_pages', '2014')
|
|
import_course_from_xml(
|
|
self.store,
|
|
self.user.id,
|
|
TEST_DATA_DIR,
|
|
source_dirs=['2014'],
|
|
static_content_store=None,
|
|
target_id=self.xml_course_key,
|
|
raise_on_failure=True,
|
|
create_if_not_present=True,
|
|
)
|
|
|
|
# this text appears in the test course's tab
|
|
# common/test/data/2014/tabs/8e4cce2b4aaf4ba28b1220804619e41f.html
|
|
self.xml_data = "static 463139"
|
|
self.xml_url = "8e4cce2b4aaf4ba28b1220804619e41f"
|
|
|
|
@patch.dict('django.conf.settings.FEATURES', {'DISABLE_START_DATES': False})
|
|
def test_logged_in_xml(self):
|
|
self.setup_user()
|
|
url = reverse('static_tab', args=[str(self.xml_course_key), self.xml_url])
|
|
resp = self.client.get(url)
|
|
self.assertContains(resp, self.xml_data)
|
|
|
|
@patch.dict('django.conf.settings.FEATURES', {'DISABLE_START_DATES': False})
|
|
def test_anonymous_user_xml(self):
|
|
url = reverse('static_tab', args=[str(self.xml_course_key), self.xml_url])
|
|
resp = self.client.get(url)
|
|
self.assertContains(resp, self.xml_data)
|
|
|
|
|
|
@patch.dict('django.conf.settings.FEATURES', {'ENTRANCE_EXAMS': True})
|
|
class EntranceExamsTabsTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase, MilestonesTestCaseMixin):
|
|
"""
|
|
Validate tab behavior when dealing with Entrance Exams
|
|
"""
|
|
@patch.dict('django.conf.settings.FEATURES', {'ENTRANCE_EXAMS': True})
|
|
def setUp(self):
|
|
"""
|
|
Test case scaffolding
|
|
"""
|
|
super().setUp()
|
|
|
|
self.course = CourseFactory.create()
|
|
self.extra_tab_2 = ItemFactory.create(
|
|
category="static_tab", parent_location=self.course.location,
|
|
data="Extra Tab", display_name="Extra Tab 2"
|
|
)
|
|
self.extra_tab_3 = ItemFactory.create(
|
|
category="static_tab", parent_location=self.course.location,
|
|
data="Extra Tab", display_name="Extra Tab 3"
|
|
)
|
|
self.setup_user()
|
|
self.enroll(self.course)
|
|
self.user.is_staff = True
|
|
self.relationship_types = get_milestone_relationship_types()
|
|
self.addCleanup(set_current_request, None)
|
|
|
|
def test_get_course_tabs_list_entrance_exam_enabled(self):
|
|
"""
|
|
Unit Test: test_get_course_tabs_list_entrance_exam_enabled
|
|
"""
|
|
entrance_exam = ItemFactory.create(
|
|
category="chapter",
|
|
parent_location=self.course.location,
|
|
display_name="Entrance Exam",
|
|
is_entrance_exam=True
|
|
)
|
|
milestone = {
|
|
'name': 'Test Milestone',
|
|
'namespace': f'{str(self.course.id)}.entrance_exams',
|
|
'description': 'Testing Courseware Tabs'
|
|
}
|
|
self.user.is_staff = False
|
|
self.course.entrance_exam_enabled = True
|
|
self.course.entrance_exam_id = str(entrance_exam.location)
|
|
milestone = add_milestone(milestone)
|
|
add_course_milestone(
|
|
str(self.course.id),
|
|
self.relationship_types['REQUIRES'],
|
|
milestone
|
|
)
|
|
add_course_content_milestone(
|
|
str(self.course.id),
|
|
str(entrance_exam.location),
|
|
self.relationship_types['FULFILLS'],
|
|
milestone
|
|
)
|
|
course_tab_list = get_course_tab_list(self.user, self.course)
|
|
assert len(course_tab_list) == 1
|
|
assert course_tab_list[0]['tab_id'] == 'courseware'
|
|
assert course_tab_list[0]['name'] == 'Entrance Exam'
|
|
|
|
def test_get_course_tabs_list_skipped_entrance_exam(self):
|
|
"""
|
|
Tests tab list is not limited if user is allowed to skip entrance exam.
|
|
"""
|
|
#create a user
|
|
student = UserFactory()
|
|
# login as instructor hit skip entrance exam api in instructor app
|
|
instructor = InstructorFactory(course_key=self.course.id)
|
|
self.client.logout()
|
|
self.client.login(username=instructor.username, password='test')
|
|
|
|
url = reverse('mark_student_can_skip_entrance_exam', kwargs={'course_id': str(self.course.id)})
|
|
response = self.client.post(url, {
|
|
'unique_student_identifier': student.email,
|
|
})
|
|
assert response.status_code == 200
|
|
|
|
# log in again as student
|
|
self.client.logout()
|
|
self.login(self.email, self.password)
|
|
course_tab_list = get_course_tab_list(self.user, self.course)
|
|
assert len(course_tab_list) == 4
|
|
|
|
def test_course_tabs_list_for_staff_members(self):
|
|
"""
|
|
Tests tab list is not limited if user is member of staff
|
|
and has not passed entrance exam.
|
|
"""
|
|
# Login as member of staff
|
|
self.client.logout()
|
|
staff_user = StaffFactory(course_key=self.course.id)
|
|
self.client.login(username=staff_user.username, password='test')
|
|
course_tab_list = get_course_tab_list(staff_user, self.course)
|
|
assert len(course_tab_list) == 4
|
|
|
|
|
|
class TextBookCourseViewsTestCase(LoginEnrollmentTestCase, SharedModuleStoreTestCase):
|
|
"""
|
|
Validate tab behavior when dealing with textbooks.
|
|
"""
|
|
MODULESTORE = TEST_DATA_MIXED_MODULESTORE
|
|
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
super().setUpClass()
|
|
cls.course = CourseFactory.create()
|
|
|
|
def setUp(self):
|
|
super().setUp()
|
|
|
|
self.set_up_books(2)
|
|
self.setup_user()
|
|
self.enroll(self.course)
|
|
self.num_textbook_tabs = sum(1 for tab in self.course.tabs if tab.type in [
|
|
'textbooks', 'pdf_textbooks', 'html_textbooks'
|
|
])
|
|
self.num_textbooks = self.num_textbook_tabs * len(self.books)
|
|
|
|
def set_up_books(self, num_books):
|
|
"""Initializes the textbooks in the course and adds the given number of books to each textbook"""
|
|
self.books = [MagicMock() for _ in range(num_books)]
|
|
for book_index, book in enumerate(self.books):
|
|
book.title = f'Book{book_index}'
|
|
self.course.textbooks = self.books
|
|
self.course.pdf_textbooks = self.books
|
|
self.course.html_textbooks = self.books
|
|
|
|
def test_pdf_textbook_tabs(self):
|
|
"""
|
|
Test that all textbooks tab links generating correctly.
|
|
"""
|
|
type_to_reverse_name = {'textbook': 'book', 'pdftextbook': 'pdf_book', 'htmltextbook': 'html_book'}
|
|
self.addCleanup(set_current_request, None)
|
|
course_tab_list = get_course_tab_list(self.user, self.course)
|
|
num_of_textbooks_found = 0
|
|
for tab in course_tab_list:
|
|
# Verify links of all textbook type tabs.
|
|
if tab.type == 'single_textbook':
|
|
book_type, book_index = tab.tab_id.split("/", 1)
|
|
expected_link = reverse(
|
|
type_to_reverse_name[book_type],
|
|
args=[str(self.course.id), book_index]
|
|
)
|
|
tab_link = tab.link_func(self.course, reverse)
|
|
assert tab_link == expected_link
|
|
num_of_textbooks_found += 1
|
|
assert num_of_textbooks_found == self.num_textbooks
|
|
|
|
@patch.dict("django.conf.settings.FEATURES", {"ENABLE_TEXTBOOK": False})
|
|
def test_textbooks_disabled(self):
|
|
tab = xmodule_tabs.CourseTab.load('textbooks')
|
|
assert not tab.is_enabled(self.course, self.user)
|
|
|
|
|
|
class TabListTestCase(TabTestCase):
|
|
"""Base class for Test cases involving tab lists."""
|
|
|
|
def setUp(self):
|
|
super().setUp()
|
|
|
|
# invalid tabs
|
|
self.invalid_tabs = [
|
|
# less than 2 tabs
|
|
[{'type': CoursewareTab.type}],
|
|
# missing course_info
|
|
[{'type': CoursewareTab.type}, {'type': 'discussion', 'name': 'fake_name'}],
|
|
[{'type': 'unknown_type'}],
|
|
# incorrect order
|
|
[{'type': 'discussion', 'name': 'fake_name'},
|
|
{'type': CourseInfoTab.type, 'name': 'fake_name'}, {'type': CoursewareTab.type}],
|
|
]
|
|
|
|
# tab types that should appear only once
|
|
unique_tab_types = [
|
|
CoursewareTab.type,
|
|
CourseInfoTab.type,
|
|
'textbooks',
|
|
'pdf_textbooks',
|
|
'html_textbooks',
|
|
]
|
|
|
|
for unique_tab_type in unique_tab_types:
|
|
self.invalid_tabs.append([
|
|
{'type': CoursewareTab.type},
|
|
{'type': CourseInfoTab.type, 'name': 'fake_name'},
|
|
# add the unique tab multiple times
|
|
{'type': unique_tab_type},
|
|
{'type': unique_tab_type},
|
|
])
|
|
|
|
# valid tabs
|
|
self.valid_tabs = [
|
|
# any empty list is valid because a default list of tabs will be
|
|
# generated to replace the empty list.
|
|
[],
|
|
# all valid tabs
|
|
[
|
|
{'type': CoursewareTab.type},
|
|
{'type': CourseInfoTab.type, 'name': 'fake_name'},
|
|
{'type': DatesTab.type}, # Add this even though we filter it out, for testing purposes
|
|
{'type': 'discussion', 'name': 'fake_name'},
|
|
{'type': ExternalLinkCourseTab.type, 'name': 'fake_name', 'link': 'fake_link'},
|
|
{'type': ExternalLinkCourseTab.type, 'name': 'fake_name', 'link': 'fake_link'},
|
|
{'type': 'textbooks'},
|
|
{'type': 'pdf_textbooks'},
|
|
{'type': 'html_textbooks'},
|
|
{'type': ProgressTab.type, 'name': 'fake_name'},
|
|
{'type': xmodule_tabs.StaticTab.type, 'name': 'fake_name', 'url_slug': 'schlug'},
|
|
{'type': 'syllabus'},
|
|
],
|
|
# with external discussion
|
|
[
|
|
{'type': CoursewareTab.type},
|
|
{'type': CourseInfoTab.type, 'name': 'fake_name'},
|
|
{'type': ExternalDiscussionCourseTab.type, 'name': 'fake_name', 'link': 'fake_link'}
|
|
],
|
|
]
|
|
|
|
self.all_valid_tab_list = xmodule_tabs.CourseTabList().from_json(self.valid_tabs[1])
|
|
|
|
|
|
class ValidateTabsTestCase(TabListTestCase):
|
|
"""Test cases for validating tabs."""
|
|
|
|
def test_validate_tabs(self):
|
|
tab_list = xmodule_tabs.CourseTabList()
|
|
for invalid_tab_list in self.invalid_tabs:
|
|
with pytest.raises(xmodule_tabs.InvalidTabsException):
|
|
tab_list.from_json(invalid_tab_list)
|
|
|
|
for valid_tab_list in self.valid_tabs:
|
|
from_json_result = tab_list.from_json(valid_tab_list)
|
|
assert len(from_json_result) == len(valid_tab_list)
|
|
|
|
def test_invalid_tab_type(self):
|
|
"""
|
|
Verifies that having an unrecognized tab type does not cause
|
|
the tabs to be undisplayable.
|
|
"""
|
|
tab_list = xmodule_tabs.CourseTabList()
|
|
assert len(tab_list.from_json([{'type': CoursewareTab.type},
|
|
{'type': CourseInfoTab.type, 'name': 'fake_name'},
|
|
{'type': 'no_such_type'}])) == 2
|
|
|
|
|
|
class CourseTabListTestCase(TabListTestCase):
|
|
"""Testing the generator method for iterating through displayable tabs"""
|
|
|
|
def setUp(self):
|
|
super().setUp()
|
|
self.addCleanup(set_current_request, None)
|
|
|
|
def has_tab(self, tab_list, tab_type):
|
|
""" Searches the given lab_list for a given tab_type. """
|
|
for tab in tab_list:
|
|
if tab.type == tab_type:
|
|
return True
|
|
return False
|
|
|
|
def test_initialize_default_without_syllabus(self):
|
|
self.course.tabs = []
|
|
self.course.syllabus_present = False
|
|
xmodule_tabs.CourseTabList.initialize_default(self.course)
|
|
assert not self.has_tab(self.course.tabs, 'syllabus')
|
|
|
|
def test_initialize_default_with_syllabus(self):
|
|
self.course.tabs = []
|
|
self.course.syllabus_present = True
|
|
xmodule_tabs.CourseTabList.initialize_default(self.course)
|
|
assert self.has_tab(self.course.tabs, 'syllabus')
|
|
|
|
def test_initialize_default_with_external_link(self):
|
|
self.course.tabs = []
|
|
self.course.discussion_link = "other_discussion_link"
|
|
xmodule_tabs.CourseTabList.initialize_default(self.course)
|
|
assert self.has_tab(self.course.tabs, 'external_discussion')
|
|
assert not self.has_tab(self.course.tabs, 'discussion')
|
|
|
|
def test_initialize_default_without_external_link(self):
|
|
self.course.tabs = []
|
|
self.course.discussion_link = ""
|
|
xmodule_tabs.CourseTabList.initialize_default(self.course)
|
|
assert not self.has_tab(self.course.tabs, 'external_discussion')
|
|
assert self.has_tab(self.course.tabs, 'discussion')
|
|
|
|
@patch.dict("django.conf.settings.FEATURES", {
|
|
"ENABLE_TEXTBOOK": True,
|
|
"ENABLE_DISCUSSION_SERVICE": True,
|
|
"ENABLE_EDXNOTES": True,
|
|
})
|
|
def test_iterate_displayable(self):
|
|
self.course.hide_progress_tab = False
|
|
|
|
# create 1 book per textbook type
|
|
self.set_up_books(1)
|
|
|
|
# initialize the course tabs to a list of all valid tabs
|
|
self.course.tabs = self.all_valid_tab_list
|
|
|
|
# enumerate the tabs with no user
|
|
expected = [tab.type for tab in
|
|
xmodule_tabs.CourseTabList.iterate_displayable(self.course, inline_collections=False)]
|
|
actual = [tab.type for tab in self.course.tabs if tab.is_enabled(self.course, user=None)]
|
|
assert actual == expected
|
|
|
|
# enumerate the tabs with a staff user
|
|
user = UserFactory(is_staff=True)
|
|
CourseEnrollment.enroll(user, self.course.id)
|
|
for i, tab in enumerate(xmodule_tabs.CourseTabList.iterate_displayable(self.course, user=user)):
|
|
if getattr(tab, 'is_collection_item', False):
|
|
# a collection item was found as a result of a collection tab
|
|
assert getattr(self.course.tabs[i], 'is_collection', False)
|
|
else:
|
|
# all other tabs must match the expected type
|
|
assert tab.type == self.course.tabs[i].type
|
|
|
|
# test including non-empty collections
|
|
assert {'type': 'html_textbooks'} in\
|
|
list(xmodule_tabs.CourseTabList.iterate_displayable(self.course, inline_collections=False))
|
|
|
|
# test not including empty collections
|
|
self.course.html_textbooks = []
|
|
assert {'type': 'html_textbooks'} not in\
|
|
list(xmodule_tabs.CourseTabList.iterate_displayable(self.course, inline_collections=False))
|
|
|
|
def test_get_tab_by_methods(self):
|
|
"""Tests the get_tab methods in CourseTabList"""
|
|
self.course.tabs = self.all_valid_tab_list
|
|
for tab in self.course.tabs:
|
|
|
|
# get tab by type
|
|
assert xmodule_tabs.CourseTabList.get_tab_by_type(self.course.tabs, tab.type) == tab
|
|
|
|
# get tab by id
|
|
assert xmodule_tabs.CourseTabList.get_tab_by_id(self.course.tabs, tab.tab_id) == tab
|
|
|
|
def test_course_tabs_staff_only(self):
|
|
"""
|
|
Tests the static tabs that available only for instructor
|
|
"""
|
|
self.course.tabs.append(xmodule_tabs.CourseTab.load('static_tab', name='Static Tab Free',
|
|
url_slug='extra_tab_1',
|
|
course_staff_only=False))
|
|
self.course.tabs.append(xmodule_tabs.CourseTab.load('static_tab', name='Static Tab Instructors Only',
|
|
url_slug='extra_tab_2',
|
|
course_staff_only=True))
|
|
self.course.save()
|
|
|
|
user = self.create_mock_user(is_staff=False, is_enrolled=True)
|
|
self.addCleanup(set_current_request, None)
|
|
course_tab_list = get_course_tab_list(user, self.course)
|
|
name_list = [x.name for x in course_tab_list]
|
|
assert 'Static Tab Free' in name_list
|
|
assert 'Static Tab Instructors Only' not in name_list
|
|
|
|
# Login as member of staff
|
|
self.client.logout()
|
|
staff_user = StaffFactory(course_key=self.course.id)
|
|
self.client.login(username=staff_user.username, password='test')
|
|
course_tab_list_staff = get_course_tab_list(staff_user, self.course)
|
|
name_list_staff = [x.name for x in course_tab_list_staff]
|
|
assert 'Static Tab Free' in name_list_staff
|
|
assert 'Static Tab Instructors Only' in name_list_staff
|
|
|
|
|
|
class ProgressTestCase(TabTestCase):
|
|
"""Test cases for Progress Tab."""
|
|
|
|
def check_progress_tab(self):
|
|
"""Helper function for verifying the progress tab."""
|
|
return self.check_tab(
|
|
tab_class=ProgressTab,
|
|
dict_tab={'type': ProgressTab.type, 'name': 'same'},
|
|
expected_link=self.reverse('progress', args=[str(self.course.id)]),
|
|
expected_tab_id=ProgressTab.type,
|
|
invalid_dict_tab=None,
|
|
)
|
|
|
|
@patch('common.djangoapps.student.models.CourseEnrollment.is_enrolled')
|
|
def test_progress(self, is_enrolled):
|
|
is_enrolled.return_value = True
|
|
self.course.hide_progress_tab = False
|
|
tab = self.check_progress_tab()
|
|
self.check_can_display_results(
|
|
tab, for_staff_only=True, for_enrolled_users_only=True
|
|
)
|
|
|
|
self.course.hide_progress_tab = True
|
|
self.check_progress_tab()
|
|
self.check_can_display_results(
|
|
tab, for_staff_only=True, for_enrolled_users_only=True, expected_value=False
|
|
)
|
|
|
|
|
|
class StaticTabTestCase(TabTestCase):
|
|
"""Test cases for Static Tab."""
|
|
|
|
def test_static_tab(self):
|
|
|
|
url_slug = 'schmug'
|
|
|
|
tab = self.check_tab(
|
|
tab_class=xmodule_tabs.StaticTab,
|
|
dict_tab={'type': xmodule_tabs.StaticTab.type, 'name': 'same', 'url_slug': url_slug},
|
|
expected_link=self.reverse('static_tab', args=[str(self.course.id), url_slug]),
|
|
expected_tab_id='static_tab_schmug',
|
|
invalid_dict_tab=self.fake_dict_tab,
|
|
)
|
|
self.check_can_display_results(tab)
|
|
self.check_get_and_set_method_for_key(tab, 'url_slug')
|
|
|
|
|
|
class CourseInfoTabTestCase(TabTestCase):
|
|
"""Test cases for the course info tab."""
|
|
def setUp(self): # lint-amnesty, pylint: disable=super-method-not-called
|
|
self.user = self.create_mock_user()
|
|
self.addCleanup(set_current_request, None)
|
|
|
|
@override_waffle_flag(DISABLE_UNIFIED_COURSE_TAB_FLAG, active=True)
|
|
def test_default_tab(self):
|
|
# Verify that the course info tab is the first tab
|
|
tabs = get_course_tab_list(self.user, self.course)
|
|
assert tabs[0].type == 'course_info'
|
|
|
|
@override_waffle_flag(DISABLE_UNIFIED_COURSE_TAB_FLAG, active=False)
|
|
def test_default_tab_for_new_course_experience(self):
|
|
# Verify that the unified course experience hides the course info tab
|
|
tabs = get_course_tab_list(self.user, self.course)
|
|
assert tabs[0].type == 'courseware'
|
|
|
|
# TODO: LEARNER-611 - remove once course_info is removed.
|
|
@override_waffle_flag(DISABLE_UNIFIED_COURSE_TAB_FLAG, active=False)
|
|
def test_default_tab_for_displayable(self):
|
|
tabs = xmodule_tabs.CourseTabList.iterate_displayable(self.course, self.user)
|
|
for i, tab in enumerate(tabs):
|
|
if i == 0:
|
|
assert tab.type == 'course_info'
|
|
|
|
|
|
@ddt.ddt
|
|
class DiscussionLinkTestCase(TabTestCase):
|
|
"""Test cases for discussion link tab."""
|
|
|
|
def setUp(self):
|
|
super().setUp()
|
|
|
|
self.tabs_with_discussion = [
|
|
xmodule_tabs.CourseTab.load('discussion'),
|
|
]
|
|
self.tabs_without_discussion = [
|
|
]
|
|
|
|
@staticmethod
|
|
def _reverse(course):
|
|
"""Custom reverse function"""
|
|
def reverse_discussion_link(viewname, args):
|
|
"""reverse lookup for discussion link"""
|
|
if viewname == "forum_form_discussion" and args == [str(course.id)]:
|
|
return "default_discussion_link"
|
|
return reverse_discussion_link
|
|
|
|
def check_discussion(
|
|
self, tab_list,
|
|
expected_discussion_link,
|
|
expected_can_display_value,
|
|
discussion_link_in_course="",
|
|
is_staff=True,
|
|
is_enrolled=True,
|
|
):
|
|
"""Helper function to verify whether the discussion tab exists and can be displayed"""
|
|
self.course.tabs = tab_list
|
|
self.course.discussion_link = discussion_link_in_course
|
|
discussion_tab = xmodule_tabs.CourseTabList.get_discussion(self.course)
|
|
user = self.create_mock_user(is_staff=is_staff, is_enrolled=is_enrolled)
|
|
with patch('common.djangoapps.student.models.CourseEnrollment.is_enrolled') as check_is_enrolled:
|
|
check_is_enrolled.return_value = is_enrolled
|
|
assert ((discussion_tab is not None) and self.is_tab_enabled(discussion_tab, self.course, user) and
|
|
(discussion_tab.link_func(self.course, self._reverse(self.course))
|
|
== expected_discussion_link)) == expected_can_display_value
|
|
|
|
@patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": False})
|
|
def test_explicit_discussion_link(self):
|
|
"""Test that setting discussion_link overrides everything else"""
|
|
self.check_discussion(
|
|
tab_list=self.tabs_with_discussion,
|
|
discussion_link_in_course="other_discussion_link",
|
|
expected_discussion_link="other_discussion_link",
|
|
expected_can_display_value=True,
|
|
)
|
|
|
|
@patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": False})
|
|
def test_discussions_disabled(self):
|
|
"""Test that other cases return None with discussions disabled"""
|
|
for tab_list in [[], self.tabs_with_discussion, self.tabs_without_discussion]:
|
|
self.check_discussion(
|
|
tab_list=tab_list,
|
|
expected_discussion_link=not None,
|
|
expected_can_display_value=False,
|
|
)
|
|
|
|
@patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True})
|
|
@ddt.data(Provider.OPEN_EDX, Provider.LEGACY)
|
|
def test_tabs_with_discussion(self, provider):
|
|
"""Test a course with a discussion tab configured"""
|
|
config = DiscussionsConfiguration.get(self.course.id)
|
|
config.provider_type = provider
|
|
config.save()
|
|
if provider == Provider.OPEN_EDX:
|
|
expected_link = get_learning_mfe_home_url(course_key=self.course.id, url_fragment="discussion")
|
|
else:
|
|
expected_link = "default_discussion_link"
|
|
self.check_discussion(
|
|
tab_list=self.tabs_with_discussion,
|
|
expected_discussion_link=expected_link,
|
|
expected_can_display_value=True,
|
|
)
|
|
|
|
@patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True})
|
|
def test_tabs_without_discussion(self):
|
|
"""Test a course with tabs configured but without a discussion tab"""
|
|
self.check_discussion(
|
|
tab_list=self.tabs_without_discussion,
|
|
expected_discussion_link=not None,
|
|
expected_can_display_value=False,
|
|
)
|
|
|
|
@patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True})
|
|
@ddt.data(Provider.OPEN_EDX, Provider.LEGACY)
|
|
def test_tabs_enrolled_or_staff(self, provider):
|
|
config = DiscussionsConfiguration.get(self.course.id)
|
|
config.provider_type = provider
|
|
config.save()
|
|
for is_enrolled, is_staff in [(True, False), (False, True)]:
|
|
if provider == Provider.OPEN_EDX:
|
|
expected_link = get_learning_mfe_home_url(course_key=self.course.id, url_fragment="discussion")
|
|
else:
|
|
expected_link = "default_discussion_link"
|
|
self.check_discussion(
|
|
tab_list=self.tabs_with_discussion,
|
|
expected_discussion_link=expected_link,
|
|
expected_can_display_value=True,
|
|
is_enrolled=is_enrolled,
|
|
is_staff=is_staff
|
|
)
|
|
|
|
@patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True})
|
|
def test_tabs_not_enrolled_or_staff(self):
|
|
is_enrolled = is_staff = False
|
|
self.check_discussion(
|
|
tab_list=self.tabs_with_discussion,
|
|
expected_discussion_link="default_discussion_link",
|
|
expected_can_display_value=False,
|
|
is_enrolled=is_enrolled,
|
|
is_staff=is_staff
|
|
)
|
|
|
|
|
|
class DatesTabTestCase(TabListTestCase):
|
|
"""Test cases for dates tab"""
|
|
@patch('common.djangoapps.student.models.CourseEnrollment.is_enrolled')
|
|
def test_dates_tab_disabled_if_unenrolled(self, is_enrolled):
|
|
tab = DatesTab({'type': DatesTab.type, 'name': 'dates'})
|
|
|
|
is_enrolled.return_value = False
|
|
unenrolled_user = self.create_mock_user(is_staff=False, is_enrolled=False)
|
|
assert not self.is_tab_enabled(tab, self.course, unenrolled_user)
|
|
|
|
staff_user = self.create_mock_user(is_staff=True, is_enrolled=False)
|
|
assert self.is_tab_enabled(tab, self.course, staff_user)
|
|
|
|
is_enrolled.return_value = True
|
|
enrolled_user = self.create_mock_user(is_staff=False, is_enrolled=True)
|
|
assert self.is_tab_enabled(tab, self.course, enrolled_user)
|
|
|
|
def test_singular_dates_tab(self):
|
|
"""Test cases for making sure no persisted dates tab is surfaced"""
|
|
user = self.create_mock_user()
|
|
self.course.tabs = self.all_valid_tab_list
|
|
self.course.save()
|
|
|
|
# Verify that there is a dates tab in the modulestore
|
|
has_dates_tab = False
|
|
for tab in self.course.tabs:
|
|
if tab.type == 'dates':
|
|
has_dates_tab = True
|
|
assert has_dates_tab
|
|
|
|
# Verify that there is only 1 'dates' tab in the returned result from get_course_tab_list()
|
|
tabs = get_course_tab_list(user, self.course)
|
|
num_dates_tabs = 0
|
|
for tab in tabs:
|
|
if tab.type == 'dates':
|
|
num_dates_tabs += 1
|
|
assert num_dates_tabs == 1
|