Exemple #1
0
 def get(self, request, usage_key_str):
     """
     Get metadata about an existing XBlock in the content library
     """
     key = LibraryUsageLocatorV2.from_string(usage_key_str)
     result = api.get_library_block(key)
     return Response(LibraryXBlockMetadataSerializer(result).data)
Exemple #2
0
 def get(self, request, usage_key_str):
     """
     Get the block's OLX
     """
     key = LibraryUsageLocatorV2.from_string(usage_key_str)
     xml_str = api.get_library_block_olx(key)
     return Response(LibraryXBlockOlxSerializer({"olx": xml_str}).data)
Exemple #3
0
 def get(self, request, usage_key_str):
     """
     List the static asset files belonging to this block.
     """
     key = LibraryUsageLocatorV2.from_string(usage_key_str)
     files = api.get_library_block_static_asset_files(key)
     return Response(LibraryXBlockStaticFilesSerializer({"files": files}).data)
Exemple #4
0
def get_library_blocks(library_key, text_search=None, block_types=None):
    """
    Get the list of top-level XBlocks in the specified library.

    Returns a list of LibraryXBlockMetadata objects
    """
    metadata = None
    if LibraryBlockIndexer.indexing_is_enabled():
        try:
            filter_terms = {
                'library_key': [str(library_key)],
                'is_child': [False],
            }
            if block_types:
                filter_terms['block_type'] = block_types
            metadata = [
                {
                    **item,
                    "id": LibraryUsageLocatorV2.from_string(item['id']),
                }
                for item in LibraryBlockIndexer.get_items(filter_terms=filter_terms, text_search=text_search)
                if item is not None
            ]
        except ElasticConnectionError as e:
            log.exception(e)

    # If indexing is disabled, or connection to elastic failed
    if metadata is None:
        metadata = []
        ref = ContentLibrary.objects.get_by_key(library_key)
        lib_bundle = LibraryBundle(library_key, ref.bundle_uuid, draft_name=DRAFT_NAME)
        usages = lib_bundle.get_top_level_usages()

        for usage_key in usages:
            # For top-level definitions, we can go from definition key to usage key using the following, but this would
            # not work for non-top-level blocks as they may have multiple usages. Top level blocks are guaranteed to
            # have only a single usage in the library, which is part of the definition of top level block.
            def_key = lib_bundle.definition_for_usage(usage_key)
            display_name = get_block_display_name(def_key)
            text_match = (text_search is None or
                          text_search.lower() in display_name.lower() or
                          text_search.lower() in str(usage_key).lower())
            type_match = (block_types is None or usage_key.block_type in block_types)
            if text_match and type_match:
                metadata.append({
                    "id": usage_key,
                    "def_key": def_key,
                    "display_name": display_name,
                    "has_unpublished_changes": lib_bundle.does_definition_have_unpublished_changes(def_key),
                })

    return [
        LibraryXBlockMetadata(
            usage_key=item['id'],
            def_key=item['def_key'],
            display_name=item['display_name'],
            has_unpublished_changes=item['has_unpublished_changes'],
        )
        for item in metadata
    ]
Exemple #5
0
 def post(self, request, lib_key_str):
     """
     Add a new XBlock to this content library
     """
     library_key = LibraryLocatorV2.from_string(lib_key_str)
     serializer = LibraryXBlockCreationSerializer(data=request.data)
     serializer.is_valid(raise_exception=True)
     parent_block_usage_str = serializer.validated_data.pop(
         "parent_block", None)
     if parent_block_usage_str:
         # Add this as a child of an existing block:
         parent_block_usage = LibraryUsageLocatorV2.from_string(
             parent_block_usage_str)
         if parent_block_usage.context_key != library_key:
             raise ValidationError(
                 detail={
                     "parent_block":
                     "Usage ID doesn't match library ID in the URL."
                 })
         result = api.create_library_block_child(
             parent_block_usage, **serializer.validated_data)
     else:
         # Create a new regular top-level block:
         result = api.create_library_block(library_key,
                                           **serializer.validated_data)
     return Response(LibraryXBlockMetadataSerializer(result).data)
Exemple #6
0
 def post(self, request, lib_key_str):
     """
     Add a new XBlock to this content library
     """
     library_key = LibraryLocatorV2.from_string(lib_key_str)
     api.require_permission_for_library_key(
         library_key, request.user,
         permissions.CAN_EDIT_THIS_CONTENT_LIBRARY)
     serializer = LibraryXBlockCreationSerializer(data=request.data)
     serializer.is_valid(raise_exception=True)
     parent_block_usage_str = serializer.validated_data.pop(
         "parent_block", None)
     if parent_block_usage_str:
         # Add this as a child of an existing block:
         parent_block_usage = LibraryUsageLocatorV2.from_string(
             parent_block_usage_str)
         if parent_block_usage.context_key != library_key:
             raise ValidationError(
                 detail={
                     "parent_block":
                     "Usage ID doesn't match library ID in the URL."
                 })
         result = api.create_library_block_child(
             parent_block_usage, **serializer.validated_data)
     else:
         # Create a new regular top-level block:
         try:
             result = api.create_library_block(library_key,
                                               **serializer.validated_data)
         except api.IncompatibleTypesError as err:
             raise ValidationError(detail={'block_type': str(err)}, )
     return Response(LibraryXBlockMetadataSerializer(result).data)
