Merge pull request #10749 from edx/aj/tnl3779-use-get-courses-summaries-for-staff
Render CMS course listing Using CourseSummary class for staff
This commit is contained in:
@@ -9,8 +9,11 @@ from mock import patch, Mock
|
||||
import ddt
|
||||
|
||||
from django.test import RequestFactory
|
||||
from xmodule.course_module import CourseSummary
|
||||
|
||||
from contentstore.views.course import _accessible_courses_list, _accessible_courses_list_from_groups, AccessListFallback
|
||||
from contentstore.views.course import (_accessible_courses_list, _accessible_courses_list_from_groups,
|
||||
AccessListFallback, get_courses_accessible_to_user,
|
||||
_staff_accessible_course_list)
|
||||
from contentstore.utils import delete_course_and_groups
|
||||
from contentstore.tests.utils import AjaxEnabledTestClient
|
||||
from student.tests.factories import UserFactory
|
||||
@@ -19,7 +22,6 @@ from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory, check_mongo_calls
|
||||
from xmodule.modulestore import ModuleStoreEnum
|
||||
from opaque_keys.edx.locations import CourseLocator
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.error_module import ErrorDescriptor
|
||||
from course_action_state.models import CourseRerunState
|
||||
|
||||
@@ -46,7 +48,7 @@ class TestCourseListing(ModuleStoreTestCase):
|
||||
self.client = AjaxEnabledTestClient()
|
||||
self.client.login(username=self.user.username, password='test')
|
||||
|
||||
def _create_course_with_access_groups(self, course_location, user=None):
|
||||
def _create_course_with_access_groups(self, course_location, user=None, store=ModuleStoreEnum.Type.split):
|
||||
"""
|
||||
Create dummy course with 'CourseFactory' and role (instructor/staff) groups
|
||||
"""
|
||||
@@ -54,7 +56,7 @@ class TestCourseListing(ModuleStoreTestCase):
|
||||
org=course_location.org,
|
||||
number=course_location.course,
|
||||
run=course_location.run,
|
||||
default_store=ModuleStoreEnum.Type.mongo
|
||||
default_store=store
|
||||
)
|
||||
|
||||
if user is not None:
|
||||
@@ -87,54 +89,101 @@ class TestCourseListing(ModuleStoreTestCase):
|
||||
# check both course lists have same courses
|
||||
self.assertEqual(courses_list, courses_list_by_groups)
|
||||
|
||||
def test_errored_course_global_staff(self):
|
||||
@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)
|
||||
|
||||
course_key = self.store.make_course_key('Org1', 'Course1', 'Run1')
|
||||
self._create_course_with_access_groups(course_key, 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('xmodule.modulestore.mongo.base.MongoKeyValueStore', Mock(side_effect=Exception)):
|
||||
self.assertIsInstance(modulestore().get_course(course_key), ErrorDescriptor)
|
||||
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_list, __ = _accessible_courses_list(self.request)
|
||||
self.assertEqual(courses_list, [])
|
||||
# get courses through iterating all courses
|
||||
courses_list, __ = _accessible_courses_list(self.request)
|
||||
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, [])
|
||||
# get courses by reversing group name formats
|
||||
courses_list_by_groups, __ = _accessible_courses_list_from_groups(self.request)
|
||||
self.assertEqual(courses_list_by_groups, [])
|
||||
|
||||
def test_errored_course_regular_access(self):
|
||||
@ddt.data(
|
||||
(ModuleStoreEnum.Type.split, 5),
|
||||
(ModuleStoreEnum.Type.mongo, 3)
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_staff_course_listing(self, default_store, mongo_calls):
|
||||
"""
|
||||
Create courses and verify they take certain amount of mongo calls to call get_courses_accessible_to_user.
|
||||
Also verify that fetch accessible courses list for staff user returns CourseSummary instances.
|
||||
"""
|
||||
|
||||
# Assign & verify staff role to the user
|
||||
GlobalStaff().add_users(self.user)
|
||||
self.assertTrue(GlobalStaff().has_user(self.user))
|
||||
|
||||
with self.store.default_store(default_store):
|
||||
# Create few courses
|
||||
for num in xrange(TOTAL_COURSES_COUNT):
|
||||
course_location = self.store.make_course_key('Org', 'CreatedCourse' + str(num), 'Run')
|
||||
self._create_course_with_access_groups(course_location, self.user, default_store)
|
||||
|
||||
# Fetch accessible courses list & verify their count
|
||||
courses_list_by_staff, __ = get_courses_accessible_to_user(self.request)
|
||||
self.assertEqual(len(courses_list_by_staff), TOTAL_COURSES_COUNT)
|
||||
|
||||
# Verify fetched accessible courses list is a list of CourseSummery instances
|
||||
self.assertTrue(all(isinstance(course, CourseSummary) for course in courses_list_by_staff))
|
||||
|
||||
# Now count the db queries for staff
|
||||
with check_mongo_calls(mongo_calls):
|
||||
_staff_accessible_course_list(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)
|
||||
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)
|
||||
with self.store.default_store(store):
|
||||
CourseStaffRole(self.store.make_course_key('Non', 'Existent', 'Course')).add_users(self.user)
|
||||
|
||||
with patch('xmodule.modulestore.mongo.base.MongoKeyValueStore', Mock(side_effect=Exception)):
|
||||
self.assertIsInstance(modulestore().get_course(course_key), ErrorDescriptor)
|
||||
course_key = self.store.make_course_key('Org1', 'Course1', 'Run1')
|
||||
self._create_course_with_access_groups(course_key, self.user, store)
|
||||
|
||||
# get courses through iterating all courses
|
||||
courses_list, __ = _accessible_courses_list(self.request)
|
||||
self.assertEqual(courses_list, [])
|
||||
with patch(path_to_patch, Mock(side_effect=Exception)):
|
||||
self.assertIsInstance(self.store.get_course(course_key), ErrorDescriptor)
|
||||
|
||||
# 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)
|
||||
# get courses through iterating all courses
|
||||
courses_list, __ = _accessible_courses_list(self.request)
|
||||
self.assertEqual(courses_list, [])
|
||||
|
||||
def test_get_course_list_with_invalid_course_location(self):
|
||||
# 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):
|
||||
"""
|
||||
Test getting courses with invalid course location (course deleted from modulestore).
|
||||
"""
|
||||
course_key = self.store.make_course_key('Org', 'Course', 'Run')
|
||||
self._create_course_with_access_groups(course_key, self.user)
|
||||
with self.store.default_store(store):
|
||||
course_key = self.store.make_course_key('Org', 'Course', 'Run')
|
||||
self._create_course_with_access_groups(course_key, self.user, store)
|
||||
|
||||
# get courses through iterating all courses
|
||||
courses_list, __ = _accessible_courses_list(self.request)
|
||||
@@ -155,7 +204,12 @@ class TestCourseListing(ModuleStoreTestCase):
|
||||
courses_list, __ = _accessible_courses_list(self.request)
|
||||
self.assertEqual(len(courses_list), 0)
|
||||
|
||||
def test_course_listing_performance(self):
|
||||
@ddt.data(
|
||||
(ModuleStoreEnum.Type.split, 150, 505),
|
||||
(ModuleStoreEnum.Type.mongo, USER_COURSES_COUNT, 3)
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_course_listing_performance(self, store, courses_list_from_group_calls, courses_list_calls):
|
||||
"""
|
||||
Create large number of courses and give access of some of these courses to the user and
|
||||
compare the time to fetch accessible courses for the user through traversing all courses and
|
||||
@@ -165,15 +219,16 @@ class TestCourseListing(ModuleStoreTestCase):
|
||||
user_course_ids = random.sample(range(TOTAL_COURSES_COUNT), USER_COURSES_COUNT)
|
||||
|
||||
# create courses and assign those to the user which have their number in user_course_ids
|
||||
for number in range(TOTAL_COURSES_COUNT):
|
||||
org = 'Org{0}'.format(number)
|
||||
course = 'Course{0}'.format(number)
|
||||
run = 'Run{0}'.format(number)
|
||||
course_location = self.store.make_course_key(org, course, run)
|
||||
if number in user_course_ids:
|
||||
self._create_course_with_access_groups(course_location, self.user)
|
||||
else:
|
||||
self._create_course_with_access_groups(course_location)
|
||||
with self.store.default_store(store):
|
||||
for number in range(TOTAL_COURSES_COUNT):
|
||||
org = 'Org{0}'.format(number)
|
||||
course = 'Course{0}'.format(number)
|
||||
run = 'Run{0}'.format(number)
|
||||
course_location = self.store.make_course_key(org, course, run)
|
||||
if number in user_course_ids:
|
||||
self._create_course_with_access_groups(course_location, self.user, store=store)
|
||||
else:
|
||||
self._create_course_with_access_groups(course_location, store=store)
|
||||
|
||||
# time the get courses by iterating through all courses
|
||||
with Timer() as iteration_over_courses_time_1:
|
||||
@@ -201,29 +256,29 @@ class TestCourseListing(ModuleStoreTestCase):
|
||||
self.assertGreaterEqual(iteration_over_courses_time_2.elapsed, iteration_over_groups_time_2.elapsed)
|
||||
|
||||
# Now count the db queries
|
||||
with check_mongo_calls(USER_COURSES_COUNT):
|
||||
with check_mongo_calls(courses_list_from_group_calls):
|
||||
_accessible_courses_list_from_groups(self.request)
|
||||
|
||||
with check_mongo_calls(courses_list_calls):
|
||||
_accessible_courses_list(self.request)
|
||||
# Calls:
|
||||
# 1) query old mongo
|
||||
# 2) get_more on old mongo
|
||||
# 3) query split (but no courses so no fetching of data)
|
||||
with check_mongo_calls(3):
|
||||
_accessible_courses_list(self.request)
|
||||
|
||||
def test_course_listing_errored_deleted_courses(self):
|
||||
@ddt.data(ModuleStoreEnum.Type.split, ModuleStoreEnum.Type.mongo)
|
||||
def test_course_listing_errored_deleted_courses(self, store):
|
||||
"""
|
||||
Create good courses, courses that won't load, and deleted courses which still have
|
||||
roles. Test course listing.
|
||||
"""
|
||||
store = modulestore()._get_modulestore_by_type(ModuleStoreEnum.Type.mongo)
|
||||
with self.store.default_store(store):
|
||||
course_location = self.store.make_course_key('testOrg', 'testCourse', 'RunBabyRun')
|
||||
self._create_course_with_access_groups(course_location, self.user, store)
|
||||
|
||||
course_location = self.store.make_course_key('testOrg', 'testCourse', 'RunBabyRun')
|
||||
self._create_course_with_access_groups(course_location, self.user)
|
||||
|
||||
course_location = self.store.make_course_key('testOrg', 'doomedCourse', 'RunBabyRun')
|
||||
self._create_course_with_access_groups(course_location, self.user)
|
||||
store.delete_course(course_location, self.user.id)
|
||||
course_location = self.store.make_course_key('testOrg', 'doomedCourse', 'RunBabyRun')
|
||||
self._create_course_with_access_groups(course_location, self.user, store)
|
||||
self.store.delete_course(course_location, self.user.id) # pylint: disable=no-member
|
||||
|
||||
courses_list, __ = _accessible_courses_list_from_groups(self.request)
|
||||
self.assertEqual(len(courses_list), 1, courses_list)
|
||||
@@ -241,7 +296,7 @@ class TestCourseListing(ModuleStoreTestCase):
|
||||
run=org_course_one.run
|
||||
)
|
||||
|
||||
org_course_two = self.store.make_course_key('AwesomeOrg', 'Course2', 'RunRunRun')
|
||||
org_course_two = self.store.make_course_key('AwesomeOrg', 'Course2', 'RunBabyRun')
|
||||
CourseFactory.create(
|
||||
org=org_course_two.org,
|
||||
number=org_course_two.course,
|
||||
|
||||
@@ -17,6 +17,7 @@ import django.utils
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.views.decorators.http import require_http_methods, require_GET
|
||||
from django.views.decorators.csrf import ensure_csrf_cookie
|
||||
|
||||
from opaque_keys import InvalidKeyError
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from opaque_keys.edx.locations import Location
|
||||
@@ -345,6 +346,39 @@ def _course_outline_json(request, course_module):
|
||||
)
|
||||
|
||||
|
||||
def get_in_process_course_actions(request):
|
||||
"""
|
||||
Get all in-process course actions
|
||||
"""
|
||||
return [
|
||||
course for course in
|
||||
CourseRerunState.objects.find_all(
|
||||
exclude_args={'state': CourseRerunUIStateManager.State.SUCCEEDED}, should_display=True
|
||||
)
|
||||
if has_studio_read_access(request.user, course.course_key)
|
||||
]
|
||||
|
||||
|
||||
def _staff_accessible_course_list(request):
|
||||
"""
|
||||
List all courses available to the logged in user by iterating through all the courses
|
||||
"""
|
||||
def course_filter(course_summary):
|
||||
"""
|
||||
Filter out unusable and inaccessible courses
|
||||
"""
|
||||
# pylint: disable=fixme
|
||||
# TODO remove this condition when templates purged from db
|
||||
if course_summary.location.course == 'templates':
|
||||
return False
|
||||
|
||||
return has_studio_read_access(request.user, course_summary.id)
|
||||
|
||||
courses_summary = filter(course_filter, modulestore().get_course_summaries())
|
||||
in_process_course_actions = get_in_process_course_actions(request)
|
||||
return courses_summary, in_process_course_actions
|
||||
|
||||
|
||||
def _accessible_courses_list(request):
|
||||
"""
|
||||
List all courses available to the logged in user by iterating through all the courses
|
||||
@@ -364,13 +398,8 @@ def _accessible_courses_list(request):
|
||||
return has_studio_read_access(request.user, course.id)
|
||||
|
||||
courses = filter(course_filter, modulestore().get_courses())
|
||||
in_process_course_actions = [
|
||||
course for course in
|
||||
CourseRerunState.objects.find_all(
|
||||
exclude_args={'state': CourseRerunUIStateManager.State.SUCCEEDED}, should_display=True
|
||||
)
|
||||
if has_studio_read_access(request.user, course.course_key)
|
||||
]
|
||||
|
||||
in_process_course_actions = get_in_process_course_actions(request)
|
||||
return courses, in_process_course_actions
|
||||
|
||||
|
||||
@@ -593,7 +622,7 @@ def get_courses_accessible_to_user(request):
|
||||
"""
|
||||
if GlobalStaff().has_user(request.user):
|
||||
# user has global access so no need to get courses from django groups
|
||||
courses, in_process_course_actions = _accessible_courses_list(request)
|
||||
courses, in_process_course_actions = _staff_accessible_course_list(request)
|
||||
else:
|
||||
try:
|
||||
courses, in_process_course_actions = _accessible_courses_list_from_groups(request)
|
||||
@@ -626,9 +655,9 @@ def _remove_in_process_courses(courses, in_process_course_actions):
|
||||
|
||||
in_process_action_course_keys = [uca.course_key for uca in in_process_course_actions]
|
||||
courses = [
|
||||
format_course_for_view(c)
|
||||
for c in courses
|
||||
if not isinstance(c, ErrorDescriptor) and (c.id not in in_process_action_course_keys)
|
||||
format_course_for_view(course)
|
||||
for course in courses
|
||||
if not isinstance(course, ErrorDescriptor) and (course.id not in in_process_action_course_keys)
|
||||
]
|
||||
return courses
|
||||
|
||||
|
||||
Reference in New Issue
Block a user