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:
Awais Jibran
2015-12-30 11:49:55 +05:00
12 changed files with 433 additions and 88 deletions

View File

@@ -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,

View File

@@ -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