Ejemplo n.º 1
0
class HierarchyMixin(ScopedStorageMixin):
    """
    This adds Fields for parents and children.
    """
    __metaclass__ = ChildrenModelMetaclass

    parent = Reference(help='The id of the parent of this XBlock',
                       default=None,
                       scope=Scope.parent)

    def __init__(self, **kwargs):
        # A cache of the parent block, retrieved from .parent
        self._parent_block = None
        self._parent_block_id = None

        super(HierarchyMixin, self).__init__(**kwargs)

    def get_parent(self):
        """Return the parent block of this block, or None if there isn't one."""
        if self._parent_block_id != self.parent:
            if self.parent is not None:
                self._parent_block = self.runtime.get_block(self.parent)
            else:
                self._parent_block = None
            self._parent_block_id = self.parent
        return self._parent_block
Ejemplo n.º 2
0
class ReferenceTestXBlock(XBlock, XModuleMixin):
    """
    Test xblock type to test the reference field types
    """
    has_children = True
    reference_link = Reference(default=None, scope=Scope.content)
    reference_list = ReferenceList(scope=Scope.content)
    reference_dict = ReferenceValueDict(scope=Scope.settings)
Ejemplo n.º 3
0
class PeerGradingFields(object):
    use_for_single_location = Boolean(
        display_name=_("Show Single Problem"),
        help=
        _('When True, only the single problem specified by "Link to Problem Location" is shown. '
          'When False, a panel is displayed with all problems available for peer grading.'
          ),
        default=False,
        scope=Scope.settings)
    link_to_location = Reference(
        display_name=_("Link to Problem Location"),
        help=
        _('The location of the problem being graded. Only used when "Show Single Problem" is True.'
          ),
        default="",
        scope=Scope.settings)
    graded = Boolean(
        display_name=_("Graded"),
        help=
        _('Defines whether the student gets credit for grading this problem. Only used when "Show Single Problem" is True.'
          ),
        default=False,
        scope=Scope.settings)
    due = Date(help=_("Due date that should be displayed."),
               scope=Scope.settings)
    extended_due = Date(
        help=_(
            "Date that this problem is due by for a particular student. This "
            "can be set by an instructor, and will override the global due "
            "date if it is set to a date that is later than the global due "
            "date."),
        default=None,
        scope=Scope.user_state,
    )
    graceperiod = Timedelta(help=_("Amount of grace to give on the due date."),
                            scope=Scope.settings)
    student_data_for_location = Dict(
        help=_("Student data for a given peer grading problem."),
        scope=Scope.user_state)
    weight = Float(
        display_name=_("Problem Weight"),
        help=
        _("Defines the number of points each problem is worth. If the value is not set, each problem is worth one point."
          ),
        scope=Scope.settings,
        values={
            "min": 0,
            "step": ".1"
        },
        default=1)
    display_name = String(display_name=_("Display Name"),
                          help=_("Display name for this module"),
                          scope=Scope.settings,
                          default=_("Peer Grading Interface"))
    data = String(help=_("Html contents to display for this module"),
                  default='<peergrading></peergrading>',
                  scope=Scope.content)
