Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
d9171bb
Add course creation condition for organisation
farhaanbukhsh Feb 18, 2021
82a14f5
Add org field in the course creator
farhaanbukhsh Apr 1, 2021
0a4b4dd
Add support for org roles
farhaanbukhsh Apr 1, 2021
e8a62db
Added roles
farhaanbukhsh Apr 1, 2021
8b77010
Add all_organization field
farhaanbukhsh Apr 12, 2021
68c79b1
Added logic for creating organizational role and update the course
farhaanbukhsh Apr 12, 2021
1787792
Added checks for easy creation and deletion of roles
farhaanbukhsh Apr 12, 2021
0de9d06
Revert "Add course creation condition for organisation"
farhaanbukhsh Apr 12, 2021
083350b
Fix styling issues
farhaanbukhsh Apr 15, 2021
e518990
Fix tests
farhaanbukhsh Apr 17, 2021
b9d9351
Fix pep8 issues
farhaanbukhsh Apr 17, 2021
619bf11
Add migrations for course_creators
farhaanbukhsh Apr 18, 2021
4cb11f7
Fixed comments
farhaanbukhsh Apr 18, 2021
ff64875
Add test for OrgContentCreatorRole
farhaanbukhsh Apr 18, 2021
56ea1a0
Refactor the permissions to be in course.py
farhaanbukhsh Apr 18, 2021
b99e3d2
Pep8 fixes
farhaanbukhsh Apr 18, 2021
8aa432b
Fix auth permissions and imports
farhaanbukhsh Apr 19, 2021
21cc756
Add test for view course creator
farhaanbukhsh Apr 19, 2021
1cb58e1
Add test for course creation
farhaanbukhsh Apr 19, 2021
a1956d6
Updated migrations file
farhaanbukhsh May 5, 2021
c83d1a8
Fixed messages in migration
farhaanbukhsh May 5, 2021
333ceb7
Imporved validation form
farhaanbukhsh May 5, 2021
4ea3008
Update the callback funtion name
farhaanbukhsh May 8, 2021
af27c09
Refactored update org role function
farhaanbukhsh May 9, 2021
148fe1e
Introduce logic to handle change in status
farhaanbukhsh May 9, 2021
7bbe298
Rename update org role function
farhaanbukhsh May 9, 2021
9d5de83
Add user message to show exception
farhaanbukhsh May 9, 2021
b2799f1
Pep8 fixes
farhaanbukhsh May 9, 2021
83b87e5
Fix pylint errors
farhaanbukhsh May 9, 2021
3a028a1
Fix typo
farhaanbukhsh May 14, 2021
70d0458
Improved message on admin and model column
farhaanbukhsh Jun 21, 2021
cf1471a
Removed lint warning and added better error message
farhaanbukhsh Jun 21, 2021
e638e73
Fix typo in test
farhaanbukhsh Jun 21, 2021
bd5b1f3
Add more test to check the orgs
farhaanbukhsh Jun 21, 2021
53cfac8
Updated the comment
farhaanbukhsh Jun 21, 2021
1206167
Improved code organization
farhaanbukhsh Jun 21, 2021
8a8f22a
Add library restriction permission
farhaanbukhsh Jun 22, 2021
48dcdf3
Fixing tests
farhaanbukhsh Jun 22, 2021
bebf252
Fix linting issues
farhaanbukhsh Jun 22, 2021
5594201
Fix lint issues
farhaanbukhsh Jun 22, 2021
6b1b7e5
Added org check for get_course_creator_status
farhaanbukhsh Aug 4, 2021
272914b
Refactor to move the fucntion to a central place.
farhaanbukhsh Aug 4, 2021
b5b0501
Remove unused import
farhaanbukhsh Aug 4, 2021
7adb7e3
Pep8 fix
farhaanbukhsh Aug 5, 2021
862e860
Removed unwanted flag
farhaanbukhsh Aug 25, 2021
5bbd5cb
Fixed tests
farhaanbukhsh Aug 25, 2021
9fe9cff
Refined the logic and revert the course creator role
farhaanbukhsh Sep 10, 2021
8d90dcb
Renamed the functions and fix tests
farhaanbukhsh Sep 10, 2021
ffd29a9
Rename the get_library_creator_status function
farhaanbukhsh Sep 10, 2021
07ea7da
Remove unused permissions
farhaanbukhsh Sep 10, 2021
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 @@ -43,10 +43,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 @@ -108,12 +106,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 @@ -564,7 +563,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 @@ -850,16 +849,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 @@ -916,6 +916,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