示例#1
0
class RoleCacheTestCase(TestCase):

    IN_KEY = SlashSeparatedCourseKey('edX', 'toy', '2012_Fall')
    NOT_IN_KEY = SlashSeparatedCourseKey('edX', 'toy', '2013_Fall')

    ROLES = (
        (CourseStaffRole(IN_KEY), ('staff', IN_KEY, 'edX')),
        (CourseInstructorRole(IN_KEY), ('instructor', IN_KEY, 'edX')),
        (OrgStaffRole(IN_KEY.org), ('staff', None, 'edX')),
        (OrgInstructorRole(IN_KEY.org), ('instructor', None, 'edX')),
        (CourseBetaTesterRole(IN_KEY), ('beta_testers', IN_KEY, 'edX')),
    )

    def setUp(self):
        self.user = UserFactory()

    @ddt.data(*ROLES)
    @ddt.unpack
    def test_only_in_role(self, role, target):
        role.add_users(self.user)
        cache = RoleCache(self.user)
        self.assertTrue(cache.has_role(*target))

        for other_role, other_target in self.ROLES:
            if other_role == role:
                continue

            self.assertFalse(cache.has_role(*other_target))

    @ddt.data(*ROLES)
    @ddt.unpack
    def test_empty_cache(self, role, target):
        cache = RoleCache(self.user)
        self.assertFalse(cache.has_role(*target))
示例#2
0
def get_user_permissions(user, course_key, org=None):
    """
    Get the bitmask of permissions that this user has in the given course context.
    Can also set course_key=None and pass in an org to get the user's
    permissions for that organization as a whole.
    """
    if org is None:
        org = course_key.org
        course_key = course_key.for_branch(None)
    else:
        assert course_key is None
    # No one has studio permissions for CCX courses
    if is_ccx_course(course_key):
        return STUDIO_NO_PERMISSIONS
    all_perms = STUDIO_EDIT_ROLES | STUDIO_VIEW_USERS | STUDIO_EDIT_CONTENT | STUDIO_VIEW_CONTENT
    # global staff, org instructors, and course instructors have all permissions:
    if GlobalStaff().has_user(user) or OrgInstructorRole(
            org=org).has_user(user):
        return all_perms
    if course_key and user_has_role(user, CourseInstructorRole(course_key)):
        return all_perms
    if course_key and user_has_role(user, GlobalCourseCreatorRole(org)):
        return all_perms
    # Staff have all permissions except EDIT_ROLES:
    if OrgStaffRole(org=org).has_user(user) or (course_key and user_has_role(
            user, CourseStaffRole(course_key))):
        return STUDIO_VIEW_USERS | STUDIO_EDIT_CONTENT | STUDIO_VIEW_CONTENT
    # Otherwise, for libraries, users can view only:
    if course_key and isinstance(course_key, LibraryLocator):
        if OrgLibraryUserRole(org=org).has_user(user) or user_has_role(
                user, LibraryUserRole(course_key)):
            return STUDIO_VIEW_USERS | STUDIO_VIEW_CONTENT
    return STUDIO_NO_PERMISSIONS
示例#3
0
    def test_org_and_course_roles(self):
        """
        Test that Org and course roles don't interfere with course roles or vice versa
        """
        OrgInstructorRole(self.course_key.org).add_users(self.student)
        CourseInstructorRole(self.course_key).add_users(self.student)
        self.assertTrue(
            OrgInstructorRole(self.course_key.org).has_user(self.student),
            "Student doesn't have access to {}".format(
                unicode(self.course_key.org)))
        self.assertTrue(
            CourseInstructorRole(self.course_key).has_user(self.student),
            "Student doesn't have access to {}".format(unicode(
                self.course_key)))

        # remove access and confirm
        OrgInstructorRole(self.course_key.org).remove_users(self.student)
        self.assertFalse(
            OrgInstructorRole(self.course_key.org).has_user(self.student),
            "Student still has access to {}".format(self.course_key.org))
        self.assertTrue(
            CourseInstructorRole(self.course_key).has_user(self.student),
            "Student doesn't have access to {}".format(unicode(
                self.course_key)))

        # ok now keep org role and get rid of course one
        OrgInstructorRole(self.course_key.org).add_users(self.student)
        CourseInstructorRole(self.course_key).remove_users(self.student)
        self.assertTrue(
            OrgInstructorRole(self.course_key.org).has_user(self.student),
            "Student lost has access to {}".format(self.course_key.org))
        self.assertFalse(
            CourseInstructorRole(self.course_key).has_user(self.student),
            "Student doesn't have access to {}".format(unicode(
                self.course_key)))
示例#4
0
def administrative_accesses_to_course_for_user(user, course_key):
    """
    Returns types of access a user have for given course.
    """
    global_staff = GlobalStaff().has_user(user)

    staff_access = (CourseStaffRole(course_key).has_user(user)
                    or OrgStaffRole(course_key.org).has_user(user))

    instructor_access = (CourseInstructorRole(course_key).has_user(user)
                         or OrgInstructorRole(course_key.org).has_user(user))

    return global_staff, staff_access, instructor_access
示例#5
0
def _has_access_to_location(user, location, access_level, course_context):
    '''
    Returns True if the given user has access_level (= staff or
    instructor) access to a location.  For now this is equivalent to
    having staff / instructor access to the course location.course.

    This means that user is in the staff_* group or instructor_* group, or is an overall admin.

    TODO (vshnayder): this needs to be changed to allow per-course_id permissions, not per-course
    (e.g. staff in 2012 is different from 2013, but maybe some people always have access)

    course is a string: the course field of the location being accessed.
    location = location
    access_level = string, either "staff" or "instructor"
    '''
    if user is None or (not user.is_authenticated()):
        debug("Deny: no user or anon user")
        return False

    if is_masquerading_as_student(user):
        return False

    if GlobalStaff().has_user(user):
        debug("Allow: user.is_staff")
        return True

    if access_level not in ('staff', 'instructor'):
        log.debug(
            "Error in access._has_access_to_location access_level=%s unknown",
            access_level)
        debug("Deny: unknown access level")
        return False

    staff_access = (CourseStaffRole(location, course_context).has_user(user)
                    or OrgStaffRole(location).has_user(user))

    if staff_access and access_level == 'staff':
        debug("Allow: user has course staff access")
        return True

    instructor_access = (CourseInstructorRole(location,
                                              course_context).has_user(user)
                         or OrgInstructorRole(location).has_user(user))

    if instructor_access and access_level in ('staff', 'instructor'):
        debug("Allow: user has course instructor access")
        return True

    debug("Deny: user did not have correct access")
    return False
