Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
155 changes: 151 additions & 4 deletions cms/djangoapps/contentstore/tests/test_course_create_rerun.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,36 @@


import datetime
from unittest import mock

import ddt
from django.contrib.admin.sites import AdminSite
from django.http import HttpRequest
from django.test import override_settings
from django.test.client import RequestFactory
from django.urls import reverse
from opaque_keys.edx.keys import CourseKey
from organizations.api import add_organization, get_course_organizations, get_organization_by_short_name
from organizations.exceptions import InvalidOrganizationException

from cms.djangoapps.contentstore.tests.utils import AjaxEnabledTestClient, parse_json
from common.djangoapps.student.roles import CourseInstructorRole, CourseStaffRole
from common.djangoapps.student.tests.factories import UserFactory
from organizations.models import Organization
from xmodule.course_module import CourseFields
from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory

from cms.djangoapps.contentstore.tests.utils import AjaxEnabledTestClient, parse_json
from cms.djangoapps.course_creators.admin import CourseCreatorAdmin
from cms.djangoapps.course_creators.models import CourseCreator
from common.djangoapps.student.auth import update_org_role
from common.djangoapps.student.roles import CourseInstructorRole, CourseStaffRole, OrgContentCreatorRole
from common.djangoapps.student.tests.factories import AdminFactory, UserFactory


def mock_render_to_string(template_name, context):
"""Return a string that encodes template_name and context"""
return str((template_name, context))


@ddt.ddt
class TestCourseListing(ModuleStoreTestCase):
Expand All @@ -37,6 +49,7 @@ def setUp(self):
# create and log in a non-staff user
self.user = UserFactory()
self.factory = RequestFactory()
self.global_admin = AdminFactory()
self.client = AjaxEnabledTestClient()
self.client.login(username=self.user.username, password='test')
self.course_create_rerun_url = reverse('course_handler')
Expand All @@ -56,6 +69,12 @@ def setUp(self):
)
self.source_course_key = source_course.id

self.course_creator_entry = CourseCreator(user=self.user)
self.course_creator_entry.save()
self.request = HttpRequest()
self.request.user = self.global_admin
self.creator_admin = CourseCreatorAdmin(self.course_creator_entry, AdminSite())

for role in [CourseInstructorRole, CourseStaffRole]:
role(self.source_course_key).add_users(self.user)

Expand Down Expand Up @@ -180,3 +199,131 @@ def test_course_creation_for_known_organization(self, organizations_autocreate):
course_orgs = get_course_organizations(new_course_key)
self.assertEqual(len(course_orgs), 1)
self.assertEqual(course_orgs[0]['short_name'], 'orgX')

@override_settings(FEATURES={'ENABLE_CREATOR_GROUP': True})
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
def test_course_creation_when_user_not_in_org(self, store):
"""
Tests course creation when user doesn't have the required role.
"""
with modulestore().default_store(store):
response = self.client.ajax_post(self.course_create_rerun_url, {
'org': 'TestorgX',
'number': 'CS101',
'display_name': 'Course with web certs enabled',
'run': '2021_T1'
})
self.assertEqual(response.status_code, 403)

@override_settings(FEATURES={'ENABLE_CREATOR_GROUP': True})
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
def test_course_creation_when_user_in_org_with_creator_role(self, store):
"""
Tests course creation with user having the organization content creation role.
"""
add_organization({
'name': 'Test Organization',
'short_name': self.source_course_key.org,
'description': 'Testing Organization Description',
})
update_org_role(self.global_admin, OrgContentCreatorRole, self.user, [self.source_course_key.org])
with modulestore().default_store(store):
response = self.client.ajax_post(self.course_create_rerun_url, {
'org': self.source_course_key.org,
'number': 'CS101',
'display_name': 'Course with web certs enabled',
'run': '2021_T1'
})
self.assertEqual(response.status_code, 200)

@override_settings(FEATURES={'ENABLE_CREATOR_GROUP': True})
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
@mock.patch(
'cms.djangoapps.course_creators.admin.render_to_string',
mock.Mock(side_effect=mock_render_to_string, autospec=True)
)
def test_course_creation_with_all_org_checked(self, store):
"""
Tests course creation with user having permission to create course for all organization.
"""
add_organization({
'name': 'Test Organization',
'short_name': self.source_course_key.org,
'description': 'Testing Organization Description',
})
self.course_creator_entry.all_organizations = True
self.course_creator_entry.state = CourseCreator.GRANTED
self.creator_admin.save_model(self.request, self.course_creator_entry, None, True)
with modulestore().default_store(store):
response = self.client.ajax_post(self.course_create_rerun_url, {
'org': self.source_course_key.org,
'number': 'CS101',
'display_name': 'Course with web certs enabled',
'run': '2021_T1'
})
self.assertEqual(response.status_code, 200)

