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
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)
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)
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)
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
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> """)]
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> """), ]