示例#6
0
def has_course_access(user, course_key, role=CourseStaffRole):
    """
    Return True if user allowed to access this course_id
    Note that the CMS permissions model is with respect to courses
    There is a super-admin permissions if user.is_staff is set
    Also, since we're unifying the user database between LMS and CAS,
    I'm presuming that the course instructor (formally known as admin)
    will not be in both INSTRUCTOR and STAFF groups, so we have to cascade our
    queries here as INSTRUCTOR has all the rights that STAFF do
    """
    if GlobalStaff().has_user(user):
        return True
    if OrgInstructorRole(org=course_key.org).has_user(user):
        return True
    if OrgStaffRole(org=course_key.org).has_user(user):
        return True
    return auth.has_access(user, role(course_key))
示例#7
0
def _has_access_to_course(user, access_level, course_key):
    '''
    Returns True if the given user has access_level (= staff or
    instructor) access to the course with the given course_key.
    This ensures the user is authenticated and checks if global staff or has
    staff / instructor access.

    access_level = string, either "staff" or "instructor"
    '''
    if user is None or (not user.is_authenticated()):
        debug("Deny: no user or anon user")
        return False

    if is_masquerading_as_student(user):
        return False

    if GlobalStaff().has_user(user):
        debug("Allow: user.is_staff")
        return True

    if access_level not in ('staff', 'instructor'):
        log.debug("Error in access._has_access_to_course access_level=%s unknown", access_level)
        debug("Deny: unknown access level")
        return False

    staff_access = (
        CourseStaffRole(course_key).has_user(user) or
        OrgStaffRole(course_key.org).has_user(user)
    )

    if staff_access and access_level == 'staff':
        debug("Allow: user has course staff access")
        return True

    instructor_access = (
        CourseInstructorRole(course_key).has_user(user) or
        OrgInstructorRole(course_key.org).has_user(user)
    )

    if instructor_access and access_level in ('staff', 'instructor'):
        debug("Allow: user has course instructor access")
        return True

    debug("Deny: user did not have correct access")
    return False
示例#8
0
def get_user_permissions(user, course_key, org=None):
    """
    Get the bitmask of permissions that this user has in the given course context.
    Can also set course_key=None and pass in an org to get the user's
    permissions for that organization as a whole.
    """
    if org is None:
        org = course_key.org
        course_key = course_key.for_branch(None)
    else:
        assert course_key is None
    # No one has studio permissions for CCX courses
    if is_ccx_course(course_key):
        return STUDIO_NO_PERMISSIONS
    all_perms = STUDIO_EDIT_ROLES | STUDIO_VIEW_USERS | STUDIO_EDIT_CONTENT | STUDIO_VIEW_CONTENT
    # global staff, org instructors, and course instructors have all permissions:
    if GlobalStaff().has_user(user) or OrgInstructorRole(
            org=org).has_user(user):
        return all_perms
    if course_key and user_has_role(user, CourseInstructorRole(course_key)):
        return all_perms
    # Staff have all permissions except EDIT_ROLES:
    if OrgStaffRole(org=org).has_user(user) or (course_key and user_has_role(
            user, CourseStaffRole(course_key))):
        return STUDIO_VIEW_USERS | STUDIO_EDIT_CONTENT | STUDIO_VIEW_CONTENT
    # Otherwise, for libraries, users can view only:
    if course_key and isinstance(course_key, LibraryLocator):
        if OrgLibraryUserRole(org=org).has_user(user) or user_has_role(
                user, LibraryUserRole(course_key)):
            return STUDIO_VIEW_USERS | STUDIO_VIEW_CONTENT
    # Finally, check if user is linked directly to the organization:
    user_org = OrganizationUser.objects.filter(
        active=True, organization__short_name=org,
        user_id=user.id).values().first()
    if user_org:
        if user_org['is_staff']:
            return all_perms
        else:
            return STUDIO_EDIT_CONTENT | STUDIO_VIEW_CONTENT
    return STUDIO_NO_PERMISSIONS
示例#9
0
def has_course_author_access(user, course_key, role=CourseStaffRole):
    """
    Return True if user has studio (write) access to the given course.
    Note that the CMS permissions model is with respect to courses.
    There is a super-admin permissions if user.is_staff is set.
    Also, since we're unifying the user database between LMS and CAS,
    I'm presuming that the course instructor (formally known as admin)
    will not be in both INSTRUCTOR and STAFF groups, so we have to cascade our
    queries here as INSTRUCTOR has all the rights that STAFF do.

    :param user:
    :param course_key: a CourseKey
    :param role: an AccessRole
    """
    if GlobalStaff().has_user(user):
        return True
    if OrgInstructorRole(org=course_key.org).has_user(user):
        return True
    if OrgStaffRole(org=course_key.org).has_user(user):
        return True
    # temporary to ensure we give universal access given a course until we impl branch specific perms
    return has_access(user, role(course_key.for_branch(None)))
