get courses by reversing django groups for non global users (studio dashboard)

STUD-1133
This commit is contained in:
zubiar-arbi
2014-02-03 19:58:22 +05:00
parent 88690dab1d
commit 6892bc73b2
5 changed files with 315 additions and 73 deletions

View File

@@ -0,0 +1,22 @@
"""
Script for traversing all courses and add/modify mapping with 'lower_id' and 'lower_course_id'
"""
from django.core.management.base import BaseCommand
from xmodule.modulestore.django import modulestore, loc_mapper
#
# To run from command line: ./manage.py cms --settings dev map_courses_location_lower
#
class Command(BaseCommand):
"""
Create or modify map entry for each course in 'loc_mapper' with 'lower_id' and 'lower_course_id'
"""
help = "Create or modify map entry for each course in 'loc_mapper' with 'lower_id' and 'lower_course_id'"
def handle(self, *args, **options):
# get all courses
courses = modulestore('direct').get_courses()
for course in courses:
# create/modify map_entry in 'loc_mapper' with 'lower_id' and 'lower_course_id'
loc_mapper().create_map_entry(course.location)

View File

@@ -0,0 +1,196 @@
"""
Unit tests for getting the list of courses for a user through iterating all courses and
by reversing group name formats.
"""
import random
from chrono import Timer
from django.contrib.auth.models import Group
from django.test import RequestFactory
from contentstore.views.course import _accessible_courses_list, _accessible_courses_list_from_groups
from contentstore.tests.utils import AjaxEnabledTestClient
from student.tests.factories import UserFactory
from student.roles import CourseInstructorRole, CourseStaffRole
from xmodule.modulestore import Location
from xmodule.modulestore.django import loc_mapper
from xmodule.modulestore.exceptions import ItemNotFoundError
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
TOTAL_COURSES_COUNT = 1000
USER_COURSES_COUNT = 50
class TestCourseListing(ModuleStoreTestCase):
"""
Unit tests for getting the list of courses for a logged in user
"""
def setUp(self):
"""
Add a user and a course
"""
super(TestCourseListing, self).setUp()
# create and log in a staff user.
self.user = UserFactory(is_staff=True) # pylint: disable=no-member
self.factory = RequestFactory()
self.client = AjaxEnabledTestClient()
self.client.login(username=self.user.username, password='test')
def _create_course_with_access_groups(self, course_location, group_name_format='group_name_with_dots', user=None):
"""
Create dummy course with 'CourseFactory' and role (instructor/staff) groups with provided group_name_format
"""
course_locator = loc_mapper().translate_location(
course_location.course_id, course_location, False, True
)
course = CourseFactory.create(
org=course_location.org,
number=course_location.course,
display_name=course_location.name
)
for role in [CourseInstructorRole, CourseStaffRole]:
# pylint: disable=protected-access
groupnames = role(course_locator)._group_names
if group_name_format == 'group_name_with_course_name_only':
# Create role (instructor/staff) groups with course_name only: 'instructor_run'
group, _ = Group.objects.get_or_create(name=groupnames[2])
elif group_name_format == 'group_name_with_slashes':
# Create role (instructor/staff) groups with format: 'instructor_edX/Course/Run'
# Since "Group.objects.get_or_create(name=groupnames[1])" would have made group with lowercase name
# so manually create group name of old type
if role == CourseInstructorRole:
group, _ = Group.objects.get_or_create(name=u'{}_{}'.format('instructor', course_location.course_id))
else:
group, _ = Group.objects.get_or_create(name=u'{}_{}'.format('staff', course_location.course_id))
else:
# Create role (instructor/staff) groups with format: 'instructor_edx.course.run'
group, _ = Group.objects.get_or_create(name=groupnames[0])
if user is not None:
user.groups.add(group)
return course
def tearDown(self):
"""
Reverse the setup
"""
self.client.logout()
ModuleStoreTestCase.tearDown(self)
def test_get_course_list(self):
"""
Test getting courses with new access group format e.g. 'instructor_edx.course.run'
"""
request = self.factory.get('/course')
request.user = self.user
course_location = Location(['i4x', 'Org1', 'Course1', 'course', 'Run1'])
self._create_course_with_access_groups(course_location, 'group_name_with_dots', self.user)
# get courses through iterating all courses
courses_list = _accessible_courses_list(request)
self.assertEqual(len(courses_list), 1)
# get courses by reversing group name formats
courses_list_by_groups = _accessible_courses_list_from_groups(request)
self.assertEqual(len(courses_list_by_groups), 1)
# check both course lists have same courses
self.assertEqual(courses_list, courses_list_by_groups)
def test_get_course_list_with_old_group_formats(self):
"""
Test getting all courses with old course role (instructor/staff) groups
"""
request = self.factory.get('/course')
request.user = self.user
# create a course with new groups name format e.g. 'instructor_edx.course.run'
course_location = Location(['i4x', 'Org_1', 'Course_1', 'course', 'Run_1'])
self._create_course_with_access_groups(course_location, 'group_name_with_dots', self.user)
# create a course with old groups name format e.g. 'instructor_edX/Course/Run'
old_course_location = Location(['i4x', 'Org_2', 'Course_2', 'course', 'Run_2'])
self._create_course_with_access_groups(old_course_location, 'group_name_with_slashes', self.user)
# get courses through iterating all courses
courses_list = _accessible_courses_list(request)
self.assertEqual(len(courses_list), 2)
# get courses by reversing groups name
courses_list_by_groups = _accessible_courses_list_from_groups(request)
self.assertEqual(len(courses_list_by_groups), 2)
# create a new course with older group name format (with dots in names) e.g. 'instructor_edX/Course.name/Run.1'
old_course_location = Location(['i4x', 'Org.Foo.Bar', 'Course.number', 'course', 'Run.name'])
self._create_course_with_access_groups(old_course_location, 'group_name_with_slashes', self.user)
# get courses through iterating all courses
courses_list = _accessible_courses_list(request)
self.assertEqual(len(courses_list), 3)
# get courses by reversing group name formats
courses_list_by_groups = _accessible_courses_list_from_groups(request)
self.assertEqual(len(courses_list_by_groups), 3)
# create a new course with older group name format e.g. 'instructor_Run'
old_course_location = Location(['i4x', 'Org_3', 'Course_3', 'course', 'Run_3'])
self._create_course_with_access_groups(old_course_location, 'group_name_with_course_name_only', self.user)
# get courses through iterating all courses
courses_list = _accessible_courses_list(request)
self.assertEqual(len(courses_list), 4)
# should raise an exception for getting courses with older format of access group by reversing django groups
with self.assertRaises(ItemNotFoundError):
courses_list_by_groups = _accessible_courses_list_from_groups(request)
def test_course_listing_performance(self):
"""
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
reversing django groups
"""
# create and log in a non-staff user
self.user = UserFactory()
request = self.factory.get('/course')
request.user = self.user
self.client.login(username=self.user.username, password='test')
# create list of random course numbers which will be accessible to the user
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(1, TOTAL_COURSES_COUNT):
org = 'Org{0}'.format(number)
course = 'Course{0}'.format(number)
run = 'Run{0}'.format(number)
course_location = Location(['i4x', org, course, 'course', run])
if number in user_course_ids:
self._create_course_with_access_groups(course_location, 'group_name_with_dots', self.user)
else:
self._create_course_with_access_groups(course_location, 'group_name_with_dots')
# time the get courses by iterating through all courses
with Timer() as iteration_over_courses_time_1:
courses_list = _accessible_courses_list(request)
self.assertEqual(len(courses_list), USER_COURSES_COUNT)
# time again the get courses by iterating through all courses
with Timer() as iteration_over_courses_time_2:
courses_list = _accessible_courses_list(request)
self.assertEqual(len(courses_list), USER_COURSES_COUNT)
# time the get courses by reversing django groups
with Timer() as iteration_over_groups_time_1:
courses_list = _accessible_courses_list_from_groups(request)
self.assertEqual(len(courses_list), USER_COURSES_COUNT)
# time again the get courses by reversing django groups
with Timer() as iteration_over_groups_time_2:
courses_list = _accessible_courses_list_from_groups(request)
self.assertEqual(len(courses_list), USER_COURSES_COUNT)
# 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)

