use CourseSummary for course listing on studio for non-global staff users
This commit is contained in:
@@ -16,7 +16,7 @@ from contentstore.tests.utils import AjaxEnabledTestClient
|
||||
from contentstore.utils import delete_course
|
||||
from contentstore.views.course import (
|
||||
AccessListFallback,
|
||||
_accessible_courses_iter,
|
||||
_accessible_courses_iter_for_tests,
|
||||
_accessible_courses_list_from_groups,
|
||||
_accessible_courses_summary_iter,
|
||||
get_courses_accessible_to_user
|
||||
@@ -103,7 +103,7 @@ class TestCourseListing(ModuleStoreTestCase):
|
||||
self._create_course_with_access_groups(course_location, self.user)
|
||||
|
||||
# get courses through iterating all courses
|
||||
courses_iter, __ = _accessible_courses_iter(self.request)
|
||||
courses_iter, __ = _accessible_courses_iter_for_tests(self.request)
|
||||
courses_list = list(courses_iter)
|
||||
self.assertEqual(len(courses_list), 1)
|
||||
|
||||
@@ -115,7 +115,10 @@ class TestCourseListing(ModuleStoreTestCase):
|
||||
self.assertEqual(len(courses_list_by_groups), 1)
|
||||
|
||||
# check both course lists have same courses
|
||||
self.assertEqual(courses_list, courses_list_by_groups)
|
||||
course_keys_in_course_list = [course.id for course in courses_list]
|
||||
course_keys_in_courses_list_by_groups = [course.id for course in courses_list_by_groups]
|
||||
|
||||
self.assertEqual(course_keys_in_course_list, course_keys_in_courses_list_by_groups)
|
||||
|
||||
def test_courses_list_with_ccx_courses(self):
|
||||
"""
|
||||
@@ -150,36 +153,10 @@ class TestCourseListing(ModuleStoreTestCase):
|
||||
|
||||
# Verify that CCX courses are filtered out while iterating over all courses
|
||||
mocked_ccx_course = Mock(id=ccx_course_key)
|
||||
with patch('xmodule.modulestore.mixed.MixedModuleStore.get_courses', return_value=[mocked_ccx_course]):
|
||||
courses_iter, __ = _accessible_courses_iter(self.request)
|
||||
with patch('xmodule.modulestore.mixed.MixedModuleStore.get_course_summaries', return_value=[mocked_ccx_course]):
|
||||
courses_iter, __ = _accessible_courses_iter_for_tests(self.request)
|
||||
self.assertEqual(len(list(courses_iter)), 0)
|
||||
|
||||
@ddt.data(
|
||||
(ModuleStoreEnum.Type.split, 'xmodule.modulestore.split_mongo.split_mongo_kvs.SplitMongoKVS'),
|
||||
(ModuleStoreEnum.Type.mongo, 'xmodule.modulestore.mongo.base.MongoKeyValueStore')
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_errored_course_global_staff(self, store, path_to_patch):
|
||||
"""
|
||||
Test the course list for global staff when get_course returns an ErrorDescriptor
|
||||
"""
|
||||
GlobalStaff().add_users(self.user)
|
||||
|
||||
with self.store.default_store(store):
|
||||
course_key = self.store.make_course_key('Org1', 'Course1', 'Run1')
|
||||
self._create_course_with_access_groups(course_key, self.user, store=store)
|
||||
|
||||
with patch(path_to_patch, Mock(side_effect=Exception)):
|
||||
self.assertIsInstance(self.store.get_course(course_key), ErrorDescriptor)
|
||||
|
||||
# get courses through iterating all courses
|
||||
courses_iter, __ = _accessible_courses_iter(self.request)
|
||||
self.assertEqual(list(courses_iter), [])
|
||||
|
||||
# get courses by reversing group name formats
|
||||
courses_list_by_groups, __ = _accessible_courses_list_from_groups(self.request)
|
||||
self.assertEqual(courses_list_by_groups, [])
|
||||
|
||||
@ddt.data(
|
||||
(ModuleStoreEnum.Type.split, 3),
|
||||
(ModuleStoreEnum.Type.mongo, 2)
|
||||
@@ -212,36 +189,6 @@ class TestCourseListing(ModuleStoreTestCase):
|
||||
with check_mongo_calls(mongo_calls):
|
||||
list(_accessible_courses_summary_iter(self.request))
|
||||
|
||||
@ddt.data(
|
||||
(ModuleStoreEnum.Type.split, 'xmodule.modulestore.split_mongo.split_mongo_kvs.SplitMongoKVS'),
|
||||
(ModuleStoreEnum.Type.mongo, 'xmodule.modulestore.mongo.base.MongoKeyValueStore')
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_errored_course_regular_access(self, store, path_to_patch):
|
||||
"""
|
||||
Test the course list for regular staff when get_course returns an ErrorDescriptor
|
||||
"""
|
||||
GlobalStaff().remove_users(self.user)
|
||||
|
||||
with self.store.default_store(store):
|
||||
CourseStaffRole(self.store.make_course_key('Non', 'Existent', 'Course')).add_users(self.user)
|
||||
|
||||
course_key = self.store.make_course_key('Org1', 'Course1', 'Run1')
|
||||
self._create_course_with_access_groups(course_key, self.user, store)
|
||||
|
||||
with patch(path_to_patch, Mock(side_effect=Exception)):
|
||||
self.assertIsInstance(self.store.get_course(course_key), ErrorDescriptor)
|
||||
|
||||
# get courses through iterating all courses
|
||||
courses_iter, __ = _accessible_courses_iter(self.request)
|
||||
courses_list = list(courses_iter)
|
||||
self.assertEqual(courses_list, [])
|
||||
|
||||
# get courses by reversing group name formats
|
||||
courses_list_by_groups, __ = _accessible_courses_list_from_groups(self.request)
|
||||
self.assertEqual(courses_list_by_groups, [])
|
||||
self.assertEqual(courses_list, courses_list_by_groups)
|
||||
|
||||
@ddt.data(ModuleStoreEnum.Type.split, ModuleStoreEnum.Type.mongo)
|
||||
def test_get_course_list_with_invalid_course_location(self, store):
|
||||
"""
|
||||
@@ -252,7 +199,7 @@ class TestCourseListing(ModuleStoreTestCase):
|
||||
self._create_course_with_access_groups(course_key, self.user, store)
|
||||
|
||||
# get courses through iterating all courses
|
||||
courses_iter, __ = _accessible_courses_iter(self.request)
|
||||
courses_iter, __ = _accessible_courses_iter_for_tests(self.request)
|
||||
courses_list = list(courses_iter)
|
||||
self.assertEqual(len(courses_list), 1)
|
||||
|
||||
@@ -268,16 +215,17 @@ class TestCourseListing(ModuleStoreTestCase):
|
||||
courses_list_by_groups, __ = _accessible_courses_list_from_groups(self.request)
|
||||
self.assertEqual(len(courses_list_by_groups), 1)
|
||||
|
||||
course_keys_in_course_list = [course.id for course in courses_list]
|
||||
course_keys_in_courses_list_by_groups = [course.id for course in courses_list_by_groups]
|
||||
# check course lists have same courses
|
||||
self.assertEqual(courses_list, courses_list_by_groups)
|
||||
|
||||
self.assertEqual(course_keys_in_course_list, course_keys_in_courses_list_by_groups)
|
||||
# now delete this course and re-add user to instructor group of this course
|
||||
delete_course(course_key, self.user.id)
|
||||
|
||||
CourseInstructorRole(course_key).add_users(self.user)
|
||||
|
||||
# Get courses through iterating all courses
|
||||
courses_iter, __ = _accessible_courses_iter(self.request)
|
||||
courses_iter, __ = _accessible_courses_iter_for_tests(self.request)
|
||||
|
||||
# Get course summaries by iterating all courses
|
||||
courses_summary_iter, __ = _accessible_courses_summary_iter(self.request)
|
||||
@@ -292,8 +240,8 @@ class TestCourseListing(ModuleStoreTestCase):
|
||||
)
|
||||
|
||||
@ddt.data(
|
||||
(ModuleStoreEnum.Type.split, 4, 23),
|
||||
(ModuleStoreEnum.Type.mongo, USER_COURSES_COUNT, 2)
|
||||
(ModuleStoreEnum.Type.split, 3, 3),
|
||||
(ModuleStoreEnum.Type.mongo, 2, 2)
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_course_listing_performance(self, store, courses_list_from_group_calls, courses_list_calls):
|
||||
@@ -319,12 +267,12 @@ class TestCourseListing(ModuleStoreTestCase):
|
||||
|
||||
# time the get courses by iterating through all courses
|
||||
with Timer() as iteration_over_courses_time_1:
|
||||
courses_iter, __ = _accessible_courses_iter(self.request)
|
||||
courses_iter, __ = _accessible_courses_iter_for_tests(self.request)
|
||||
self.assertEqual(len(list(courses_iter)), USER_COURSES_COUNT)
|
||||
|
||||
# time again the get courses by iterating through all courses
|
||||
with Timer() as iteration_over_courses_time_2:
|
||||
courses_iter, __ = _accessible_courses_iter(self.request)
|
||||
courses_iter, __ = _accessible_courses_iter_for_tests(self.request)
|
||||
self.assertEqual(len(list(courses_iter)), USER_COURSES_COUNT)
|
||||
|
||||
# time the get courses by reversing django groups
|
||||
@@ -337,25 +285,18 @@ class TestCourseListing(ModuleStoreTestCase):
|
||||
courses_list, __ = _accessible_courses_list_from_groups(self.request)
|
||||
self.assertEqual(len(courses_list), USER_COURSES_COUNT)
|
||||
|
||||
# TODO (cdyer) : iteration over courses was optimized, and is now
|
||||
# sometimes faster than iteration over groups. One of the following
|
||||
# should be done to resolve this:
|
||||
# * Iteration over groups should be sped up.
|
||||
# * Iteration over groups should be removed, as it no longer saves time.
|
||||
# * Or this part of the test should be removed.
|
||||
|
||||
# Test that the time taken by getting courses through reversing django
|
||||
# groups is lower then the time taken by traversing through all courses
|
||||
# (if accessible courses are relatively small).
|
||||
#self.assertGreaterEqual(iteration_over_courses_time_1.elapsed, iteration_over_groups_time_1.elapsed)
|
||||
#self.assertGreaterEqual(iteration_over_courses_time_2.elapsed, iteration_over_groups_time_2.elapsed)
|
||||
self.assertGreaterEqual(iteration_over_courses_time_1.elapsed, iteration_over_groups_time_1.elapsed)
|
||||
self.assertGreaterEqual(iteration_over_courses_time_2.elapsed, iteration_over_groups_time_2.elapsed)
|
||||
|
||||
# Now count the db queries
|
||||
with check_mongo_calls(courses_list_from_group_calls):
|
||||
_accessible_courses_list_from_groups(self.request)
|
||||
|
||||
with check_mongo_calls(courses_list_calls):
|
||||
list(_accessible_courses_iter(self.request))
|
||||
list(_accessible_courses_iter_for_tests(self.request))
|
||||
# Calls:
|
||||
# 1) query old mongo
|
||||
# 2) get_more on old mongo
|
||||
@@ -436,13 +377,12 @@ class TestCourseListing(ModuleStoreTestCase):
|
||||
)
|
||||
|
||||
# verify return values
|
||||
for method in (_accessible_courses_list_from_groups, _accessible_courses_iter):
|
||||
def set_of_course_keys(course_list, key_attribute_name='id'):
|
||||
"""Returns a python set of course keys by accessing the key with the given attribute name."""
|
||||
return set(getattr(c, key_attribute_name) for c in course_list)
|
||||
def _set_of_course_keys(course_list, key_attribute_name='id'):
|
||||
"""Returns a python set of course keys by accessing the key with the given attribute name."""
|
||||
return set(getattr(c, key_attribute_name) for c in course_list)
|
||||
|
||||
found_courses, unsucceeded_course_actions = method(self.request)
|
||||
self.assertSetEqual(set_of_course_keys(courses + courses_in_progress), set_of_course_keys(found_courses))
|
||||
self.assertSetEqual(
|
||||
set_of_course_keys(courses_in_progress), set_of_course_keys(unsucceeded_course_actions, 'course_key')
|
||||
)
|
||||
found_courses, unsucceeded_course_actions = _accessible_courses_iter_for_tests(self.request)
|
||||
self.assertSetEqual(_set_of_course_keys(courses + courses_in_progress), _set_of_course_keys(found_courses))
|
||||
self.assertSetEqual(
|
||||
_set_of_course_keys(courses_in_progress), _set_of_course_keys(unsucceeded_course_actions, 'course_key')
|
||||
)
|
||||
|
||||
@@ -390,7 +390,6 @@ def _accessible_courses_summary_iter(request, org=None):
|
||||
def _accessible_courses_iter(request):
|
||||
"""
|
||||
List all courses available to the logged in user by iterating through all the courses.
|
||||
This method is only used by tests.
|
||||
"""
|
||||
def course_filter(course):
|
||||
"""
|
||||
@@ -417,6 +416,35 @@ def _accessible_courses_iter(request):
|
||||
return courses, in_process_course_actions
|
||||
|
||||
|
||||
def _accessible_courses_iter_for_tests(request):
|
||||
"""
|
||||
List all courses available to the logged in user by iterating through all the courses.
|
||||
CourseSummary objects are used for listing purposes.
|
||||
This method is only used by tests.
|
||||
"""
|
||||
def course_filter(course):
|
||||
"""
|
||||
Filter out unusable and inaccessible courses
|
||||
"""
|
||||
|
||||
# Custom Courses for edX (CCX) is an edX feature for re-using course content.
|
||||
# CCXs cannot be edited in Studio (aka cms) and should not be shown in this dashboard.
|
||||
if isinstance(course.id, CCXLocator):
|
||||
return False
|
||||
|
||||
# pylint: disable=fixme
|
||||
# TODO remove this condition when templates purged from db
|
||||
if course.location.course == 'templates':
|
||||
return False
|
||||
|
||||
return has_studio_read_access(request.user, course.id)
|
||||
|
||||
courses = six.moves.filter(course_filter, modulestore().get_course_summaries())
|
||||
|
||||
in_process_course_actions = get_in_process_course_actions(request)
|
||||
return courses, in_process_course_actions
|
||||
|
||||
|
||||
def _accessible_courses_list_from_groups(request):
|
||||
"""
|
||||
List all courses available to the logged in user by reversing access group names
|
||||
@@ -425,39 +453,23 @@ def _accessible_courses_list_from_groups(request):
|
||||
""" CCXs cannot be edited in Studio and should not be shown in this dashboard """
|
||||
return not isinstance(course_access.course_id, CCXLocator)
|
||||
|
||||
courses_list = {}
|
||||
in_process_course_actions = []
|
||||
|
||||
instructor_courses = UserBasedRole(request.user, CourseInstructorRole.ROLE).courses_with_role()
|
||||
staff_courses = UserBasedRole(request.user, CourseStaffRole.ROLE).courses_with_role()
|
||||
all_courses = filter(filter_ccx, instructor_courses | staff_courses)
|
||||
courses_list = []
|
||||
course_keys = {}
|
||||
|
||||
for course_access in all_courses:
|
||||
course_key = course_access.course_id
|
||||
if course_key is None:
|
||||
# If the course_access does not have a course_id, it's an org-based role, so we fall back
|
||||
if course_access.course_id is None:
|
||||
raise AccessListFallback
|
||||
if course_key not in courses_list:
|
||||
# check for any course action state for this course
|
||||
in_process_course_actions.extend(
|
||||
CourseRerunState.objects.find_all(
|
||||
exclude_args={'state': CourseRerunUIStateManager.State.SUCCEEDED},
|
||||
should_display=True,
|
||||
course_key=course_key,
|
||||
)
|
||||
)
|
||||
# check for the course itself
|
||||
try:
|
||||
course = modulestore().get_course(course_key)
|
||||
except ItemNotFoundError:
|
||||
# If a user has access to a course that doesn't exist, don't do anything with that course
|
||||
pass
|
||||
course_keys[course_access.course_id] = course_access.course_id
|
||||
|
||||
if course is not None and not isinstance(course, ErrorDescriptor):
|
||||
# ignore deleted, errored or ccx courses
|
||||
courses_list[course_key] = course
|
||||
course_keys = course_keys.values()
|
||||
|
||||
return courses_list.values(), in_process_course_actions
|
||||
if course_keys:
|
||||
courses_list = modulestore().get_course_summaries(course_keys=course_keys)
|
||||
|
||||
return courses_list, []
|
||||
|
||||
|
||||
def _accessible_libraries_iter(user, org=None):
|
||||
|
||||
@@ -392,8 +392,8 @@ class TestCourseIndexArchived(CourseTestCase):
|
||||
|
||||
@ddt.data(
|
||||
# Staff user has course staff access
|
||||
(True, 'staff', None, 4, 21),
|
||||
(False, 'staff', None, 4, 21),
|
||||
(True, 'staff', None, 3, 17),
|
||||
(False, 'staff', None, 3, 17),
|
||||
# Base user has global staff access
|
||||
(True, 'user', ORG, 3, 17),
|
||||
(False, 'user', ORG, 3, 17),
|
||||
|
||||
Reference in New Issue
Block a user