def get_learning_context_impl(key):
    """
    Given an opaque key, get the implementation of its learning context.

    Returns a subclass of LearningContext

    Raises TypeError if the specified key isn't a type that has a learning
    context.
    Raises PluginError if there is some misconfiguration causing the context
    implementation to not be installed.
    """
    if isinstance(key, LearningContextKey):
        context_type = key.CANONICAL_NAMESPACE  # e.g. 'lib'
    elif isinstance(key, UsageKeyV2):
        context_type = key.context_key.CANONICAL_NAMESPACE
    elif isinstance(key, OpaqueKey):
        # Maybe this is an older modulestore key etc.
        raise TypeError(f"Opaque key {key} does not have a learning context.")
    else:
        raise TypeError(
            f"key '{key}' is not an opaque key. You probably forgot [KeyType].from_string(...)"
        )

    try:
        return _learning_context_cache[context_type]
    except KeyError:
        # Load this learning context type.
        params = get_xblock_app_config().get_learning_context_params()
        _learning_context_cache[
            context_type] = LearningContextPluginManager.get_plugin(
                context_type)(**params)
        return _learning_context_cache[context_type]
Exemple #2
0
def get_block_display_name(block_or_key):
    """
    Efficiently get the display name of the specified block. This is done in a
    way that avoids having to load and parse the block's entire XML field data
    using its parse_xml() method, which may be very expensive (e.g. the video
    XBlock parse_xml leads to various slow edxval API calls in some cases).

    This method also defines and implements various fallback mechanisms in case
    the ID can't be loaded.

    block_or_key can be an XBlock instance, a usage key or a definition key.

    Returns the display name as a string
    """
    def_key = resolve_definition(block_or_key)
    use_draft = get_xblock_app_config().get_learning_context_params().get('use_draft')
    cache = BundleCache(def_key.bundle_uuid, draft_name=use_draft)
    cache_key = ('block_display_name', str(def_key))
    display_name = cache.get(cache_key)
    if display_name is None:
        # Instead of loading the block, just load its XML and parse it
        try:
            olx_node = xml_for_definition(def_key)
        except Exception:  # pylint: disable=broad-except
            log.exception("Error when trying to get display_name for block definition %s", def_key)
            # Return now so we don't cache the error result
            return xblock_type_display_name(def_key.block_type)
        try:
            display_name = olx_node.attrib['display_name']
        except KeyError:
            display_name = xblock_type_display_name(def_key.block_type)
        cache.set(cache_key, display_name)
    return display_name
Exemple #3
0
def get_runtime_system():
    """
    Get the XBlockRuntimeSystem, which is a single long-lived factory that can
    create user-specific runtimes.

    The Runtime System isn't always needed (e.g. for management commands), so to
    keep application startup faster, it's only initialized when first accessed
    via this method.
    """
    # The runtime system should not be shared among threads, as there is currently a race condition when parsing XML
    # that can lead to duplicate children.
    # (In BlockstoreXBlockRuntime.get_block(), has_cached_definition(def_id) returns false so parse_xml is called, but
    # meanwhile another thread parses the XML and caches the definition; then when parse_xml gets to XML nodes for
    # child blocks, it appends them to the children already cached by the other thread and saves the doubled list of
    # children; this happens only occasionally but is very difficult to avoid in a clean way due to the API of parse_xml
    # and XBlock field data in general [does not distinguish between setting initial values during parsing and changing
    # values at runtime due to user interaction], and how it interacts with BlockstoreFieldData. Keeping the caches
    # local to each thread completely avoids this problem.)
    cache_name = '_system_{}'.format(threading.get_ident())
    if not hasattr(get_runtime_system, cache_name):
        params = dict(
            handler_url=get_handler_url,
            runtime_class=BlockstoreXBlockRuntime,
        )
        params.update(get_xblock_app_config().get_runtime_system_params())
        setattr(get_runtime_system, cache_name, XBlockRuntimeSystem(**params))
    return getattr(get_runtime_system, cache_name)