@override_settings(FEATURES={'ENABLE_CREATOR_GROUP': True})
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
@mock.patch(
'cms.djangoapps.course_creators.admin.render_to_string',
mock.Mock(side_effect=mock_render_to_string, autospec=True)
)
def test_course_creation_with_permission_for_specific_organization(self, store):
"""
Tests course creation with user having permission to create course for specific organization.
"""
add_organization({
'name': 'Test Organization',
'short_name': self.source_course_key.org,
'description': 'Testing Organization Description',
})
self.course_creator_entry.all_organizations = False
self.course_creator_entry.state = CourseCreator.GRANTED
self.creator_admin.save_model(self.request, self.course_creator_entry, None, True)
dc_org_object = Organization.objects.get(name='Test Organization')
self.course_creator_entry.organizations.add(dc_org_object)
with modulestore().default_store(store):
response = self.client.ajax_post(self.course_create_rerun_url, {
'org': self.source_course_key.org,
'number': 'CS101',
'display_name': 'Course with web certs enabled',
'run': '2021_T1'
})
self.assertEqual(response.status_code, 200)

@override_settings(FEATURES={'ENABLE_CREATOR_GROUP': True})
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
@mock.patch(
'cms.djangoapps.course_creators.admin.render_to_string',
mock.Mock(side_effect=mock_render_to_string, autospec=True)
)
def test_course_creation_without_permission_for_specific_organization(self, store):
"""
Tests course creation with user not having permission to create course for specific organization.
"""
add_organization({
'name': 'Test Organization',
'short_name': self.source_course_key.org,
'description': 'Testing Organization Description',
})
add_organization({
'name': 'DC',
'short_name': 'DC',
'description': 'DC Comics',
})
self.course_creator_entry.all_organizations = False
self.course_creator_entry.state = CourseCreator.GRANTED
self.creator_admin.save_model(self.request, self.course_creator_entry, None, True)
# User has been given the permission to create course under `DC` organization.
# When the user tries to create course under `Test Organization` it throws a 403.
dc_org_object = Organization.objects.get(name='DC')
self.course_creator_entry.organizations.add(dc_org_object)
with modulestore().default_store(store):
response = self.client.ajax_post(self.course_create_rerun_url, {
'org': self.source_course_key.org,
'number': 'CS101',
'display_name': 'Course with web certs enabled',
'run': '2021_T1'
})
self.assertEqual(response.status_code, 403)
28 changes: 21 additions & 7 deletions cms/djangoapps/contentstore/views/course.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,8 @@
from common.djangoapps.course_action_state.models import CourseRerunState, CourseRerunUIStateManager
from common.djangoapps.course_modes.models import CourseMode
from common.djangoapps.edxmako.shortcuts import render_to_response
from common.djangoapps.student import auth
from common.djangoapps.student.auth import has_course_author_access, has_studio_read_access, has_studio_write_access
from common.djangoapps.student.roles import (
CourseCreatorRole,
CourseInstructorRole,
CourseStaffRole,
GlobalStaff,
Expand Down Expand Up @@ -105,12 +103,13 @@
reverse_usage_url
)
from .component import ADVANCED_COMPONENT_TYPES
from .helpers import is_content_creator
from .entrance_exam import create_entrance_exam, delete_entrance_exam, update_entrance_exam
from .item import create_xblock_info
from .library import (
LIBRARIES_ENABLED,
LIBRARY_AUTHORING_MICROFRONTEND_URL,
get_library_creator_status,
user_can_create_library,
should_redirect_to_library_authoring_mfe
)

Expand Down Expand Up @@ -560,7 +559,7 @@ def format_in_process_course_view(uca):
'redirect_to_library_authoring_mfe': should_redirect_to_library_authoring_mfe(),
'library_authoring_mfe_url': LIBRARY_AUTHORING_MICROFRONTEND_URL,
'libraries': [_format_library_for_view(lib, request) for lib in libraries],
'show_new_library_button': get_library_creator_status(user) and not should_redirect_to_library_authoring_mfe(),
'show_new_library_button': user_can_create_library(user) and not should_redirect_to_library_authoring_mfe(),
'user': user,
'request_course_creator_url': reverse('request_course_creator'),
'course_creator_status': _get_course_creator_status(user),
Expand Down Expand Up @@ -849,16 +848,17 @@ def _create_or_rerun_course(request):
Returns the destination course_key and overriding fields for the new course.
Raises DuplicateCourseError and InvalidKeyError
"""
if not auth.user_has_role(request.user, CourseCreatorRole()):
raise PermissionDenied()

try:
org = request.json.get('org')
course = request.json.get('number', request.json.get('course'))
display_name = request.json.get('display_name')
# force the start date for reruns and allow us to override start via the client
start = request.json.get('start', CourseFields.start.default)
run = request.json.get('run')
has_course_creator_role = is_content_creator(request.user, org)

if not has_course_creator_role:
raise PermissionDenied()

# allow/disable unicode characters in course_id according to settings
if not settings.FEATURES.get('ALLOW_UNICODE_COURSE_ID'):
Expand Down Expand Up @@ -915,6 +915,20 @@ def _create_or_rerun_course(request):
return JsonResponse({
"ErrMsg": _("Unable to create course '{name}'.\n\n{err}").format(name=display_name, err=str(error))}
)
except PermissionDenied as error:
log.info(
"User does not have the permission to create course in this organization"
"or course creation is disabled."
"User: '%s' Org: '%s' Course #: '%s'.",
request.user.id,
org,
course,
)
return JsonResponse({
'error': _('User does not have the permission to create courses in this organization '
'or course creation is disabled')},
status=403
)


def create_new_course(user, org, number, run, fields):
Expand Down
18 changes: 16 additions & 2 deletions cms/djangoapps/contentstore/views/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,14 @@
from django.utils.translation import ugettext as _
from opaque_keys.edx.keys import UsageKey
from xblock.core import XBlock
from xmodule.modulestore.django import modulestore
from xmodule.tabs import StaticTab

from cms.djangoapps.models.settings.course_grading import CourseGradingModel
from common.djangoapps.edxmako.shortcuts import render_to_string
from common.djangoapps.student import auth
from common.djangoapps.student.roles import CourseCreatorRole, OrgContentCreatorRole
from openedx.core.toggles import ENTRANCE_EXAMS
from xmodule.modulestore.django import modulestore
from xmodule.tabs import StaticTab

from ..utils import reverse_course_url, reverse_library_url, reverse_usage_url

Expand Down Expand Up @@ -290,3 +292,15 @@ def is_item_in_course_tree(item):
ancestor = ancestor.get_parent()

return ancestor is not None


def is_content_creator(user, org):
"""
Check if the user has the role to create content.