Ejemplo n.º 4
0
class HierarchyMixin(
        six.with_metaclass(ChildrenModelMetaclass, ScopedStorageMixin)):
    """
    This adds Fields for parents and children.
    """

    parent = Reference(help='The id of the parent of this XBlock',
                       default=None,
                       scope=Scope.parent)

    def __init__(self, **kwargs):
        # A cache of the parent block, retrieved from .parent
        self._parent_block = None
        self._parent_block_id = None
        self._child_cache = {}

        for_parent = kwargs.pop('for_parent', None)

        if for_parent is not None:
            self._parent_block = for_parent
            self._parent_block_id = for_parent.scope_ids.usage_id

        super(HierarchyMixin, self).__init__(**kwargs)

    def get_parent(self):
        """Return the parent block of this block, or None if there isn't one."""
        if not self.has_cached_parent:
            if self.parent is not None:
                self._parent_block = self.runtime.get_block(self.parent)
            else:
                self._parent_block = None
            self._parent_block_id = self.parent
        return self._parent_block

    @property
    def has_cached_parent(self):
        """Return whether this block has a cached parent block."""
        return self.parent is not None and self._parent_block_id == self.parent

    def get_child(self, usage_id):
        """Return the child identified by ``usage_id``."""
        if usage_id in self._child_cache:
            return self._child_cache[usage_id]

        child_block = self.runtime.get_block(usage_id, for_parent=self)
        self._child_cache[usage_id] = child_block
        return child_block

    def get_children(self, usage_id_filter=None):
        """
        Return instantiated XBlocks for each of this blocks ``children``.
        """
        if not self.has_children:
            return []

        return [
            self.get_child(usage_id) for usage_id in self.children
            if usage_id_filter is None or usage_id_filter(usage_id)
        ]

    def clear_child_cache(self):
        """
        Reset the cache of children stored on this XBlock.
        """
        self._child_cache.clear()

    def add_children_to_node(self, node):
        """
        Add children to etree.Element `node`.
        """
        if self.has_children:
            for child_id in self.children:
                child = self.runtime.get_block(child_id)
                self.runtime.add_block_as_child_node(child, node)
