def test_creation(self): """ The user that creates a library should have instructor (admin) and staff permissions """ # self.library has been auto-created by the staff user. self.assertTrue(has_studio_write_access(self.user, self.lib_key)) self.assertTrue(has_studio_read_access(self.user, self.lib_key)) # Make sure the user was actually assigned the instructor role and not just using is_staff superpowers: self.assertTrue(CourseInstructorRole(self.lib_key).has_user(self.user)) # Now log out and ensure we are forbidden from creating a library: self.client.logout() self._assert_cannot_create_library( expected_code=302) # 302 redirect to login expected # Now check that logged-in users without CourseCreator role cannot create libraries self._login_as_non_staff_user(logout_first=False) with patch.dict('django.conf.settings.FEATURES', {'ENABLE_CREATOR_GROUP': True}): self._assert_cannot_create_library( expected_code=403) # 403 user is not CourseCreator # Now check that logged-in users with CourseCreator role can create libraries add_user_with_status_granted(self.user, self.non_staff_user) with patch.dict('django.conf.settings.FEATURES', {'ENABLE_CREATOR_GROUP': True}): lib_key2 = self._create_library(library="lib2", display_name="Test Library 2") library2 = modulestore().get_library(lib_key2) self.assertIsNotNone(library2)
def has_permission(self, request, view): """ Check if the user has write access to studio. """ user = request.user course_key_string = view.kwargs.get("course_id") course_key = validate_course_key(course_key_string) return has_studio_write_access(user, course_key)
def _get_course_and_check_access(course_key, user, depth=0): """ Internal method used to calculate and return the locator and course module for the view functions in this file. """ if not has_studio_write_access(user, course_key): raise PermissionDenied() course_module = modulestore().get_course(course_key, depth=depth) return course_module
def user_has_studio_write_access(*args, **kwargs): """ Import and run `has_studio_write_access` from common modules. Used to check if someone saving deep linking content has the correct write permissions for a given. """ # pylint: disable=import-error,import-outside-toplevel from common.djangoapps.student.auth import has_studio_write_access return has_studio_write_access(*args, **kwargs)
def patch(self, request: Request, course_id: str): """ Update a course's advanced settings. **Example Request** PATCH /api/contentstore/v0/advanced_settings/{course_id} { "{setting_name}": { "value": {setting_value} } } **PATCH Parameters** The data sent for a patch request should follow a similar format as is returned by a ``GET`` request. Multiple settings can be updated in a single request, however only the ``value`` field can be updated any other fields, if included, will be ignored. Here is an example request that updates the ``advanced_modules`` available for the course, and enables the calculator tool: ```json { "advanced_modules": { "value": [ "poll", "survey", "drag-and-drop-v2", "lti_consumer" ] }, "show_calculator": { "value": true } } ``` **Response Values** If the request is successful, an HTTP 200 "OK" response is returned, along with all the course's settings similar to a ``GET`` request. """ course_key = CourseKey.from_string(course_id) if not has_studio_write_access(request.user, course_key): self.permission_denied(request) course_module = modulestore().get_course(course_key) updated_data = update_course_advanced_settings(course_module, request.data, request.user) return Response(updated_data)
def import_from_blockstore(self, dest_block, blockstore_block_ids): """ Imports a block from a blockstore-based learning context (usually a content library) into modulestore, as a new child of dest_block. Any existing children of dest_block are replaced. This is only used by LibrarySourcedBlock. It should verify first that the number of block IDs is reasonable. """ dest_key = dest_block.scope_ids.usage_id if not isinstance(dest_key, BlockUsageLocator): raise TypeError( "Destination {} should be a modulestore course.".format( dest_key)) if self.user_id is None: raise ValueError( "Cannot check user permissions - LibraryTools user_id is None") if len(set(blockstore_block_ids)) != len(blockstore_block_ids): # We don't support importing the exact same block twice because it would break the way we generate new IDs # for each block and then overwrite existing copies of blocks when re-importing the same blocks. raise ValueError( "One or more library component IDs is a duplicate.") dest_course_key = dest_key.context_key user = User.objects.get(id=self.user_id) if not has_studio_write_access(user, dest_course_key): raise PermissionDenied() # Read the source block; this will also confirm that user has permission to read it. # (This could be slow and use lots of memory, except for the fact that LibrarySourcedBlock which calls this # should be limiting the number of blocks to a reasonable limit. We load them all now instead of one at a # time in order to raise any errors before we start actually copying blocks over.) orig_blocks = [ load_block(UsageKey.from_string(key), user) for key in blockstore_block_ids ] with self.store.bulk_operations(dest_course_key): child_ids_updated = set() for block in orig_blocks: new_block_id = self._import_block(block, dest_key) child_ids_updated.add(new_block_id) # Remove any existing children that are no longer used for old_child_id in set(dest_block.children) - child_ids_updated: self.store.delete_item(old_child_id, self.user_id) # If this was called from a handler, it will save dest_block at the end, so we must update # dest_block.children to avoid it saving the old value of children and deleting the new ones. dest_block.children = self.store.get_item(dest_key).children
def post(self, request: Request, course_id: str) -> Response: """ Reorder tabs in a course. **Example Requests** Move course tabs: POST /api/contentstore/v0/tabs/{course_id}/reorder [ { "tab_id": "info" }, { "tab_id": "courseware" }, { "tab_locator": "block-v1:TstX+DemoX+Demo+type@static_tab+block@d26fcb0e93824fbfa5c9e5f100e2511a" }, { "tab_id": "wiki" }, { "tab_id": "discussion" }, { "tab_id": "progress" } ] **Response Values** If the request is successful, an HTTP 204 response is returned without any content. """ course_key = CourseKey.from_string(course_id) if not has_studio_write_access(request.user, course_key): self.permission_denied(request) course_module = modulestore().get_course(course_key) tab_id_locators = TabIDLocatorSerializer(data=request.data, many=True) tab_id_locators.is_valid(raise_exception=True) reorder_tabs_handler( course_module, {"tabs": tab_id_locators.validated_data}, request.user, ) return Response(status=status.HTTP_204_NO_CONTENT)
def library_blocks_view(library, user, response_format): """ The main view of a course's content library. Shows all the XBlocks in the library, and allows adding/editing/deleting them. Can be called with response_format="json" to get a JSON-formatted list of the XBlocks in the library along with library metadata. Assumes that read permissions have been checked before calling this. """ assert isinstance(library.location.library_key, LibraryLocator) assert isinstance(library.location, LibraryUsageLocator) children = library.children if response_format == "json": # The JSON response for this request is short and sweet: prev_version = library.runtime.course_entry.structure[ 'previous_version'] return JsonResponse({ "display_name": library.display_name, "library_id": str(library.location.library_key), "version": str(library.runtime.course_entry.course_key.version_guid), "previous_version": str(prev_version) if prev_version else None, "blocks": [str(x) for x in children], }) can_edit = has_studio_write_access(user, library.location.library_key) xblock_info = create_xblock_info(library, include_ancestor_info=False, graders=[]) component_templates = get_component_templates( library, library=True) if can_edit else [] return render_to_response( 'library.html', { 'can_edit': can_edit, 'context_library': library, 'component_templates': component_templates, 'xblock_info': xblock_info, 'templates': CONTAINER_TEMPLATES, })
def post(self, request: Request, course_id: str) -> Response: """ Change visibility of tabs in a course. **Example Requests** You can provide either a tab_id or a tab_location. Hide a course tab using ``tab_id``: POST /api/contentstore/v0/tabs/{course_id}/settings/?tab_id={tab_id} { "is_hidden": true } Hide a course tab using ``tab_location`` POST /api/contentstore/v0/tabs/{course_id}/settings/?tab_location={tab_location} { "is_hidden": true } **Response Values** If the request is successful, an HTTP 204 response is returned without any content. """ course_key = CourseKey.from_string(course_id) if not has_studio_write_access(request.user, course_key): self.permission_denied(request) tab_id_locator = TabIDLocatorSerializer(data=request.query_params) tab_id_locator.is_valid(raise_exception=True) course_module = modulestore().get_course(course_key) serializer = CourseTabUpdateSerializer(data=request.data) serializer.is_valid(raise_exception=True) edit_tab_handler( course_module, { "tab_id_locator": tab_id_locator.data, **serializer.data, }, request.user, ) return Response(status=status.HTTP_204_NO_CONTENT)
def transcript_delete_handler(request, course_key_string, edx_video_id, language_code): """ View to delete a transcript file. Arguments: request: A WSGI request object course_key_string: Course key identifying a course. edx_video_id: edX video identifier whose transcript need to be deleted. language_code: transcript's language code. Returns - A 404 if the user does not have required permisions - A 200 if transcript is deleted without any error(s) """ # Check whether the feature is available for this course. course_key = CourseKey.from_string(course_key_string) # User needs to have studio write access for this course. if not has_studio_write_access(request.user, course_key): return HttpResponseNotFound() delete_video_transcript(video_id=edx_video_id, language_code=language_code) return JsonResponse(status=200)
def test_no_staff_write_access(self): """ Test that course staff have no write access """ assert not has_studio_write_access(self.staff, self.ccx_course_key)
def test_no_global_admin_write_access(self): """ Test that global admins have no write access """ assert not has_studio_write_access(self.global_admin, self.ccx_course_key)