This function checks if the User has role to create content
or if the org is supplied, it checks for Org level course content
creator.
"""
return (auth.user_has_role(user, CourseCreatorRole()) or
auth.user_has_role(user, OrgContentCreatorRole(org=org)))
37 changes: 30 additions & 7 deletions cms/djangoapps/contentstore/views/library.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@
from opaque_keys.edx.locator import LibraryLocator, LibraryUsageLocator
from organizations.api import ensure_organization
from organizations.exceptions import InvalidOrganizationException
from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.exceptions import DuplicateCourseError

from cms.djangoapps.course_creators.views import get_course_creator_status
from common.djangoapps.edxmako.shortcuts import render_to_response
Expand All @@ -29,15 +32,17 @@
has_studio_read_access,
has_studio_write_access
)
from common.djangoapps.student.roles import CourseInstructorRole, CourseStaffRole, LibraryUserRole
from common.djangoapps.student.roles import (
CourseInstructorRole,
CourseStaffRole,
LibraryUserRole
)
from common.djangoapps.util.json_request import JsonResponse, JsonResponseBadRequest, expect_json
from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.exceptions import DuplicateCourseError

from ..config.waffle import REDIRECT_TO_LIBRARY_AUTHORING_MICROFRONTEND
from ..utils import add_instructor, reverse_library_url
from .component import CONTAINER_TEMPLATES, get_component_templates
from .helpers import is_content_creator
from .item import create_xblock_info
from .user import user_with_role

Expand All @@ -63,7 +68,7 @@ def should_redirect_to_library_authoring_mfe():
)


def get_library_creator_status(user):
def user_can_create_library(user, org=None):
"""
Helper method for returning the library creation status for a particular user,
taking into account the value LIBRARIES_ENABLED.
Expand All @@ -74,7 +79,10 @@ def get_library_creator_status(user):
elif user.is_staff:
return True
elif settings.FEATURES.get('ENABLE_CREATOR_GROUP', False):
return get_course_creator_status(user) == 'granted'
has_course_creator_role = True
if org:
has_course_creator_role = is_content_creator(user, org)
return get_course_creator_status(user) == 'granted' and has_course_creator_role
else:
# EDUCATOR-1924: DISABLE_LIBRARY_CREATION overrides DISABLE_COURSE_CREATION, if present.
disable_library_creation = settings.FEATURES.get('DISABLE_LIBRARY_CREATION', None)
Expand All @@ -97,7 +105,7 @@ def library_handler(request, library_key_string=None):
raise Http404 # Should never happen because we test the feature in urls.py also

if request.method == 'POST':
if not get_library_creator_status(request.user):
if not user_can_create_library(request.user):
return HttpResponseForbidden()

if library_key_string is not None:
Expand Down Expand Up @@ -188,6 +196,8 @@ def _create_library(request):
library = request.json.get('number', None)
if library is None:
library = request.json['library']
if not user_can_create_library(request.user, org):
raise PermissionDenied()
store = modulestore()
with store.default_store(ModuleStoreEnum.Type.split):
new_lib = store.create_library(
Expand All @@ -198,6 +208,19 @@ def _create_library(request):
)
# Give the user admin ("Instructor") role for this library:
add_instructor(new_lib.location.library_key, request.user, request.user)
except PermissionDenied as error:
log.info(
"User does not have the permission to create LIBRARY in this organization."
"User: '%s' Org: '%s' LIBRARY #: '%s'.",
request.user.id,
org,
library,
)
return JsonResponse({
'error': _('User does not have the permission to create library in this organization '
'or course creation is disabled')},
status=403
)
except KeyError as error:
log.exception("Unable to create library - missing required JSON key.")
return JsonResponseBadRequest({
Expand Down
Loading