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)
Пример #2
0
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)
Пример #3
0
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)
Пример #4
0
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
Пример #5
0
    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)
Пример #6
0
 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)
Пример #7
0
 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)
Пример #8
0
    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'],
            ))
Пример #9
0
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)