View File

@@ -48,7 +48,7 @@ from django_comment_common.utils import seed_permissions_roles
from student.models import CourseEnrollment
from xmodule.html_module import AboutDescriptor
from xmodule.modulestore.locator import BlockUsageLocator
from xmodule.modulestore.locator import BlockUsageLocator, CourseLocator
from course_creators.views import get_course_creator_status, add_user_with_status_unrequested
from contentstore import utils
from student.roles import CourseInstructorRole, CourseStaffRole, CourseCreatorRole, GlobalStaff
@@ -168,76 +168,55 @@ def _accessible_courses_list(request):
"""
Get courses to which this user has access
"""
if GlobalStaff().has_user(request.user):
return course.location.course != 'templates'
return (has_course_access(request.user, course.location)
# pylint: disable=fixme
# TODO remove this condition when templates purged from db
and course.location.course != 'templates'
and course.location.org != ''
and course.location.course != ''
and course.location.name != '')
)
courses = filter(course_filter, courses)
return courses
# pylint: disable=invalid-name
def _accessible_courses_list_from_groups(request):
"""
List all courses available to the logged in user by reversing access group names
"""
courses_list = []
course_id_list = []
course_ids = set()
user_staff_group_names = request.user.groups.filter(Q(name__startswith='instructor_') | Q(name__startswith='staff_')).values_list('name', flat=True)
# first try to get new and unique group names (course_id's) before querying database
user_staff_group_names = request.user.groups.filter(
Q(name__startswith='instructor_') | Q(name__startswith='staff_')
).values_list('name', flat=True)
# we can only get course_ids from role names with the new format (instructor_org/number/run or
# instructor_org.number.run but not instructor_number).
for user_staff_group_name in user_staff_group_names:
# to avoid duplication try to convert all course_id's to new format e.g. "edX.course.run"
course_id = re.sub(r'^(instructor_|staff_)', '', user_staff_group_name).replace('/', '.')
if len(course_id.split('.')) == 3:
if course_id not in course_id_list:
course_id_list.append(course_id)
# to avoid duplication try to convert all course_id's to format with dots e.g. "edx.course.run"
if user_staff_group_name.startswith("instructor_"):
# strip starting text "instructor_"
course_id = user_staff_group_name[11:]
else:
# old group format: course number only e.g. "course"
# not preferable (so return)
#
# Note: We can try to get course with only course number if there is exactly
# one course with this course number
return (False, courses_list)
# strip starting text "staff_"
course_id = user_staff_group_name[6:]
for course_id in course_id_list:
# new group format: id of course e.g. "edX.course.run"
org, course, name = course_id.split('.')
course_location = Location('i4x', org, course, 'course', name)
try:
course = modulestore('direct').get_item(course_location)
courses_list.append(course)
except ItemNotFoundError:
course = None
course_ids.add(course_id.replace('/', '.').lower())
if not course:
# since access groups are being stored in lowercase, also do a case-insensitive search
# for the potential course id.
course_search_location = bson.son.SON({
'_id.tag': 'i4x',
# pylint: disable=no-member
'_id.org': re.compile(u'^{}$'.format(course_location.org), re.IGNORECASE | re.UNICODE),
# pylint: disable=no-member
'_id.course': re.compile(u'^{}$'.format(course_location.course), re.IGNORECASE | re.UNICODE),
'_id.category': 'course',
# pylint: disable=no-member
'_id.name': re.compile(u'^{}$'.format(course_location.name), re.IGNORECASE | re.UNICODE)
})
courses = modulestore().collection.find(course_search_location, fields=('_id'))
if courses.count() == 1:
course_id = courses[0].get('_id')
course_location = Location('i4x', course_id.get('org'), course_id.get('course'), 'course', course_id.get('name'))
try:
course = modulestore('direct').get_item(course_location)
courses_list.append(course)
except ItemNotFoundError:
return (False, courses_list)
else:
return (False, courses_list)
for course_id in course_ids:
# get course_location with lowercase idget_item
course_location = loc_mapper().translate_locator_to_location(
CourseLocator(package_id=course_id), get_course=True, lower_only=True
)
if course_location is None:
raise ItemNotFoundError(course_id)
return (True, courses_list)
course = modulestore('direct').get_course(course_location.course_id)
courses_list.append(course)
return courses_list
@login_required
@@ -252,14 +231,17 @@ def course_listing(request):
# user has global access so no need to get courses from django groups
courses = _accessible_courses_list(request)
else:
success, courses_from_groups = _accessible_courses_list_from_groups(request)
if success:
courses = courses_from_groups
else:
try:
courses = _accessible_courses_list_from_groups(request)
except ItemNotFoundError:
# user have some old groups or there was some error getting courses from django groups
# so fallback to iterating through all courses
courses = _accessible_courses_list(request)
# update location entry in "loc_mapper" for user courses (add keys 'lower_id' and 'lower_course_id')
for course in courses:
loc_mapper().create_map_entry(course.location)
def format_course_for_view(course):
"""
return tuple of the data which the view requires for each course