diff --git a/cms/djangoapps/contentstore/views/course.py b/cms/djangoapps/contentstore/views/course.py index 52365d370253..dcee04480885 100644 --- a/cms/djangoapps/contentstore/views/course.py +++ b/cms/djangoapps/contentstore/views/course.py @@ -31,7 +31,7 @@ from openedx.core.djangoapps.waffle_utils import WaffleSwitchNamespace from openedx.features.course_experience.waffle import waffle as course_experience_waffle from openedx.features.course_experience.waffle import ENABLE_COURSE_ABOUT_SIDEBAR_HTML -from openedx.features.edly.utils import get_enabled_organizations +from openedx.features.edly.utils import get_edx_org_from_cookie, get_enabled_organizations from six import text_type from contentstore.course_group_config import ( @@ -75,7 +75,14 @@ from openedx.features.content_type_gating.models import ContentTypeGatingConfig from student import auth from student.auth import has_course_author_access, has_studio_read_access, has_studio_write_access -from student.roles import CourseCreatorRole, CourseInstructorRole, CourseStaffRole, GlobalStaff, UserBasedRole +from student.roles import ( + CourseCreatorRole, + CourseInstructorRole, + CourseStaffRole, + GlobalCourseCreatorRole, + GlobalStaff, + UserBasedRole, +) from util.course import get_link_for_about_page from util.date_utils import get_default_time_display from util.json_request import JsonResponse, JsonResponseBadRequest, expect_json @@ -334,9 +341,6 @@ def course_rerun_handler(request, course_key_string): GET html: return html page with form to rerun a course for the given course id """ - # Only global staff (PMs) are able to rerun courses during the soft launch - if not GlobalStaff().has_user(request.user): - raise PermissionDenied() course_key = CourseKey.from_string(course_key_string) with modulestore().bulk_operations(course_key): course_module = get_course_and_check_access(course_key, request.user, depth=3) @@ -508,7 +512,8 @@ def filter_ccx(course_access): instructor_courses = UserBasedRole(request.user, CourseInstructorRole.ROLE).courses_with_role() staff_courses = UserBasedRole(request.user, CourseStaffRole.ROLE).courses_with_role() - all_courses = filter(filter_ccx, instructor_courses | staff_courses) + site_courses = UserBasedRole(request.user, GlobalCourseCreatorRole.ROLE).courses_with_role() + all_courses = filter(filter_ccx, instructor_courses | staff_courses | site_courses) courses_list = [] course_keys = {} @@ -555,6 +560,9 @@ def course_listing(request): enabled_organizations = get_enabled_organizations(request) org = enabled_organizations[0].get('short_name', '') if enabled_organizations else None + edly_user_info_cookie = request.COOKIES.get(settings.EDLY_USER_INFO_COOKIE_NAME, None) + org = get_edx_org_from_cookie(edly_user_info_cookie) + courses_iter, in_process_course_actions = get_courses_accessible_to_user(request, org) user = request.user libraries = _accessible_libraries_iter(request.user, org) if LIBRARIES_ENABLED else [] @@ -608,7 +616,7 @@ def format_library_for_view(library): u'user': user, u'request_course_creator_url': reverse('request_course_creator'), u'course_creator_status': _get_course_creator_status(user), - u'rerun_creator_status': GlobalStaff().has_user(user), + u'rerun_creator_status': _get_course_creator_status(user), u'allow_unicode_course_id': settings.FEATURES.get(u'ALLOW_UNICODE_COURSE_ID', False), u'allow_course_reruns': settings.FEATURES.get(u'ALLOW_COURSE_RERUNS', True), u'optimization_enabled': optimization_enabled diff --git a/common/djangoapps/student/auth.py b/common/djangoapps/student/auth.py index 065661041252..60033f7282e7 100644 --- a/common/djangoapps/student/auth.py +++ b/common/djangoapps/student/auth.py @@ -14,6 +14,7 @@ CourseCreatorRole, CourseInstructorRole, CourseRole, + GlobalCourseCreatorRole, CourseStaffRole, GlobalStaff, LibraryUserRole, @@ -88,6 +89,8 @@ def get_user_permissions(user, course_key, org=None): 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 @@ -167,7 +170,7 @@ def _check_caller_authority(caller, role): if not (caller.is_authenticated and caller.is_active): raise PermissionDenied # superuser - if GlobalStaff().has_user(caller): + if GlobalStaff().has_user(caller) or GlobalCourseCreatorRole().has_user(caller): return if isinstance(role, (GlobalStaff, CourseCreatorRole)): diff --git a/common/djangoapps/student/roles.py b/common/djangoapps/student/roles.py index da31eb57beff..d158afe2abbd 100644 --- a/common/djangoapps/student/roles.py +++ b/common/djangoapps/student/roles.py @@ -246,6 +246,23 @@ def __repr__(self): return '<{}>'.format(self.__class__.__name__) +@register_access_role +class GlobalCourseCreatorRole(OrgRole): + """ + A global course creator with access to all courses of the current site. + """ + ROLE = 'global_course_creator' + + def __init__(self, *args, **kwargs): + """ + Initialization method for GlobalCourseCreatorRole. + + Arguments: + org (str): Name of the organization of GlobalCourseCreatorRole + """ + super(GlobalCourseCreatorRole, self).__init__(self.ROLE, *args, **kwargs) + + @register_access_role class CourseStaffRole(CourseRole): """A Staff member of a course""" diff --git a/openedx/features/edly/tests/test_utils.py b/openedx/features/edly/tests/test_utils.py index 1bd8fa002ebb..bc35732db7f1 100644 --- a/openedx/features/edly/tests/test_utils.py +++ b/openedx/features/edly/tests/test_utils.py @@ -2,6 +2,7 @@ Tests for Edly Utils Functions. """ import jwt +import mock from mock import MagicMock from django.conf import settings @@ -9,6 +10,7 @@ from django.test import TestCase from django.test.client import RequestFactory +from openedx.core.djangolib.testing.utils import skip_unless_cms from openedx.features.edly import cookies as cookies_api from openedx.features.edly.tests.factories import EdlySubOrganizationFactory, EdlyUserProfileFactory, SiteFactory from openedx.features.edly.utils import ( @@ -16,10 +18,23 @@ decode_edly_user_info_cookie, encode_edly_user_info_cookie, get_edly_sub_org_from_cookie, - user_has_edly_organization_access + get_edx_org_from_cookie, + set_global_course_creator_status, + update_course_creator_status, + user_has_edly_organization_access, +) +from student import auth +from student.roles import ( + CourseCreatorRole, + GlobalCourseCreatorRole, ) from student.tests.factories import UserFactory +def mock_render_to_string(template_name, context): + """ + Return a string that encodes template_name and context + """ + return str((template_name, context)) class UtilsTests(TestCase): """ @@ -32,6 +47,7 @@ def setUp(self): """ super(UtilsTests, self).setUp() self.user = UserFactory.create() + self.admin_user = UserFactory.create(is_staff=True) self.request = RequestFactory().get('/') self.request.user = self.user self.request.session = self._get_stub_session() @@ -62,6 +78,14 @@ def _create_edly_sub_organization(self): """ return EdlySubOrganizationFactory(lms_site=self.request.site) + def _get_course_creator_status(self, user): + """ + Helper method to get user's course creator status. + """ + from course_creators.views import get_course_creator_status + + return get_course_creator_status(user) + def test_encode_edly_user_info_cookie(self): """ Test that "encode_edly_user_info_cookie" method encodes data correctly. @@ -114,6 +138,14 @@ def test_get_edly_sub_org_from_cookie(self): edly_user_info_cookie = cookies_api._get_edly_user_info_cookie_string(self.request) assert edly_sub_organization.slug == get_edly_sub_org_from_cookie(edly_user_info_cookie) + def test_get_edx_org_from_cookie(self): + """ + Test that "get_edx_org_from_cookie" method returns edx-org short name correctly. + """ + edly_sub_organization = self._create_edly_sub_organization() + edly_user_info_cookie = cookies_api._get_edly_user_info_cookie_string(self.request) + assert edly_sub_organization.edx_organization.short_name == get_edx_org_from_cookie(edly_user_info_cookie) + def test_create_user_link_with_edly_sub_organization(self): """ Test that "create_user_link_with_edly_sub_organization" method create "EdlyUserProfile" link with User. @@ -123,3 +155,39 @@ def test_create_user_link_with_edly_sub_organization(self): edly_user_profile = create_user_link_with_edly_sub_organization(self.request, user) assert edly_user_profile == user.edly_profile assert edly_sub_organization.slug in user.edly_profile.get_linked_edly_sub_organizations + + @skip_unless_cms + @mock.patch('course_creators.admin.render_to_string', mock.Mock(side_effect=mock_render_to_string, autospec=True)) + def test_update_course_creator_status(self): + """ + Test that "update_course_creator_status" method sets/removes a User as Course Creator correctly. + """ + settings.FEATURES['ENABLE_CREATOR_GROUP'] = True + update_course_creator_status(self.admin_user, self.user, True) + assert self._get_course_creator_status(self.user) == 'granted' + assert auth.user_has_role(self.user, CourseCreatorRole()) + + update_course_creator_status(self.admin_user, self.user, False) + assert self._get_course_creator_status(self.user) == 'unrequested' + assert not auth.user_has_role(self.user, CourseCreatorRole()) + + @skip_unless_cms + @mock.patch('course_creators.admin.render_to_string', mock.Mock(side_effect=mock_render_to_string, autospec=True)) + def test_set_global_course_creator_status(self): + """ + Test that "set_global_course_creator_status" method sets/removes a User as Global Course Creator correctly. + """ + self._create_edly_sub_organization() + response = cookies_api.set_logged_in_edly_cookies(self.request, HttpResponse(), self.user) + self._copy_cookies_to_request(response, self.request) + edly_user_info_cookie = self.request.COOKIES.get(settings.EDLY_USER_INFO_COOKIE_NAME) + edx_org = get_edx_org_from_cookie(edly_user_info_cookie) + self.request.user = self.admin_user + + set_global_course_creator_status(self.request, self.user, True) + assert self._get_course_creator_status(self.user) == 'granted' + assert auth.user_has_role(self.user, GlobalCourseCreatorRole(edx_org)) + + set_global_course_creator_status(self.request, self.user, False) + assert self._get_course_creator_status(self.user) == 'unrequested' + assert not auth.user_has_role(self.user, GlobalCourseCreatorRole(edx_org)) diff --git a/openedx/features/edly/utils.py b/openedx/features/edly/utils.py index e97af3522c39..756bc3141149 100644 --- a/openedx/features/edly/utils.py +++ b/openedx/features/edly/utils.py @@ -7,6 +7,13 @@ from django.forms.models import model_to_dict from openedx.features.edly.models import EdlyUserProfile, EdlySubOrganization +from student import auth +from student.roles import ( + CourseInstructorRole, + CourseStaffRole, + GlobalCourseCreatorRole, + UserBasedRole, +) from util.organizations_helpers import get_organizations LOGGER = logging.getLogger(__name__) @@ -91,6 +98,24 @@ def get_edly_sub_org_from_cookie(encoded_cookie_data): return decoded_cookie_data['edly-sub-org'] +def get_edx_org_from_cookie(encoded_cookie_data): + """ + Returns edx-org short name from the edly-user-info cookie. + + Arguments: + encoded_cookie_data (dict): Edly user info cookie JWT encoded string. + + Returns: + string + """ + + if not encoded_cookie_data: + return '' + + decoded_cookie_data = decode_edly_user_info_cookie(encoded_cookie_data) + return decoded_cookie_data['edx-org'] + + def get_enabled_organizations(request): """ Helper method to get linked organizations for request site. @@ -139,8 +164,38 @@ def update_course_creator_status(request_user, user, set_creator): Updates course creator status of a user. """ from course_creators.models import CourseCreator + from course_creators.views import update_course_creator_group + course_creator, __ = CourseCreator.objects.get_or_create(user=user) - course_creator.state = CourseCreator.GRANTED if set_creator else CourseCreator.DENIED + course_creator.state = CourseCreator.GRANTED if set_creator else CourseCreator.UNREQUESTED course_creator.note = 'Course creator user was updated by panel admin {}'.format(request_user.email) course_creator.admin = request_user course_creator.save() + if not set_creator: + update_course_creator_group(request_user, user, set_creator) + instructor_courses = UserBasedRole(user, CourseInstructorRole.ROLE).courses_with_role() + staff_courses = UserBasedRole(user, CourseStaffRole.ROLE).courses_with_role() + instructor_courses_keys = [course.course_id for course in instructor_courses] + staff_courses_keys = [course.course_id for course in staff_courses] + UserBasedRole(user, CourseInstructorRole.ROLE).remove_courses(*instructor_courses_keys) + UserBasedRole(user, CourseStaffRole.ROLE).remove_courses(*staff_courses_keys) + + +def set_global_course_creator_status(request, user, set_global_creator): + """ + Updates global course creator status of a user. + """ + from course_creators.models import CourseCreator + + request_user = request.user + course_creator, __ = CourseCreator.objects.get_or_create(user=user) + course_creator.state = CourseCreator.GRANTED if set_global_creator else CourseCreator.UNREQUESTED + course_creator.note = 'Global course creator user was updated by panel admin {}'.format(request_user.email) + course_creator.admin = request_user + course_creator.save() + edly_user_info_cookie = request.COOKIES.get(settings.EDLY_USER_INFO_COOKIE_NAME, None) + edx_org = get_edx_org_from_cookie(edly_user_info_cookie) + if set_global_creator: + GlobalCourseCreatorRole(edx_org).add_users(user) + else: + GlobalCourseCreatorRole(edx_org).remove_users(user)