Exemple #7
0
 def get(self, request, usage_key_str):
     """
     List the static asset files belonging to this block.
     """
     key = LibraryUsageLocatorV2.from_string(usage_key_str)
     api.require_permission_for_library_key(key.lib_key, request.user, permissions.CAN_VIEW_THIS_CONTENT_LIBRARY)
     files = api.get_library_block_static_asset_files(key)
     return Response(LibraryXBlockStaticFilesSerializer({"files": files}).data)
Exemple #8
0
 def get(self, request, usage_key_str):
     """
     Get the block's OLX
     """
     key = LibraryUsageLocatorV2.from_string(usage_key_str)
     api.require_permission_for_library_key(key.lib_key, request.user, permissions.CAN_VIEW_THIS_CONTENT_LIBRARY)
     xml_str = api.get_library_block_olx(key)
     return Response(LibraryXBlockOlxSerializer({"olx": xml_str}).data)
Exemple #9
0
 def get(self, request, usage_key_str):
     """
     Get metadata about an existing XBlock in the content library
     """
     key = LibraryUsageLocatorV2.from_string(usage_key_str)
     api.require_permission_for_library_key(key.lib_key, request.user, permissions.CAN_VIEW_THIS_CONTENT_LIBRARY)
     result = api.get_library_block(key)
     return Response(LibraryXBlockMetadataSerializer(result).data)
Exemple #10
0
 def get(self, request, usage_key_str, file_path):
     """
     Get a static asset file belonging to this block.
     """
     key = LibraryUsageLocatorV2.from_string(usage_key_str)
     files = api.get_library_block_static_asset_files(key)
     for f in files:
         if f.path == file_path:
             return Response(LibraryXBlockStaticFileSerializer(f).data)
     raise NotFound
Exemple #11
0
 def delete(self, request, usage_key_str, file_path):  # pylint: disable=unused-argument
     """
     Delete a static asset file belonging to this block.
     """
     usage_key = LibraryUsageLocatorV2.from_string(usage_key_str)
     try:
         api.delete_library_block_static_asset_file(usage_key, file_path)
     except ValueError:
         raise ValidationError("Invalid file path")
     return Response(status=status.HTTP_204_NO_CONTENT)
Exemple #12
0
    def get(self, request, usage_key_str):
        """
        Get the LTI launch URL for the XBlock.
        """
        key = LibraryUsageLocatorV2.from_string(usage_key_str)
        api.require_permission_for_library_key(key.lib_key, request.user, permissions.CAN_VIEW_THIS_CONTENT_LIBRARY)

        # Get the block to validate its existence
        api.get_library_block(key)
        lti_login_url = f"{reverse('content_libraries:lti-launch')}?id={key}"
        return Response({"lti_url": lti_login_url})
Exemple #13
0
 def get(self, request, usage_key_str, file_path):
     """
     Get a static asset file belonging to this block.
     """
     key = LibraryUsageLocatorV2.from_string(usage_key_str)
     api.require_permission_for_library_key(key.lib_key, request.user, permissions.CAN_VIEW_THIS_CONTENT_LIBRARY)
     files = api.get_library_block_static_asset_files(key)
     for f in files:
         if f.path == file_path:
             return Response(LibraryXBlockStaticFileSerializer(f).data)
     raise NotFound
Exemple #14
0
 def delete(self, request, usage_key_str, file_path):
     """
     Delete a static asset file belonging to this block.
     """
     usage_key = LibraryUsageLocatorV2.from_string(usage_key_str)
     api.require_permission_for_library_key(
         usage_key.lib_key, request.user, permissions.CAN_EDIT_THIS_CONTENT_LIBRARY,
     )
     try:
         api.delete_library_block_static_asset_file(usage_key, file_path)
     except ValueError:
         raise ValidationError("Invalid file path")
     return Response(status=status.HTTP_204_NO_CONTENT)