Ejemplo n.º 5
0
class XBlock(Plugin):
    """Base class for XBlocks.

    Derive from this class to create a new kind of XBlock.  There are no
    required methods, but you will probably need at least one view.

    Don't provide the ``__init__`` method when deriving from this class.

    """

    __metaclass__ = XBlockMetaclass

    entry_point = 'xblock.v1'

    parent = Reference(help='The id of the parent of this XBlock',
                       default=None,
                       scope=Scope.parent)
    name = String(help="Short name for the block", scope=Scope.settings)
    tags = List(help="Tags for this block", scope=Scope.settings)

    _class_tags = set()

    FIELDS_TO_INIT = tuple()

    @classmethod
    def json_handler(cls, func):
        """Wrap a handler to consume and produce JSON.

        Rather than a Request object, the method will now be passed the
        JSON-decoded body of the request.  Any data returned by the function
        will be JSON-encoded and returned as the response.

        The wrapped function can raise JsonHandlerError to return an error
        response with a non-200 status code.
        """
        @XBlock.handler
        @functools.wraps(func)
        def wrapper(self, request, suffix=''):
            """The wrapper function `json_handler` returns."""
            if request.method != "POST":
                return JsonHandlerError(
                    405, "Method must be POST").get_response(allow=["POST"])
            try:
                request_json = json.loads(request.body)
            except ValueError:
                return JsonHandlerError(400, "Invalid JSON").get_response()
            try:
                response_json = json.dumps(func(self, request_json, suffix))
            except JsonHandlerError as e:
                return e.get_response()
            return Response(response_json, content_type='application/json')

        return wrapper

    @classmethod
    def handler(cls, func):
        """A decorator to indicate a function is usable as a handler."""
        func._is_xblock_handler = True  # pylint: disable=protected-access
        return func

    @staticmethod
    def tag(tags):
        """Returns a function that adds the words in `tags` as class tags to this class."""
        def dec(cls):
            """Add the words in `tags` as class tags to this class."""
            # Add in this class's tags
            cls._class_tags.update(tags.replace(",", " ").split())  # pylint: disable=protected-access
            return cls

        return dec

    @classmethod
    def load_tagged_classes(cls, tag):
        """Produce a sequence of all XBlock classes tagged with `tag`."""
        # Allow this method to access the `_class_tags`
        # pylint: disable=W0212
        for name, class_ in cls.load_classes():
            if tag in class_._class_tags:
                yield name, class_

    @classmethod
    def open_local_resource(cls, uri):
        """Open a local resource.

        The container calls this method when it receives a request for a
        resource on a URL which was generated by Runtime.local_resource_url().
        It will pass the URI from the original call to local_resource_url()
        back to this method. The XBlock must parse this URI and return an open
        file-like object for the resource.

        For security reasons, the default implementation will return only a
        very restricted set of file types, which must be located in a folder
        called "public". XBlock authors who want to override this behavior will
        need to take care to ensure that the method only serves legitimate
        public resources. At the least, the URI should be matched against a
        whitelist regex to ensure that you do not serve an unauthorized
        resource.

        """
        # Verify the URI is in whitelisted form before opening for serving.
        # URI must begin with public/, and no file path component can start
        # with a dot, which prevents ".." and ".hidden" files.
        if not uri.startswith("public/"):
            raise DisallowedFileError(
                "Only files from public/ are allowed: %r" % uri)
        if "/." in uri:
            raise DisallowedFileError("Only safe file names are allowed: %r" %
                                      uri)
        return pkg_resources.resource_stream(cls.__module__, uri)

    @staticmethod
    def needs(service_name):
        """A class decorator to indicate that an XBlock class needs a particular service."""
        def _decorator(cls):  # pylint: disable=missing-docstring
            cls._services_requested[service_name] = "need"  # pylint: disable=protected-access
            return cls

        return _decorator

    @staticmethod
    def wants(service_name):
        """A class decorator to indicate that an XBlock class wants a particular service."""
        def _decorator(cls):  # pylint: disable=missing-docstring
            cls._services_requested[service_name] = "want"  # pylint: disable=protected-access
            return cls

        return _decorator

    @classmethod
    def service_declaration(cls, service_name):
        """
        Find and return a service declaration.

        XBlocks declare their service requirements with @XBlock.needs and
        @XBlock.wants decorators.  These store information on the class.
        This function finds those declarations for a block.

        Arguments:
            service_name (string): the name of the service requested.

        Returns:
            One of "need", "want", or None.

        """
        # The class declares what services it desires. To deal with subclasses,
        # especially mixins, properly, we have to walk up the inheritance
        # hierarchy, and combine all the declared services into one dictionary.
        # We do this once per class, then store the result on the class.
        if "_combined_services" not in cls.__dict__:
            # Walk the MRO chain, collecting all the services together.
            combined = {}
            for parent in reversed(cls.__mro__):
                combined.update(getattr(parent, "_services_requested", {}))
            cls._combined_services = combined
        declaration = cls._combined_services.get(service_name)
        return declaration

    def __init__(self, runtime, field_data, scope_ids):
        """
        Construct a new XBlock.

        This class should only be instantiated by runtimes.

        Arguments:

            runtime (:class:`.Runtime`): Use it to access the environment.
                It is available in XBlock code as ``self.runtime``.

            field_data (:class:`.FieldData`): Interface used by the XBlock
                fields to access their data from wherever it is persisted.

            scope_ids (:class:`.ScopeIds`): Identifiers needed to resolve
                scopes.

        """
        self.runtime = runtime
        self._field_data = field_data
        self._field_data_cache = {}
        self._dirty_fields = {}
        self.scope_ids = scope_ids

        # A cache of the parent block, retrieved from .parent
        self._parent_block = None
        self._parent_block_id = None

    def __repr__(self):
        # `XBlock` obtains the `fields` attribute from the `ModelMetaclass`.
        # Since this is not understood by static analysis, silence this error.
        # pylint: disable=E1101
        attrs = []
        for field in self.fields.values():
            try:
                value = getattr(self, field.name)
            except Exception:  # pylint: disable=W0703
                # Ensure we return a string, even if unanticipated exceptions.
                attrs.append(" %s=???" % (field.name, ))
            else:
                if isinstance(value, basestring):
                    value = value.strip()
                    if len(value) > 40:
                        value = value[:37] + "..."
                attrs.append(" %s=%r" % (field.name, value))
        return "<%s @%04X%s>" % (self.__class__.__name__, id(self) % 0xFFFF,
                                 ','.join(attrs))

    def get_parent(self):
        """Return the parent block of this block, or None if there isn't one."""
        if self._parent_block_id != self.parent:
            if self.parent is not None:
                self._parent_block = self.runtime.get_block(self.parent)
            else:
                self._parent_block = None
            self._parent_block_id = self.parent
        return self._parent_block

    def render(self, view, context=None):
        """Render `view` with this block's runtime and the supplied `context`"""
        return self.runtime.render(self, view, context)

    def handle(self, handler_name, request, suffix=''):
        """Handle `request` with this block's runtime."""
        return self.runtime.handle(self, handler_name, request, suffix)

    def save(self):
        """Save all dirty fields attached to this XBlock."""
        if not self._dirty_fields:
            # nop if _dirty_fields attribute is empty
            return
        try:
            fields_to_save = self._get_fields_to_save()
            # Throws KeyValueMultiSaveError if things go wrong
            self._field_data.set_many(self, fields_to_save)

        except KeyValueMultiSaveError as save_error:
            saved_fields = [
                field for field in self._dirty_fields
                if field.name in save_error.saved_field_names
            ]
            for field in saved_fields:
                # should only find one corresponding field
                del self._dirty_fields[field]
            raise XBlockSaveError(saved_fields, self._dirty_fields.keys())

        # Remove all dirty fields, since the save was successful
        self._clear_dirty_fields()

    def _get_fields_to_save(self):
        """
        Create dictionary mapping between dirty fields and data cache values.
        A `field` is an instance of `Field`.
        """
        fields_to_save = {}
        for field in self._dirty_fields.keys():
            # If the field value isn't the same as the baseline we recorded
            # when it was read, then save it
            if field._is_dirty(self):  # pylint: disable=protected-access
                fields_to_save[field.name] = field.to_json(
                    self._field_data_cache[field.name])
        return fields_to_save

    def _clear_dirty_fields(self):
        """
        Remove all dirty fields from an XBlock.
        """
        self._dirty_fields.clear()

    @classmethod
    def parse_xml(cls, node, runtime, keys, id_generator):
        """
        Use `node` to construct a new block.

        Arguments:
            node (etree.Element): The xml node to parse into an xblock.

            runtime (:class:`.Runtime`): The runtime to use while parsing.

            keys (:class:`.ScopeIds`): The keys identifying where this block
                will store its data.

            id_generator (:class:`.IdGenerator`): An object that will allow the
                runtime to generate correct definition and usage ids for
                children of this block.

        """
        block = runtime.construct_xblock_from_class(cls, keys)

        # The base implementation: child nodes become child blocks.
        for child in node:
            block.runtime.add_node_as_child(block, child, id_generator)

        # Attributes become fields.
        for name, value in node.items():
            if name in block.fields:
                setattr(block, name, value)

        # Text content becomes "content", if such a field exists.
        if "content" in block.fields and block.fields[
                "content"].scope == Scope.content:
            text = node.text
            if text:
                text = text.strip()
                if text:
                    block.content = text

        return block

    def add_xml_to_node(self, node):
        """
        For exporting, set data on `node` from ourselves.
        """
        # pylint: disable=E1101
        # Set node.tag based on our class name.
        node.tag = self.xml_element_name()

        # Set node attributes based on our fields.
        for field_name, field in self.fields.items():
            if field_name in ('children', 'parent', 'content'):
                continue
            if field.is_set_on(self):
                node.set(field_name, unicode(field.read_from(self)))

        # Add children for each of our children.
        if self.has_children:
            for child_id in self.children:
                child = self.runtime.get_block(child_id)
                self.runtime.add_block_as_child_node(child, node)

        # A content field becomes text content.
        text = self.xml_text_content()
        if text is not None:
            node.text = text

    def xml_element_name(self):
        """What XML element name should be used for this block?"""
        return self.scope_ids.block_type

    def xml_text_content(self):
        """What is the text content for this block's XML node?"""
        # pylint: disable=E1101
        if 'content' in self.fields and self.content:
            return self.content
        else:
            return None
