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 _list_libraries(request): """ List all accessible libraries, after applying filters in the request Query params: org - The organization used to filter libraries text_search - The string used to filter libraries by searching in title, id or org """ org = request.GET.get('org', '') text_search = request.GET.get('text_search', '').lower() if org: libraries = modulestore().get_libraries(org=org) else: libraries = modulestore().get_libraries() lib_info = [ { "display_name": lib.display_name, "library_key": str(lib.location.library_key), } for lib in libraries if ( ( text_search in lib.display_name.lower() or text_search in lib.location.library_key.org.lower() or text_search in lib.location.library_key.library.lower() ) and has_studio_read_access(request.user, lib.location.library_key) ) ] return JsonResponse(lib_info)
def _display_library(library_key_string, request): """ Displays single library """ library_key = CourseKey.from_string(library_key_string) if not isinstance(library_key, LibraryLocator): log.exception("Non-library key passed to content libraries API.") # Should never happen due to url regex raise Http404 # This is not a library if not has_studio_read_access(request.user, library_key): log.exception( "User %s tried to access library %s without permission", request.user.username, str(library_key) ) raise PermissionDenied() library = modulestore().get_library(library_key) if library is None: log.exception("Library %s not found", str(library_key)) raise Http404 response_format = 'html' if ( request.GET.get('format', 'html') == 'json' or 'application/json' in request.META.get('HTTP_ACCEPT', 'text/html') ): response_format = 'json' return library_blocks_view(library, request.user, response_format)
def get_block_exportfs_file(request, usage_key_str, path): """ Serve a static file that got added to the XBlock's export_fs during XBlock serialization. Typically these would be video transcript files. """ # Parse the usage key: try: usage_key = UsageKey.from_string(usage_key_str) except (ValueError, InvalidKeyError): raise ValidationError('Invalid usage key') if usage_key.block_type in ('course', 'chapter', 'sequential'): raise ValidationError('Requested XBlock tree is too large - export verticals or their children only') course_key = usage_key.context_key if not isinstance(course_key, CourseLocator): raise ValidationError('Invalid usage key: not a modulestore course') # Make sure the user has permission on that course if not has_studio_read_access(request.user, course_key): raise PermissionDenied("You must be a member of the course team in Studio to export OLX using this API.") block = adapters.get_block(usage_key) serialized = XBlockSerializer(block) static_file = None for f in serialized.static_files: if f.name == path: static_file = f break if static_file is None: raise NotFound response = HttpResponse(static_file.data, content_type='application/octet-stream') response['Content-Disposition'] = 'attachment; filename="{}"'.format(path) return response
def get(self, request: Request, course_id: str) -> Response: """ Get a list of all the static tabs in a course including hidden tabs. **Example Request** GET /api/contentstore/v0/tabs/{course_id} **Response Values** If the request is successful, an HTTP 200 "OK" response is returned. The HTTP 200 response contains a list of objects that contain info about each tab. **Example Response** ```json [ { "course_staff_only": false, "is_hidden": false, "is_hideable": false, "is_movable": false, "name": "Home", "settings": {}, "tab_id": "info", "title": "Home", "type": "course_info" }, { "course_staff_only": false, "is_hidden": false, "is_hideable": false, "is_movable": false, "name": "Course", "settings": {}, "tab_id": "courseware", "title": "Course", "type": "courseware" }, ... } ``` """ course_key = CourseKey.from_string(course_id) if not has_studio_read_access(request.user, course_key): self.permission_denied(request) course_module = modulestore().get_course(course_key) tabs_to_render = get_course_static_tabs(course_module, request.user) return Response(CourseTabSerializer(tabs_to_render, many=True).data)
def test_no_staff_read_access(self): """ Test that course staff have no read access """ assert not has_studio_read_access(self.staff, self.ccx_course_key)
def test_no_global_admin_read_access(self): """ Test that global admins have no read access """ assert not has_studio_read_access(self.global_admin, self.ccx_course_key)
def get(self, request: Request, course_id: str): """ Get an object containing all the advanced settings in a course. **Example Request** GET /api/contentstore/v0/advanced_settings/{course_id} **Response Values** If the request is successful, an HTTP 200 "OK" response is returned. The HTTP 200 response contains a single dict that contains keys that are the course's advanced settings. For each setting a dictionary is returned that contains the following fields: * **deprecated**: This is true for settings that are deprecated. * **display_name**: This is a friendly name for the setting. * **help**: Contains help text that explains how the setting works. * **value**: Contains the value of the setting. The exact format depends on the setting and is often explained in the ``help`` field above. There may be other fields returned by the response. **Example Response** ```json { "display_name": { "value": "Demonstration Course", "display_name": "Course Display Name", "help": "Enter the name of the course as it should appear in the course list.", "deprecated": false, "hide_on_enabled_publisher": false }, "course_edit_method": { "value": "Studio", "display_name": "Course Editor", "help": "Enter the method by which this course is edited (\"XML\" or \"Studio\").", "deprecated": true, "hide_on_enabled_publisher": false }, "days_early_for_beta": { "value": null, "display_name": "Days Early for Beta Users", "help": "Enter the number of days before the start date that beta users can access the course.", "deprecated": false, "hide_on_enabled_publisher": false }, ... } ``` """ filter_query_data = AdvancedCourseSettingsView.FilterQuery( request.query_params) if not filter_query_data.is_valid(): raise ValidationError(filter_query_data.errors) course_key = CourseKey.from_string(course_id) if not has_studio_read_access(request.user, course_key): self.permission_denied(request) course_module = modulestore().get_course(course_key) return Response( CourseMetadata.fetch_all( course_module, filter_fields=filter_query_data.cleaned_data['filter_fields'], ))
def get_block_olx(request, usage_key_str): """ Given a modulestore XBlock usage ID (block-v1:...), get its OLX and a list of any static asset files it uses. (There are other APIs for getting the OLX of Blockstore XBlocks.) """ # Parse the usage key: try: usage_key = UsageKey.from_string(usage_key_str) except (ValueError, InvalidKeyError): raise ValidationError('Invalid usage key') # lint-amnesty, pylint: disable=raise-missing-from if usage_key.block_type in ('course', 'chapter', 'sequential'): raise ValidationError( 'Requested XBlock tree is too large - export verticals or their children only' ) course_key = usage_key.context_key if not isinstance(course_key, CourseLocator): raise ValidationError('Invalid usage key: not a modulestore course') # Make sure the user has permission on that course if not has_studio_read_access(request.user, course_key): raise PermissionDenied( "You must be a member of the course team in Studio to export OLX using this API." ) # Step 1: Serialize the XBlocks to OLX files + static asset files serialized_blocks = {} # Key is each XBlock's original usage key def serialize_block(block_key): """ Inner method to recursively serialize an XBlock to OLX """ if block_key in serialized_blocks: return block = adapters.get_block(block_key) serialized_blocks[block_key] = XBlockSerializer(block) if block.has_children: for child_id in block.children: serialize_block(child_id) serialize_block(usage_key) result = { "root_block_id": str(usage_key), "blocks": {}, } # For each XBlock that we're exporting: for this_usage_key, data in serialized_blocks.items(): block_data_out = {"olx": data.olx_str} for asset_file in data.static_files: if asset_file.url: url = request.build_absolute_uri(asset_file.url) else: # The file is not in GridFS so we don't have a URL for it; serve it # via our own get_block_exportfs_file API endpoint. url = request.build_absolute_uri( '/api/olx-export/v1/xblock-export-file/' + str(this_usage_key) + '/' + asset_file.name, ) block_data_out.setdefault("static_files", {})[asset_file.name] = { "url": url } result["blocks"][str(data.orig_block_key)] = block_data_out return Response(result)