Exemple #15
0
    def delete(self, request, usage_key_str):  # pylint: disable=unused-argument
        """
        Delete a usage of a block from the library (and any children it has).

        If this is the only usage of the block's definition within this library,
        both the definition and the usage will be deleted. If this is only one
        of several usages, the definition will be kept. Usages by linked bundles
        are ignored and will not prevent deletion of the definition.

        If the usage points to a definition in a linked bundle, the usage will
        be deleted but the link and the linked bundle will be unaffected.
        """
        key = LibraryUsageLocatorV2.from_string(usage_key_str)
        api.delete_library_block(key)
        return Response({})
def index_library(sender, library_key, **kwargs):  # pylint: disable=unused-argument
    """
    Index library when created or updated, or when its blocks are modified.
    """
    if ContentLibraryIndexer.indexing_is_enabled():
        try:
            ContentLibraryIndexer.index_items([library_key])
            if kwargs.get('update_blocks', False):
                blocks = LibraryBlockIndexer.get_items(filter_terms={
                    'library_key': str(library_key)
                })
                usage_keys = [LibraryUsageLocatorV2.from_string(block['id']) for block in blocks]
                LibraryBlockIndexer.index_items(usage_keys)
        except ElasticConnectionError as e:
            log.exception(e)
Exemple #17
0
    def post(self, request, usage_key_str):
        """
        Replace the block's OLX.

        This API is only meant for use by developers or API client applications.
        Very little validation is done.
        """
        key = LibraryUsageLocatorV2.from_string(usage_key_str)
        serializer = LibraryXBlockOlxSerializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        new_olx_str = serializer.validated_data["olx"]
        try:
            api.set_library_block_olx(key, new_olx_str)
        except ValueError as err:
            raise ValidationError(detail=str(err))
        return Response(LibraryXBlockOlxSerializer({"olx": new_olx_str}).data)
Exemple #18
0
    def post(self, request, usage_key_str):
        """
        Replace the block's OLX.

        This API is only meant for use by developers or API client applications.
        Very little validation is done.
        """
        key = LibraryUsageLocatorV2.from_string(usage_key_str)
        api.require_permission_for_library_key(key.lib_key, request.user, permissions.CAN_EDIT_THIS_CONTENT_LIBRARY)
        serializer = LibraryXBlockOlxSerializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        new_olx_str = serializer.validated_data["olx"]
        try:
            api.set_library_block_olx(key, new_olx_str)
        except ValueError as err:
            raise ValidationError(detail=str(err))  # lint-amnesty, pylint: disable=raise-missing-from
        return Response(LibraryXBlockOlxSerializer({"olx": new_olx_str}).data)
Exemple #19
0
 def put(self, request, usage_key_str, file_path):
     """
     Replace a static asset file belonging to this block.
     """
     usage_key = LibraryUsageLocatorV2.from_string(usage_key_str)
     file_wrapper = request.data['content']
     if file_wrapper.size > 20 * 1024 * 1024:  # > 20 MiB
         # In the future, we need a way to use file_wrapper.chunks() to read
         # the file in chunks and stream that to Blockstore, but Blockstore
         # currently lacks an API for streaming file uploads.
         raise ValidationError("File too big")
     file_content = file_wrapper.read()
     try:
         result = api.add_library_block_static_asset_file(usage_key, file_path, file_content)
     except ValueError:
         raise ValidationError("Invalid file path")
     return Response(LibraryXBlockStaticFileSerializer(result).data)
Exemple #20
0
    def post(self, request):
        """
        Process LTI platform launch requests.
        """

        # Parse LTI launch message.

        try:
            self.launch_message = self.get_launch_message()
        except LtiException as exc:
            log.exception('LTI 1.3: Tool launch failed: %s', exc)
            return self._bad_request_response()

        log.info("LTI 1.3: Launch message body: %s",
                 json.dumps(self.launch_data))

        # Parse content key.

        usage_key_str = request.GET.get('id')
        if not usage_key_str:
            return self._bad_request_response()
        usage_key = LibraryUsageLocatorV2.from_string(usage_key_str)
        log.info('LTI 1.3: Launch block: id=%s', usage_key)

        # Authenticate the launch and setup LTI profiles.

        edx_user = self._authenticate_and_login(usage_key)
        if not edx_user:
            return self._bad_request_response()

        # Get the block.

        self.block = xblock_api.load_block(
            usage_key,
            user=self.request.user)

        # Handle Assignment and Grade Service request.

        self.handle_ags()

        # Render context and response.
        context = self.get_context_data()
        response = self.render_to_response(context)
        mark_user_change_as_expected(edx_user.id)
        return response
    def test_index_block(self):
        """
        Test if libraries are being indexed correctly
        """
        lib = self._create_library(slug="test-lib-index-1", title="Title 1", description="Description")
        block1 = self._add_block_to_library(lib['id'], "problem", "problem1")
        block2 = self._add_block_to_library(lib['id'], "problem", "problem2")

        self.assertEqual(len(LibraryBlockIndexer.get_items()), 2)

        for block in [block1, block2]:
            usage_key = LibraryUsageLocatorV2.from_string(block['id'])
            response = LibraryBlockIndexer.get_items([usage_key])[0]

            self.assertEqual(response['id'], block['id'])
            self.assertEqual(response['def_key'], block['def_key'])
            self.assertEqual(response['block_type'], block['block_type'])
            self.assertEqual(response['display_name'], block['display_name'])
            self.assertEqual(response['has_unpublished_changes'], block['has_unpublished_changes'])