Ejemplo n.º 6
0
class AssessmentEndcapXBlock(XBlock):
    """
    An XBlock that gives feedback to users after they finish an assessment
    """

    graded_target_id = Reference(
        scope=Scope.settings,
        help="Which graded component to use as the basis of the leaderboard.",
    )

    CSS_FILE = "static/css/leaderboard.css"

    def author_view(self, context=None):
        graded_target_name = self.graded_target_id
        graded_target = self.runtime.get_block(
            self.graded_target_id) if self.graded_target_id else None
        if graded_target:
            graded_target_name = getattr(graded_target, "display_name",
                                         graded_target_name)
        return self.create_fragment(
            "static/html/assessmentendcap_studio.html",
            context={
                'graded_target_id': self.graded_target_id,
                'graded_target_name': graded_target_name,
                'display_name': self.display_name,
            },
        )

    def studio_view(self, context=None):
        """
        Display the form for changing this XBlock's settings.
        """
        own_id = normalize_id(
            self.scope_ids.usage_id)  # Normalization needed in edX Studio :-/

        flat_block_tree = []

        def build_tree(block, ancestors):
            """
            Build up a tree of information about the XBlocks descending from root_block
            """
            block_name = getattr(block, "display_name", None)
            if not block_name:
                block_type = block.runtime.id_reader.get_block_type(
                    block.scope_ids.def_id)
                block_name = "{} ({})".format(block_type,
                                              block.scope_ids.usage_id)
            eligible = getattr(block, "has_score", False)
            if eligible:
                # If this block is graded, we mark all its ancestors as gradeable too
                if ancestors and not ancestors[-1]["eligible"]:
                    for ancestor in ancestors:
                        ancestor["eligible"] = True
            block_id = normalize_id(block.scope_ids.usage_id)
            new_entry = {
                "depth": len(ancestors),
                "id": block_id,
                "name": block_name,
                "eligible": eligible,
                "is_this": block_id == own_id,
            }
            flat_block_tree.append(new_entry)
            if block.has_children and not getattr(
                    block, "has_dynamic_children", lambda: False)():
                for child_id in block.children:
                    build_tree(block.runtime.get_block(child_id),
                               ancestors=(ancestors + [new_entry]))

        # Determine the root block and build the tree from its immediate children.
        # We don't include the root (course) block because it has too complex a
        # grading calculation and it's not required for intended uses of this block.
        root_block = self
        while root_block.parent:
            root_block = root_block.get_parent()
        for child_id in root_block.children:
            build_tree(root_block.runtime.get_block(child_id), [])

        return self.create_fragment(
            "static/html/assessmentendcap_studio_edit.html",
            context={
                'graded_target_id': self.graded_target_id,
                'block_tree': flat_block_tree,
            },
            javascript=[
                "static/js/leaderboard_studio.js",
                "static/js/grade_leaderboard_studio.js"
            ],
            initialize='GradeLeaderboardStudioXBlock')

    def student_view(self, context):
        """
        Create a fragment used to display the XBlock to a student.
        `context` is a dictionary used to configure the display (unused).

        Returns a `Fragment` object specifying the HTML, CSS, and JavaScript
        to display.
        """

        attempts = None
        max_attempts = None
        score = None
        passing = False
        is_attempted = False

        # On first adding the block, the studio calls student_view instead of
        # author_view, which causes problems. Force call of the author view.
        if getattr(self.runtime, 'is_author_mode', False):
            return self.author_view()

        if EDX_FOUND:

            total_correct, total_possible = 0, 0

            target_block_id = self.graded_target_id
            course_id = target_block_id.course_key.for_branch(
                None).version_agnostic()

            target_block = self.runtime.get_block(target_block_id)

            student = user_by_anonymous_id(self.runtime.anonymous_student_id)

            count = 0

            if student:

                def create_module(descriptor):
                    return target_block.runtime.get_block(descriptor.location)

                for module_descriptor in yield_dynamic_descriptor_descendents(
                        target_block, create_module):

                    (correct, total) = get_score(course_id, student,
                                                 module_descriptor,
                                                 create_module)
                    if (correct is None and total is None) or (not total > 0):
                        continue

                    # Note we ignore the 'graded' flag since authors may wish to use a leaderboard for non-graded content
                    total_correct += correct
                    total_possible += total
                    count += 1

                    attempts = module_descriptor.problem_attempts
                    max_attempts = module_descriptor.max_attempts

                    is_attempted = module_descriptor.is_attempted
                    embed_code = module_descriptor.get_problem_html
            else:
                embed_code = "aaaa"
        else:
            embed_code = "Error: EdX not found."

        #lets do some math to see if we passed
        score = 0
        # calculate score
        passing = False
        if total_possible > 0:
            score = total_correct / total_possible * 100  #get score out of 100, not 1
        if score >= 80:
            passing = True

        if attempts > 0 or is_attempted == True:
            # student has submitted this assessment
            result_file = "assessmentendcap.html"

            if passing:
                result_file = "assessmentendcap-pass.html"
            else:
                if max_attempts and attempts < max_attempts:
                    # student can still submit this problem again
                    result_file = "assessmentendcap-tryagain.html"

                elif attempts >= max_attempts:
                    # student has submitted this problem the max number of times
                    result_file = "assessmentendcap-fail.html"

        else:
            #student has not submitted assessment. We don't need to render anything.
            result_file = "assessmentendcap-blank.html"

        return self.create_fragment(
            "static/html/" + result_file,
            context={
                'embed_code': embed_code,
                'total_correct': total_correct,
                'total_possible': total_possible,
                'score': score,
                'attempts': attempts,
                'max_attempts': max_attempts,
                'count': count,
                'is_attempted': is_attempted
            },
            javascript=["static/js/assessmentendcap.js"],
            initialize='AssessmentEndcapXBlock')

    def resource_string(self, path):  # pylint:disable=no-self-use
        """
        Handy helper for getting resources from our kit.
        """
        data = pkg_resources.resource_string(__name__, path)
        return data.decode("utf8")

    def create_fragment(self,
                        html,
                        context=None,
                        css=None,
                        javascript=None,
                        initialize=None):
        """
        Create an XBlock, given an HTML resource string and an optional context, list of CSS
        resource strings, list of JavaScript resource strings, and initialization function name.
        """
        html = Template(self.resource_string(html))
        context = context or {}
        css = css or [self.CSS_FILE]
        javascript = javascript or []
        frag = Fragment(html.render(Context(context)))
        for sheet in css:
            frag.add_css(self.resource_string(sheet))
        for script in javascript:
            frag.add_javascript(self.resource_string(script))
        if initialize:
            frag.initialize_js(initialize)
        return frag

    @XBlock.json_handler
    def studio_submit(self, data, suffix=''):

        graded_target_id = data.get(
            'graded_target_id')  # We cannot validate this ourselves
        if not graded_target_id:
            graded_target_id = None  # Avoid trying to set to an empty string - won't work
        self.graded_target_id = graded_target_id
        return {}

    @staticmethod
    def workbench_scenarios():
        """A canned scenario for display in the workbench."""
        return [("Assessment Endcap", """
            <vertical_demo>
                <assessmentendcap maxwidth="800" />
                <html_demo><div>Rate the video:</div></html_demo>
                <thumbs />
            </vertical_demo>
            """)]
