def orphan_handler(request, course_key_string): """ View for handling orphan related requests. GET gets all of the current orphans. DELETE removes all orphans (requires is_staff access) An orphan is a block whose category is not in the DETACHED_CATEGORY list, is not the root, and is not reachable from the root via children """ course_usage_key = CourseKey.from_string(course_key_string) if request.method == 'GET': if has_studio_read_access(request.user, course_usage_key): return JsonResponse([ unicode(item) for item in modulestore().get_orphans(course_usage_key) ]) else: raise PermissionDenied() if request.method == 'DELETE': if request.user.is_staff: deleted_items = _delete_orphans(course_usage_key, request.user.id, commit=True) return JsonResponse({'deleted': deleted_items}) else: raise PermissionDenied()
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": text_type(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 xblock_outline_handler(request, usage_key_string): """ The restful handler for requests for XBlock information about the block and its children. This is used by the course outline in particular to construct the tree representation of a course. """ usage_key = usage_key_with_run(usage_key_string) if not has_studio_read_access(request.user, usage_key.course_key): raise PermissionDenied() response_format = request.REQUEST.get("format", "html") if response_format == "json" or "application/json" in request.META.get("HTTP_ACCEPT", "application/json"): store = modulestore() with store.bulk_operations(usage_key.course_key): root_xblock = store.get_item(usage_key, depth=None) return JsonResponse( create_xblock_info( root_xblock, include_child_info=True, course_outline=True, include_children_predicate=lambda xblock: not xblock.category == "vertical", ) ) else: return Http404
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( u"User %s tried to access library %s without permission", request.user.username, unicode(library_key) ) raise PermissionDenied() library = modulestore().get_library(library_key) if library is None: log.exception(u"Library %s not found", unicode(library_key)) raise Http404 response_format = 'html' if ( request.REQUEST.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 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 _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_read_access(user, course_key): raise PermissionDenied() course_module = modulestore().get_course(course_key, depth=depth) return course_module
def course_filter(course_summary): """ Filter out unusable and inaccessible courses """ # pylint: disable=fixme # TODO remove this condition when templates purged from db if course_summary.location.course == 'templates': return False return has_studio_read_access(user, course_summary.id)
def course_filter(course_summary): """ Filter out unusable and inaccessible courses """ # pylint: disable=fixme # TODO remove this condition when templates purged from db if course_summary.location.course == 'templates': return False return has_studio_read_access(request.user, course_summary.id)
def get_in_process_course_actions(request): """ Get all in-process course actions """ return [ course for course in CourseRerunState.objects.find_all( exclude_args={'state': CourseRerunUIStateManager.State.SUCCEEDED}, should_display=True) if has_studio_read_access(request.user, course.course_key) ]
def _list_libraries(request): """ List all accessible libraries """ lib_info = [ { "display_name": lib.display_name, "library_key": unicode(lib.location.library_key), } for lib in modulestore().get_libraries() if has_studio_read_access(request.user, lib.location.library_key) ] return JsonResponse(lib_info)
def course_filter(course): """ Filter out unusable and inaccessible courses """ if isinstance(course, ErrorDescriptor): return False # pylint: disable=fixme # TODO remove this condition when templates purged from db if course.location.course == 'templates': return False return has_studio_read_access(user, course.id)
def get_in_process_course_actions(request, this_user=None): """ Get all in-process course actions """ if this_user: user = this_user else: user = request.user return [ course for course in CourseRerunState.objects.find_all( exclude_args={'state': CourseRerunUIStateManager.State.SUCCEEDED}, should_display=True ) if has_studio_read_access(user, course.course_key) ]
def orphan_handler(request, course_key_string): """ View for handling orphan related requests. GET gets all of the current orphans. DELETE removes all orphans (requires is_staff access) An orphan is a block whose category is not in the DETACHED_CATEGORY list, is not the root, and is not reachable from the root via children """ course_usage_key = CourseKey.from_string(course_key_string) if request.method == 'GET': if has_studio_read_access(request.user, course_usage_key): return JsonResponse([unicode(item) for item in modulestore().get_orphans(course_usage_key)]) else: raise PermissionDenied() if request.method == 'DELETE': if request.user.is_staff: deleted_items = _delete_orphans(course_usage_key, request.user.id, commit=True) return JsonResponse({'deleted': deleted_items}) else: raise PermissionDenied()
def xblock_container_handler(request, usage_key_string): """ The restful handler for requests for XBlock information about the block and its children. This is used by the container page in particular to get additional information about publish state and ancestor state. """ usage_key = usage_key_with_run(usage_key_string) if not has_studio_read_access(request.user, usage_key.course_key): raise PermissionDenied() response_format = request.REQUEST.get('format', 'html') if response_format == 'json' or 'application/json' in request.META.get('HTTP_ACCEPT', 'application/json'): with modulestore().bulk_operations(usage_key.course_key): response = _get_module_info( _get_xblock(usage_key, request.user), include_ancestor_info=True, include_publishing_info=True ) return JsonResponse(response) else: return Http404
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 create a non-staff user with no permissions: self._login_as_non_staff_user(logout_first=False) self.assertFalse(CourseCreatorRole().has_user(self.non_staff_user)) # Now check that logged-in users without any permissions cannot create libraries with patch.dict("django.conf.settings.FEATURES", {"ENABLE_CREATOR_GROUP": True}): self._assert_cannot_create_library()
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 create a non-staff user with no permissions: self._login_as_non_staff_user(logout_first=False) self.assertFalse(CourseCreatorRole().has_user(self.non_staff_user)) # Now check that logged-in users without any permissions cannot create libraries with patch.dict('django.conf.settings.FEATURES', {'ENABLE_CREATOR_GROUP': True}): self._assert_cannot_create_library()
def xblock_outline_handler(request, usage_key_string): """ The restful handler for requests for XBlock information about the block and its children. This is used by the course outline in particular to construct the tree representation of a course. """ usage_key = usage_key_with_run(usage_key_string) if not has_studio_read_access(request.user, usage_key.course_key): raise PermissionDenied() response_format = request.REQUEST.get('format', 'html') if response_format == 'json' or 'application/json' in request.META.get('HTTP_ACCEPT', 'application/json'): store = modulestore() root_xblock = store.get_item(usage_key) return JsonResponse(create_xblock_info( root_xblock, include_child_info=True, course_outline=True, include_children_predicate=lambda xblock: not xblock.category == 'vertical' )) else: return Http404
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 = compat.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 xblock_handler(request, usage_key_string): """ The restful handler for xblock requests. DELETE json: delete this xblock instance from the course. GET json: returns representation of the xblock (locator id, data, and metadata). if ?fields=graderType, it returns the graderType for the unit instead of the above. html: returns HTML for rendering the xblock (which includes both the "preview" view and the "editor" view) PUT or POST or PATCH json: if xblock locator is specified, update the xblock instance. The json payload can contain these fields, all optional: :data: the new value for the data. :children: the unicode representation of the UsageKeys of children for this xblock. :metadata: new values for the metadata fields. Any whose values are None will be deleted not set to None! Absent ones will be left alone. :nullout: which metadata fields to set to None :graderType: change how this unit is graded :publish: can be: 'make_public': publish the content 'republish': publish this item *only* if it was previously published 'discard_changes' - reverts to the last published version Note: If 'discard_changes', the other fields will not be used; that is, it is not possible to update and discard changes in a single operation. The JSON representation on the updated xblock (minus children) is returned. if usage_key_string is not specified, create a new xblock instance, either by duplicating an existing xblock, or creating an entirely new one. The json playload can contain these fields: :parent_locator: parent for new xblock, required for both duplicate and create new instance :duplicate_source_locator: if present, use this as the source for creating a duplicate copy :category: type of xblock, required if duplicate_source_locator is not present. :display_name: name for new xblock, optional :boilerplate: template name for populating fields, optional and only used if duplicate_source_locator is not present The locator (unicode representation of a UsageKey) for the created xblock (minus children) is returned. """ if usage_key_string: usage_key = usage_key_with_run(usage_key_string) access_check = has_studio_read_access if request.method == 'GET' else has_studio_write_access if not access_check(request.user, usage_key.course_key): raise PermissionDenied() if request.method == 'GET': accept_header = request.META.get('HTTP_ACCEPT', 'application/json') if 'application/json' in accept_header: fields = request.REQUEST.get('fields', '').split(',') if 'graderType' in fields: # right now can't combine output of this w/ output of _get_module_info, but worthy goal return JsonResponse( CourseGradingModel.get_section_grader_type(usage_key)) # TODO: pass fields to _get_module_info and only return those with modulestore().bulk_operations(usage_key.course_key): response = _get_module_info( _get_xblock(usage_key, request.user)) return JsonResponse(response) else: return HttpResponse(status=406) elif request.method == 'DELETE': _delete_item(usage_key, request.user) return JsonResponse() else: # Since we have a usage_key, we are updating an existing xblock. return _save_xblock( request.user, _get_xblock(usage_key, request.user), data=request.json.get('data'), children_strings=request.json.get('children'), metadata=request.json.get('metadata'), nullout=request.json.get('nullout'), grader_type=request.json.get('graderType'), publish=request.json.get('publish'), ) elif request.method in ('PUT', 'POST'): if 'duplicate_source_locator' in request.json: parent_usage_key = usage_key_with_run( request.json['parent_locator']) duplicate_source_usage_key = usage_key_with_run( request.json['duplicate_source_locator']) source_course = duplicate_source_usage_key.course_key dest_course = parent_usage_key.course_key if (not has_studio_write_access(request.user, dest_course) or not has_studio_read_access(request.user, source_course)): raise PermissionDenied() dest_usage_key = _duplicate_item( parent_usage_key, duplicate_source_usage_key, request.user, request.json.get('display_name'), ) return JsonResponse({ "locator": unicode(dest_usage_key), "courseKey": unicode(dest_usage_key.course_key) }) else: return _create_item(request) else: return HttpResponseBadRequest( "Only instance creation is supported without a usage key.", content_type="text/plain")
def xblock_handler(request, usage_key_string): """ The restful handler for xblock requests. DELETE json: delete this xblock instance from the course. GET json: returns representation of the xblock (locator id, data, and metadata). if ?fields=graderType, it returns the graderType for the unit instead of the above. html: returns HTML for rendering the xblock (which includes both the "preview" view and the "editor" view) PUT or POST or PATCH json: if xblock locator is specified, update the xblock instance. The json payload can contain these fields, all optional: :data: the new value for the data. :children: the unicode representation of the UsageKeys of children for this xblock. :metadata: new values for the metadata fields. Any whose values are None will be deleted not set to None! Absent ones will be left alone. :nullout: which metadata fields to set to None :graderType: change how this unit is graded :publish: can be: 'make_public': publish the content 'republish': publish this item *only* if it was previously published 'discard_changes' - reverts to the last published version Note: If 'discard_changes', the other fields will not be used; that is, it is not possible to update and discard changes in a single operation. The JSON representation on the updated xblock (minus children) is returned. if usage_key_string is not specified, create a new xblock instance, either by duplicating an existing xblock, or creating an entirely new one. The json playload can contain these fields: :parent_locator: parent for new xblock, required for both duplicate and create new instance :duplicate_source_locator: if present, use this as the source for creating a duplicate copy :category: type of xblock, required if duplicate_source_locator is not present. :display_name: name for new xblock, optional :boilerplate: template name for populating fields, optional and only used if duplicate_source_locator is not present The locator (unicode representation of a UsageKey) for the created xblock (minus children) is returned. """ if usage_key_string: usage_key = usage_key_with_run(usage_key_string) access_check = has_studio_read_access if request.method == 'GET' else has_studio_write_access if not access_check(request.user, usage_key.course_key): raise PermissionDenied() if request.method == 'GET': accept_header = request.META.get('HTTP_ACCEPT', 'application/json') if 'application/json' in accept_header: fields = request.REQUEST.get('fields', '').split(',') if 'graderType' in fields: # right now can't combine output of this w/ output of _get_module_info, but worthy goal return JsonResponse(CourseGradingModel.get_section_grader_type(usage_key)) # TODO: pass fields to _get_module_info and only return those with modulestore().bulk_operations(usage_key.course_key): response = _get_module_info(_get_xblock(usage_key, request.user)) return JsonResponse(response) else: return HttpResponse(status=406) elif request.method == 'DELETE': _delete_item(usage_key, request.user) return JsonResponse() else: # Since we have a usage_key, we are updating an existing xblock. return _save_xblock( request.user, _get_xblock(usage_key, request.user), data=request.json.get('data'), children_strings=request.json.get('children'), metadata=request.json.get('metadata'), nullout=request.json.get('nullout'), grader_type=request.json.get('graderType'), publish=request.json.get('publish'), ) elif request.method in ('PUT', 'POST'): if 'duplicate_source_locator' in request.json: parent_usage_key = usage_key_with_run(request.json['parent_locator']) duplicate_source_usage_key = usage_key_with_run(request.json['duplicate_source_locator']) source_course = duplicate_source_usage_key.course_key dest_course = parent_usage_key.course_key if ( not has_studio_write_access(request.user, dest_course) or not has_studio_read_access(request.user, source_course) ): raise PermissionDenied() dest_usage_key = _duplicate_item( parent_usage_key, duplicate_source_usage_key, request.user, request.json.get('display_name'), ) return JsonResponse({"locator": unicode(dest_usage_key), "courseKey": unicode(dest_usage_key.course_key)}) else: return _create_item(request) else: return HttpResponseBadRequest( "Only instance creation is supported without a usage key.", content_type="text/plain" )
def xblock_view_handler(request, usage_key_string, view_name): """ The restful handler for requests for rendered xblock views. Returns a json object containing two keys: html: The rendered html of the view resources: A list of tuples where the first element is the resource hash, and the second is the resource description """ usage_key = usage_key_with_run(usage_key_string) if not has_studio_read_access(request.user, usage_key.course_key): raise PermissionDenied() accept_header = request.META.get('HTTP_ACCEPT', 'application/json') if 'application/json' in accept_header: store = modulestore() xblock = store.get_item(usage_key) container_views = ['container_preview', 'reorderable_container_child_preview', 'container_child_preview'] # wrap the generated fragment in the xmodule_editor div so that the javascript # can bind to it correctly xblock.runtime.wrappers.append(partial( wrap_xblock, 'StudioRuntime', usage_id_serializer=unicode, request_token=request_token(request), )) if view_name in (STUDIO_VIEW, VISIBILITY_VIEW): try: fragment = xblock.render(view_name) # catch exceptions indiscriminately, since after this point they escape the # dungeon and surface as uneditable, unsaveable, and undeletable # component-goblins. except Exception as exc: # pylint: disable=broad-except log.debug("Unable to render %s for %r", view_name, xblock, exc_info=True) fragment = Fragment(render_to_string('html_error.html', {'message': str(exc)})) elif view_name in PREVIEW_VIEWS + container_views: is_pages_view = view_name == STUDENT_VIEW # Only the "Pages" view uses student view in Studio can_edit = has_studio_write_access(request.user, usage_key.course_key) # Determine the items to be shown as reorderable. Note that the view # 'reorderable_container_child_preview' is only rendered for xblocks that # are being shown in a reorderable container, so the xblock is automatically # added to the list. reorderable_items = set() if view_name == 'reorderable_container_child_preview': reorderable_items.add(xblock.location) paging = None try: if request.REQUEST.get('enable_paging', 'false') == 'true': paging = { 'page_number': int(request.REQUEST.get('page_number', 0)), 'page_size': int(request.REQUEST.get('page_size', 0)), } except ValueError: # pylint: disable=too-many-format-args return HttpResponse( content="Couldn't parse paging parameters: enable_paging: " "{0}, page_number: {1}, page_size: {2}".format( request.REQUEST.get('enable_paging', 'false'), request.REQUEST.get('page_number', 0), request.REQUEST.get('page_size', 0) ), status=400, content_type="text/plain", ) force_render = request.REQUEST.get('force_render', None) # Set up the context to be passed to each XBlock's render method. context = { 'is_pages_view': is_pages_view, # This setting disables the recursive wrapping of xblocks 'is_unit_page': is_unit(xblock), 'can_edit': can_edit, 'root_xblock': xblock if (view_name == 'container_preview') else None, 'reorderable_items': reorderable_items, 'paging': paging, 'force_render': force_render, } fragment = get_preview_fragment(request, xblock, context) # Note that the container view recursively adds headers into the preview fragment, # so only the "Pages" view requires that this extra wrapper be included. if is_pages_view: fragment.content = render_to_string('component.html', { 'xblock_context': context, 'xblock': xblock, 'locator': usage_key, 'preview': fragment.content, 'label': xblock.display_name or xblock.scope_ids.block_type, }) else: raise Http404 hashed_resources = OrderedDict() for resource in fragment.resources: hashed_resources[hash_resource(resource)] = resource return JsonResponse({ 'html': fragment.content, 'resources': hashed_resources.items() }) else: return HttpResponse(status=406)
def test_no_global_admin_read_access(self): """ Test that global admins have no read access """ self.assertFalse(has_studio_read_access(self.global_admin, self.ccx_course_key))
def test_no_staff_read_access(self): """ Test that course staff have no read access """ self.assertFalse( 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 """ self.assertFalse( has_studio_read_access(self.global_admin, self.ccx_course_key))
def can_read(self, course_key): """ Does the user have read access to the given course/library? """ return has_studio_read_access(self._request.user, course_key)
def xblock_view_handler(request, usage_key_string, view_name): """ The restful handler for requests for rendered xblock views. Returns a json object containing two keys: html: The rendered html of the view resources: A list of tuples where the first element is the resource hash, and the second is the resource description """ usage_key = usage_key_with_run(usage_key_string) if not has_studio_read_access(request.user, usage_key.course_key): raise PermissionDenied() accept_header = request.META.get('HTTP_ACCEPT', 'application/json') if 'application/json' in accept_header: store = modulestore() xblock = store.get_item(usage_key) container_views = [ 'container_preview', 'reorderable_container_child_preview', 'container_child_preview' ] # wrap the generated fragment in the xmodule_editor div so that the javascript # can bind to it correctly xblock.runtime.wrappers.append( partial( wrap_xblock, 'StudioRuntime', usage_id_serializer=unicode, request_token=request_token(request), )) if view_name in (STUDIO_VIEW, VISIBILITY_VIEW): try: fragment = xblock.render(view_name) # catch exceptions indiscriminately, since after this point they escape the # dungeon and surface as uneditable, unsaveable, and undeletable # component-goblins. except Exception as exc: # pylint: disable=broad-except log.debug("Unable to render %s for %r", view_name, xblock, exc_info=True) fragment = Fragment( render_to_string('html_error.html', {'message': str(exc)})) elif view_name in (PREVIEW_VIEWS + container_views): is_pages_view = view_name == STUDENT_VIEW # Only the "Pages" view uses student view in Studio can_edit = has_studio_write_access(request.user, usage_key.course_key) # Determine the items to be shown as reorderable. Note that the view # 'reorderable_container_child_preview' is only rendered for xblocks that # are being shown in a reorderable container, so the xblock is automatically # added to the list. reorderable_items = set() if view_name == 'reorderable_container_child_preview': reorderable_items.add(xblock.location) paging = None try: if request.REQUEST.get('enable_paging', 'false') == 'true': paging = { 'page_number': int(request.REQUEST.get('page_number', 0)), 'page_size': int(request.REQUEST.get('page_size', 0)), } except ValueError: # pylint: disable=too-many-format-args return HttpResponse( content="Couldn't parse paging parameters: enable_paging: " "{0}, page_number: {1}, page_size: {2}".format( request.REQUEST.get('enable_paging', 'false'), request.REQUEST.get('page_number', 0), request.REQUEST.get('page_size', 0)), status=400, content_type="text/plain", ) force_render = request.REQUEST.get('force_render', None) # Set up the context to be passed to each XBlock's render method. context = { 'is_pages_view': is_pages_view, # This setting disables the recursive wrapping of xblocks 'is_unit_page': is_unit(xblock), 'can_edit': can_edit, 'root_xblock': xblock if (view_name == 'container_preview') else None, 'reorderable_items': reorderable_items, 'paging': paging, 'force_render': force_render, } fragment = get_preview_fragment(request, xblock, context) # Note that the container view recursively adds headers into the preview fragment, # so only the "Pages" view requires that this extra wrapper be included. if is_pages_view: fragment.content = render_to_string( 'component.html', { 'xblock_context': context, 'xblock': xblock, 'locator': usage_key, 'preview': fragment.content, 'label': xblock.display_name or xblock.scope_ids.block_type, }) else: raise Http404 hashed_resources = OrderedDict() for resource in fragment.resources: hashed_resources[hash_resource(resource)] = resource return JsonResponse({ 'html': fragment.content, 'resources': hashed_resources.items() }) else: return HttpResponse(status=406)
def accessible_libraries_list(user): """ List all libraries available to the logged in user by iterating through all libraries """ # No need to worry about ErrorDescriptors - split's get_libraries() never returns them. return [lib for lib in modulestore().get_libraries() if has_studio_read_access(user, lib.location.library_key)]
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. """ # 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." ) # 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 = compat.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": six.text_type(usage_key), "blocks": {}, } # For each XBlock that we're exporting: for this_usage_key, data in serialized_blocks.items(): olx_str = data.olx_str 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( '../' + six.text_type(this_usage_key) + '/export-file/' + asset_file.name, ) block_data_out.setdefault("static_files", {})[asset_file.name] = { "url": url } result["blocks"][six.text_type(data.orig_block_key)] = block_data_out return Response(result)
def test_no_staff_read_access(self): """ Test that course staff have no read access """ self.assertFalse(has_studio_read_access(self.staff, self.ccx_course_key))