Exemple #4
0
    def render(self, block, view_name, context=None):
        """
        Render a specific view of an XBlock.
        """
        # Users who aren't logged in are not allowed to view any views other
        # than public_view. They may call any handlers though.
        if (self.user is None or self.user.is_anonymous) and view_name != 'public_view':
            raise PermissionDenied
        # We also need to override this method because some XBlocks in the
        # edx-platform codebase use methods like add_webpack_to_fragment()
        # which create relative URLs (/static/studio/bundles/webpack-foo.js).
        # We want all resource URLs to be absolute, such as is done when
        # local_resource_url() is used.
        fragment = super(XBlockRuntime, self).render(block, view_name, context)
        needs_fix = False
        for resource in fragment.resources:
            if resource.kind == 'url' and resource.data.startswith('/'):
                needs_fix = True
                break
        if needs_fix:
            log.warning("XBlock %s returned relative resource URLs, which are deprecated", block.scope_ids.usage_id)
            # The Fragment API is mostly immutable, so changing a resource requires this:
            frag_data = fragment.to_dict()
            for resource in frag_data['resources']:
                if resource['kind'] == 'url' and resource['data'].startswith('/'):
                    log.debug("-> Relative resource URL: %s", resource['data'])
                    resource['data'] = get_xblock_app_config().get_site_root_url() + resource['data']
            fragment = Fragment.from_dict(frag_data)

        # Apply any required transforms to the fragment.
        # We could move to doing this in wrap_xblock() and/or use an array of
        # wrapper methods like the ConfigurableFragmentWrapper mixin does.
        fragment = wrap_fragment(fragment, self.transform_static_paths_to_urls(block, fragment.content))

        return fragment
 def render(self, block, view_name, context=None):
     """
     Render a specific view of an XBlock.
     """
     # We only need to override this method because some XBlocks in the
     # edx-platform codebase use methods like add_webpack_to_fragment()
     # which create relative URLs (/static/studio/bundles/webpack-foo.js).
     # We want all resource URLs to be absolute, such as is done when
     # local_resource_url() is used.
     fragment = super(XBlockRuntime, self).render(block, view_name, context)
     needs_fix = False
     for resource in fragment.resources:
         if resource.kind == 'url' and resource.data.startswith('/'):
             needs_fix = True
             break
     if needs_fix:
         log.warning(
             "XBlock %s returned relative resource URLs, which are deprecated",
             block.scope_ids.usage_id)
         # The Fragment API is mostly immutable, so changing a resource requires this:
         frag_data = fragment.to_dict()
         for resource in frag_data['resources']:
             if resource['kind'] == 'url' and resource['data'].startswith(
                     '/'):
                 log.debug("-> Relative resource URL: %s", resource['data'])
                 resource['data'] = get_xblock_app_config(
                 ).get_site_root_url() + resource['data']
         fragment = Fragment.from_dict(frag_data)
     return fragment
Exemple #6
0
def load_block(usage_key, user):
    """
    Load the specified XBlock for the given user.

    Returns an instantiated XBlock.

    Exceptions:
        NotFound - if the XBlock doesn't exist or if the user doesn't have the
                   necessary permissions
    """
    # Is this block part of a course, a library, or what?
    # Get the Learning Context Implementation based on the usage key
    context_impl = get_learning_context_impl(usage_key)
    # Now, using the LearningContext and the Studio/LMS-specific logic, check if
    # the block exists in this context and if the user has permission to render
    # this XBlock view:
    if get_xblock_app_config().require_edit_permission:
        authorized = context_impl.can_edit_block(user, usage_key)
    else:
        authorized = context_impl.can_view_block(user, usage_key)
    if not authorized:
        # We do not know if the block was not found or if the user doesn't have
        # permission, but we want to return the same result in either case:
        raise NotFound(
            "XBlock {} does not exist, or you don't have permission to view it."
            .format(usage_key))

    # TODO: load field overrides from the context
    # e.g. a course might specify that all 'problem' XBlocks have 'max_attempts'
    # set to 3.
    # field_overrides = context_impl.get_field_overrides(usage_key)

    runtime = get_runtime_system().get_runtime(user=user)

    return runtime.get_block(usage_key)
Exemple #7
0
 def local_resource_url(self, block, uri):
     """
     Get the absolute URL to a resource file (like a CSS/JS file or an image)
     that is part of an XBlock's python module.
     """
     relative_url = xblock_local_resource_url(block, uri)
     site_root_url = get_xblock_app_config().get_site_root_url()
     absolute_url = urljoin(site_root_url, relative_url)
     return absolute_url
Exemple #8
0
    def STATIC_URL(self):
        """
        Get the django STATIC_URL path.

        Seems only to be used by capa. Remove this if capa can be refactored.
        """
        # TODO: Refactor capa to access this directly, don't bother the runtime. Then remove it from here.
        static_url = settings.STATIC_URL
        if static_url.startswith('/') and not static_url.startswith('//'):
            # This is not a full URL - should start with https:// to support loading assets from an iframe sandbox
            site_root_url = get_xblock_app_config().get_site_root_url()
            static_url = site_root_url + static_url
        return static_url
