Merge branch 'release'
Conflicts: common/djangoapps/course_modes/views.py common/djangoapps/student/tests/test_roles.py common/djangoapps/student/views.py common/lib/opaque_keys/opaque_keys/__init__.py common/lib/opaque_keys/opaque_keys/tests/test_opaque_keys.py common/lib/xmodule/xmodule/contentstore/mongo.py lms/djangoapps/certificates/management/commands/gen_cert_report.py lms/djangoapps/notes/views.py
This commit is contained in:
@@ -171,7 +171,7 @@ def remove_transcripts_from_store(_step, subs_id):
|
||||
)
|
||||
try:
|
||||
content = contentstore().find(content_location)
|
||||
contentstore().delete(content.get_id())
|
||||
contentstore().delete(content.location)
|
||||
print('Transcript file was removed from store.')
|
||||
except NotFoundError:
|
||||
print('Transcript file was NOT found and not removed.')
|
||||
|
||||
@@ -6,14 +6,15 @@ import random
|
||||
|
||||
from chrono import Timer
|
||||
from mock import patch, Mock
|
||||
import ddt
|
||||
|
||||
from django.test import RequestFactory
|
||||
|
||||
from contentstore.views.course import _accessible_courses_list, _accessible_courses_list_from_groups
|
||||
from contentstore.views.course import _accessible_courses_list, _accessible_courses_list_from_groups, AccessListFallback
|
||||
from contentstore.utils import delete_course_and_groups, reverse_course_url
|
||||
from contentstore.tests.utils import AjaxEnabledTestClient
|
||||
from student.tests.factories import UserFactory
|
||||
from student.roles import CourseInstructorRole, CourseStaffRole, GlobalStaff
|
||||
from student.roles import CourseInstructorRole, CourseStaffRole, GlobalStaff, OrgStaffRole, OrgInstructorRole
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
from opaque_keys.edx.locations import SlashSeparatedCourseKey
|
||||
@@ -24,6 +25,7 @@ TOTAL_COURSES_COUNT = 500
|
||||
USER_COURSES_COUNT = 50
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class TestCourseListing(ModuleStoreTestCase):
|
||||
"""
|
||||
Unit tests for getting the list of courses for a logged in user
|
||||
@@ -263,7 +265,7 @@ class TestCourseListing(ModuleStoreTestCase):
|
||||
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_db_record['_id']},
|
||||
{'_id': course.location.to_deprecated_son()},
|
||||
{'$set': {
|
||||
'metadata.tabs': course_db_record['metadata']['tabs'],
|
||||
}},
|
||||
@@ -271,3 +273,31 @@ class TestCourseListing(ModuleStoreTestCase):
|
||||
|
||||
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)
|
||||
|
||||
@@ -11,7 +11,7 @@ from contentstore.tests.modulestore_config import TEST_MODULESTORE
|
||||
from contentstore.tests.utils import AjaxEnabledTestClient
|
||||
from opaque_keys.edx.locations import SlashSeparatedCourseKey
|
||||
from contentstore.utils import reverse_url, reverse_course_url
|
||||
from student.roles import CourseInstructorRole, CourseStaffRole
|
||||
from student.roles import CourseInstructorRole, CourseStaffRole, OrgStaffRole, OrgInstructorRole
|
||||
from contentstore.views.access import has_course_access
|
||||
from student import auth
|
||||
|
||||
@@ -95,10 +95,13 @@ class TestCourseAccess(ModuleStoreTestCase):
|
||||
# doesn't use role.users_with_role b/c it's verifying the roles.py behavior
|
||||
user_by_role = {}
|
||||
# add the misc users to the course in different groups
|
||||
for role in [CourseInstructorRole, CourseStaffRole]:
|
||||
for role in [CourseInstructorRole, CourseStaffRole, OrgStaffRole, OrgInstructorRole]:
|
||||
user_by_role[role] = []
|
||||
# pylint: disable=protected-access
|
||||
group = role(self.course_key)
|
||||
# Org-based roles are created via org name, rather than course_key
|
||||
if (role is OrgStaffRole) or (role is OrgInstructorRole):
|
||||
group = role(self.course_key.org)
|
||||
else:
|
||||
group = role(self.course_key)
|
||||
# NOTE: this loop breaks the roles.py abstraction by purposely assigning
|
||||
# users to one of each possible groupname in order to test that has_course_access
|
||||
# and remove_user work
|
||||
@@ -109,21 +112,28 @@ class TestCourseAccess(ModuleStoreTestCase):
|
||||
|
||||
course_team_url = reverse_course_url('course_team_handler', self.course_key)
|
||||
response = self.client.get_html(course_team_url)
|
||||
for role in [CourseInstructorRole, CourseStaffRole]:
|
||||
for role in [CourseInstructorRole, CourseStaffRole]: # Global and org-based roles don't appear on this page
|
||||
for user in user_by_role[role]:
|
||||
self.assertContains(response, user.email)
|
||||
|
||||
# test copying course permissions
|
||||
copy_course_key = SlashSeparatedCourseKey('copyu', 'copydept.mycourse', 'myrun')
|
||||
for role in [CourseInstructorRole, CourseStaffRole]:
|
||||
auth.add_users(
|
||||
self.user,
|
||||
role(copy_course_key),
|
||||
*role(self.course_key).users_with_role()
|
||||
)
|
||||
for role in [CourseInstructorRole, CourseStaffRole, OrgStaffRole, OrgInstructorRole]:
|
||||
if (role is OrgStaffRole) or (role is OrgInstructorRole):
|
||||
auth.add_users(
|
||||
self.user,
|
||||
role(copy_course_key.org),
|
||||
*role(self.course_key.org).users_with_role()
|
||||
)
|
||||
else:
|
||||
auth.add_users(
|
||||
self.user,
|
||||
role(copy_course_key),
|
||||
*role(self.course_key).users_with_role()
|
||||
)
|
||||
# verify access in copy course and verify that removal from source course w/ the various
|
||||
# groupnames works
|
||||
for role in [CourseInstructorRole, CourseStaffRole]:
|
||||
for role in [CourseInstructorRole, CourseStaffRole, OrgStaffRole, OrgInstructorRole]:
|
||||
for user in user_by_role[role]:
|
||||
# forcefully decache the groups: premise is that any real request will not have
|
||||
# multiple objects repr the same user but this test somehow uses different instance
|
||||
@@ -132,5 +142,8 @@ class TestCourseAccess(ModuleStoreTestCase):
|
||||
del user._roles
|
||||
|
||||
self.assertTrue(has_course_access(user, copy_course_key), "{} no copy access".format(user))
|
||||
auth.remove_users(self.user, role(self.course_key), user)
|
||||
if (role is OrgStaffRole) or (role is OrgInstructorRole):
|
||||
auth.remove_users(self.user, role(self.course_key.org), user)
|
||||
else:
|
||||
auth.remove_users(self.user, role(self.course_key), user)
|
||||
self.assertFalse(has_course_access(user, self.course_key), "{} remove didn't work".format(user))
|
||||
|
||||
@@ -88,7 +88,7 @@ class TestSaveSubsToStore(ModuleStoreTestCase):
|
||||
"""Remove, if subtitles content exists."""
|
||||
try:
|
||||
content = contentstore().find(self.content_location)
|
||||
contentstore().delete(content.get_id())
|
||||
contentstore().delete(content.location)
|
||||
except NotFoundError:
|
||||
pass
|
||||
|
||||
@@ -171,7 +171,7 @@ class TestDownloadYoutubeSubs(ModuleStoreTestCase):
|
||||
content_location = StaticContent.compute_location(self.course.id, filename)
|
||||
try:
|
||||
content = contentstore().find(content_location)
|
||||
contentstore().delete(content.get_id())
|
||||
contentstore().delete(content.location)
|
||||
except NotFoundError:
|
||||
pass
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
from student.roles import CourseStaffRole, GlobalStaff, CourseInstructorRole
|
||||
""" Helper methods for determining user access permissions in Studio """
|
||||
|
||||
from student.roles import CourseStaffRole, GlobalStaff, CourseInstructorRole, OrgStaffRole, OrgInstructorRole
|
||||
from student import auth
|
||||
|
||||
|
||||
@@ -14,6 +16,10 @@ def has_course_access(user, course_key, role=CourseStaffRole):
|
||||
"""
|
||||
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))
|
||||
|
||||
|
||||
|
||||
@@ -68,6 +68,14 @@ __all__ = ['course_info_handler', 'course_handler', 'course_info_update_handler'
|
||||
'textbooks_list_handler', 'textbooks_detail_handler']
|
||||
|
||||
|
||||
class AccessListFallback(Exception):
|
||||
"""
|
||||
An exception that is raised whenever we need to `fall back` to fetching *all* courses
|
||||
available to a user, rather than using a shorter method (i.e. fetching by group)
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
def _get_course_module(course_key, user, depth=0):
|
||||
"""
|
||||
Internal method used to calculate and return the locator and course module
|
||||
@@ -190,11 +198,17 @@ def _accessible_courses_list_from_groups(request):
|
||||
|
||||
for course_access in all_courses:
|
||||
course_key = course_access.course_id
|
||||
if course_key not in courses_list:
|
||||
if course_key is None:
|
||||
# If the course_access does not have a course_id, it's an org-based role, so we fall back
|
||||
raise AccessListFallback
|
||||
try:
|
||||
course = modulestore('direct').get_course(course_key)
|
||||
if course is not None and not isinstance(course, ErrorDescriptor):
|
||||
# ignore deleted or errored courses
|
||||
courses_list[course_key] = course
|
||||
except ItemNotFoundError:
|
||||
# If a user has access to a course that doesn't exist, don't do anything with that course
|
||||
pass
|
||||
if course is not None and not isinstance(course, ErrorDescriptor):
|
||||
# ignore deleted or errored courses
|
||||
courses_list[course_key] = course
|
||||
|
||||
return courses_list.values()
|
||||
|
||||
@@ -213,7 +227,7 @@ def course_listing(request):
|
||||
else:
|
||||
try:
|
||||
courses = _accessible_courses_list_from_groups(request)
|
||||
except ItemNotFoundError:
|
||||
except AccessListFallback:
|
||||
# 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)
|
||||
|
||||
Reference in New Issue
Block a user