Exemple #22
0
 def put(self, request, usage_key_str, file_path):
     """
     Replace a static asset file belonging to this block.
     """
     usage_key = LibraryUsageLocatorV2.from_string(usage_key_str)
     api.require_permission_for_library_key(
         usage_key.lib_key, request.user, permissions.CAN_EDIT_THIS_CONTENT_LIBRARY,
     )
     file_wrapper = request.data['content']
     if file_wrapper.size > 20 * 1024 * 1024:  # > 20 MiB
         # In the future, we need a way to use file_wrapper.chunks() to read
         # the file in chunks and stream that to Blockstore, but Blockstore
         # currently lacks an API for streaming file uploads.
         raise ValidationError("File too big")
     file_content = file_wrapper.read()
     try:
         result = api.add_library_block_static_asset_file(usage_key, file_path, file_content)
     except ValueError:
         raise ValidationError("Invalid file path")  # lint-amnesty, pylint: disable=raise-missing-from
     return Response(LibraryXBlockStaticFileSerializer(result).data)
Exemple #23
0
    def test_upsert_from_ags_launch(self):
        """
        Give no graded resource
        When get_or_create_from_launch twice
        Then created at first, retrieved at second.
        """

        resource_id = 'resource-foobar'
        usage_key = 'lb:foo:bar:fooz:barz'
        lineitem = 'http://canvas.docker/api/lti/courses/1/line_items/7'
        resource_endpoint = {
            "lineitem":
            lineitem,
            "scope": [
                "https://purl.imsglobal.org/spec/lti-ags/scope/lineitem",
                "https://purl.imsglobal.org/spec/lti-ags/scope/score"
            ],
        }
        resource_link = {
            "id": resource_id,
            "title": "A custom title",
        }

        profile = LtiProfile.objects.get_or_create_from_claims(iss=self.iss,
                                                               aud=self.aud,
                                                               sub=self.sub)
        block_mock = mock.Mock()
        block_mock.scope_ids.usage_id = LibraryUsageLocatorV2.from_string(
            usage_key)
        res = LtiGradedResource.objects.upsert_from_ags_launch(
            profile.user, block_mock, resource_endpoint, resource_link)

        self.assertEqual(resource_id, res.resource_id)
        self.assertEqual(lineitem, res.ags_lineitem)
        self.assertEqual(usage_key, str(res.usage_key))
        self.assertEqual(profile, res.profile)

        res2 = LtiGradedResource.objects.upsert_from_ags_launch(
            profile.user, block_mock, resource_endpoint, resource_link)

        self.assertEqual(res, res2)
Exemple #24
0
def score_changed_handler(sender, **kwargs):  # pylint: disable=unused-argument
    """
    Match the score event to an LTI resource and update.
    """

    lti_enabled = (settings.FEATURES.get('ENABLE_CONTENT_LIBRARIES') and
                   settings.FEATURES.get('ENABLE_CONTENT_LIBRARIES_LTI_TOOL'))
    if not lti_enabled:
        return

    modified = kwargs.get('modified')
    usage_id = kwargs.get('usage_id')
    user_id = kwargs.get('user_id')
    weighted_earned = kwargs.get('weighted_earned')
    weighted_possible = kwargs.get('weighted_possible')

    if None in (modified, usage_id, user_id, weighted_earned,
                weighted_possible):
        log.debug(
            "LTI 1.3: Score Signal: Missing a required parameters, "
            "ignoring: kwargs=%s", kwargs)
        return
    try:
        usage_key = LibraryUsageLocatorV2.from_string(usage_id)
    except InvalidKeyError:
        log.debug(
            "LTI 1.3: Score Signal: Not a content libraries v2 usage key, "
            "ignoring: usage_id=%s", usage_id)
        return
    try:
        resource = LtiGradedResource.objects.get_from_user_id(
            user_id, usage_key=usage_key)
    except LtiGradedResource.DoesNotExist:
        log.debug(
            "LTI 1.3: Score Signal: Unknown resource, ignoring: kwargs=%s",
            kwargs)
    else:
        resource.update_score(weighted_earned, weighted_possible, modified)
        log.info("LTI 1.3: Score Signal: Grade upgraded: resource; %s",
                 resource)