示例#10
0
def has_staff_roles(user, course_key):
    """
    Return true if a user has any of the following roles
    Staff, Instructor, Beta Tester, Forum Community TA, Forum Group Moderator, Forum Moderator, Forum Administrator
    """
    forum_roles = [
        FORUM_ROLE_COMMUNITY_TA, FORUM_ROLE_GROUP_MODERATOR,
        FORUM_ROLE_MODERATOR, FORUM_ROLE_ADMINISTRATOR
    ]
    is_staff = CourseStaffRole(course_key).has_user(user)
    is_instructor = CourseInstructorRole(course_key).has_user(user)
    is_beta_tester = CourseBetaTesterRole(course_key).has_user(user)
    is_org_staff = OrgStaffRole(course_key.org).has_user(user)
    is_org_instructor = OrgInstructorRole(course_key.org).has_user(user)
    is_global_staff = GlobalStaff().has_user(user)
    has_forum_role = Role.user_has_role_for_course(user, course_key,
                                                   forum_roles)
    if any([
            is_staff, is_instructor, is_beta_tester, is_org_staff,
            is_org_instructor, is_global_staff, has_forum_role
    ]):
        return True
    return False
示例#11
0
class TestCourseListing(ModuleStoreTestCase, XssTestMixin):
    """
    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.
        # create and log in a non-staff user
        self.user = UserFactory()
        self.factory = RequestFactory()
        self.request = self.factory.get('/course')
        self.request.user = self.user
        self.client = AjaxEnabledTestClient()
        self.client.login(username=self.user.username, password='******')

    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
        """
        course = CourseFactory.create(
            org=course_location.org,
            number=course_location.course,
            run=course_location.run,
            default_store=store
        )

        if user is not None:
            for role in [CourseInstructorRole, CourseStaffRole]:
                role(course.id).add_users(user)

        return course

    def tearDown(self):
        """
        Reverse the setup
        """
        self.client.logout()
        ModuleStoreTestCase.tearDown(self)

    def test_course_listing_is_escaped(self):
        """
        Tests course listing returns escaped data.
        """
        escaping_content = "<script>alert('ESCAPE')</script>"

        # Make user staff to access course listing
        self.user.is_staff = True
        self.user.save()  # pylint: disable=no-member

        self.client = Client()
        self.client.login(username=self.user.username, password='******')

        # Change 'display_coursenumber' field and update the course.
        course = CourseFactory.create()
        course.display_coursenumber = escaping_content
        course = self.store.update_item(course, self.user.id)  # pylint: disable=no-member
        self.assertEqual(course.display_coursenumber, escaping_content)

        # Check if response is escaped
        response = self.client.get('/home')
        self.assertEqual(response.status_code, 200)
        self.assert_no_xss(response, escaping_content)

    def test_get_course_list(self):
        """
        Test getting courses with new access group format e.g. 'instructor_edx.course.run'
        """
        course_location = self.store.make_course_key('Org1', 'Course1', 'Run1')
        self._create_course_with_access_groups(course_location, self.user)

        # get courses through iterating all courses
        courses_list, __ = _accessible_courses_list(self.request)
        self.assertEqual(len(courses_list), 1)

        courses_summary_list, __ = _accessible_courses_summary_list(self.request)
        self.assertEqual(len(courses_summary_list), 1)

        # get courses by reversing group name formats
        courses_list_by_groups, __ = _accessible_courses_list_from_groups(self.request)
        self.assertEqual(len(courses_list_by_groups), 1)

        # check both course lists have same courses
        self.assertEqual(courses_list, courses_list_by_groups)

    @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_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, [])

    @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):
            _accessible_courses_summary_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)

        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_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, [])
                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).
        """
        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)
        self.assertEqual(len(courses_list), 1)

        courses_summary_list, __ = _accessible_courses_summary_list(self.request)

        # Verify fetched accessible courses list is a list of CourseSummery instances and only one course
        # is returned
        self.assertTrue(all(isinstance(course, CourseSummary) for course in courses_summary_list))
        self.assertEqual(len(courses_summary_list), 1)

        # get courses by reversing group name formats
        courses_list_by_groups, __ = _accessible_courses_list_from_groups(self.request)
        self.assertEqual(len(courses_list_by_groups), 1)

        # check course lists have same courses
        self.assertEqual(courses_list, courses_list_by_groups)

        # now delete this course and re-add user to instructor group of this course
        delete_course_and_groups(course_key, self.user.id)

        CourseInstructorRole(course_key).add_users(self.user)

        # Get courses through iterating all courses
        courses_list, __ = _accessible_courses_list(self.request)

        # Get course summaries by iterating all courses
        courses_summary_list, __ = _accessible_courses_summary_list(self.request)

        # Get courses by reversing group name formats
        courses_list_by_groups, __ = _accessible_courses_list_from_groups(self.request)

        # Test that course list returns no course
        self.assertEqual(
            [len(courses_list), len(courses_list_by_groups), len(courses_summary_list)],
            [0, 0, 0]
        )

    @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
        reversing django groups
        """
        # 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
        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:
            courses_list, __ = _accessible_courses_list(self.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(self.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(self.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(self.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)

        # 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):
            _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)

    @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.
        """
        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', '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)

    @ddt.data(OrgStaffRole('AwesomeOrg'), OrgInstructorRole('AwesomeOrg'))
    def test_course_listing_org_permissions(self, role):
        """
        Create multiple courses within the same org.  Verify that someone with org-wide permissions can access
        all of them.
        """
        org_course_one = self.store.make_course_key('AwesomeOrg', 'Course1', 'RunBabyRun')
        CourseFactory.create(
            org=org_course_one.org,
            number=org_course_one.course,
            run=org_course_one.run
        )

        org_course_two = self.store.make_course_key('AwesomeOrg', 'Course2', 'RunBabyRun')
        CourseFactory.create(
            org=org_course_two.org,
            number=org_course_two.course,
            run=org_course_two.run
        )

        # Two types of org-wide roles have edit permissions: staff and instructor.  We test both
        role.add_users(self.user)

        with self.assertRaises(AccessListFallback):
            _accessible_courses_list_from_groups(self.request)
        courses_list, __ = get_courses_accessible_to_user(self.request)

        # Verify fetched accessible courses list is a list of CourseSummery instances and test expacted
        # course count is returned
        self.assertEqual(len(courses_list), 2)
        self.assertTrue(all(isinstance(course, CourseSummary) for course in courses_list))

    def test_course_listing_with_actions_in_progress(self):
        sourse_course_key = CourseLocator('source-Org', 'source-Course', 'source-Run')

        num_courses_to_create = 3
        courses = [
            self._create_course_with_access_groups(CourseLocator('Org', 'CreatedCourse' + str(num), 'Run'), self.user)
            for num in range(num_courses_to_create)
        ]
        courses_in_progress = [
            self._create_course_with_access_groups(CourseLocator('Org', 'InProgressCourse' + str(num), 'Run'), self.user)
            for num in range(num_courses_to_create)
        ]

        # simulate initiation of course actions
        for course in courses_in_progress:
            CourseRerunState.objects.initiated(
                sourse_course_key, destination_course_key=course.id, user=self.user, display_name="test course"
            )

        # verify return values
        for method in (_accessible_courses_list_from_groups, _accessible_courses_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')
            )
示例#12
0
 def course(self, create, extracted, **kwargs):
     if extracted is None:
         raise ValueError(
             "Must specify a course location for an org-instructor user")
     OrgInstructorRole(extracted).add_users(self)
示例#13
0
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.
        # create and log in a non-staff user
        self.user = UserFactory()
        self.factory = RequestFactory()
        self.request = self.factory.get('/course')
        self.request.user = self.user
        self.client = AjaxEnabledTestClient()
        self.client.login(username=self.user.username, password='******')

    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
        """
        course = CourseFactory.create(org=course_location.org,
                                      number=course_location.course,
                                      run=course_location.run,
                                      default_store=store)
        self._add_role_access_to_user(user, course.id)
        return course

    def _add_role_access_to_user(self, user, course_id):
        """ Assign access roles to user in the course. """
        if user is not None:
            for role in [CourseInstructorRole, CourseStaffRole]:
                role(course_id).add_users(user)

    def tearDown(self):
        """
        Reverse the setup
        """
        self.client.logout()
        ModuleStoreTestCase.tearDown(self)

    def test_empty_course_listing(self):
        """
        Test on empty course listing, studio name is properly displayed
        """
        message = u"Are you staff on an existing {studio_name} course?".format(
            studio_name=settings.STUDIO_SHORT_NAME)
        response = self.client.get('/home')
        self.assertContains(response, message)

    def test_get_course_list(self):
        """
        Test getting courses with new access group format e.g. 'instructor_edx.course.run'
        """
        course_location = self.store.make_course_key('Org1', 'Course1', 'Run1')
        self._create_course_with_access_groups(course_location, self.user)

        # get courses through iterating all courses
        courses_iter, __ = _accessible_courses_iter_for_tests(self.request)
        courses_list = list(courses_iter)
        self.assertEqual(len(courses_list), 1)

        courses_summary_list, __ = _accessible_courses_summary_iter(
            self.request)
        self.assertEqual(len(list(courses_summary_list)), 1)

        # get courses by reversing group name formats
        courses_list_by_groups, __ = _accessible_courses_list_from_groups(
            self.request)
        self.assertEqual(len(courses_list_by_groups), 1)

        # check both course lists have same courses
        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):
        """
        Tests that CCX courses are filtered in course listing.
        """
        # Create a course and assign access roles to user.
        course_location = self.store.make_course_key('Org1', 'Course1', 'Run1')
        course = self._create_course_with_access_groups(
            course_location, self.user)

        # Create a ccx course key and add assign access roles to user.
        ccx_course_key = CCXLocator.from_course_locator(course.id, '1')
        self._add_role_access_to_user(self.user, ccx_course_key)

        # Test that CCX courses are filtered out.
        courses_list, __ = _accessible_courses_list_from_groups(self.request)
        self.assertEqual(len(courses_list), 1)
        self.assertNotIn(ccx_course_key,
                         [course.id for course in courses_list])

        # Get all courses which user has access.
        instructor_courses = UserBasedRole(
            self.user, CourseInstructorRole.ROLE).courses_with_role()
        staff_courses = UserBasedRole(
            self.user, CourseStaffRole.ROLE).courses_with_role()
        all_courses = (instructor_courses | staff_courses)

        # Verify that CCX course exists in access but filtered by `_accessible_courses_list_from_groups`.
        self.assertIn(ccx_course_key,
                      [access.course_id for access in all_courses])

        # 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_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, 3), (ModuleStoreEnum.Type.mongo, 2))
    @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 range(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(list(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):
            list(_accessible_courses_summary_iter(self.request))

    @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).
        """
        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_iter, __ = _accessible_courses_iter_for_tests(self.request)
        courses_list = list(courses_iter)
        self.assertEqual(len(courses_list), 1)

        courses_summary_iter, __ = _accessible_courses_summary_iter(
            self.request)
        courses_summary_list = list(courses_summary_iter)

        # Verify fetched accessible courses list is a list of CourseSummery instances and only one course
        # is returned
        self.assertTrue(
            all(
                isinstance(course, CourseSummary)
                for course in courses_summary_list))
        self.assertEqual(len(courses_summary_list), 1)

        # get courses by reversing group name formats
        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(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_for_tests(self.request)

        # Get course summaries by iterating all courses
        courses_summary_iter, __ = _accessible_courses_summary_iter(
            self.request)

        # Get courses by reversing group name formats
        courses_list_by_groups, __ = _accessible_courses_list_from_groups(
            self.request)

        # Test that course list returns no course
        self.assertEqual([
            len(list(courses_iter)),
            len(courses_list_by_groups),
            len(list(courses_summary_iter))
        ], [0, 0, 0])

    @ddt.data((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):
        """
        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 list of random course numbers which will be accessible to the user
        user_course_ids = random.sample(list(range(TOTAL_COURSES_COUNT)),
                                        USER_COURSES_COUNT)

        # create courses and assign those to the user which have their number in user_course_ids
        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)

        # get courses by iterating through all courses
        courses_iter, __ = _accessible_courses_iter_for_tests(self.request)
        self.assertEqual(len(list(courses_iter)), USER_COURSES_COUNT)

        # again get courses by iterating through all courses
        courses_iter, __ = _accessible_courses_iter_for_tests(self.request)
        self.assertEqual(len(list(courses_iter)), USER_COURSES_COUNT)

        # get courses by reversing django groups
        courses_list, __ = _accessible_courses_list_from_groups(self.request)
        self.assertEqual(len(courses_list), USER_COURSES_COUNT)

        # again get courses by reversing django groups
        courses_list, __ = _accessible_courses_list_from_groups(self.request)
        self.assertEqual(len(courses_list), USER_COURSES_COUNT)

        # 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_for_tests(self.request))
        # Calls:
        #    1) query old mongo
        #    2) get_more on old mongo
        #    3) query split (but no courses so no fetching of data)

    @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.
        """
        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', 'doomedCourse', 'RunBabyRun')
            self._create_course_with_access_groups(course_location, self.user,
                                                   store)
            self.store.delete_course(course_location, self.user.id)

        courses_list, __ = _accessible_courses_list_from_groups(self.request)
        self.assertEqual(len(courses_list), 1, courses_list)

    @ddt.data(OrgStaffRole('AwesomeOrg'), OrgInstructorRole('AwesomeOrg'))
    def test_course_listing_org_permissions(self, role):
        """
        Create multiple courses within the same org.  Verify that someone with org-wide permissions can access
        all of them.
        """
        org_course_one = self.store.make_course_key('AwesomeOrg', 'Course1',
                                                    'RunBabyRun')
        CourseFactory.create(org=org_course_one.org,
                             number=org_course_one.course,
                             run=org_course_one.run)

        org_course_two = self.store.make_course_key('AwesomeOrg', 'Course2',
                                                    'RunBabyRun')
        CourseFactory.create(org=org_course_two.org,
                             number=org_course_two.course,
                             run=org_course_two.run)

        # Two types of org-wide roles have edit permissions: staff and instructor.  We test both
        role.add_users(self.user)

        with self.assertRaises(AccessListFallback):
            _accessible_courses_list_from_groups(self.request)
        courses_list, __ = get_courses_accessible_to_user(self.request)

        # Verify fetched accessible courses list is a list of CourseSummery instances and test expacted
        # course count is returned
        self.assertEqual(len(list(courses_list)), 2)
        self.assertTrue(
            all(isinstance(course, CourseSummary) for course in courses_list))

    def test_course_listing_with_actions_in_progress(self):
        sourse_course_key = CourseLocator('source-Org', 'source-Course',
                                          'source-Run')

        num_courses_to_create = 3
        courses = [
            self._create_course_with_access_groups(
                CourseLocator('Org', 'CreatedCourse' + str(num), 'Run'),
                self.user,
            ) for num in range(num_courses_to_create)
        ]
        courses_in_progress = [
            self._create_course_with_access_groups(
                CourseLocator('Org', 'InProgressCourse' + str(num), 'Run'),
                self.user,
            ) for num in range(num_courses_to_create)
        ]

        # simulate initiation of course actions
        for course in courses_in_progress:
            CourseRerunState.objects.initiated(
                sourse_course_key,
                destination_course_key=course.id,
                user=self.user,
                display_name="test course")

        # verify return values
        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 = _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'))
示例#14
0
 def test_request_org_instructor_courses_with_claims(self):
     OrgInstructorRole(self.course_key.org).add_users(self.user)
     self._assert_role_using_claim('course_instructor',
                                   'instructor_courses')
示例#15
0
 def course_key(self, create, extracted, **kwargs):
     if extracted is None:
         raise ValueError(
             "Must specify a CourseKey for an org-instructor user")
     OrgInstructorRole(extracted.org).add_users(self)
示例#16
0
def set_roles_for_edx_users(user, permissions, strategy):
    '''
    This function is specific functional for open-edx platform.
    It create roles for edx users from sso permissions.
    '''

    log_message = 'For User: {}, object_type {} and object_id {} there is not matched Role for Permission set: {}'

    global_perm = {
        'Read', 'Update', 'Delete', 'Publication', 'Enroll',
        'Manage(permissions)'
    }
    staff_perm = {'Read', 'Update', 'Delete', 'Publication', 'Enroll'}
    tester_perm = {'Read', 'Enroll'}

    role_ids = set(user.courseaccessrole_set.values_list('id', flat=True))
    new_role_ids = []

    is_global_staff = False
    for role in permissions:
        _log = False
        if role['obj_type'] == '*':
            if '*' in role['obj_perm'] or global_perm.issubset(
                    set(role['obj_perm'])):
                GlobalStaff().add_users(user)
                is_global_staff = True

            elif 'Create' in role['obj_perm']:
                if not CourseCreatorRole().has_user(user):
                    CourseCreatorRole().add_users(user)
                car = CourseAccessRole.objects.get(user=user,
                                                   role=CourseCreatorRole.ROLE)
                new_role_ids.append(car.id)

            if role['obj_perm'] != '*' and global_perm != set(
                    role['obj_perm']) and ['Create'] != role['obj_perm']:
                _log = True

        elif role['obj_type'] == 'edxorg':
            if '*' in role['obj_perm'] or global_perm.issubset(
                    set(role['obj_perm'])):
                if not OrgInstructorRole(role['obj_id']).has_user(user):
                    OrgInstructorRole(role['obj_id']).add_users(user)
                car = CourseAccessRole.objects.get(
                    user=user,
                    role=OrgInstructorRole(role['obj_id'])._role_name,
                    org=role['obj_id'])
                new_role_ids.append(car.id)

            elif staff_perm.issubset(set(role['obj_perm'])):
                if not OrgStaffRole(role['obj_id']).has_user(user):
                    OrgStaffRole(role['obj_id']).add_users(user)
                car = CourseAccessRole.objects.get(
                    user=user,
                    role=OrgStaffRole(role['obj_id'])._role_name,
                    org=role['obj_id'])
                new_role_ids.append(car.id)

            elif 'Read' in role['obj_perm']:
                if not OrgLibraryUserRole(role['obj_id']).has_user(user):
                    OrgLibraryUserRole(role['obj_id']).add_users(user)
                car = CourseAccessRole.objects.get(
                    user=user,
                    role=OrgLibraryUserRole.ROLE,
                    org=role['obj_id'])
                new_role_ids.append(car.id)

            if role['obj_perm'] != '*' and global_perm != set(role['obj_perm']) and \
                    staff_perm != set(role['obj_perm']) and 'Read' not in role['obj_perm']:
                _log = True

        elif role['obj_type'] in ['edxcourse', 'edxlibrary']:

            course_key = CourseKey.from_string(role['obj_id'])

            if '*' in role['obj_perm'] or global_perm.issubset(
                    set(role['obj_perm'])):
                if not CourseInstructorRole(course_key).has_user(user):
                    CourseInstructorRole(course_key).add_users(user)
                car = CourseAccessRole.objects.get(
                    user=user,
                    role=CourseInstructorRole.ROLE,
                    course_id=course_key)
                new_role_ids.append(car.id)

            elif staff_perm.issubset(set(role['obj_perm'])):
                if not CourseStaffRole(course_key).has_user(user):
                    CourseStaffRole(course_key).add_users(user)
                car = CourseAccessRole.objects.get(user=user,
                                                   role=CourseStaffRole.ROLE,
                                                   course_id=course_key)
                new_role_ids.append(car.id)

            elif tester_perm.issubset(set(role['obj_perm'])):
                if not CourseBetaTesterRole(course_key).has_user(user):
                    CourseBetaTesterRole(course_key).add_users(user)
                car = CourseAccessRole.objects.get(
                    user=user,
                    role=CourseBetaTesterRole.ROLE,
                    course_id=course_key)
                new_role_ids.append(car.id)

            elif role['obj_type'] == 'edxlibrary' and 'Read' in role[
                    'obj_perm']:
                if not LibraryUserRole(course_key).has_user(user):
                    LibraryUserRole(course_key).add_users(user)
                car = CourseAccessRole.objects.get(
                    user=user,
                    role=CourseBetaTesterRole.ROLE,
                    course_id=course_key)
                new_role_ids.append(car.id)

            if role['obj_perm'] != '*' and global_perm != set(role['obj_perm']) and \
                staff_perm != set(role['obj_perm']) and tester_perm != set(role['obj_perm']) and 'Read' not in role['obj_perm']:
                _log = True

        elif role['obj_type'] == 'edxcourserun':

            course_key = CourseKey.from_string(role['obj_id'])

            if '*' in role['obj_perm'] or global_perm.issubset(
                    set(role['obj_perm'])):
                if not CourseInstructorRole(course_key).has_user(user):
                    CourseInstructorRole(course_key).add_users(user)
                car = CourseAccessRole.objects.get(
                    user=user,
                    role=CourseInstructorRole.ROLE,
                    course_id=course_key)
                new_role_ids.append(car.id)
            elif staff_perm.issubset(set(role['obj_perm'])):
                if not CourseStaffRole(course_key).has_user(user):
                    CourseStaffRole(course_key).add_users(user)
                car = CourseAccessRole.objects.get(user=user,
                                                   role=CourseStaffRole.ROLE,
                                                   course_id=course_key)
                new_role_ids.append(car.id)
            elif tester_perm.issubset(set(role['obj_perm'])):
                if not CourseBetaTesterRole(course_key).has_user(user):
                    CourseBetaTesterRole(course_key).add_users(user)
                car = CourseAccessRole.objects.get(
                    user=user,
                    role=CourseBetaTesterRole.ROLE,
                    course_id=course_key)
                new_role_ids.append(car.id)

            if role['obj_perm'] != '*' and global_perm != set(role['obj_perm']) and \
                staff_perm != set(role['obj_perm']) and tester_perm != set(role['obj_perm']):
                _log = True

        if _log:
            logging.warning(
                log_message.format(user.id, role['obj_type'], role['obj_id'],
                                   str(role['obj_perm'])))

    if (not is_global_staff) and GlobalStaff().has_user(user):
        GlobalStaff().remove_users(user)

    remove_roles = role_ids - set(new_role_ids)

    if remove_roles:
        entries = CourseAccessRole.objects.filter(id__in=list(remove_roles))
        entries.delete()
示例#17
0
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.
        # create and log in a non-staff user
        self.user = UserFactory()
        self.factory = RequestFactory()
        self.request = self.factory.get('/course')
        self.request.user = self.user
        self.client = AjaxEnabledTestClient()
        self.client.login(username=self.user.username, password='******')

    def _create_course_with_access_groups(self, course_location, user=None):
        """
        Create dummy course with 'CourseFactory' and role (instructor/staff) groups
        """
        course = CourseFactory.create(org=course_location.org,
                                      number=course_location.course,
                                      run=course_location.run)

        if user is not None:
            for role in [CourseInstructorRole, CourseStaffRole]:
                role(course.id).add_users(user)

        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'
        """
        course_location = SlashSeparatedCourseKey('Org1', 'Course1', 'Run1')
        self._create_course_with_access_groups(course_location, self.user)

        # get courses through iterating all courses
        courses_list = _accessible_courses_list(self.request)
        self.assertEqual(len(courses_list), 1)

        # get courses by reversing group name formats
        courses_list_by_groups = _accessible_courses_list_from_groups(
            self.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_errored_course_global_staff(self):
        """
        Test the course list for global staff when get_course returns an ErrorDescriptor
        """
        GlobalStaff().add_users(self.user)

        course_key = SlashSeparatedCourseKey('Org1', 'Course1', 'Run1')
        self._create_course_with_access_groups(course_key, self.user)

        with patch('xmodule.modulestore.mongo.base.MongoKeyValueStore',
                   Mock(side_effect=Exception)):
            self.assertIsInstance(modulestore().get_course(course_key),
                                  ErrorDescriptor)

            # 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, [])

    def test_errored_course_regular_access(self):
        """
        Test the course list for regular staff when get_course returns an ErrorDescriptor
        """
        GlobalStaff().remove_users(self.user)
        CourseStaffRole(SlashSeparatedCourseKey('Non', 'Existent',
                                                'Course')).add_users(self.user)

        course_key = SlashSeparatedCourseKey('Org1', 'Course1', 'Run1')
        self._create_course_with_access_groups(course_key, self.user)

        with patch('xmodule.modulestore.mongo.base.MongoKeyValueStore',
                   Mock(side_effect=Exception)):
            self.assertIsInstance(modulestore().get_course(course_key),
                                  ErrorDescriptor)

            # 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, [])
            self.assertEqual(courses_list, courses_list_by_groups)

    def test_get_course_list_with_invalid_course_location(self):
        """
        Test getting courses with invalid course location (course deleted from modulestore).
        """
        course_key = SlashSeparatedCourseKey('Org', 'Course', 'Run')
        self._create_course_with_access_groups(course_key, self.user)

        # get courses through iterating all courses
        courses_list = _accessible_courses_list(self.request)
        self.assertEqual(len(courses_list), 1)

        # get courses by reversing group name formats
        courses_list_by_groups = _accessible_courses_list_from_groups(
            self.request)
        self.assertEqual(len(courses_list_by_groups), 1)
        # check both course lists have same courses
        self.assertEqual(courses_list, courses_list_by_groups)

        # now delete this course and re-add user to instructor group of this course
        delete_course_and_groups(course_key, commit=True)

        CourseInstructorRole(course_key).add_users(self.user)

        # test that get courses through iterating all courses now returns no course
        courses_list = _accessible_courses_list(self.request)
        self.assertEqual(len(courses_list), 0)

    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 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(TOTAL_COURSES_COUNT):
            org = 'Org{0}'.format(number)
            course = 'Course{0}'.format(number)
            run = 'Run{0}'.format(number)
            course_location = SlashSeparatedCourseKey(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)

        # time the get courses by iterating through all courses
        with Timer() as iteration_over_courses_time_1:
            courses_list = _accessible_courses_list(self.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(self.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(self.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(self.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)

    def test_get_course_list_with_same_course_id(self):
        """
        Test getting courses with same id but with different name case. Then try to delete one of them and
        check that it is properly deleted and other one is accessible
        """
        course_location_caps = SlashSeparatedCourseKey('Org', 'COURSE', 'Run')
        self._create_course_with_access_groups(course_location_caps, self.user)

        # get courses through iterating all courses
        courses_list = _accessible_courses_list(self.request)
        self.assertEqual(len(courses_list), 1)

        # get courses by reversing group name formats
        courses_list_by_groups = _accessible_courses_list_from_groups(
            self.request)
        self.assertEqual(len(courses_list_by_groups), 1)
        # check both course lists have same courses
        self.assertEqual(courses_list, courses_list_by_groups)

        # now create another course with same course_id but different name case
        course_location_camel = SlashSeparatedCourseKey('Org', 'Course', 'Run')
        self._create_course_with_access_groups(course_location_camel,
                                               self.user)

        # test that get courses through iterating all courses returns both courses
        courses_list = _accessible_courses_list(self.request)
        self.assertEqual(len(courses_list), 2)

        # test that get courses by reversing group name formats returns both courses
        courses_list_by_groups = _accessible_courses_list_from_groups(
            self.request)
        self.assertEqual(len(courses_list_by_groups), 2)

        # now delete first course (course_location_caps) and check that it is no longer accessible
        delete_course_and_groups(course_location_caps, commit=True)

        # test that get courses through iterating all courses now returns one course
        courses_list = _accessible_courses_list(self.request)
        self.assertEqual(len(courses_list), 1)

        # test that get courses by reversing group name formats also returns one course
        courses_list_by_groups = _accessible_courses_list_from_groups(
            self.request)
        self.assertEqual(len(courses_list_by_groups), 1)

        # now check that deleted course is not accessible
        outline_url = reverse_course_url('course_handler',
                                         course_location_caps)
        response = self.client.get(outline_url, HTTP_ACCEPT='application/json')
        self.assertEqual(response.status_code, 403)

        # now check that other course is accessible
        outline_url = reverse_course_url('course_handler',
                                         course_location_camel)
        response = self.client.get(outline_url, HTTP_ACCEPT='application/json')
        self.assertEqual(response.status_code, 200)

    def test_course_listing_errored_deleted_courses(self):
        """
        Create good courses, courses that won't load, and deleted courses which still have
        roles. Test course listing.
        """
        course_location = SlashSeparatedCourseKey('testOrg', 'testCourse',
                                                  'RunBabyRun')
        self._create_course_with_access_groups(course_location, self.user)

        course_location = SlashSeparatedCourseKey('testOrg', 'doomedCourse',
                                                  'RunBabyRun')
        self._create_course_with_access_groups(course_location, self.user)
        modulestore().delete_course(course_location)

        course_location = SlashSeparatedCourseKey('testOrg', 'erroredCourse',
                                                  'RunBabyRun')
        course = self._create_course_with_access_groups(
            course_location, self.user)
        course_db_record = modulestore()._find_one(course.location)
        course_db_record.setdefault('metadata', {}).get('tabs', []).append({
            "type":
            "wiko",
            "name":
            "Wiki"
        })
        modulestore().collection.update(
            {'_id': course.location.to_deprecated_son()},
            {
                '$set': {
                    'metadata.tabs': course_db_record['metadata']['tabs'],
                }
            },
        )

        courses_list = _accessible_courses_list_from_groups(self.request)
        self.assertEqual(len(courses_list), 1, courses_list)

    @ddt.data(OrgStaffRole('AwesomeOrg'), OrgInstructorRole('AwesomeOrg'))
    def test_course_listing_org_permissions(self, role):
        """
        Create multiple courses within the same org.  Verify that someone with org-wide permissions can access
        all of them.
        """
        org_course_one = SlashSeparatedCourseKey('AwesomeOrg', 'Course1',
                                                 'RunBabyRun')
        CourseFactory.create(org=org_course_one.org,
                             number=org_course_one.course,
                             run=org_course_one.run)

        org_course_two = SlashSeparatedCourseKey('AwesomeOrg', 'Course2',
                                                 'RunRunRun')
        CourseFactory.create(org=org_course_two.org,
                             number=org_course_two.course,
                             run=org_course_two.run)

        # Two types of org-wide roles have edit permissions: staff and instructor.  We test both
        role.add_users(self.user)

        with self.assertRaises(AccessListFallback):
            _accessible_courses_list_from_groups(self.request)
        courses_list = _accessible_courses_list(self.request)
        self.assertEqual(len(courses_list), 2)
示例#18
0
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.
        # create and log in a non-staff user
        self.user = UserFactory()
        self.factory = RequestFactory()
        self.request = self.factory.get('/course')
        self.request.user = self.user
        self.client = AjaxEnabledTestClient()
        self.client.login(username=self.user.username, password='******')

    def _create_course_with_access_groups(self, course_location, user=None):
        """
        Create dummy course with 'CourseFactory' and role (instructor/staff) groups
        """
        course = CourseFactory.create(org=course_location.org,
                                      number=course_location.course,
                                      run=course_location.run,
                                      default_store=ModuleStoreEnum.Type.mongo)

        if user is not None:
            for role in [CourseInstructorRole, CourseStaffRole]:
                role(course.id).add_users(user)

        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'
        """
        course_location = self.store.make_course_key('Org1', 'Course1', 'Run1')
        self._create_course_with_access_groups(course_location, self.user)

        # get courses through iterating all courses
        courses_list, __ = _accessible_courses_list(self.request)
        self.assertEqual(len(courses_list), 1)

        # get courses by reversing group name formats
        courses_list_by_groups, __ = _accessible_courses_list_from_groups(
            self.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_errored_course_global_staff(self):
        """
        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 patch('xmodule.modulestore.mongo.base.MongoKeyValueStore',
                   Mock(side_effect=Exception)):
            self.assertIsInstance(modulestore().get_course(course_key),
                                  ErrorDescriptor)

            # 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, [])

    def test_errored_course_regular_access(self):
        """
        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 patch('xmodule.modulestore.mongo.base.MongoKeyValueStore',
                   Mock(side_effect=Exception)):
            self.assertIsInstance(modulestore().get_course(course_key),
                                  ErrorDescriptor)

            # 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, [])
            self.assertEqual(courses_list, courses_list_by_groups)

    def test_get_course_list_with_invalid_course_location(self):
        """
        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)

        # get courses through iterating all courses
        courses_list, __ = _accessible_courses_list(self.request)
        self.assertEqual(len(courses_list), 1)

        # get courses by reversing group name formats
        courses_list_by_groups, __ = _accessible_courses_list_from_groups(
            self.request)
        self.assertEqual(len(courses_list_by_groups), 1)
        # check both course lists have same courses
        self.assertEqual(courses_list, courses_list_by_groups)

        # now delete this course and re-add user to instructor group of this course
        delete_course_and_groups(course_key, self.user.id)

        CourseInstructorRole(course_key).add_users(self.user)

        # test that get courses through iterating all courses now returns no course
        courses_list, __ = _accessible_courses_list(self.request)
        self.assertEqual(len(courses_list), 0)

    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 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(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)

        # time the get courses by iterating through all courses
        with Timer() as iteration_over_courses_time_1:
            courses_list, __ = _accessible_courses_list(self.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(self.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(
                self.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(
                self.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)

        # Now count the db queries
        with check_mongo_calls(USER_COURSES_COUNT):
            _accessible_courses_list_from_groups(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):
        """
        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)

        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)

        courses_list, __ = _accessible_courses_list_from_groups(self.request)
        self.assertEqual(len(courses_list), 1, courses_list)

    @ddt.data(OrgStaffRole('AwesomeOrg'), OrgInstructorRole('AwesomeOrg'))
    def test_course_listing_org_permissions(self, role):
        """
        Create multiple courses within the same org.  Verify that someone with org-wide permissions can access
        all of them.
        """
        org_course_one = self.store.make_course_key('AwesomeOrg', 'Course1',
                                                    'RunBabyRun')
        CourseFactory.create(org=org_course_one.org,
                             number=org_course_one.course,
                             run=org_course_one.run)

        org_course_two = self.store.make_course_key('AwesomeOrg', 'Course2',
                                                    'RunRunRun')
        CourseFactory.create(org=org_course_two.org,
                             number=org_course_two.course,
                             run=org_course_two.run)

        # Two types of org-wide roles have edit permissions: staff and instructor.  We test both
        role.add_users(self.user)

        with self.assertRaises(AccessListFallback):
            _accessible_courses_list_from_groups(self.request)
        courses_list, __ = _accessible_courses_list(self.request)
        self.assertEqual(len(courses_list), 2)

    def test_course_listing_with_actions_in_progress(self):
        sourse_course_key = CourseLocator('source-Org', 'source-Course',
                                          'source-Run')

        num_courses_to_create = 3
        courses = [
            self._create_course_with_access_groups(
                CourseLocator('Org', 'CreatedCourse' + str(num), 'Run'),
                self.user) for num in range(num_courses_to_create)
        ]
        courses_in_progress = [
            self._create_course_with_access_groups(
                CourseLocator('Org', 'InProgressCourse' + str(num), 'Run'),
                self.user) for num in range(num_courses_to_create)
        ]

        # simulate initiation of course actions
        for course in courses_in_progress:
            CourseRerunState.objects.initiated(
                sourse_course_key,
                destination_course_key=course.id,
                user=self.user,
                display_name="test course")

        # verify return values
        for method in (_accessible_courses_list_from_groups,
                       _accessible_courses_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'))