test: Drop tests related to the legacy course home view.
This view has been replaced with a new courses API endpoint `/contentstore/v2/home/courses` which has its own tests.
This commit is contained in:
@@ -8,495 +8,32 @@ import json
|
||||
from unittest import mock, skip
|
||||
|
||||
import ddt
|
||||
import lxml
|
||||
import pytz
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.test.utils import override_settings
|
||||
from django.utils.translation import gettext as _
|
||||
from edx_toggles.toggles.testutils import override_waffle_flag
|
||||
from opaque_keys.edx.locator import CourseLocator
|
||||
from search.api import perform_search
|
||||
|
||||
from cms.djangoapps.contentstore import toggles
|
||||
from cms.djangoapps.contentstore.courseware_index import CoursewareSearchIndexer, SearchIndexingError
|
||||
from cms.djangoapps.contentstore.tests.utils import CourseTestCase
|
||||
from cms.djangoapps.contentstore.utils import (
|
||||
add_instructor,
|
||||
get_proctored_exam_settings_url,
|
||||
reverse_course_url,
|
||||
reverse_usage_url
|
||||
)
|
||||
from common.djangoapps.course_action_state.managers import CourseRerunUIStateManager
|
||||
from common.djangoapps.course_action_state.models import CourseRerunState
|
||||
from common.djangoapps.student.auth import has_course_author_access
|
||||
from common.djangoapps.student.roles import CourseStaffRole, GlobalStaff, LibraryUserRole
|
||||
from common.djangoapps.student.tests.factories import UserFactory
|
||||
from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory
|
||||
from openedx.core.djangoapps.waffle_utils.testutils import WAFFLE_TABLES
|
||||
from xmodule.modulestore.django import modulestore # lint-amnesty, pylint: disable=wrong-import-order
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError # lint-amnesty, pylint: disable=wrong-import-order
|
||||
from xmodule.modulestore.tests.django_utils import TEST_DATA_SPLIT_MODULESTORE
|
||||
from xmodule.modulestore.tests.factories import CourseFactory, BlockFactory, LibraryFactory, check_mongo_calls # lint-amnesty, pylint: disable=wrong-import-order
|
||||
from xmodule.modulestore.tests.factories import BlockFactory, check_mongo_calls # lint-amnesty, pylint: disable=wrong-import-order
|
||||
|
||||
from ..course import _deprecated_blocks_info, course_outline_initial_state, reindex_course_and_check_access
|
||||
from cms.djangoapps.contentstore.xblock_storage_handlers.view_handlers import VisibilityState, create_xblock_info
|
||||
|
||||
|
||||
@override_waffle_flag(toggles.LEGACY_STUDIO_HOME, True)
|
||||
@override_waffle_flag(toggles.LEGACY_STUDIO_COURSE_OUTLINE, True)
|
||||
class TestCourseIndex(CourseTestCase):
|
||||
"""
|
||||
Unit tests for getting the list of courses and the course outline.
|
||||
"""
|
||||
|
||||
MODULESTORE = TEST_DATA_SPLIT_MODULESTORE
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Add a course with odd characters in the fields
|
||||
"""
|
||||
super().setUp()
|
||||
# had a problem where index showed course but has_access failed to retrieve it for non-staff
|
||||
self.odd_course = CourseFactory.create(
|
||||
org='test.org_1-2',
|
||||
number='test-2.3_course',
|
||||
display_name='dotted.course.name-2',
|
||||
)
|
||||
CourseOverviewFactory.create(
|
||||
id=self.odd_course.id,
|
||||
org=self.odd_course.org,
|
||||
display_name=self.odd_course.display_name,
|
||||
)
|
||||
|
||||
def check_courses_on_index(self, authed_client, expected_course_tab_len):
|
||||
"""
|
||||
Test that the React course listing is present.
|
||||
"""
|
||||
index_url = '/home/'
|
||||
index_response = authed_client.get(index_url, {}, HTTP_ACCEPT='text/html')
|
||||
parsed_html = lxml.html.fromstring(index_response.content)
|
||||
courses_tab = parsed_html.find_class('react-course-listing')
|
||||
self.assertEqual(len(courses_tab), expected_course_tab_len)
|
||||
|
||||
def test_libraries_on_index(self):
|
||||
"""
|
||||
Test that the library tab is present.
|
||||
"""
|
||||
def _assert_library_tab_present(response):
|
||||
"""
|
||||
Asserts there's a library tab.
|
||||
"""
|
||||
parsed_html = lxml.html.fromstring(response.content)
|
||||
library_tab = parsed_html.find_class('react-library-listing')
|
||||
self.assertEqual(len(library_tab), 1)
|
||||
|
||||
# Add a library:
|
||||
lib1 = LibraryFactory.create() # lint-amnesty, pylint: disable=unused-variable
|
||||
|
||||
index_url = '/home/'
|
||||
index_response = self.client.get(index_url, {}, HTTP_ACCEPT='text/html')
|
||||
_assert_library_tab_present(index_response)
|
||||
|
||||
# Make sure libraries are visible to non-staff users too
|
||||
self.client.logout()
|
||||
non_staff_user, non_staff_userpassword = self.create_non_staff_user()
|
||||
lib2 = LibraryFactory.create(user_id=non_staff_user.id)
|
||||
LibraryUserRole(lib2.location.library_key).add_users(non_staff_user)
|
||||
self.client.login(username=non_staff_user.username, password=non_staff_userpassword)
|
||||
index_response = self.client.get(index_url, {}, HTTP_ACCEPT='text/html')
|
||||
_assert_library_tab_present(index_response)
|
||||
|
||||
def test_is_staff_access(self):
|
||||
"""
|
||||
Test that people with is_staff see the courses and can navigate into them
|
||||
"""
|
||||
self.check_courses_on_index(self.client, 1)
|
||||
|
||||
def test_negative_conditions(self):
|
||||
"""
|
||||
Test the error conditions for the access
|
||||
"""
|
||||
outline_url = reverse_course_url('course_handler', self.course.id)
|
||||
# register a non-staff member and try to delete the course branch
|
||||
non_staff_client, _ = self.create_non_staff_authed_user_client()
|
||||
response = non_staff_client.delete(outline_url, {}, HTTP_ACCEPT='application/json')
|
||||
if self.course.id.deprecated:
|
||||
self.assertEqual(response.status_code, 404)
|
||||
else:
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
def test_course_staff_access(self):
|
||||
"""
|
||||
Make and register course_staff and ensure they can access the courses
|
||||
"""
|
||||
course_staff_client, course_staff = self.create_non_staff_authed_user_client()
|
||||
for course in [self.course, self.odd_course]:
|
||||
permission_url = reverse_course_url('course_team_handler', course.id, kwargs={'email': course_staff.email})
|
||||
|
||||
self.client.post(
|
||||
permission_url,
|
||||
data=json.dumps({"role": "staff"}),
|
||||
content_type="application/json",
|
||||
HTTP_ACCEPT="application/json",
|
||||
)
|
||||
|
||||
# test access
|
||||
self.check_courses_on_index(course_staff_client, 1)
|
||||
|
||||
def test_json_responses(self):
|
||||
|
||||
outline_url = reverse_course_url('course_handler', self.course.id)
|
||||
chapter = BlockFactory.create(parent_location=self.course.location, category='chapter', display_name="Week 1")
|
||||
lesson = BlockFactory.create(parent_location=chapter.location, category='sequential', display_name="Lesson 1")
|
||||
subsection = BlockFactory.create(
|
||||
parent_location=lesson.location,
|
||||
category='vertical',
|
||||
display_name='Subsection 1'
|
||||
)
|
||||
BlockFactory.create(parent_location=subsection.location, category="video", display_name="My Video")
|
||||
|
||||
resp = self.client.get(outline_url, HTTP_ACCEPT='application/json')
|
||||
|
||||
if self.course.id.deprecated:
|
||||
self.assertEqual(resp.status_code, 404)
|
||||
return
|
||||
|
||||
json_response = json.loads(resp.content.decode('utf-8'))
|
||||
|
||||
# First spot check some values in the root response
|
||||
self.assertEqual(json_response['category'], 'course')
|
||||
self.assertEqual(json_response['id'], str(self.course.location))
|
||||
self.assertEqual(json_response['display_name'], self.course.display_name)
|
||||
self.assertTrue(json_response['published'])
|
||||
self.assertIsNone(json_response['visibility_state'])
|
||||
|
||||
# Now verify the first child
|
||||
children = json_response['child_info']['children']
|
||||
self.assertGreater(len(children), 0)
|
||||
first_child_response = children[0]
|
||||
self.assertEqual(first_child_response['category'], 'chapter')
|
||||
self.assertEqual(first_child_response['id'], str(chapter.location))
|
||||
self.assertEqual(first_child_response['display_name'], 'Week 1')
|
||||
self.assertTrue(json_response['published'])
|
||||
self.assertEqual(first_child_response['visibility_state'], VisibilityState.unscheduled)
|
||||
self.assertGreater(len(first_child_response['child_info']['children']), 0)
|
||||
|
||||
# Finally, validate the entire response for consistency
|
||||
self.assert_correct_json_response(json_response)
|
||||
|
||||
def test_notifications_handler_get(self):
|
||||
state = CourseRerunUIStateManager.State.FAILED
|
||||
action = CourseRerunUIStateManager.ACTION
|
||||
should_display = True
|
||||
|
||||
# try when no notification exists
|
||||
notification_url = reverse_course_url('course_notifications_handler', self.course.id, kwargs={
|
||||
'action_state_id': 1,
|
||||
})
|
||||
|
||||
resp = self.client.get(notification_url, HTTP_ACCEPT='application/json')
|
||||
|
||||
# verify that we get an empty dict out
|
||||
self.assertEqual(resp.status_code, 400)
|
||||
|
||||
# create a test notification
|
||||
rerun_state = CourseRerunState.objects.update_state(
|
||||
course_key=self.course.id,
|
||||
new_state=state,
|
||||
allow_not_found=True
|
||||
)
|
||||
CourseRerunState.objects.update_should_display(
|
||||
entry_id=rerun_state.id,
|
||||
user=UserFactory(),
|
||||
should_display=should_display
|
||||
)
|
||||
|
||||
# try to get information on this notification
|
||||
notification_url = reverse_course_url('course_notifications_handler', self.course.id, kwargs={
|
||||
'action_state_id': rerun_state.id,
|
||||
})
|
||||
resp = self.client.get(notification_url, HTTP_ACCEPT='application/json')
|
||||
|
||||
json_response = json.loads(resp.content.decode('utf-8'))
|
||||
|
||||
self.assertEqual(json_response['state'], state)
|
||||
self.assertEqual(json_response['action'], action)
|
||||
self.assertEqual(json_response['should_display'], should_display)
|
||||
|
||||
def test_notifications_handler_dismiss(self):
|
||||
state = CourseRerunUIStateManager.State.FAILED
|
||||
should_display = True
|
||||
rerun_course_key = CourseLocator(org='testx', course='test_course', run='test_run')
|
||||
|
||||
# add an instructor to this course
|
||||
user2 = UserFactory()
|
||||
add_instructor(rerun_course_key, self.user, user2)
|
||||
|
||||
# create a test notification
|
||||
rerun_state = CourseRerunState.objects.update_state(
|
||||
course_key=rerun_course_key,
|
||||
new_state=state,
|
||||
allow_not_found=True
|
||||
)
|
||||
CourseRerunState.objects.update_should_display(
|
||||
entry_id=rerun_state.id,
|
||||
user=user2,
|
||||
should_display=should_display
|
||||
)
|
||||
|
||||
# try to get information on this notification
|
||||
notification_dismiss_url = reverse_course_url('course_notifications_handler', self.course.id, kwargs={
|
||||
'action_state_id': rerun_state.id,
|
||||
})
|
||||
resp = self.client.delete(notification_dismiss_url)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
with self.assertRaises(CourseRerunState.DoesNotExist):
|
||||
# delete nofications that are dismissed
|
||||
CourseRerunState.objects.get(id=rerun_state.id)
|
||||
|
||||
self.assertFalse(has_course_author_access(user2, rerun_course_key))
|
||||
|
||||
def assert_correct_json_response(self, json_response):
|
||||
"""
|
||||
Asserts that the JSON response is syntactically consistent
|
||||
"""
|
||||
self.assertIsNotNone(json_response['display_name'])
|
||||
self.assertIsNotNone(json_response['id'])
|
||||
self.assertIsNotNone(json_response['category'])
|
||||
self.assertTrue(json_response['published'])
|
||||
if json_response.get('child_info', None):
|
||||
for child_response in json_response['child_info']['children']:
|
||||
self.assert_correct_json_response(child_response)
|
||||
|
||||
def test_course_updates_invalid_url(self):
|
||||
"""
|
||||
Tests the error conditions for the invalid course updates URL.
|
||||
"""
|
||||
# Testing the response code by passing slash separated course id whose format is valid but no course
|
||||
# having this id exists.
|
||||
invalid_course_key = f'{self.course.id}_blah_blah_blah'
|
||||
course_updates_url = reverse_course_url('course_info_handler', invalid_course_key)
|
||||
response = self.client.get(course_updates_url)
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
# Testing the response code by passing split course id whose format is valid but no course
|
||||
# having this id exists.
|
||||
split_course_key = CourseLocator(org='orgASD', course='course_01213', run='Run_0_hhh_hhh_hhh')
|
||||
course_updates_url_split = reverse_course_url('course_info_handler', split_course_key)
|
||||
response = self.client.get(course_updates_url_split)
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
# Testing the response by passing split course id whose format is invalid.
|
||||
invalid_course_id = f'invalid.course.key/{split_course_key}'
|
||||
course_updates_url_split = reverse_course_url('course_info_handler', invalid_course_id)
|
||||
response = self.client.get(course_updates_url_split)
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
def test_course_index_invalid_url(self):
|
||||
"""
|
||||
Tests the error conditions for the invalid course index URL.
|
||||
"""
|
||||
# Testing the response code by passing slash separated course key, no course
|
||||
# having this key exists.
|
||||
invalid_course_key = f'{self.course.id}_some_invalid_run'
|
||||
course_outline_url = reverse_course_url('course_handler', invalid_course_key)
|
||||
response = self.client.get_html(course_outline_url)
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
# Testing the response code by passing split course key, no course
|
||||
# having this key exists.
|
||||
split_course_key = CourseLocator(org='invalid_org', course='course_01111', run='Run_0_invalid')
|
||||
course_outline_url_split = reverse_course_url('course_handler', split_course_key)
|
||||
response = self.client.get_html(course_outline_url_split)
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
def test_course_outline_with_display_course_number_as_none(self):
|
||||
"""
|
||||
Tests course outline when 'display_coursenumber' field is none.
|
||||
"""
|
||||
# Change 'display_coursenumber' field to None and update the course.
|
||||
self.course.display_coursenumber = None
|
||||
updated_course = self.update_course(self.course, self.user.id)
|
||||
|
||||
# Assert that 'display_coursenumber' field has been changed successfully.
|
||||
self.assertEqual(updated_course.display_coursenumber, None)
|
||||
|
||||
# Perform GET request on course outline url with the course id.
|
||||
course_outline_url = reverse_course_url('course_handler', updated_course.id)
|
||||
response = self.client.get_html(course_outline_url)
|
||||
|
||||
# course_handler raise 404 for old mongo course
|
||||
if self.course.id.deprecated:
|
||||
self.assertEqual(response.status_code, 404)
|
||||
return
|
||||
|
||||
# Assert that response code is 200.
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Assert that 'display_course_number' is being set to "" (as display_coursenumber was None).
|
||||
self.assertContains(response, 'display_course_number: ""')
|
||||
|
||||
|
||||
@override_waffle_flag(toggles.LEGACY_STUDIO_HOME, True)
|
||||
@ddt.ddt
|
||||
class TestCourseIndexArchived(CourseTestCase):
|
||||
"""
|
||||
Unit tests for testing the course index list when there are archived courses.
|
||||
"""
|
||||
|
||||
MODULESTORE = TEST_DATA_SPLIT_MODULESTORE
|
||||
|
||||
NOW = datetime.datetime.now(pytz.utc)
|
||||
DAY = datetime.timedelta(days=1)
|
||||
YESTERDAY = NOW - DAY
|
||||
TOMORROW = NOW + DAY
|
||||
|
||||
ORG = 'MyOrg'
|
||||
|
||||
ENABLE_SEPARATE_ARCHIVED_COURSES = settings.FEATURES.copy()
|
||||
ENABLE_SEPARATE_ARCHIVED_COURSES['ENABLE_SEPARATE_ARCHIVED_COURSES'] = True
|
||||
DISABLE_SEPARATE_ARCHIVED_COURSES = settings.FEATURES.copy()
|
||||
DISABLE_SEPARATE_ARCHIVED_COURSES['ENABLE_SEPARATE_ARCHIVED_COURSES'] = False
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Add courses with the end date set to various values
|
||||
"""
|
||||
super().setUp()
|
||||
|
||||
# Base course has no end date (so is active)
|
||||
self.course.end = None
|
||||
self.course.display_name = 'Active Course 1'
|
||||
self.ORG = self.course.location.org
|
||||
self.save_course()
|
||||
CourseOverviewFactory.create(id=self.course.id, org=self.ORG)
|
||||
|
||||
# Active course has end date set to tomorrow
|
||||
self.active_course = CourseFactory.create(
|
||||
display_name='Active Course 2',
|
||||
org=self.ORG,
|
||||
end=self.TOMORROW,
|
||||
)
|
||||
CourseOverviewFactory.create(
|
||||
id=self.active_course.id,
|
||||
org=self.ORG,
|
||||
end=self.TOMORROW,
|
||||
)
|
||||
|
||||
# Archived course has end date set to yesterday
|
||||
self.archived_course = CourseFactory.create(
|
||||
display_name='Archived Course',
|
||||
org=self.ORG,
|
||||
end=self.YESTERDAY,
|
||||
)
|
||||
CourseOverviewFactory.create(
|
||||
id=self.archived_course.id,
|
||||
org=self.ORG,
|
||||
end=self.YESTERDAY,
|
||||
)
|
||||
|
||||
# Base user has global staff access
|
||||
self.assertTrue(GlobalStaff().has_user(self.user))
|
||||
|
||||
# Staff user just has course staff access
|
||||
self.staff, self.staff_password = self.create_non_staff_user()
|
||||
for course in (self.course, self.active_course, self.archived_course):
|
||||
CourseStaffRole(course.id).add_users(self.staff)
|
||||
|
||||
def check_index_page_with_query_count(self, separate_archived_courses, org, mongo_queries, sql_queries):
|
||||
"""
|
||||
Checks the index page, and ensures the number of database queries is as expected.
|
||||
"""
|
||||
with self.assertNumQueries(sql_queries, table_ignorelist=WAFFLE_TABLES):
|
||||
with check_mongo_calls(mongo_queries):
|
||||
self.check_index_page(separate_archived_courses=separate_archived_courses, org=org)
|
||||
|
||||
def check_index_page(self, separate_archived_courses, org):
|
||||
"""
|
||||
Ensure that the index page displays the archived courses as expected.
|
||||
"""
|
||||
index_url = '/home/'
|
||||
index_params = {}
|
||||
if org is not None:
|
||||
index_params['org'] = org
|
||||
index_response = self.client.get(index_url, index_params, HTTP_ACCEPT='text/html')
|
||||
self.assertEqual(index_response.status_code, 200)
|
||||
|
||||
parsed_html = lxml.html.fromstring(index_response.content)
|
||||
course_tab = parsed_html.find_class('courses')
|
||||
self.assertEqual(len(course_tab), 1)
|
||||
archived_course_tab = parsed_html.find_class('archived-courses')
|
||||
self.assertEqual(len(archived_course_tab), 1 if separate_archived_courses else 0)
|
||||
|
||||
@ddt.data(
|
||||
# Staff user has course staff access
|
||||
(True, 'staff', None, 23),
|
||||
(False, 'staff', None, 23),
|
||||
# Base user has global staff access
|
||||
(True, 'user', ORG, 23),
|
||||
(False, 'user', ORG, 23),
|
||||
(True, 'user', None, 23),
|
||||
(False, 'user', None, 23),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_separate_archived_courses(self, separate_archived_courses, username, org, sql_queries):
|
||||
"""
|
||||
Ensure that archived courses are shown as expected for all user types, when the feature is enabled/disabled.
|
||||
Also ensure that enabling the feature does not adversely affect the database query count.
|
||||
"""
|
||||
# Authenticate the requested user
|
||||
user = getattr(self, username)
|
||||
password = getattr(self, username + '_password')
|
||||
self.client.login(username=user, password=password)
|
||||
|
||||
# Enable/disable the feature before viewing the index page.
|
||||
features = settings.FEATURES.copy()
|
||||
features['ENABLE_SEPARATE_ARCHIVED_COURSES'] = separate_archived_courses
|
||||
with override_settings(FEATURES=features):
|
||||
self.check_index_page_with_query_count(separate_archived_courses=separate_archived_courses,
|
||||
org=org,
|
||||
mongo_queries=0,
|
||||
sql_queries=sql_queries)
|
||||
|
||||
@ddt.data(
|
||||
# Staff user has course staff access
|
||||
(True, 'staff', None, 23),
|
||||
(False, 'staff', None, 23),
|
||||
# Base user has global staff access
|
||||
(True, 'user', ORG, 23),
|
||||
(False, 'user', ORG, 23),
|
||||
(True, 'user', None, 23),
|
||||
(False, 'user', None, 23),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_separate_archived_courses_with_home_page_course_v2_api(
|
||||
self,
|
||||
separate_archived_courses,
|
||||
username,
|
||||
org,
|
||||
sql_queries
|
||||
):
|
||||
"""
|
||||
Ensure that archived courses are shown as expected for all user types, when the feature is enabled/disabled.
|
||||
Also ensure that enabling the feature does not adversely affect the database query count.
|
||||
"""
|
||||
# Authenticate the requested user
|
||||
user = getattr(self, username)
|
||||
password = getattr(self, username + '_password')
|
||||
self.client.login(username=user, password=password)
|
||||
|
||||
# Enable/disable the feature before viewing the index page.
|
||||
features = settings.FEATURES.copy()
|
||||
features['ENABLE_SEPARATE_ARCHIVED_COURSES'] = separate_archived_courses
|
||||
with override_settings(FEATURES=features):
|
||||
self.check_index_page_with_query_count(separate_archived_courses=separate_archived_courses,
|
||||
org=org,
|
||||
mongo_queries=0,
|
||||
sql_queries=sql_queries)
|
||||
|
||||
|
||||
@override_waffle_flag(toggles.LEGACY_STUDIO_COURSE_OUTLINE, True)
|
||||
@ddt.ddt
|
||||
class TestCourseOutline(CourseTestCase):
|
||||
|
||||
Reference in New Issue
Block a user