Exemple #9
0
def get_handler_url(usage_key, handler_name, user, extra_params=None):
    """
    A method for getting the URL to any XBlock handler. The URL must be usable
    without any authentication (no cookie, no OAuth/JWT), and may expire. (So
    that we can render the XBlock in a secure IFrame without any access to
    existing cookies.)

    The returned URL will contain the provided handler_name, but is valid for
    any other handler on the same XBlock. Callers may replace any occurrences of
    the handler name in the resulting URL with the name of any other handler and
    the URL will still work. (This greatly reduces the number of calls to this
    API endpoint that are needed to interact with any given XBlock.)

    Params:
        usage_key       - Usage Key (Opaque Key object or string)
        handler_name    - Name of the handler or a dummy name like 'any_handler'
        user            - Django User (registered or anonymous)
        extra_params    - Optional extra params to append to the handler_url (dict)

    This view does not check/care if the XBlock actually exists.
    """
    usage_key_str = str(usage_key)
    site_root_url = get_xblock_app_config().get_site_root_url()
    if not user:  # lint-amnesty, pylint: disable=no-else-raise
        raise TypeError(
            "Cannot get handler URLs without specifying a specific user ID.")
    elif user.is_authenticated:
        user_id = user.id
    elif user.is_anonymous:
        user_id = get_xblock_id_for_anonymous_user(user)
    else:
        raise ValueError("Invalid user value")
    # Now generate a token-secured URL for this handler, specific to this user
    # and this XBlock:
    secure_token = get_secure_token_for_xblock_handler(user_id, usage_key_str)
    # Now generate the URL to that handler:
    path = reverse('xblock_api:xblock_handler',
                   kwargs={
                       'usage_key_str': usage_key_str,
                       'user_id': user_id,
                       'secure_token': secure_token,
                       'handler_name': handler_name,
                   })
    qstring = urlencode(extra_params) if extra_params else ''
    if qstring:
        qstring = '?' + qstring
    # We must return an absolute URL. We can't just use
    # rest_framework.reverse.reverse to get the absolute URL because this method
    # can be called by the XBlock from python as well and in that case we don't
    # have access to the request.
    return site_root_url + path + qstring
Exemple #10
0
def get_runtime_system():
    """
    Get the XBlockRuntimeSystem, which is a single long-lived factory that can
    create user-specific runtimes.

    The Runtime System isn't always needed (e.g. for management commands), so to
    keep application startup faster, it's only initialized when first accessed
    via this method.
    """
    # pylint: disable=protected-access
    if not hasattr(get_runtime_system, '_system'):
        params = dict(
            handler_url=get_handler_url,
            runtime_class=BlockstoreXBlockRuntime,
        )
        params.update(get_xblock_app_config().get_runtime_system_params())
        get_runtime_system._system = XBlockRuntimeSystem(**params)
    return get_runtime_system._system
Exemple #11
0
def get_handler_url(usage_key, handler_name, user_id):
    """
    A method for getting the URL to any XBlock handler. The URL must be usable
    without any authentication (no cookie, no OAuth/JWT), and may expire. (So
    that we can render the XBlock in a secure IFrame without any access to
    existing cookies.)

    The returned URL will contain the provided handler_name, but is valid for
    any other handler on the same XBlock. Callers may replace any occurrences of
    the handler name in the resulting URL with the name of any other handler and
    the URL will still work. (This greatly reduces the number of calls to this
    API endpoint that are needed to interact with any given XBlock.)

    Params:
        usage_key       - Usage Key (Opaque Key object or string)
        handler_name    - Name of the handler or a dummy name like 'any_handler'
        user_id         - User ID or XBlockRuntimeSystem.ANONYMOUS_USER

    This view does not check/care if the XBlock actually exists.
    """
    usage_key_str = six.text_type(usage_key)
    site_root_url = get_xblock_app_config().get_site_root_url()
    if user_id is None:
        raise TypeError("Cannot get handler URLs without specifying a specific user ID.")
    elif user_id == XBlockRuntimeSystem.ANONYMOUS_USER:
        raise NotImplementedError("handler links for anonymous users are not yet implemented")  # TODO: implement
    else:
        # Normal case: generate a token-secured URL for this handler, specific
        # to this user and this XBlock.
        secure_token = get_secure_token_for_xblock_handler(user_id, usage_key_str)
        path = reverse('xblock_api:xblock_handler', kwargs={
            'usage_key_str': usage_key_str,
            'user_id': user_id,
            'secure_token': secure_token,
            'handler_name': handler_name,
        })
    # We must return an absolute URL. We can't just use
    # rest_framework.reverse.reverse to get the absolute URL because this method
    # can be called by the XBlock from python as well and in that case we don't
    # have access to the request.
    return site_root_url + path