Ejemplo n.º 7
0
class GradeLeaderboardXBlock(LeaderboardXBlock):
    STUDENT_VIEW_TEMPLATE = "grade_leaderboard.html"
    # Ordered list of class types that know how to get student grades
    GRADE_SOURCES = (EdxLmsGradeSource, MockGradeSource)

    display_name = String(default="Grade Leaderboard",
                          scope=Scope.settings,
                          help="Display name for this block.")
    graded_target_id = Reference(
        scope=Scope.settings,
        help="Which graded component to use as the basis of the leaderboard.",
    )
    grades_cache = Dict(
        scope=Scope.user_state_summary,
        # This field is a cache for use by the edX grade_source.
        # It will need to be removed - see note in grade_source/edx.py
    )

    def validate(self):
        """
        Validates the state of this xblock
        """
        _ = self.runtime.service(self, "i18n").ugettext
        validation = super(GradeLeaderboardXBlock, self).validate()
        if not self.graded_target_id:
            validation.add(
                ValidationMessage(
                    ValidationMessage.WARNING,
                    _(u"A graded activity must be chosen as a basis for the leaderboard."
                      )))
        elif not self.runtime.get_block(self.graded_target_id):
            validation.add(
                ValidationMessage(
                    ValidationMessage.ERROR,
                    _(u"The graded activity specified could not be found.")))
        return validation

    def get_scores(self):
        """
        Compute the top students based on grade and return them.

        Any exceptions thrown will be logged but are not user-visible.
        """
        if not self.graded_target_id:
            raise RuntimeError("graded_target_id not set.")
        for grade_source_type in self.GRADE_SOURCES:
            grade_source = grade_source_type(self)
            if grade_source.is_supported():
                return grade_source.get_grades(self.graded_target_id,
                                               self.count)
        raise RuntimeError("No grade sources available.")

    def author_view(self, context=None):
        graded_target_name = self.graded_target_id
        graded_target = self.runtime.get_block(
            self.graded_target_id) if self.graded_target_id else None
        if graded_target:
            graded_target_name = getattr(graded_target, "display_name",
                                         graded_target_name)
        return self.create_fragment(
            "static/html/grade_leaderboard_studio.html",
            context={
                'graded_target_id': self.graded_target_id,
                'graded_target_name': graded_target_name,
                'display_name': self.display_name,
                'count': self.count,
            },
        )

    def studio_view(self, context=None):
        """
        Display the form for changing this XBlock's settings.
        """
        own_id = normalize_id(
            self.scope_ids.usage_id)  # Normalization needed in edX Studio :-/

        flat_block_tree = []

        def build_tree(block, ancestors):
            """
            Build up a tree of information about the XBlocks descending from root_block
            """
            block_name = getattr(block, "display_name", None)
            if not block_name:
                block_type = block.runtime.id_reader.get_block_type(
                    block.scope_ids.def_id)
                block_name = "{} ({})".format(block_type,
                                              block.scope_ids.usage_id)
            eligible = getattr(block, "has_score", False)
            if eligible:
                # If this block is graded, we mark all its ancestors as gradeable too
                if ancestors and not ancestors[-1]["eligible"]:
                    for ancestor in ancestors:
                        ancestor["eligible"] = True
            block_id = normalize_id(block.scope_ids.usage_id)
            new_entry = {
                "depth": len(ancestors),
                "id": block_id,
                "name": block_name,
                "eligible": eligible,
                "is_this": block_id == own_id,
            }
            flat_block_tree.append(new_entry)
            if block.has_children and not getattr(
                    block, "has_dynamic_children", lambda: False)():
                for child_id in block.children:
                    build_tree(block.runtime.get_block(child_id),
                               ancestors=(ancestors + [new_entry]))

        # Determine the root block and build the tree from its immediate children.
        # We don't include the root (course) block because it has too complex a
        # grading calculation and it's not required for intended uses of this block.
        root_block = self
        while root_block.parent:
            root_block = root_block.get_parent()
        for child_id in root_block.children:
            build_tree(root_block.runtime.get_block(child_id), [])

        return self.create_fragment(
            "static/html/grade_leaderboard_studio_edit.html",
            context={
                'count': self.count,
                'graded_target_id': self.graded_target_id,
                'block_tree': flat_block_tree,
            },
            javascript=[
                "static/js/src/leaderboard_studio.js",
                "static/js/src/grade_leaderboard_studio.js"
            ],
            initialize='GradeLeaderboardStudioXBlock')

    @XBlock.json_handler
    def studio_submit(self, data, suffix=''):
        try:
            count = int(data.get('count', LeaderboardXBlock.count.default))
            if not count > 0:
                raise ValueError
        except ValueError:
            raise JsonHandlerError(
                400, "'count' must be an integer and greater than 0.")

        graded_target_id = data.get(
            'graded_target_id')  # We cannot validate this ourselves
        if not graded_target_id:
            graded_target_id = None  # Avoid trying to set to an empty string - won't work
        self.count = count
        self.graded_target_id = graded_target_id
        return {}

    @staticmethod
    def workbench_scenarios():
        """A canned scenario for display in the workbench."""
        return [
            ("Grade Leaderboard (problem and linked leaderboard)", """
             <vertical_demo>
                <problem_demo>
                    <html_demo><p>What is $a+$b?</p></html_demo>
                    <textinput_demo name="sum_input" input_type="int" />
                    <equality_demo name="sum_checker" left="./sum_input/@student_input" right="$c" />
                    <script>
                        import random
                        a = random.randint(2, 5)
                        b = random.randint(1, 4)
                        c = a + b
                    </script>
                </problem_demo>
                <grade_leaderboard
                    graded_target_id="grade-leaderboard-problem-and-linked-leaderboard.problem_demo.d0.u0"/>
             </vertical_demo>
             """),
            # Note the graded_target ID above is specific to workbench and this scenario.
            ("Grade Leaderboard (invalid block configuration)", """
             <vertical_demo>
                <grade_leaderboard graded_target_id="invalid"/>
             </vertical_demo>
             """),
        ]