Esempio n. 1
0
class ImageMagickXBlockFields(XBlockFieldsMixin):

    display_name = String(
        display_name="Display name",
        default='ImageMagick Assignment',
        help=
        "This name appears in the horizontal navigation at the top of the page.",
        scope=Scope.settings)

    instructor_image_meta = Dict(
        default={},
        scope=Scope.settings,
    )

    report_storage = DefaultedDescriptor(
        base_class=String,
        display_name="Report storage",
        default=REPORT_STORAGE,
        help="",
        scope=Scope.settings,
    )

    latest_check = Dict(scope=Scope.user_state, default=None)

    allowable_fuzz = Integer(scope=Scope.settings, default=DEFAULT_FUZZ)

    cut_off = String(
        scope=Scope.settings,
        default=DEFAULT_CUT_OFF,
    )

    extra_cmd_settings = String(
        scope=Scope.settings,
        default=DEFAULT_EXTRA_CMD_SETTINGS,
    )
Esempio n. 2
0
class CrowdsourceHinterFields(object):
    """Defines fields for the crowdsource hinter module."""
    has_children = True

    moderate = String(help='String "True"/"False" - activates moderation', scope=Scope.content,
                      default='False')
    debug = String(help='String "True"/"False" - allows multiple voting', scope=Scope.content,
                   default='False')
    # Usage: hints[answer] = {str(pk): [hint_text, #votes]}
    # hints is a dictionary that takes answer keys.
    # Each value is itself a dictionary, accepting hint_pk strings as keys,
    # and returning [hint text, #votes] pairs as values
    hints = Dict(help='A dictionary containing all the active hints.', scope=Scope.content, default={})
    mod_queue = Dict(help='A dictionary containing hints still awaiting approval', scope=Scope.content,
                     default={})
    hint_pk = Integer(help='Used to index hints.', scope=Scope.content, default=0)

    # A list of previous hints that a student viewed.
    # Of the form [answer, [hint_pk_1, ...]] for each problem.
    # Sorry about the variable name - I know it's confusing.
    previous_answers = List(help='A list of hints viewed.', scope=Scope.user_state, default=[])

    # user_submissions actually contains a list of previous answers submitted.
    # (Originally, preivous_answers did this job, hence the name confusion.)
    user_submissions = List(help='A list of previous submissions', scope=Scope.user_state, default=[])
    user_voted = Boolean(help='Specifies if the user has voted on this problem or not.',
                         scope=Scope.user_state, default=False)
Esempio n. 3
0
class WordCloudFields(object):
    """XFields for word cloud."""
    display_name = String(
        display_name=_("Display Name"),
        help=_("The label for this word cloud on the course page."),
        scope=Scope.settings,
        default="Word cloud"
    )
    instructions = String(
        display_name=_("Instructions"),
        help=_("Add instructions to help learners understand how to use the word cloud. Clear instructions are important, especially for learners who have accessibility requirements."),  # nopep8 pylint: disable=C0301
        scope=Scope.settings,
    )
    num_inputs = Integer(
        display_name=_("Inputs"),
        help=_("The number of text boxes available for learners to add words and sentences."),
        scope=Scope.settings,
        default=5,
        values={"min": 1}
    )
    num_top_words = Integer(
        display_name=_("Maximum Words"),
        help=_("The maximum number of words displayed in the generated word cloud."),
        scope=Scope.settings,
        default=250,
        values={"min": 1}
    )
    display_student_percents = Boolean(
        display_name=_("Show Percents"),
        help=_("Statistics are shown for entered words near that word."),
        scope=Scope.settings,
        default=True
    )

    # Fields for descriptor.
    submitted = Boolean(
        help=_("Whether this learner has posted words to the cloud."),
        scope=Scope.user_state,
        default=False
    )
    student_words = List(
        help=_("Student answer."),
        scope=Scope.user_state,
        default=[]
    )
    all_words = Dict(
        help=_("All possible words from all learners."),
        scope=Scope.user_state_summary
    )
    top_words = Dict(
        help=_("Top num_top_words words for word cloud."),
        scope=Scope.user_state_summary
    )
Esempio n. 4
0
class ABTestFields(object):
    group_portions = Dict(
        help="What proportions of students should go in each group",
        default={DEFAULT: 1},
        scope=Scope.content)
    group_assignments = Dict(help="What group this user belongs to",
                             scope=Scope.preferences,
                             default={})
    group_content = Dict(help="What content to display to each group",
                         scope=Scope.content,
                         default={DEFAULT: []})
    experiment = String(help="Experiment that this A/B test belongs to",
                        scope=Scope.content)
    has_children = True
Esempio n. 5
0
class WordCloudFields(object):
    """XFields for word cloud."""
    display_name = String(
        display_name=_("Display Name"),
        help=_("Display name for this module"),
        scope=Scope.settings,
        default="Word cloud"
    )
    num_inputs = Integer(
        display_name=_("Inputs"),
        help=_("Number of text boxes available for students to input words/sentences."),
        scope=Scope.settings,
        default=5,
        values={"min": 1}
    )
    num_top_words = Integer(
        display_name=_("Maximum Words"),
        help=_("Maximum number of words to be displayed in generated word cloud."),
        scope=Scope.settings,
        default=250,
        values={"min": 1}
    )
    display_student_percents = Boolean(
        display_name=_("Show Percents"),
        help=_("Statistics are shown for entered words near that word."),
        scope=Scope.settings,
        default=True
    )

    # Fields for descriptor.
    submitted = Boolean(
        help=_("Whether this student has posted words to the cloud."),
        scope=Scope.user_state,
        default=False
    )
    student_words = List(
        help=_("Student answer."),
        scope=Scope.user_state,
        default=[]
    )
    all_words = Dict(
        help=_("All possible words from all students."),
        scope=Scope.user_state_summary
    )
    top_words = Dict(
        help=_("Top num_top_words words for word cloud."),
        scope=Scope.user_state_summary
    )
Esempio n. 6
0
class StructuredTagsAside(XBlockAside):
    """
    Aside that allows tagging blocks
    """
    saved_tags = Dict(
        help=_("Dictionary with the available tags"),
        scope=Scope.content,
        default={},
    )
    available_tags = [LearningOutcomeTag()]

    @XBlockAside.aside_for(STUDENT_VIEW)
    def student_view_aside(self, block, context):
        """
        Display the tag selector with specific categories and allowed values,
        depending on the context.
        """
        if isinstance(block, CapaModule):
            tags = []
            for tag in self.available_tags:
                tags.append({
                    'key':
                    tag.key,
                    'title':
                    tag.name,
                    'values':
                    tag.allowed_values,
                    'current_value':
                    self.saved_tags.get(tag.key, None),
                })
            return Fragment(
                render_to_string('structured_tags_block.html', {'tags': tags}))
            #return Fragment(u'<div class="xblock-render">Hello world!!!</div>')
        else:
            return Fragment(u'')
Esempio n. 7
0
class CheckerBlock(XBlock):
    """Base class for blocks that check answers.

    """
    arguments = Dict(help="The arguments expected by `check`")

    def set_arguments_from_xml(self, node):
        """
        Set the `arguments` field from XML attributes based on `check` arguments.
        """
        # Introspect the .check() method, and collect arguments it expects.
        argspec = inspect.getargspec(self.check)
        arguments = {}
        for arg in argspec.args[1:]:
            arguments[arg] = node.attrib.pop(arg)
        self.arguments = arguments

    @classmethod
    def parse_xml(cls, node, runtime, keys, id_generator):
        """
        Parse the XML for a checker. A few arguments are handled specially,
        then the rest get the usual treatment.
        """
        block = super(CheckerBlock, cls).parse_xml(node, runtime, keys, id_generator)
        block.set_arguments_from_xml(node)
        return block

    def check(self, **kwargs):
        """
        Called with the data provided by the ProblemBlock.
        Returns any data, which will be passed to the Javascript handle_check
        function.

        """
        raise NotImplementedError()
Esempio n. 8
0
class InheritanceMixin(XBlockMixin):
    """Field definitions for inheritable fields."""

    graded = Boolean(
        help="Whether this module contributes to the final course grade",
        scope=Scope.settings,
        default=False,
    )
    start = Date(help="Start time when this module is visible",
                 default=datetime(2030, 1, 1, tzinfo=UTC),
                 scope=Scope.settings)
    due = Date(
        help="Date that this problem is due by",
        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,
    )
    giturl = String(
        help="url root for course data git repository",
        scope=Scope.settings,
    )
    xqa_key = String(help="DO NOT USE", scope=Scope.settings)
    graceperiod = Timedelta(
        help=
        "Amount of time after the due date that submissions will be accepted",
        scope=Scope.settings,
    )
    showanswer = String(
        help="When to show the problem answer to the student",
        scope=Scope.settings,
        default="finished",
    )
    rerandomize = String(
        help="When to rerandomize the problem",
        scope=Scope.settings,
        default="never",
    )
    days_early_for_beta = Float(
        help="Number of days early to show content to beta users",
        scope=Scope.settings,
        default=None,
    )
    static_asset_path = String(
        help="Path to use for static assets - overrides Studio c4x://",
        scope=Scope.settings,
        default='',
    )
    text_customization = Dict(
        help="String customization substitutions for particular locations",
        scope=Scope.settings,
    )
    use_latex_compiler = Boolean(help="Enable LaTeX templates?",
                                 default=False,
                                 scope=Scope.settings)
Esempio n. 9
0
class PollFields(object):  # lint-amnesty, pylint: disable=missing-class-docstring
    # Name of poll to use in links to this poll
    display_name = String(
        help=_("The display name for this component."),
        scope=Scope.settings
    )

    voted = Boolean(
        help=_("Whether this student has voted on the poll"),
        scope=Scope.user_state,
        default=False
    )
    poll_answer = String(
        help=_("Student answer"),
        scope=Scope.user_state,
        default=''
    )
    poll_answers = Dict(
        help=_("Poll answers from all students"),
        scope=Scope.user_state_summary
    )

    # List of answers, in the form {'id': 'some id', 'text': 'the answer text'}
    answers = List(
        help=_("Poll answers from xml"),
        scope=Scope.content,
        default=[]
    )

    question = String(
        help=_("Poll question"),
        scope=Scope.content,
        default=''
    )
Esempio n. 10
0
class TestFields(object):
    # Will be returned by editable_metadata_fields.
    max_attempts = Integer(scope=Scope.settings, default=1000, values={'min': 1, 'max': 10})
    # Will not be returned by editable_metadata_fields because filtered out by non_editable_metadata_fields.
    due = Date(scope=Scope.settings)
    # Will not be returned by editable_metadata_fields because is not Scope.settings.
    student_answers = Dict(scope=Scope.user_state)
    # Will be returned, and can override the inherited value from XModule.
    display_name = String(scope=Scope.settings, default='local default', display_name='Local Display Name',
                          help='local help')
    # Used for testing select type, effect of to_json method
    string_select = CrazyJsonString(
        scope=Scope.settings,
        default='default value',
        values=[{'display_name': 'first', 'value': 'value a'},
                {'display_name': 'second', 'value': 'value b'}]
    )
    showanswer = InheritanceMixin.showanswer
    # Used for testing select type
    float_select = Float(scope=Scope.settings, default=.999, values=[1.23, 0.98])
    # Used for testing float type
    float_non_select = Float(scope=Scope.settings, default=.999, values={'min': 0, 'step': .3})
    # Used for testing that Booleans get mapped to select type
    boolean_select = Boolean(scope=Scope.settings)
    # Used for testing Lists
    list_field = List(scope=Scope.settings, default=[])
Esempio n. 11
0
class LeafWithOption(Leaf):
    """A leaf with an additional option set via xml attribute."""
    data3 = Dict(
        default={}, scope=Scope.user_state, enforce_type=True,
        xml_node=True)
    data4 = List(
        default=[], scope=Scope.user_state, enforce_type=True,
        xml_node=True)
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)
Esempio n. 13
0
class PollFields(object):
    # Name of poll to use in links to this poll
    display_name = String(help="Display name for this module", scope=Scope.settings)

    voted = Boolean(help="Whether this student has voted on the poll", scope=Scope.user_state, default=False)
    poll_answer = String(help="Student answer", scope=Scope.user_state, default='')
    poll_answers = Dict(help="All possible answers for the poll fro other students", scope=Scope.user_state_summary)

    answers = List(help="Poll answers from xml", scope=Scope.content, default=[])
    question = String(help="Poll question", scope=Scope.content, default='')
Esempio n. 14
0
def generate_fields(cls):
    """
    A class decorator that generates fields for all field scopes.
    These fields are Dicts, and map usage_ids to values.
    """
    for scope in Scope.scopes():
        setattr(
            cls, scope.name,
            Dict(help="Values stored in the {} scope".format(scope),
                 scope=scope))

    return cls
Esempio n. 15
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 = String(
        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)
    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
    )
Esempio n. 16
0
class ScilabXBlockFields(XBlockFieldsMixin):

    instructor_filename = String(display_name="Instructor file name",
                                 scope=Scope.settings)

    instructor_archive_meta = Dict(
        default={},
        scope=Scope.settings,
    )

    celery_task_id = String(scope=Scope.user_state)

    message = String(
        scope=Scope.user_state,
        default=None,
    )

    need_generate = Boolean(default=False,
                            scope=Scope.settings,
                            display_name="",
                            help="")

    pregenerated = String(
        default=None,
        scope=Scope.user_state,
    )

    time_limit_generate = Integer(
        scope=Scope.settings,
        default=10,
    )

    time_limit_execute = Integer(
        scope=Scope.settings,
        default=10,
    )

    time_limit_check = Integer(
        scope=Scope.settings,
        default=10,
    )
Esempio n. 17
0
    class MultiProctoringFields(cls):
        exam_review_checkbox = Dict(
            display_name=_("exam_review_checkbox"),
            help=_("exam_review_checkbox"),
            default={
                "calculator": True,
                "excel": False,
                "messengers": False,
                "absence": False,
                "books": False,
                "papersheet": True,
                "aid": False,
                "web_sites": False,
                "voice": False,
                "gaze_averted": True,
                "asynchronous": False,
            },
            scope=Scope.settings,
        )

        exam_proctoring_system = String(
            display_name=_("Proctoring system"),
            help=_(""),
            default='',
            scope=Scope.settings,
        )

        @property
        def available_proctoring_services(self):
            """
            Returns the list of proctoring services for the course if available, else None
            """
            # TODO: find all places where this called and refactor them
            string = ",".join(
                CourseMultiproctoringState.get_service_names(
                    self.location.course_key))
            return string
Esempio n. 18
0
class XmlDescriptor(XModuleDescriptor):
    """
    Mixin class for standardized parsing of from xml
    """

    xml_attributes = Dict(
        help=
        "Map of unhandled xml attributes, used only for storage between import and export",
        default={},
        scope=Scope.settings)

    # Extension to append to filename paths
    filename_extension = 'xml'

    # The attributes will be removed from the definition xml passed
    # to definition_from_xml, and from the xml returned by definition_to_xml

    # Note -- url_name isn't in this list because it's handled specially on
    # import and export.

    # TODO (vshnayder): Do we need a list of metadata we actually
    # understand?  And if we do, is this the place?
    # Related: What's the right behavior for clean_metadata?
    metadata_attributes = (
        'format',
        'graceperiod',
        'showanswer',
        'rerandomize',
        'start',
        'due',
        'graded',
        'display_name',
        'url_name',
        'hide_from_toc',
        'ispublic',  # if True, then course is listed for all users; see
        'xqa_key',  # for xqaa server access
        'giturl',  # url of git server for origin of file
        # information about testcenter exams is a dict (of dicts), not a string,
        # so it cannot be easily exportable as a course element's attribute.
        'testcenter_info',
        # VS[compat] Remove once unused.
        'name',
        'slug')

    metadata_to_strip = (
        'data_dir',
        'tabs',
        'grading_policy',
        'published_by',
        'published_date',
        'discussion_blackouts',
        'testcenter_info',
        # VS[compat] -- remove the below attrs once everything is in the CMS
        'course',
        'org',
        'url_name',
        'filename',
        # Used for storing xml attributes between import and export, for roundtrips
        'xml_attributes')

    metadata_to_export_to_policy = ('discussion_topics', 'checklists')

    @classmethod
    def definition_from_xml(cls, xml_object, system):
        """
        Return the definition to be passed to the newly created descriptor
        during from_xml

        xml_object: An etree Element
        """
        raise NotImplementedError("%s does not implement definition_from_xml" %
                                  cls.__name__)

    @classmethod
    def clean_metadata_from_xml(cls, xml_object):
        """
        Remove any attribute named in cls.metadata_attributes from the supplied
        xml_object
        """
        for attr in cls.metadata_attributes:
            if xml_object.get(attr) is not None:
                del xml_object.attrib[attr]

    @classmethod
    def file_to_xml(cls, file_object):
        """
        Used when this module wants to parse a file object to xml
        that will be converted to the definition.

        Returns an lxml Element
        """
        return etree.parse(file_object, parser=edx_xml_parser).getroot()

    @classmethod
    def load_file(cls, filepath, fs, location):
        '''
        Open the specified file in fs, and call cls.file_to_xml on it,
        returning the lxml object.

        Add details and reraise on error.
        '''
        try:
            with fs.open(filepath) as file:
                return cls.file_to_xml(file)
        except Exception as err:
            # Add info about where we are, but keep the traceback
            msg = 'Unable to load file contents at path %s for item %s: %s ' % (
                filepath, location.url(), str(err))
            raise Exception, msg, sys.exc_info()[2]

    @classmethod
    def load_definition(cls, xml_object, system, location):
        '''Load a descriptor definition from the specified xml_object.
        Subclasses should not need to override this except in special
        cases (e.g. html module)'''

        # VS[compat] -- the filename attr should go away once everything is
        # converted.  (note: make sure html files still work once this goes away)
        filename = xml_object.get('filename')
        if filename is None:
            definition_xml = copy.deepcopy(xml_object)
            filepath = ''
        else:
            filepath = cls._format_filepath(xml_object.tag, filename)

            # VS[compat]
            # TODO (cpennington): If the file doesn't exist at the right path,
            # give the class a chance to fix it up. The file will be written out
            # again in the correct format.  This should go away once the CMS is
            # online and has imported all current (fall 2012) courses from xml
            if not system.resources_fs.exists(filepath) and hasattr(
                    cls, 'backcompat_paths'):
                candidates = cls.backcompat_paths(filepath)
                for candidate in candidates:
                    if system.resources_fs.exists(candidate):
                        filepath = candidate
                        break

            definition_xml = cls.load_file(filepath, system.resources_fs,
                                           location)

        definition_metadata = get_metadata_from_xml(definition_xml)
        cls.clean_metadata_from_xml(definition_xml)
        definition, children = cls.definition_from_xml(definition_xml, system)
        if definition_metadata:
            definition['definition_metadata'] = definition_metadata
        definition['filename'] = [filepath, filename]

        return definition, children

    @classmethod
    def load_metadata(cls, xml_object):
        """
        Read the metadata attributes from this xml_object.

        Returns a dictionary {key: value}.
        """
        metadata = {'xml_attributes': {}}
        for attr, val in xml_object.attrib.iteritems():
            # VS[compat].  Remove after all key translations done
            attr = cls._translate(attr)

            if attr in cls.metadata_to_strip:
                # don't load these
                continue

            if attr not in cls.fields:
                metadata['xml_attributes'][attr] = val
            else:
                metadata[attr] = deserialize_field(cls.fields[attr], val)
        return metadata

    @classmethod
    def apply_policy(cls, metadata, policy):
        """
        Add the keys in policy to metadata, after processing them
        through the attrmap.  Updates the metadata dict in place.
        """
        for attr, value in policy.iteritems():
            attr = cls._translate(attr)
            if attr not in cls.fields:
                # Store unknown attributes coming from policy.json
                # in such a way that they will export to xml unchanged
                metadata['xml_attributes'][attr] = value
            else:
                metadata[attr] = value

    @classmethod
    def from_xml(cls, xml_data, system, org=None, course=None):
        """
        Creates an instance of this descriptor from the supplied xml_data.
        This may be overridden by subclasses

        xml_data: A string of xml that will be translated into data and children for
            this module
        system: A DescriptorSystem for interacting with external resources
        org and course are optional strings that will be used in the generated modules
            url identifiers
        """

        xml_object = etree.fromstring(xml_data)
        # VS[compat] -- just have the url_name lookup, once translation is done
        url_name = xml_object.get('url_name', xml_object.get('slug'))
        location = Location('i4x', org, course, xml_object.tag, url_name)

        # VS[compat] -- detect new-style each-in-a-file mode
        if is_pointer_tag(xml_object):
            # new style:
            # read the actual definition file--named using url_name.replace(':','/')
            filepath = cls._format_filepath(xml_object.tag,
                                            name_to_pathname(url_name))
            definition_xml = cls.load_file(filepath, system.resources_fs,
                                           location)
        else:
            definition_xml = xml_object
            filepath = None

        definition, children = cls.load_definition(
            definition_xml, system, location)  # note this removes metadata

        # VS[compat] -- make Ike's github preview links work in both old and
        # new file layouts
        if is_pointer_tag(xml_object):
            # new style -- contents actually at filepath
            definition['filename'] = [filepath, filepath]

        metadata = cls.load_metadata(definition_xml)

        # move definition metadata into dict
        dmdata = definition.get('definition_metadata', '')
        if dmdata:
            metadata['definition_metadata_raw'] = dmdata
            try:
                metadata.update(json.loads(dmdata))
            except Exception as err:
                log.debug('Error %s in loading metadata %s' % (err, dmdata))
                metadata['definition_metadata_err'] = str(err)

        # Set/override any metadata specified by policy
        k = policy_key(location)
        if k in system.policy:
            cls.apply_policy(metadata, system.policy[k])

        field_data = {}
        field_data.update(metadata)
        field_data.update(definition)
        field_data['children'] = children

        field_data['xml_attributes']['filename'] = definition.get(
            'filename', ['', None])  # for git link
        field_data['location'] = location
        field_data['category'] = xml_object.tag
        kvs = InheritanceKeyValueStore(initial_values=field_data)
        field_data = DbModel(kvs)

        return system.construct_xblock_from_class(
            cls,
            # We're loading a descriptor, so student_id is meaningless
            # We also don't have separate notions of definition and usage ids yet,
            # so we use the location for both
            ScopeIds(None, location.category, location, location),
            field_data,
        )

    @classmethod
    def _format_filepath(cls, category, name):
        return u'{category}/{name}.{ext}'.format(category=category,
                                                 name=name,
                                                 ext=cls.filename_extension)

    def export_to_file(self):
        """If this returns True, write the definition of this descriptor to a separate
        file.

        NOTE: Do not override this without a good reason.  It is here
        specifically for customtag...
        """
        return True

    def export_to_xml(self, resource_fs):
        """
        Returns an xml string representing this module, and all modules
        underneath it.  May also write required resources out to resource_fs

        Assumes that modules have single parentage (that no module appears twice
        in the same course), and that it is thus safe to nest modules as xml
        children as appropriate.

        The returned XML should be able to be parsed back into an identical
        XModuleDescriptor using the from_xml method with the same system, org,
        and course

        resource_fs is a pyfilesystem object (from the fs package)
        """

        # Get the definition
        xml_object = self.definition_to_xml(resource_fs)
        self.clean_metadata_from_xml(xml_object)

        # Set the tag so we get the file path right
        xml_object.tag = self.category

        # Add the non-inherited metadata
        for attr in sorted(own_metadata(self)):
            # don't want e.g. data_dir
            if attr not in self.metadata_to_strip and attr not in self.metadata_to_export_to_policy:
                val = serialize_field(self._field_data.get(self, attr))
                try:
                    xml_object.set(attr, val)
                except Exception, e:
                    logging.exception(
                        'Failed to serialize metadata attribute {0} with value {1}. This could mean data loss!!!  Exception: {2}'
                        .format(attr, val, e))
                    pass

        for key, value in self.xml_attributes.items():
            if key not in self.metadata_to_strip:
                xml_object.set(key, value)

        if self.export_to_file():
            # Write the definition to a file
            url_path = name_to_pathname(self.url_name)
            filepath = self._format_filepath(self.category, url_path)
            resource_fs.makedir(os.path.dirname(filepath),
                                recursive=True,
                                allow_recreate=True)
            with resource_fs.open(filepath, 'w') as file:
                file.write(
                    etree.tostring(xml_object,
                                   pretty_print=True,
                                   encoding='utf-8'))

            # And return just a pointer with the category and filename.
            record_object = etree.Element(self.category)
        else:
            record_object = xml_object

        record_object.set('url_name', self.url_name)

        # Special case for course pointers:
        if self.category == 'course':
            # add org and course attributes on the pointer tag
            record_object.set('org', self.location.org)
            record_object.set('course', self.location.course)

        return etree.tostring(record_object,
                              pretty_print=True,
                              encoding='utf-8')
Esempio n. 19
0
class LeafWithDictAndList(XBlock):
    """A leaf containing dict and list options."""
    dictionary = Dict(default={"default": True}, scope=Scope.user_state)
    sequence = List(default=[1, 2, 3], scope=Scope.user_state)
Esempio n. 20
0
class InheritanceMixin(XBlockMixin):
    """Field definitions for inheritable fields."""

    graded = Boolean(
        help="Whether this module contributes to the final course grade",
        scope=Scope.settings,
        default=False,
    )
    start = Date(help="Start time when this module is visible",
                 default=datetime(2030, 1, 1, tzinfo=UTC),
                 scope=Scope.settings)
    due = Date(
        display_name=_("Due Date"),
        help=_("Enter the default date by which problems are due."),
        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,
    )
    lti_enabled = Boolean(
        display_name=_("Course as LTI Tool Provider"),
        help=
        _("Enter true or false. If true, subsections in the course can act as LTI tool providers."
          ),
        default=False,
        scope=Scope.settings,
    )
    visible_to_staff_only = Boolean(
        help=
        _("If true, can be seen only by course staff, regardless of start date."
          ),
        default=False,
        scope=Scope.settings,
    )
    group_access = Dict(
        help=
        "A dictionary that maps which groups can be shown this block. The keys "
        "are group configuration ids and the values are a list of group IDs. "
        "If there is no key for a group configuration or if the list of group IDs "
        "is empty then the block is considered visible to all. Note that this "
        "field is ignored if the block is visible_to_staff_only.",
        default={},
        scope=Scope.settings,
    )
    course_edit_method = String(
        display_name=_("Course Editor"),
        help=
        _("Enter the method by which this course is edited (\"XML\" or \"Studio\")."
          ),
        default="Studio",
        scope=Scope.settings,
        deprecated=
        True  # Deprecated because user would not change away from Studio within Studio.
    )
    giturl = String(
        display_name=_("GIT URL"),
        help=_("Enter the URL for the course data GIT repository."),
        scope=Scope.settings)
    xqa_key = String(display_name=_("XQA Key"),
                     help=_("This setting is not currently supported."),
                     scope=Scope.settings,
                     deprecated=True)
    annotation_storage_url = String(help=_(
        "Enter the location of the annotation storage server. The textannotation, videoannotation, and imageannotation advanced modules require this setting."
    ),
                                    scope=Scope.settings,
                                    default=
                                    "http://your_annotation_storage.com",
                                    display_name=_(
                                        "URL for Annotation Storage"))
    annotation_token_secret = String(help=_(
        "Enter the secret string for annotation storage. The textannotation, videoannotation, and imageannotation advanced modules require this string."
    ),
                                     scope=Scope.settings,
                                     default=
                                     "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
                                     display_name=_(
                                         "Secret Token String for Annotation"))
    graceperiod = Timedelta(
        help=
        "Amount of time after the due date that submissions will be accepted",
        scope=Scope.settings,
    )
    showanswer = String(
        display_name=_("Show Answer"),
        help=
        _("Specify when the Show Answer button appears for each problem. Valid values are \"always\", \"answered\", \"attempted\", \"closed\", \"finished\", \"past_due\", and \"never\"."
          ),
        scope=Scope.settings,
        default="finished",
    )
    rerandomize = String(
        display_name=_("Randomization"),
        help=
        _("Specify how often variable values in a problem are randomized when a student loads the problem. Valid values are \"always\", \"onreset\", \"never\", and \"per_student\". This setting only applies to problems that have randomly generated numeric values."
          ),
        scope=Scope.settings,
        default="never",
    )
    days_early_for_beta = Float(
        display_name=_("Days Early for Beta Users"),
        help=
        _("Enter the number of days before the start date that beta users can access the course."
          ),
        scope=Scope.settings,
        default=None,
    )
    static_asset_path = String(
        display_name=_("Static Asset Path"),
        help=
        _("Enter the path to use for files on the Files & Uploads page. This value overrides the Studio default, c4x://."
          ),
        scope=Scope.settings,
        default='',
    )
    text_customization = Dict(
        display_name=_("Text Customization"),
        help=_(
            "Enter string customization substitutions for particular locations."
        ),
        scope=Scope.settings,
    )
    use_latex_compiler = Boolean(
        display_name=_("Enable LaTeX Compiler"),
        help=
        _("Enter true or false. If true, you can use the LaTeX templates for HTML components and advanced Problem components."
          ),
        default=False,
        scope=Scope.settings)
    max_attempts = Integer(
        display_name=_("Maximum Attempts"),
        help=
        _("Enter the maximum number of times a student can try to answer problems. By default, Maximum Attempts is set to null, meaning that students have an unlimited number of attempts for problems. You can override this course-wide setting for individual problems. However, if the course-wide setting is a specific number, you cannot set the Maximum Attempts for individual problems to unlimited."
          ),
        values={"min": 0},
        scope=Scope.settings)
    matlab_api_key = String(
        display_name=_("Matlab API key"),
        help=
        _("Enter the API key provided by MathWorks for accessing the MATLAB Hosted Service. "
          "This key is granted for exclusive use in this course for the specified duration. "
          "Do not share the API key with other courses. Notify MathWorks immediately "
          "if you believe the key is exposed or compromised. To obtain a key for your course, "
          "or to report an issue, please contact [email protected]"),
        scope=Scope.settings)
    # This is should be scoped to content, but since it's defined in the policy
    # file, it is currently scoped to settings.
    user_partitions = UserPartitionList(
        display_name=_("Group Configurations"),
        help=
        _("Enter the configurations that govern how students are grouped together."
          ),
        default=[],
        scope=Scope.settings)
    video_speed_optimizations = Boolean(
        display_name=_("Enable video caching system"),
        help=
        _("Enter true or false. If true, video caching will be used for HTML5 videos."
          ),
        default=True,
        scope=Scope.settings)

    reset_key = "DEFAULT_SHOW_RESET_BUTTON"
    default_reset_button = getattr(settings, reset_key) if hasattr(
        settings, reset_key) else False
    show_reset_button = Boolean(
        display_name=_("Show Reset Button for Problems"),
        help=
        _("Enter true or false. If true, problems in the course default to always displaying a 'Reset' button. You can "
          "override this in each problem's settings. All existing problems are affected when this course-wide setting is changed."
          ),
        scope=Scope.settings,
        default=default_reset_button)
Esempio n. 21
0
class StructuredTagsAside(XBlockAside):
    """
    Aside that allows tagging blocks
    """
    saved_tags = Dict(help=_("Dictionary with the available tags"),
                      scope=Scope.content,
                      default={},)

    def get_available_tags(self):
        """
        Return available tags
        """
        # Import is placed here to avoid model import at project startup.
        from .models import TagCategories
        return TagCategories.objects.all()

    def _get_studio_resource_url(self, relative_url):
        """
        Returns the Studio URL to a static resource.
        """
        return settings.STATIC_URL + relative_url

    @XBlockAside.aside_for(AUTHOR_VIEW)
    def student_view_aside(self, block, context):  # pylint: disable=unused-argument
        """
        Display the tag selector with specific categories and allowed values,
        depending on the context.
        """
        if isinstance(block, ProblemBlock):
            tags = []
            for tag in self.get_available_tags():
                tag_available_values = tag.get_values()
                tag_current_values = self.saved_tags.get(tag.name, [])

                if isinstance(tag_current_values, six.string_types):
                    tag_current_values = [tag_current_values]

                tag_values_not_exists = [cur_val for cur_val in tag_current_values
                                         if cur_val not in tag_available_values]

                tag_values_available_to_choose = tag_available_values + tag_values_not_exists
                tag_values_available_to_choose.sort()

                tags.append({
                    'key': tag.name,
                    'title': tag.title,
                    'values': tag_values_available_to_choose,
                    'current_values': tag_current_values,
                })
            fragment = Fragment(render_to_string('structured_tags_block.html', {'tags': tags,
                                                                                'tags_count': len(tags),
                                                                                'block_location': block.location}))
            fragment.add_javascript_url(self._get_studio_resource_url('/js/xblock_asides/structured_tags.js'))
            fragment.initialize_js('StructuredTagsInit')
            return fragment
        else:
            return Fragment(u'')

    @XBlock.handler
    def save_tags(self, request=None, suffix=None):
        """
        Handler to save choosen tags with connected XBlock
        """
        try:
            posted_data = request.json
        except ValueError:
            return Response("Invalid request body", status=400)

        saved_tags = {}
        need_update = False

        for av_tag in self.get_available_tags():
            if av_tag.name in posted_data and posted_data[av_tag.name]:
                tag_available_values = av_tag.get_values()
                tag_current_values = self.saved_tags.get(av_tag.name, [])

                if isinstance(tag_current_values, six.string_types):
                    tag_current_values = [tag_current_values]

                for posted_tag_value in posted_data[av_tag.name]:
                    if posted_tag_value not in tag_available_values and posted_tag_value not in tag_current_values:
                        return Response(u"Invalid tag value was passed: %s" % posted_tag_value, status=400)

                saved_tags[av_tag.name] = posted_data[av_tag.name]
                need_update = True
            if av_tag.name in posted_data:
                need_update = True

        if need_update:
            self.saved_tags = saved_tags
            return Response()
        else:
            return Response("Tags parameters were not passed", status=400)

    def get_event_context(self, event_type, event):  # pylint: disable=unused-argument
        """
        This method return data that should be associated with the "check_problem" event
        """
        if self.saved_tags and event_type == "problem_check":
            return {'saved_tags': self.saved_tags}
        else:
            return None
Esempio n. 22
0
class FileSubmissionMixin(XBlockMixin):
    """
	Mixin for handling file submissions.
	"""
    uploaded_files = Dict(
        display_name="Uploaded Files",
        scope=Scope.user_state,
        default=dict(),
        help=
        "Files uploaded by the user. Tuple of filename, mimetype and timestamp"
    )

    @XBlock.handler
    def student_upload_file(self, request, suffix=''):
        """Allows a student to upload a file for submission.

		Keyword arguments:
		request: holds the file to be added to the submission.
		suffix:  not used.
		"""

        key, uploaded = self.upload_file(self.uploaded_files,
                                         request.params['uploadedFile'])

        return Response(
            json_body={
                "sha1": key,
                "filename": uploaded.filename,
                "timestamp": uploaded.timestamp
            })

    @XBlock.handler
    def student_download_file(self, request, suffix=''):
        """Returns a temporary download link for a file.

		Keyword arguments:
		request: not used
		suffix:  the hash of the file.
		"""
        return self.download_file(self.uploaded_files, suffix)

    @XBlock.handler
    def staff_download_file(self, request, suffix=''):
        """Returns a temporary download link for a file.

		Keyword arguments:
		request: holds the module_id for a student module.
		suffix:  the hash of the file.
		"""
        self.validate_staff_request(request)

        return self.download_file(
            self.uploaded_file_list(request.params['module_id']), suffix)

    @XBlock.handler
    def staff_download_zipped(self, request, suffix=''):
        """Returns all uploaded files in a zip file.

		Keyword arguments:
		request: holds the module_id for a student module.
		suffix:  not used.
		"""
        self.validate_staff_request(request)

        module_id = request.params['module_id']
        module = self.get_module(module_id)
        return self.download_zipped(
            self.uploaded_file_list(module_id),
            self.display_name + "-" + module.student.username)

    @XBlock.handler
    def student_download_zipped(self, request, suffix=''):
        """Returns all uploaded files in a zip file.

		Keyword arguments:
		request: not used.
		suffix:  not used.
		"""
        return self.download_zipped(self.uploaded_files,
                                    self.display_name + "assignment")

    @XBlock.handler
    def student_delete_file(self, request, suffix=''):
        """Removes an uploaded file from the assignemnt

		Keyword arguments:
		request: not used.
		suffix:  holds the key hash of the file to be deleted.
		"""
        assert self.upload_allowed()
        self.delete_file(self.uploaded_files, suffix)
        return Response(status=204)

    @XBlock.handler
    def staff_delete_file(self, request, suffix=''):
        """Removes an uploaded file from the assignemnt

		Keyword arguments:
		request: holds module_id.
		suffix:  holds the key hash of the file to be deleted.
		"""
        self.validate_staff_request(request)

        module_id = request.params['module_id']
        uploaded = self.get_student_state(module_id).get('uploaded_files')

        newFilelist = self.delete_file(uploaded, suffix)
        self.set_student_state(module_id, uploaded_files=newFilelist)

        return Response(status=204)

    def uploaded_file_list(self, module_id):
        """Returns a list of files uploaded by a student.
		
		Keyword arguments:
		module_id: A student module id.
		"""
        assert self.is_course_staff()
        return self.get_student_state(module_id)['uploaded_files']
class DragAndDropBlock(ScorableXBlockMixin, XBlock, XBlockWithSettingsMixin,
                       ThemableXBlockMixin):
    """
    XBlock that implements a friendly Drag-and-Drop problem
    """

    CATEGORY = "drag-and-drop-v2"

    SOLUTION_CORRECT = "correct"
    SOLUTION_PARTIAL = "partial"
    SOLUTION_INCORRECT = "incorrect"

    GRADE_FEEDBACK_CLASSES = {
        SOLUTION_CORRECT: FeedbackMessages.MessageClasses.CORRECT_SOLUTION,
        SOLUTION_PARTIAL: FeedbackMessages.MessageClasses.PARTIAL_SOLUTION,
        SOLUTION_INCORRECT: FeedbackMessages.MessageClasses.INCORRECT_SOLUTION,
    }

    PROBLEM_FEEDBACK_CLASSES = {
        SOLUTION_CORRECT: FeedbackMessages.MessageClasses.CORRECT_SOLUTION,
        SOLUTION_PARTIAL: None,
        SOLUTION_INCORRECT: None
    }

    display_name = String(
        display_name=_("Title"),
        help=
        _("The title of the drag and drop problem. The title is displayed to learners."
          ),
        scope=Scope.settings,
        default=_("Drag and Drop"),
        enforce_type=True,
    )

    mode = String(
        display_name=_("Mode"),
        help=_(
            "Standard mode: the problem provides immediate feedback each time "
            "a learner drops an item on a target zone. "
            "Assessment mode: the problem provides feedback only after "
            "a learner drops all available items on target zones."),
        scope=Scope.settings,
        values=[
            {
                "display_name": _("Standard"),
                "value": Constants.STANDARD_MODE
            },
            {
                "display_name": _("Assessment"),
                "value": Constants.ASSESSMENT_MODE
            },
        ],
        default=Constants.STANDARD_MODE,
        enforce_type=True,
    )

    max_attempts = Integer(
        display_name=_("Maximum attempts"),
        help=_(
            "Defines the number of times a student can try to answer this problem. "
            "If the value is not set, infinite attempts are allowed."),
        scope=Scope.settings,
        default=None,
        enforce_type=True,
    )

    show_title = Boolean(
        display_name=_("Show title"),
        help=_("Display the title to the learner?"),
        scope=Scope.settings,
        default=True,
        enforce_type=True,
    )

    question_text = String(
        display_name=_("Problem text"),
        help=
        _("The description of the problem or instructions shown to the learner."
          ),
        scope=Scope.settings,
        default="",
        enforce_type=True,
    )

    show_question_header = Boolean(
        display_name=_('Show "Problem" heading'),
        help=_('Display the heading "Problem" above the problem text?'),
        scope=Scope.settings,
        default=True,
        enforce_type=True,
    )

    weight = Float(
        display_name=_("Problem Weight"),
        help=_("Defines the number of points the problem is worth."),
        scope=Scope.settings,
        default=1,
        enforce_type=True,
    )

    item_background_color = String(
        display_name=_("Item background color"),
        help=
        _("The background color of draggable items in the problem (example: 'blue' or '#0000ff')."
          ),
        scope=Scope.settings,
        default="",
        enforce_type=True,
    )

    item_text_color = String(
        display_name=_("Item text color"),
        help=
        _("Text color to use for draggable items (example: 'white' or '#ffffff')."
          ),
        scope=Scope.settings,
        default="",
        enforce_type=True,
    )

    max_items_per_zone = Integer(
        display_name=_("Maximum items per zone"),
        help=
        _("This setting limits the number of items that can be dropped into a single zone."
          ),
        scope=Scope.settings,
        default=None,
        enforce_type=True,
    )

    data = Dict(
        display_name=_("Problem data"),
        help=
        _("Information about zones, items, feedback, and background image for this problem. "
          "This information is derived from the input that a course author provides via the interactive editor "
          "when configuring the problem."),
        scope=Scope.content,
        default=DEFAULT_DATA,
        enforce_type=True,
    )

    item_state = Dict(
        help=
        _("Information about current positions of items that a learner has dropped on the target image."
          ),
        scope=Scope.user_state,
        default={},
        enforce_type=True,
    )

    attempts = Integer(
        help=_("Number of attempts learner used"),
        scope=Scope.user_state,
        default=0,
        enforce_type=True,
    )

    completed = Boolean(
        help=
        _("Indicates whether a learner has completed the problem at least once"
          ),
        scope=Scope.user_state,
        default=False,
        enforce_type=True,
    )

    grade = Float(help=_(
        "DEPRECATED. Keeps maximum score achieved by student as a weighted value."
    ),
                  scope=Scope.user_state,
                  default=0)

    raw_earned = Float(
        help=
        _("Keeps maximum score achieved by student as a raw value between 0 and 1."
          ),
        scope=Scope.user_state,
        default=0,
        enforce_type=True,
    )

    block_settings_key = 'drag-and-drop-v2'

    def max_score(self):  # pylint: disable=no-self-use
        """
        Return the problem's max score, which for DnDv2 always equals 1.
        Required by the grading system in the LMS.
        """
        return 1

    def get_score(self):
        """
        Return the problem's current score as raw values.
        """
        if self._get_raw_earned_if_set() is None:
            self.raw_earned = self._learner_raw_score()
        return Score(self.raw_earned, self.max_score())

    def set_score(self, score):
        """
        Sets the score on this block.
        Takes a Score namedtuple containing a raw
        score and possible max (for this block, we expect that this will
        always be 1).
        """
        assert score.raw_possible == self.max_score()
        self.raw_earned = score.raw_earned

    def calculate_score(self):
        """
        Returns a newly-calculated raw score on the problem for the learner
        based on the learner's current state.
        """
        return Score(self._learner_raw_score(), self.max_score())

    def has_submitted_answer(self):
        """
        Returns True if the user has made a submission.
        """
        return self.fields['raw_earned'].is_set_on(
            self) or self.fields['grade'].is_set_on(self)

    def weighted_grade(self):
        """
        Returns the block's current saved grade multiplied by the block's
        weight- the number of points earned by the learner.
        """
        return self.raw_earned * self.weight

    def _learner_raw_score(self):
        """
        Calculate raw score for learner submission.

        As it is calculated as ratio of correctly placed (or left in bank in case of decoys) items to
        total number of items, it lays in interval [0..1]
        """
        correct_count, total_count = self._get_item_stats()
        return correct_count / float(total_count)

    @staticmethod
    def _get_statici18n_js_url():
        """
        Returns the Javascript translation file for the currently selected language, if any found by
        `pkg_resources`
        """
        lang_code = translation.get_language()
        if not lang_code:
            return None
        text_js = 'public/js/translations/{lang_code}/text.js'
        country_code = lang_code.split('-')[0]
        for code in (lang_code, country_code):
            if pkg_resources.resource_exists(loader.module_name,
                                             text_js.format(lang_code=code)):
                return text_js.format(lang_code=code)
        return None

    @XBlock.supports(
        "multi_device"
    )  # Enable this block for use in the mobile app via webview
    def student_view(self, context):
        """
        Player view, displayed to the student
        """

        fragment = Fragment()
        fragment.add_content(
            loader.render_django_template('/templates/html/drag_and_drop.html',
                                          i18n_service=self.i18n_service))
        css_urls = ('public/css/drag_and_drop.css', )
        js_urls = [
            'public/js/vendor/virtual-dom-1.3.0.min.js',
            'public/js/drag_and_drop.js',
        ]

        statici18n_js_url = self._get_statici18n_js_url()
        if statici18n_js_url:
            js_urls.append(statici18n_js_url)

        for css_url in css_urls:
            fragment.add_css_url(self.runtime.local_resource_url(
                self, css_url))
        for js_url in js_urls:
            fragment.add_javascript_url(
                self.runtime.local_resource_url(self, js_url))

        self.include_theme_files(fragment)

        fragment.initialize_js('DragAndDropBlock', self.student_view_data())

        return fragment

    def student_view_data(self, context=None):
        """
        Get the configuration data for the student_view.
        The configuration is all the settings defined by the author, except for correct answers
        and feedback.
        """
        def items_without_answers():
            """
            Removes feedback and answer from items
            """
            items = copy.deepcopy(self.data.get('items', ''))
            for item in items:
                del item['feedback']
                # Use item.pop to remove both `item['zone']` and `item['zones']`; we don't have
                # a guarantee that either will be present, so we can't use `del`. Legacy instances
                # will have `item['zone']`, while current versions will have `item['zones']`.
                item.pop('zone', None)
                item.pop('zones', None)
                # Fall back on "backgroundImage" to be backward-compatible.
                image_url = item.get('imageURL') or item.get('backgroundImage')
                if image_url:
                    item['expandedImageURL'] = self._expand_static_url(
                        image_url)
                else:
                    item['expandedImageURL'] = ''
            return items

        return {
            "block_id": six.text_type(self.scope_ids.usage_id),
            "display_name": self.display_name,
            "type": self.CATEGORY,
            "weight": self.weight,
            "mode": self.mode,
            "zones": self.zones,
            "max_attempts": self.max_attempts,
            "graded": getattr(self, 'graded', False),
            "weighted_max_score": self.max_score() * self.weight,
            "max_items_per_zone": self.max_items_per_zone,
            # SDK doesn't supply url_name.
            "url_name": getattr(self, 'url_name', ''),
            "display_zone_labels": self.data.get('displayLabels', False),
            "display_zone_borders": self.data.get('displayBorders', False),
            "items": items_without_answers(),
            "title": self.display_name,
            "show_title": self.show_title,
            "problem_text": self.question_text,
            "show_problem_header": self.show_question_header,
            "target_img_expanded_url": self.target_img_expanded_url,
            "target_img_description": self.target_img_description,
            "item_background_color": self.item_background_color or None,
            "item_text_color": self.item_text_color or None,
            "has_deadline_passed": self.has_submission_deadline_passed,
            # final feedback (data.feedback.finish) is not included - it may give away answers.
        }

    def studio_view(self, context):
        """
        Editing view in Studio
        """
        js_templates = loader.load_unicode('/templates/html/js_templates.html')
        # Get an 'id_suffix' string that is unique for this block.
        # We append it to HTML element ID attributes to ensure multiple instances of the DnDv2 block
        # on the same page don't share the same ID value.
        # We avoid using ID attributes in preference to classes, but sometimes we still need IDs to
        # connect 'for' and 'aria-describedby' attributes to the associated elements.
        id_suffix = self._get_block_id()
        js_templates = js_templates.replace('{{id_suffix}}', id_suffix)
        context = {
            'js_templates': js_templates,
            'id_suffix': id_suffix,
            'fields': self.fields,
            'self': self,
            'data': six.moves.urllib.parse.quote(json.dumps(self.data)),
        }

        fragment = Fragment()
        fragment.add_content(
            loader.render_django_template(
                '/templates/html/drag_and_drop_edit.html',
                context=context,
                i18n_service=self.i18n_service))
        css_urls = ('public/css/drag_and_drop_edit.css', )
        js_urls = [
            'public/js/vendor/handlebars-v1.1.2.js',
            'public/js/drag_and_drop_edit.js',
        ]

        statici18n_js_url = self._get_statici18n_js_url()
        if statici18n_js_url:
            js_urls.append(statici18n_js_url)

        for css_url in css_urls:
            fragment.add_css_url(self.runtime.local_resource_url(
                self, css_url))
        for js_url in js_urls:
            fragment.add_javascript_url(
                self.runtime.local_resource_url(self, js_url))

        # Do a bit of manipulation so we get the appearance of a list of zone options on
        # items that still have just a single zone stored

        items = self.data.get('items', [])

        for item in items:
            zones = self.get_item_zones(item['id'])
            # Note that we appear to be mutating the state of the XBlock here, but because
            # the change won't be committed, we're actually just affecting the data that
            # we're going to send to the client, not what's saved in the backing store.
            item['zones'] = zones
            item.pop('zone', None)

        fragment.initialize_js(
            'DragAndDropEditBlock', {
                'data': self.data,
                'target_img_expanded_url': self.target_img_expanded_url,
                'default_background_image_url':
                self.default_background_image_url,
            })

        return fragment

    @XBlock.json_handler
    def studio_submit(self, submissions, suffix=''):
        """
        Handles studio save.
        """
        self.display_name = submissions['display_name']
        self.mode = submissions['mode']
        self.max_attempts = submissions['max_attempts']
        self.show_title = submissions['show_title']
        self.question_text = submissions['problem_text']
        self.show_question_header = submissions['show_problem_header']
        self.weight = float(submissions['weight'])
        self.item_background_color = submissions['item_background_color']
        self.item_text_color = submissions['item_text_color']
        self.max_items_per_zone = self._get_max_items_per_zone(submissions)
        self.data = submissions['data']

        return {
            'result': 'success',
        }

    def _get_block_id(self):
        """
        Return unique ID of this block. Useful for HTML ID attributes.
        Works both in LMS/Studio and workbench runtimes:
        - In LMS/Studio, use the location.html_id method.
        - In the workbench, use the usage_id.
        """
        if hasattr(self, 'location'):
            return self.location.html_id()  # pylint: disable=no-member
        else:
            return six.text_type(self.scope_ids.usage_id)

    @staticmethod
    def _get_max_items_per_zone(submissions):
        """
        Parses Max items per zone value coming from editor.

        Returns:
            * None if invalid value is passed (i.e. not an integer)
            * None if value is parsed into zero or negative integer
            * Positive integer otherwise.

        Examples:
            * _get_max_items_per_zone(None) -> None
            * _get_max_items_per_zone('string') -> None
            * _get_max_items_per_zone('-1') -> None
            * _get_max_items_per_zone(-1) -> None
            * _get_max_items_per_zone('0') -> None
            * _get_max_items_per_zone('') -> None
            * _get_max_items_per_zone('42') -> 42
            * _get_max_items_per_zone(42) -> 42
        """
        raw_max_items_per_zone = submissions.get('max_items_per_zone', None)

        # Entries that aren't numbers should be treated as null. We assume that if we can
        # turn it into an int, a number was submitted.
        try:
            max_attempts = int(raw_max_items_per_zone)
            if max_attempts > 0:
                return max_attempts
            else:
                return None
        except (ValueError, TypeError):
            return None

    @XBlock.json_handler
    def drop_item(self, item_attempt, suffix=''):
        """
        Handles dropping item into a zone.
        """
        self._validate_drop_item(item_attempt)

        if self.mode == Constants.ASSESSMENT_MODE:
            return self._drop_item_assessment(item_attempt)
        elif self.mode == Constants.STANDARD_MODE:
            return self._drop_item_standard(item_attempt)
        else:
            raise JsonHandlerError(
                500,
                self.i18n_service.gettext(
                    "Unknown DnDv2 mode {mode} - course is misconfigured").
                format(self.mode))

    @XBlock.json_handler
    def do_attempt(self, data, suffix=''):
        """
        Checks submitted solution and returns feedback.

        Raises:
             * JsonHandlerError with 400 error code in standard mode.
             * JsonHandlerError with 409 error code if no more attempts left
        """
        self._validate_do_attempt()

        self.attempts += 1
        # pylint: disable=fixme
        # TODO: Refactor this method to "freeze" item_state and pass it to methods that need access to it.
        # These implicit dependencies between methods exist because most of them use `item_state` or other
        # fields, either as an "input" (i.e. read value) or as output (i.e. set value) or both. As a result,
        # incorrect order of invocation causes issues:
        self._mark_complete_and_publish_grade(
        )  # must happen before _get_feedback - sets grade
        correct = self._is_answer_correct(
        )  # must happen before manipulating item_state - reads item_state

        overall_feedback_msgs, misplaced_ids = self._get_feedback(
            include_item_feedback=True)

        misplaced_items = []
        for item_id in misplaced_ids:
            # Don't delete misplaced item states on the final attempt.
            if self.attempts_remain:
                del self.item_state[item_id]
            misplaced_items.append(self._get_item_definition(int(item_id)))

        feedback_msgs = [
            FeedbackMessage(item['feedback']['incorrect'], None)
            for item in misplaced_items
        ]
        return {
            'correct': correct,
            'attempts': self.attempts,
            'grade': self._get_weighted_earned_if_set(),
            'misplaced_items': list(misplaced_ids),
            'feedback': self._present_feedback(feedback_msgs),
            'overall_feedback': self._present_feedback(overall_feedback_msgs)
        }

    @XBlock.json_handler
    def publish_event(self, data, suffix=''):
        """
        Handler to publish XBlock event from frontend
        """
        try:
            event_type = data.pop('event_type')
        except KeyError:
            return {
                'result': 'error',
                'message': 'Missing event_type in JSON data'
            }

        self.runtime.publish(self, event_type, data)
        return {'result': 'success'}

    @XBlock.json_handler
    def reset(self, data, suffix=''):
        """
        Resets problem to initial state
        """
        self.item_state = {}
        return self._get_user_state()

    @XBlock.json_handler
    def show_answer(self, data, suffix=''):
        """
        Returns correct answer in assessment mode.

        Raises:
             * JsonHandlerError with 400 error code in standard mode.
             * JsonHandlerError with 409 error code if there are still attempts left
        """
        if self.mode != Constants.ASSESSMENT_MODE:
            raise JsonHandlerError(
                400,
                self.i18n_service.gettext(
                    "show_answer handler should only be called for assessment mode"
                ))
        if self.attempts_remain:
            raise JsonHandlerError(
                409, self.i18n_service.gettext("There are attempts remaining"))

        return self._get_correct_state()

    @XBlock.json_handler
    def expand_static_url(self, url, suffix=''):
        """ AJAX-accessible handler for expanding URLs to static [image] files """
        return {'url': self._expand_static_url(url)}

    @property
    def i18n_service(self):
        """ Obtains translation service """
        i18n_service = self.runtime.service(self, "i18n")
        if i18n_service:
            return i18n_service
        else:
            return DummyTranslationService()

    @property
    def target_img_expanded_url(self):
        """ Get the expanded URL to the target image (the image items are dragged onto). """
        if self.data.get("targetImg"):
            return self._expand_static_url(self.data["targetImg"])
        else:
            return self.default_background_image_url

    @property
    def target_img_description(self):
        """ Get the description for the target image (the image items are dragged onto). """
        return self.data.get("targetImgDescription", "")

    @property
    def default_background_image_url(self):
        """ The URL to the default background image, shown when no custom background is used """
        return self.runtime.local_resource_url(self, "public/img/triangle.png")

    @property
    def attempts_remain(self):
        """
        Checks if current student still have more attempts.
        """
        return self.max_attempts is None or self.max_attempts == 0 or self.attempts < self.max_attempts

    @property
    def has_submission_deadline_passed(self):
        """
        Returns a boolean indicating if the submission is past its deadline.

        Using the `has_deadline_passed` method from InheritanceMixin which gets
        added on the LMS/Studio, return if the submission is past its due date.
        If the method not found, which happens for pure DragAndDropXblock,
        return False which makes sure submission checks don't affect other
        functionality.
        """
        if hasattr(self, "has_deadline_passed"):
            return self.has_deadline_passed()  # pylint: disable=no-member
        else:
            return False

    @XBlock.handler
    def student_view_user_state(self, request, suffix=''):
        """ GET all user-specific data, and any applicable feedback """
        data = self._get_user_state()
        return webob.Response(body=json.dumps(data).encode('utf-8'),
                              content_type='application/json')

    def _validate_do_attempt(self):
        """
        Validates if `do_attempt` handler should be executed
        """
        if self.mode != Constants.ASSESSMENT_MODE:
            raise JsonHandlerError(
                400,
                self.i18n_service.gettext(
                    "do_attempt handler should only be called for assessment mode"
                ))
        if not self.attempts_remain:
            raise JsonHandlerError(
                409,
                self.i18n_service.gettext("Max number of attempts reached"))
        if self.has_submission_deadline_passed:
            raise JsonHandlerError(
                409,
                self.i18n_service.gettext("Submission deadline has passed."))

    def _get_feedback(self, include_item_feedback=False):
        """
        Builds overall feedback for both standard and assessment modes
        """
        answer_correctness = self._answer_correctness()
        is_correct = answer_correctness == self.SOLUTION_CORRECT

        if self.mode == Constants.STANDARD_MODE or not self.attempts:
            feedback_key = 'finish' if is_correct else 'start'
            return [
                FeedbackMessage(self.data['feedback'][feedback_key], None)
            ], set()

        items = self._get_item_raw_stats()
        missing_ids = items.required - items.placed
        misplaced_ids = items.placed - items.correctly_placed

        feedback_msgs = []

        def _add_msg_if_exists(ids_list, message_template, message_class):
            """ Adds message to feedback messages if corresponding items list is not empty """
            if ids_list:
                message = message_template(len(ids_list),
                                           self.i18n_service.ngettext)
                feedback_msgs.append(FeedbackMessage(message, message_class))

        if self.item_state or include_item_feedback:
            _add_msg_if_exists(
                items.correctly_placed, FeedbackMessages.correctly_placed,
                FeedbackMessages.MessageClasses.CORRECTLY_PLACED)

            # Misplaced items are not returned to the bank on the final attempt.
            if self.attempts_remain:
                misplaced_template = FeedbackMessages.misplaced_returned
            else:
                misplaced_template = FeedbackMessages.misplaced

            _add_msg_if_exists(misplaced_ids, misplaced_template,
                               FeedbackMessages.MessageClasses.MISPLACED)
            _add_msg_if_exists(missing_ids, FeedbackMessages.not_placed,
                               FeedbackMessages.MessageClasses.NOT_PLACED)

        if self.attempts_remain and (misplaced_ids or missing_ids):
            problem_feedback_message = self.data['feedback']['start']
        else:
            problem_feedback_message = self.data['feedback']['finish']

        problem_feedback_class = self.PROBLEM_FEEDBACK_CLASSES.get(
            answer_correctness, None)
        grade_feedback_class = self.GRADE_FEEDBACK_CLASSES.get(
            answer_correctness, None)

        feedback_msgs.append(
            FeedbackMessage(problem_feedback_message, problem_feedback_class))

        if self.weight > 0:
            if self.attempts_remain:
                grade_feedback_template = FeedbackMessages.GRADE_FEEDBACK_TPL
            else:
                grade_feedback_template = FeedbackMessages.FINAL_ATTEMPT_TPL

            feedback_msgs.append(
                FeedbackMessage(
                    self.i18n_service.gettext(grade_feedback_template).format(
                        score=self.weighted_grade()), grade_feedback_class))

        return feedback_msgs, misplaced_ids

    @staticmethod
    def _present_feedback(feedback_messages):
        """
        Transforms feedback messages into format expected by frontend code
        """
        return [{
            "message": msg.message,
            "message_class": msg.message_class
        } for msg in feedback_messages if msg.message]

    def _drop_item_standard(self, item_attempt):
        """
        Handles dropping item to a zone in standard mode.
        """
        item = self._get_item_definition(item_attempt['val'])

        is_correct = self._is_attempt_correct(
            item_attempt)  # Student placed item in a correct zone
        if is_correct:  # In standard mode state is only updated when attempt is correct
            self.item_state[str(item['id'])] = self._make_state_from_attempt(
                item_attempt, is_correct)

        self._mark_complete_and_publish_grade(
        )  # must happen before _get_feedback
        self._publish_item_dropped_event(item_attempt, is_correct)

        item_feedback_key = 'correct' if is_correct else 'incorrect'
        item_feedback = FeedbackMessage(
            self._expand_static_url(item['feedback'][item_feedback_key]), None)
        overall_feedback, __ = self._get_feedback()

        return {
            'correct': is_correct,
            'grade': self._get_weighted_earned_if_set(),
            'finished': self._is_answer_correct(),
            'overall_feedback': self._present_feedback(overall_feedback),
            'feedback': self._present_feedback([item_feedback])
        }

    def _drop_item_assessment(self, item_attempt):
        """
        Handles dropping item into a zone in assessment mode
        """
        if not self.attempts_remain:
            raise JsonHandlerError(
                409,
                self.i18n_service.gettext("Max number of attempts reached"))

        item = self._get_item_definition(item_attempt['val'])
        is_correct = self._is_attempt_correct(item_attempt)
        if item_attempt['zone'] is None:
            self.item_state.pop(str(item['id']), None)
            self._publish_item_to_bank_event(item['id'], is_correct)
        else:
            # State is always updated in assessment mode to store intermediate item positions
            self.item_state[str(item['id'])] = self._make_state_from_attempt(
                item_attempt, is_correct)
            self._publish_item_dropped_event(item_attempt, is_correct)

        return {}

    def _validate_drop_item(self, item):
        """
        Validates `drop_item` parameters. Assessment mode allows returning
        items to the bank, so validation is unnecessary.
        """
        if self.mode != Constants.ASSESSMENT_MODE:
            zone = self._get_zone_by_uid(item['zone'])
            if not zone:
                raise JsonHandlerError(400, "Item zone data invalid.")

    @staticmethod
    def _make_state_from_attempt(attempt, correct):
        """
        Converts "attempt" data coming from browser into "state" entry stored in item_state
        """
        return {'zone': attempt['zone'], 'correct': correct}

    def _mark_complete_and_publish_grade(self):
        """
        Helper method to update `self.completed` and submit grade event if appropriate conditions met.
        """
        # pylint: disable=fixme
        # TODO: (arguable) split this method into "clean" functions (with no side effects and implicit state)
        # This method implicitly depends on self.item_state (via _is_answer_correct and _learner_raw_score)
        # and also updates self.raw_earned if some conditions are met. As a result this method implies some order of
        # invocation:
        # * it should be called after learner-caused updates to self.item_state is applied
        # * it should be called before self.item_state cleanup is applied (i.e. returning misplaced items to item bank)
        # * it should be called before any method that depends on self.raw_earned (i.e. self._get_feedback)

        # Splitting it into a "clean" functions will allow to capture this implicit invocation order in caller method
        # and help avoid bugs caused by invocation order violation in future.

        # There's no going back from "completed" status to "incomplete"
        self.completed = self.completed or self._is_answer_correct(
        ) or not self.attempts_remain
        current_raw_earned = self._learner_raw_score()
        # ... and from higher grade to lower
        # if we have an old-style (i.e. unreliable) grade, override no matter what
        saved_raw_earned = self._get_raw_earned_if_set()

        current_raw_earned_is_greater = False
        if current_raw_earned is None or saved_raw_earned is None:
            current_raw_earned_is_greater = True

        if current_raw_earned is not None and saved_raw_earned is not None and current_raw_earned > saved_raw_earned:
            current_raw_earned_is_greater = True

        if current_raw_earned is None or current_raw_earned_is_greater:
            self.raw_earned = current_raw_earned
            self._publish_grade(Score(self.raw_earned, self.max_score()))

        # and no matter what - emit progress event for current user
        self.runtime.publish(self, "progress", {})

    def _publish_item_dropped_event(self, attempt, is_correct):
        """
        Publishes item dropped event.
        """
        item = self._get_item_definition(attempt['val'])
        # attempt should already be validated here - not doing the check for existing zone again
        zone = self._get_zone_by_uid(attempt['zone'])

        item_label = item.get("displayName")
        if not item_label:
            item_label = item.get("imageURL")

        self.runtime.publish(
            self, 'edx.drag_and_drop_v2.item.dropped', {
                'item': item_label,
                'item_id': item['id'],
                'location': zone.get("title"),
                'location_id': zone.get("uid"),
                'is_correct': is_correct,
            })

    def _publish_item_to_bank_event(self, item_id, is_correct):
        """
        Publishes event when item moved back to the bank in assessment mode.
        """
        item = self._get_item_definition(item_id)

        item_label = item.get("displayName")
        if not item_label:
            item_label = item.get("imageURL")

        self.runtime.publish(
            self, 'edx.drag_and_drop_v2.item.dropped', {
                'item': item_label,
                'item_id': item['id'],
                'location': 'item bank',
                'location_id': -1,
                'is_correct': is_correct,
            })

    def _is_attempt_correct(self, attempt):
        """
        Check if the item was placed correctly.
        """
        correct_zones = self.get_item_zones(attempt['val'])
        if correct_zones == [] and attempt[
                'zone'] is None and self.mode == Constants.ASSESSMENT_MODE:
            return True
        return attempt['zone'] in correct_zones

    def _expand_static_url(self, url):
        """
        This is required to make URLs like '/static/dnd-test-image.png' work (note: that is the
        only portable URL format for static files that works across export/import and reruns).
        This method is unfortunately a bit hackish since XBlock does not provide a low-level API
        for this.
        """
        if hasattr(self.runtime, 'replace_urls'):
            url = self.runtime.replace_urls(u'"{}"'.format(url))[1:-1]
        elif hasattr(self.runtime, 'course_id'):
            # edX Studio uses a different runtime for 'studio_view' than 'student_view',
            # and the 'studio_view' runtime doesn't provide the replace_urls API.
            try:
                from static_replace import replace_static_urls  # pylint: disable=import-error
                url = replace_static_urls(
                    u'"{}"'.format(url),
                    None,
                    course_id=self.runtime.course_id)[1:-1]
            except ImportError:
                pass
        return url

    def _get_user_state(self):
        """ Get all user-specific data, and any applicable feedback """
        item_state = self._get_item_state()
        # In assessment mode, we do not want to leak the correctness info for individual items to the frontend,
        # so we remove "correct" from all items when in assessment mode.
        if self.mode == Constants.ASSESSMENT_MODE:
            for item in item_state.values():
                del item["correct"]

        overall_feedback_msgs, __ = self._get_feedback()
        if self.mode == Constants.STANDARD_MODE:
            is_finished = self._is_answer_correct()
        else:
            is_finished = not self.attempts_remain
        return {
            'items': item_state,
            'finished': is_finished,
            'attempts': self.attempts,
            'grade': self._get_weighted_earned_if_set(),
            'overall_feedback': self._present_feedback(overall_feedback_msgs)
        }

    def _get_correct_state(self):
        """
        Returns one of the possible correct states for the configured data.
        """
        state = {}
        items = copy.deepcopy(self.data.get('items', []))
        for item in items:
            zones = item.get('zones')

            # For backwards compatibility
            if zones is None:
                zones = []
                zone = item.get('zone')
                if zone is not None and zone != 'none':
                    zones.append(zone)

            if zones:
                zone = zones.pop()
                state[str(item['id'])] = {
                    'zone': zone,
                    'correct': True,
                }

        return {'items': state}

    def _get_item_state(self):
        """
        Returns a copy of the user item state.
        Converts to a dict if data is stored in legacy tuple form.
        """

        # IMPORTANT: this method should always return a COPY of self.item_state - it is called from
        # student_view_user_state handler and the data it returns is manipulated there to hide
        # correctness of items placed.
        state = {}
        migrator = StateMigration(self)

        for item_id, item in six.iteritems(self.item_state):
            state[item_id] = migrator.apply_item_state_migrations(
                item_id, item)

        return state

    def _get_item_definition(self, item_id):
        """
        Returns definition (settings) for item identified by `item_id`.
        """
        return next(i for i in self.data['items'] if i['id'] == item_id)

    def get_item_zones(self, item_id):
        """
        Returns a list of the zones that are valid options for the item.

        If the item is configured with a list of zones, return that list. If
        the item is configured with a single zone, encapsulate that zone's
        ID in a list and return the list. If the item is not configured with
        any zones, or if it's configured explicitly with no zones, return an
        empty list.
        """
        item = self._get_item_definition(item_id)
        if item.get('zones') is not None:
            return item.get('zones')
        elif item.get('zone') is not None and item.get('zone') != 'none':
            return [item.get('zone')]
        else:
            return []

    @property
    def zones(self):
        """
        Get drop zone data, defined by the author.
        """
        # Convert zone data from old to new format if necessary
        migrator = StateMigration(self)
        return [
            migrator.apply_zone_migrations(zone)
            for zone in self.data.get('zones', [])
        ]

    def _get_zone_by_uid(self, uid):
        """
        Given a zone UID, return that zone, or None.
        """
        for zone in self.zones:
            if zone["uid"] == uid:
                return zone

    def _get_item_stats(self):
        """
        Returns a tuple representing the number of correctly placed items,
        and the total number of items required (including decoy items).
        """
        items = self._get_item_raw_stats()

        correct_count = len(items.correctly_placed) + len(items.decoy_in_bank)
        total_count = len(items.required) + len(items.decoy)

        return correct_count, total_count

    def _get_item_raw_stats(self):
        """
        Returns a named tuple containing required, decoy, placed, correctly
        placed, and correctly unplaced decoy items.

        Returns:
            namedtuple: (required, placed, correctly_placed, decoy, decoy_in_bank)
                * required - IDs of items that must be placed on the board
                * placed - IDs of items actually placed on the board
                * correctly_placed - IDs of items that were placed correctly
                * decoy - IDs of decoy items
                * decoy_in_bank - IDs of decoy items that were unplaced
        """
        item_state = self._get_item_state()

        all_items = set(str(item['id']) for item in self.data['items'])
        required = set(item_id for item_id in all_items
                       if self.get_item_zones(int(item_id)) != [])
        placed = set(item_id for item_id in all_items if item_id in item_state)
        correctly_placed = set(item_id for item_id in placed
                               if item_state[item_id]['correct'])
        decoy = all_items - required
        decoy_in_bank = set(item_id for item_id in decoy
                            if item_id not in item_state)

        return ItemStats(required, placed, correctly_placed, decoy,
                         decoy_in_bank)

    def _get_raw_earned_if_set(self):
        """
        Returns student's grade if already explicitly set, otherwise returns None.
        This is different from self.raw_earned which returns 0 by default.
        """
        if self.fields['raw_earned'].is_set_on(self):
            return self.raw_earned
        else:
            return None

    def _get_weighted_earned_if_set(self):
        """
        Returns student's grade with the problem weight applied if set, otherwise
        None.
        """
        if self.fields['raw_earned'].is_set_on(self):
            return self.weighted_grade()
        else:
            return None

    def _answer_correctness(self):
        """
        Checks answer correctness:

        Returns:
            string: Correct/Incorrect/Partial
                * Correct: All items are at their correct place.
                * Partial: Some items are at their correct place.
                * Incorrect: None items are at their correct place.
        """
        correct_count, total_count = self._get_item_stats()
        if correct_count == total_count:
            return self.SOLUTION_CORRECT
        elif correct_count == 0:
            return self.SOLUTION_INCORRECT
        else:
            return self.SOLUTION_PARTIAL

    def _is_answer_correct(self):
        """
        Helper - checks if answer is correct

        Returns:
            bool: True if current answer is correct
        """
        return self._answer_correctness() == self.SOLUTION_CORRECT

    @staticmethod
    def workbench_scenarios():
        """
        A canned scenario for display in the workbench.
        """
        return [
            ("Drag-and-drop-v2 standard",
             "<vertical_demo><drag-and-drop-v2/></vertical_demo>"),
            ("Drag-and-drop-v2 assessment",
             "<vertical_demo><drag-and-drop-v2 mode='assessment' max_attempts='3'/></vertical_demo>"
             ),
        ]
Esempio n. 24
0
class CourseFields(object):
    lti_passports = List(
        help="LTI tools passports as id:client_key:client_secret",
        scope=Scope.settings)
    textbooks = TextbookList(
        help="List of pairs of (title, url) for textbooks used in this course",
        default=[],
        scope=Scope.content)

    # This field is intended for Studio to update, not to be exposed directly via
    # advanced_settings.
    user_partitions = UserPartitionList(
        help=
        "List of user partitions of this course into groups, used e.g. for experiments",
        default=[],
        scope=Scope.content)

    wiki_slug = String(help="Slug that points to the wiki for this course",
                       scope=Scope.content)
    enrollment_start = Date(
        help="Date that enrollment for this class is opened",
        scope=Scope.settings)
    enrollment_end = Date(help="Date that enrollment for this class is closed",
                          scope=Scope.settings)
    start = Date(help="Start time when this module is visible",
                 default=datetime(datetime.now().year,
                                  datetime.now().month,
                                  datetime.now().day,
                                  tzinfo=UTC()),
                 scope=Scope.settings)
    end = Date(help="Date that this class ends", scope=Scope.settings)
    advertised_start = String(
        help="Date that this course is advertised to start",
        scope=Scope.settings)
    grading_policy = Dict(help="Grading policy definition for this class",
                          default={
                              "GRADER": [{
                                  "type": "Homework",
                                  "min_count": 12,
                                  "drop_count": 2,
                                  "short_label": "HW",
                                  "weight": 0.15
                              }, {
                                  "type": "Lab",
                                  "min_count": 12,
                                  "drop_count": 2,
                                  "weight": 0.15
                              }, {
                                  "type": "Midterm Exam",
                                  "short_label": "Midterm",
                                  "min_count": 1,
                                  "drop_count": 0,
                                  "weight": 0.3
                              }, {
                                  "type": "Final Exam",
                                  "short_label": "Final",
                                  "min_count": 1,
                                  "drop_count": 0,
                                  "weight": 0.4
                              }],
                              "GRADE_CUTOFFS": {
                                  "Pass": 0.5
                              }
                          },
                          scope=Scope.content)
    # grading_policy = Dict(help="Grading policy definition for this class",
    #                       default={
    #                         "GRADER": [
    #                           {
    #                             "type": "家庭作业",
    #                             "min_count": 12,
    #                             "drop_count": 2,
    #                             "short_label": "HW",
    #                             "weight": 0.15
    #                           },
    #                           {
    #                             "type": "实验",
    #                             "min_count": 12,
    #                             "drop_count": 2,
    #                             "weight": 0.15
    #                           },
    #                           {
    #                             "type": "期中测验",
    #                             "short_label": "Midterm",
    #                             "min_count": 1,
    #                             "drop_count": 0,
    #                             "weight": 0.3
    #                           },
    #                           {
    #                             "type": "期末测验",
    #                             "short_label": "Final",
    #                             "min_count": 1,
    #                             "drop_count": 0,
    #                             "weight": 0.4
    #                           }
    #                         ],
    #                         "GRADE_CUTOFFS": {
    #                           "Pass": 0.5
    #                         }
    #                       },
    #                       scope=Scope.content)
    show_calculator = Boolean(
        help="Whether to show the calculator in this course",
        default=False,
        scope=Scope.settings)
    display_name = String(help="Display name for this module",
                          default="Empty",
                          display_name="Display Name",
                          scope=Scope.settings)
    show_chat = Boolean(help="Whether to show the chat widget in this course",
                        default=False,
                        scope=Scope.settings)
    tabs = List(help="List of tabs to enable in this course",
                scope=Scope.settings)
    end_of_course_survey_url = String(help="Url for the end-of-course survey",
                                      scope=Scope.settings)
    discussion_blackouts = List(
        help="List of pairs of start/end dates for discussion blackouts",
        scope=Scope.settings)
    discussion_topics = Dict(help="Map of topics names to ids",
                             scope=Scope.settings)
    discussion_sort_alpha = Boolean(
        scope=Scope.settings,
        default=False,
        help="Sort forum categories and subcategories alphabetically.")
    announcement = Date(help="Date this course is announced",
                        scope=Scope.settings)
    cohort_config = Dict(help="Dictionary defining cohort configuration",
                         scope=Scope.settings)
    is_new = Boolean(help="Whether this course should be flagged as new",
                     scope=Scope.settings)
    no_grade = Boolean(help="True if this course isn't graded",
                       default=False,
                       scope=Scope.settings)
    disable_progress_graph = Boolean(
        help="True if this course shouldn't display the progress graph",
        default=False,
        scope=Scope.settings)
    pdf_textbooks = List(
        help="List of dictionaries containing pdf_textbook configuration",
        scope=Scope.settings)
    html_textbooks = List(
        help="List of dictionaries containing html_textbook configuration",
        scope=Scope.settings)
    remote_gradebook = Dict(scope=Scope.settings)
    allow_anonymous = Boolean(scope=Scope.settings, default=True)
    allow_anonymous_to_peers = Boolean(scope=Scope.settings, default=False)
    advanced_modules = List(help="Beta modules used in your course",
                            scope=Scope.settings)
    has_children = True

    checklists = List(
        scope=Scope.settings,
        default=[{
            "short_description":
            "准备开始工作台",
            "items": [{
                "short_description": "添加课程队伍成员",
                "long_description": "授予您的合作者权限编辑您的课程,实现协同合作。",
                "is_checked": 0,
                "action_url": "ManageUsers",
                "action_text": "编辑课程队伍",
                "action_external": 0
            }, {
                "short_description": "为您的课程设置主要日期",
                "long_description": "在日程与细节节设置课程的学生入学与发布日期。",
                "is_checked": 0,
                "action_url": "SettingsDetails",
                "action_text": "设置课程详细信息和日程",
                "action_external": 0
            }, {
                "short_description": "起草课程评分标准",
                "long_description": "设置您的任务类型和评分标准,即使你还没有创建所有的任务。",
                "is_checked": 0,
                "action_url": "SettingsGrading",
                "action_text": "编辑评分设置",
                "action_external": 0
            }, {
                "short_description": "探索其它工作台的检查表",
                "long_description": "发现其他可用的课程制作工具,在你需要它时,得到帮助。",
                "is_checked": 0,
                "action_url": "",
                "action_text": "",
                "action_external": 0
            }]
        }, {
            "short_description":
            "起草一份简略的课程大纲",
            "items": [{
                "short_description": "创建第一个章节及其小节",
                "long_description": "使用大纲来创建第一个章节及其小节。",
                "is_checked":
                0,
                "action_url":
                "CourseOutline",
                "action_text": "编辑课程大纲",
                "action_external": 0
            }, {
                "short_description": "设置章节发布日期",
                "long_description": "为每个节指定的发布日期,并在发布日期对学生变成可见。",
                "is_checked":
                0,
                "action_url":
                "CourseOutline",
                "action_text": "编辑课程大纲",
                "action_external": 0
            }, {
                "short_description": "指明一个小节的评分类别",
                "long_description": "设置一个评定的小节作为一个特定的任务类别。小节内作业算入学生的最终成绩。",
                "is_checked":
                0,
                "action_url":
                "CourseOutline",
                "action_text": "编辑课程大纲",
                "action_external": 0
            }, {
                "short_description": "重新安排课程内容",
                "long_description": "使用拖放来重新安排您的课程。",
                "is_checked":
                0,
                "action_url":
                "CourseOutline",
                "action_text": "编辑课程大纲",
                "action_external": 0
            }, {
                "short_description": "课程章节重命名",
                "long_description": "单击课程大纲的课程章节名进行重命名",
                "is_checked":
                0,
                "action_url":
                "CourseOutline",
                "action_text": "编辑课程大纲",
                "action_external": 0
            }, {
                "short_description": "删除课程内容",
                "long_description": "删除任何不需要课程章节、节、和单元。删除之后不能恢复,请谨慎操作!",
                "is_checked":
                0,
                "action_url":
                "CourseOutline",
                "action_text": "编辑课程大纲",
                "action_external": 0
            }, {
                "short_description": "为你的大纲增加一个导师专用章节",
                "long_description":
                "有些作者发现使用有用的一个章节为未排序的,进行中的工作。要做到这一点,创建一个章节,并设置发布日期在很久以后。",
                "is_checked": 0,
                "action_url": "CourseOutline",
                "action_text": "编辑课程大纲",
                "action_external": 0
            }]
        }, {
            "short_description":
            "探索一些支持工具",
            "items": [{
                "short_description": "探索工作台帮助论坛",
                "long_description": "通过点击工作台中右上角您的名字来访问工作台帮助论坛",
                "is_checked": 0,
                "action_url": "http://help.edge.edx.org/",
                "action_text": "访问工作台帮助",
                "action_external": 1
            }, {
                "short_description": "注册加入校盾平台",
                "long_description": "注册校盾、阅读创建校盾课程的入门教程",
                "is_checked": 0,
                "action_url":
                "https://edge.edx.org/courses/edX/edX101/How_to_Create_an_edX_Course/about",
                "action_text": "注册校盾平台",
                "action_external": 1
            }, {
                "short_description": "下载校盾工作平台的相关文档",
                "long_description": "以PDF格式搜索下载工作平台的参考文档。",
                "is_checked": 0,
                "action_url":
                "http://files.edx.org/Getting_Started_with_Studio.pdf",
                "action_text": "下载文档",
                "action_external": 1
            }]
        }, {
            "short_description":
            "起草关于课程页面",
            "items": [{
                "short_description": "起草课程描述",
                "long_description":
                "在校盾中的课程都有相关的页面,包含了该课程的视频、描述和其他信息。学生在您的课程报名前该仔细阅读该介绍。",
                "is_checked": 0,
                "action_url": "SettingsDetails",
                "action_text": "编辑课程详细及日程页",
                "action_external": 0
            }, {
                "short_description": "添加教师介绍",
                "long_description": "显示导师信息给今后报名的学生是很有用的。包括关于页上教师介绍",
                "is_checked": 0,
                "action_url": "SettingsDetails",
                "action_text": "编辑课程详细及日程页",
                "action_external": 0
            }, {
                "short_description": "添加课程FAQ(常见问题及回答)",
                "long_description": "包括一些经常被询问的关于课程的问题清单。",
                "is_checked": 0,
                "action_url": "SettingsDetails",
                "action_text": "编辑课程详细及日程页",
                "action_external": 0
            }, {
                "short_description": "添加课程要求",
                "long_description": "让学生了解在报名课程之前,需要掌握的知识或者技术",
                "is_checked": 0,
                "action_url": "SettingsDetails",
                "action_text": "编辑课程详细及日程页",
                "action_external": 0
            }]
        }])

    info_sidebar_name = String(scope=Scope.settings, default='课程讲义')

    show_timezone = Boolean(
        help=
        "True if timezones should be shown on dates in the courseware. Deprecated in favor of due_date_display_format.",
        scope=Scope.settings,
        default=True)
    due_date_display_format = String(
        help=
        "Format supported by strftime for displaying due dates. Takes precedence over show_timezone.",
        scope=Scope.settings,
        default=None)
    enrollment_domain = String(
        help=
        "External login method associated with user accounts allowed to register in course",
        scope=Scope.settings)
    course_image = String(
        help="Filename of the course image",
        scope=Scope.settings,
        # Ensure that courses imported from XML keep their image
        default="images_course_image.jpg")

    ## Course level Certificate Name overrides.
    cert_name_short = String(
        help=
        "Sitewide name of completion statements given to students (short).",
        scope=Scope.settings,
        default="")
    cert_name_long = String(
        help="Sitewide name of completion statements given to students (long).",
        scope=Scope.settings,
        default="")

    # An extra property is used rather than the wiki_slug/number because
    # there are courses that change the number for different runs. This allows
    # courses to share the same css_class across runs even if they have
    # different numbers.
    #
    # TODO get rid of this as soon as possible or potentially build in a robust
    # way to add in course-specific styling. There needs to be a discussion
    # about the right way to do this, but arjun will address this ASAP. Also
    # note that the courseware template needs to change when this is removed.
    css_class = String(help="DO NOT USE THIS",
                       scope=Scope.settings,
                       default="")

    # TODO: This is a quick kludge to allow CS50 (and other courses) to
    # specify their own discussion forums as external links by specifying a
    # "discussion_link" in their policy JSON file. This should later get
    # folded in with Syllabus, Course Info, and additional Custom tabs in a
    # more sensible framework later.
    discussion_link = String(help="DO NOT USE THIS", scope=Scope.settings)

    # TODO: same as above, intended to let internal CS50 hide the progress tab
    # until we get grade integration set up.
    # Explicit comparison to True because we always want to return a bool.
    hide_progress_tab = Boolean(help="DO NOT USE THIS", scope=Scope.settings)

    display_organization = String(
        help=
        "An optional display string for the course organization that will get rendered in the LMS",
        scope=Scope.settings)

    display_coursenumber = String(
        help=
        "An optional display string for the course number that will get rendered in the LMS",
        scope=Scope.settings)

    max_student_enrollments_allowed = Integer(
        help="Limit the number of students allowed to enroll in this course.",
        scope=Scope.settings)

    allow_public_wiki_access = Boolean(
        help="Whether to allow an unenrolled user to view the Wiki",
        default=False,
        scope=Scope.settings)
Esempio n. 25
0
class InheritanceMixin(XBlockMixin):
    """Field definitions for inheritable fields."""

    graded = Boolean(
        help="Whether this module contributes to the final course grade",
        scope=Scope.settings,
        default=False,
    )
    start = Date(help="Start time when this module is visible",
                 default=DEFAULT_START_DATE,
                 scope=Scope.settings)
    due = Date(
        display_name=_("Due Date"),
        help=_("Enter the default date by which problems are due."),
        scope=Scope.settings,
    )
    visible_to_staff_only = Boolean(
        help=
        _("If true, can be seen only by course staff, regardless of start date."
          ),
        default=False,
        scope=Scope.settings,
    )
    course_edit_method = String(
        display_name=_("Course Editor"),
        help=
        _("Enter the method by which this course is edited (\"XML\" or \"Studio\")."
          ),
        default="Studio",
        scope=Scope.settings,
        deprecated=
        True  # Deprecated because user would not change away from Studio within Studio.
    )
    giturl = String(
        display_name=_("GIT URL"),
        help=_("Enter the URL for the course data GIT repository."),
        scope=Scope.settings)
    xqa_key = String(display_name=_("XQA Key"),
                     help=_("This setting is not currently supported."),
                     scope=Scope.settings,
                     deprecated=True)
    graceperiod = Timedelta(
        help=
        "Amount of time after the due date that submissions will be accepted",
        scope=Scope.settings,
    )
    group_access = Dict(
        help=_(
            "Enter the ids for the content groups this problem belongs to."),
        scope=Scope.settings,
    )

    showanswer = String(
        display_name=_("Show Answer"),
        help=_(
            # Translators: DO NOT translate the words in quotes here, they are
            # specific words for the acceptable values.
            'Specify when the Show Answer button appears for each problem. '
            'Valid values are "always", "answered", "attempted", "closed", '
            '"finished", "past_due", "correct_or_past_due", and "never".'),
        scope=Scope.settings,
        default="finished",
    )

    show_correctness = String(
        display_name=_("Show Results"),
        help=_(
            # Translators: DO NOT translate the words in quotes here, they are
            # specific words for the acceptable values.
            'Specify when to show answer correctness and score to learners. '
            'Valid values are "always", "never", and "past_due".'),
        scope=Scope.settings,
        default="always",
    )

    rerandomize = String(
        display_name=_("Randomization"),
        help=_(
            # Translators: DO NOT translate the words in quotes here, they are
            # specific words for the acceptable values.
            'Specify the default for how often variable values in a problem are randomized. '
            'This setting should be set to "never" unless you plan to provide a Python '
            'script to identify and randomize values in most of the problems in your course. '
            'Valid values are "always", "onreset", "never", and "per_student".'
        ),
        scope=Scope.settings,
        default="never",
    )
    days_early_for_beta = Float(
        display_name=_("Days Early for Beta Users"),
        help=
        _("Enter the number of days before the start date that beta users can access the course."
          ),
        scope=Scope.settings,
        default=None,
    )
    static_asset_path = String(
        display_name=_("Static Asset Path"),
        help=
        _("Enter the path to use for files on the Files & Uploads page. This value overrides the Studio default, c4x://."
          ),
        scope=Scope.settings,
        default='',
    )
    use_latex_compiler = Boolean(
        display_name=_("Enable LaTeX Compiler"),
        help=
        _("Enter true or false. If true, you can use the LaTeX templates for HTML components and advanced Problem components."
          ),
        default=False,
        scope=Scope.settings)
    max_attempts = Integer(
        display_name=_("Maximum Attempts"),
        help=
        _("Enter the maximum number of times a student can try to answer problems. By default, Maximum Attempts is set to null, meaning that students have an unlimited number of attempts for problems. You can override this course-wide setting for individual problems. However, if the course-wide setting is a specific number, you cannot set the Maximum Attempts for individual problems to unlimited."
          ),
        values={"min": 0},
        scope=Scope.settings)
    matlab_api_key = String(
        display_name=_("Matlab API key"),
        help=
        _("Enter the API key provided by MathWorks for accessing the MATLAB Hosted Service. "
          "This key is granted for exclusive use in this course for the specified duration. "
          "Do not share the API key with other courses. Notify MathWorks immediately "
          "if you believe the key is exposed or compromised. To obtain a key for your course, "
          "or to report an issue, please contact [email protected]"),
        scope=Scope.settings)
    # This is should be scoped to content, but since it's defined in the policy
    # file, it is currently scoped to settings.
    user_partitions = UserPartitionList(
        display_name=_("Group Configurations"),
        help=
        _("Enter the configurations that govern how students are grouped together."
          ),
        default=[],
        scope=Scope.settings)
    video_speed_optimizations = Boolean(
        display_name=_("Enable video caching system"),
        help=
        _("Enter true or false. If true, video caching will be used for HTML5 videos."
          ),
        default=True,
        scope=Scope.settings)
    video_auto_advance = Boolean(
        display_name=_("Enable video auto-advance"),
        help=
        _("Specify whether to show an auto-advance button in videos. If the student clicks it, when the last video in a unit finishes it will automatically move to the next unit and autoplay the first video."
          ),
        scope=Scope.settings,
        default=False)
    video_bumper = Dict(
        display_name=_("Video Pre-Roll"),
        help=
        _("Identify a video, 5-10 seconds in length, to play before course videos. Enter the video ID from "
          "the Video Uploads page and one or more transcript files in the following format: {format}. "
          "For example, an entry for a video with two transcripts looks like this: {example}"
          ),
        help_format_args=dict(
            format=
            '{"video_id": "ID", "transcripts": {"language": "/static/filename.srt"}}',
            example=
            ('{'
             '"video_id": "77cef264-d6f5-4cf2-ad9d-0178ab8c77be", '
             '"transcripts": {"en": "/static/DemoX-D01_1.srt", "uk": "/static/DemoX-D01_1_uk.srt"}'
             '}'),
        ),
        scope=Scope.settings)

    reset_key = "DEFAULT_SHOW_RESET_BUTTON"
    default_reset_button = getattr(settings, reset_key) if hasattr(
        settings, reset_key) else False
    show_reset_button = Boolean(
        display_name=_("Show Reset Button for Problems"),
        help=
        _("Enter true or false. If true, problems in the course default to always displaying a 'Reset' button. "
          "You can override this in each problem's settings. All existing problems are affected when "
          "this course-wide setting is changed."),
        scope=Scope.settings,
        default=default_reset_button)
    edxnotes = Boolean(
        display_name=_("Enable Student Notes"),
        help=
        _("Enter true or false. If true, students can use the Student Notes feature."
          ),
        default=False,
        scope=Scope.settings)
    edxnotes_visibility = Boolean(
        display_name="Student Notes Visibility",
        help=_(
            "Indicates whether Student Notes are visible in the course. "
            "Students can also show or hide their notes in the courseware."),
        default=True,
        scope=Scope.user_info)

    in_entrance_exam = Boolean(
        display_name=_("Tag this module as part of an Entrance Exam section"),
        help=_(
            "Enter true or false. If true, answer submissions for problem modules will be "
            "considered in the Entrance Exam scoring/gating algorithm."),
        scope=Scope.settings,
        default=False)

    self_paced = Boolean(
        display_name=_('Self Paced'),
        help=
        _('Set this to "true" to mark this course as self-paced. Self-paced courses do not have '
          'due dates for assignments, and students can progress through the course at any rate before '
          'the course ends.'),
        default=False,
        scope=Scope.settings)
Esempio n. 26
0
class CourseFields(object):
    lti_passports = List(
        display_name=_("LTI Passports"),
        help=
        _("Enter the passports for course LTI tools in the following format: \"id\":\"client_key:client_secret\"."
          ),
        scope=Scope.settings)
    textbooks = TextbookList(
        help="List of pairs of (title, url) for textbooks used in this course",
        default=[],
        scope=Scope.content)

    wiki_slug = String(help="Slug that points to the wiki for this course",
                       scope=Scope.content)
    enrollment_start = Date(
        help="Date that enrollment for this class is opened",
        scope=Scope.settings)
    enrollment_end = Date(help="Date that enrollment for this class is closed",
                          scope=Scope.settings)
    start = Date(help="Start time when this module is visible",
                 default=DEFAULT_START_DATE,
                 scope=Scope.settings)
    end = Date(help="Date that this class ends", scope=Scope.settings)
    advertised_start = String(
        display_name=_("Course Advertised Start Date"),
        help=
        _("Enter the date you want to advertise as the course start date, if this date is different from the set start date. To advertise the set start date, enter null."
          ),
        scope=Scope.settings)
    grading_policy = Dict(help="Grading policy definition for this class",
                          default={
                              "GRADER": [{
                                  "type": "Homework",
                                  "min_count": 12,
                                  "drop_count": 2,
                                  "short_label": "HW",
                                  "weight": 0.15
                              }, {
                                  "type": "Lab",
                                  "min_count": 12,
                                  "drop_count": 2,
                                  "weight": 0.15
                              }, {
                                  "type": "Midterm Exam",
                                  "short_label": "Midterm",
                                  "min_count": 1,
                                  "drop_count": 0,
                                  "weight": 0.3
                              }, {
                                  "type": "Final Exam",
                                  "short_label": "Final",
                                  "min_count": 1,
                                  "drop_count": 0,
                                  "weight": 0.4
                              }],
                              "GRADE_CUTOFFS": {
                                  "Pass": 0.5
                              }
                          },
                          scope=Scope.content)
    show_calculator = Boolean(
        display_name=_("Show Calculator"),
        help=
        _("Enter true or false. When true, students can see the calculator in the course."
          ),
        default=False,
        scope=Scope.settings)
    display_name = String(help=_(
        "Enter the name of the course as it should appear in the edX.org course list."
    ),
                          default="Empty",
                          display_name=_("Course Display Name"),
                          scope=Scope.settings)
    course_edit_method = String(
        display_name=_("Course Editor"),
        help=
        _("Enter the method by which this course is edited (\"XML\" or \"Studio\")."
          ),
        default="Studio",
        scope=Scope.settings,
        deprecated=
        True  # Deprecated because someone would not edit this value within Studio.
    )
    show_chat = Boolean(
        display_name=_("Show Chat Widget"),
        help=
        _("Enter true or false. When true, students can see the chat widget in the course."
          ),
        default=False,
        scope=Scope.settings)
    tabs = CourseTabList(help="List of tabs to enable in this course",
                         scope=Scope.settings,
                         default=[])
    end_of_course_survey_url = String(
        display_name=_("Course Survey URL"),
        help=
        _("Enter the URL for the end-of-course survey. If your course does not have a survey, enter null."
          ),
        scope=Scope.settings)
    discussion_blackouts = List(
        display_name=_("Discussion Blackout Dates"),
        help=
        _("Enter pairs of dates between which students cannot post to discussion forums, formatted as \"YYYY-MM-DD-YYYY-MM-DD\". To specify times as well as dates, format the pairs as \"YYYY-MM-DDTHH:MM-YYYY-MM-DDTHH:MM\" (be sure to include the \"T\" between the date and time)."
          ),
        scope=Scope.settings)
    discussion_topics = Dict(
        display_name=_("Discussion Topic Mapping"),
        help=
        _("Enter discussion categories in the following format: \"CategoryName\": {\"id\": \"i4x-InstitutionName-CourseNumber-course-CourseRun\"}. For example, one discussion category may be \"Lydian Mode\": {\"id\": \"i4x-UniversityX-MUS101-course-2014_T1\"}."
          ),
        scope=Scope.settings)
    discussion_sort_alpha = Boolean(
        display_name=_("Discussion Sorting Alphabetical"),
        scope=Scope.settings,
        default=False,
        help=
        _("Enter true or false. If true, discussion categories and subcategories are sorted alphabetically. If false, they are sorted chronologically."
          ))
    announcement = Date(display_name=_("Course Announcement Date"),
                        help=_("Enter the date to announce your course."),
                        scope=Scope.settings)
    cohort_config = Dict(display_name=_("Cohort Configuration"),
                         help=_("Cohorts are not currently supported by edX."),
                         scope=Scope.settings)
    is_new = Boolean(
        display_name=_("Course Is New"),
        help=
        _("Enter true or false. If true, the course appears in the list of new courses on edx.org, and a New! badge temporarily appears next to the course image."
          ),
        scope=Scope.settings)
    no_grade = Boolean(
        display_name=_("Course Not Graded"),
        help=_("Enter true or false. If true, the course will not be graded."),
        default=False,
        scope=Scope.settings)
    disable_progress_graph = Boolean(
        display_name=_("Disable Progress Graph"),
        help=
        _("Enter true or false. If true, students cannot view the progress graph."
          ),
        default=False,
        scope=Scope.settings)
    pdf_textbooks = List(
        display_name=_("PDF Textbooks"),
        help=_("List of dictionaries containing pdf_textbook configuration"),
        scope=Scope.settings)
    html_textbooks = List(
        display_name=_("HTML Textbooks"),
        help=
        _("For HTML textbooks that appear as separate tabs in the courseware, enter the name of the tab (usually the name of the book) as well as the URLs and titles of all the chapters in the book."
          ),
        scope=Scope.settings)
    remote_gradebook = Dict(
        display_name=_("Remote Gradebook"),
        help=
        _("Enter the remote gradebook mapping. Only use this setting when REMOTE_GRADEBOOK_URL has been specified."
          ),
        scope=Scope.settings)
    allow_anonymous = Boolean(
        display_name=_("Allow Anonymous Discussion Posts"),
        help=
        _("Enter true or false. If true, students can create discussion posts that are anonymous to all users."
          ),
        scope=Scope.settings,
        default=True)
    allow_anonymous_to_peers = Boolean(
        display_name=_("Allow Anonymous Discussion Posts to Peers"),
        help=
        _("Enter true or false. If true, students can create discussion posts that are anonymous to other students. This setting does not make posts anonymous to course staff."
          ),
        scope=Scope.settings,
        default=False)
    advanced_modules = List(
        display_name=_("Advanced Module List"),
        help=_(
            "Enter the names of the advanced components to use in your course."
        ),
        scope=Scope.settings)
    has_children = True
    checklists = List(
        scope=Scope.settings,
        default=[{
            "short_description":
            _("Getting Started With Studio"),
            "items": [{
                "short_description":
                _("Add Course Team Members"),
                "long_description":
                _("Grant your collaborators permission to edit your course so you can work together."
                  ),
                "is_checked":
                False,
                "action_url":
                "ManageUsers",
                "action_text":
                _("Edit Course Team"),
                "action_external":
                False
            }, {
                "short_description":
                _("Set Important Dates for Your Course"),
                "long_description":
                _("Establish your course's student enrollment and launch dates on the Schedule and Details page."
                  ),
                "is_checked":
                False,
                "action_url":
                "SettingsDetails",
                "action_text":
                _("Edit Course Details &amp; Schedule"),
                "action_external":
                False
            }, {
                "short_description":
                _("Draft Your Course's Grading Policy"),
                "long_description":
                _("Set up your assignment types and grading policy even if you haven't created all your assignments."
                  ),
                "is_checked":
                False,
                "action_url":
                "SettingsGrading",
                "action_text":
                _("Edit Grading Settings"),
                "action_external":
                False
            }, {
                "short_description":
                _("Explore the Other Studio Checklists"),
                "long_description":
                _("Discover other available course authoring tools, and find help when you need it."
                  ),
                "is_checked":
                False,
                "action_url":
                "",
                "action_text":
                "",
                "action_external":
                False
            }]
        }, {
            "short_description":
            _("Draft a Rough Course Outline"),
            "items": [{
                "short_description":
                _("Create Your First Section and Subsection"),
                "long_description":
                _("Use your course outline to build your first Section and Subsection."
                  ),
                "is_checked":
                False,
                "action_url":
                "CourseOutline",
                "action_text":
                _("Edit Course Outline"),
                "action_external":
                False
            }, {
                "short_description":
                _("Set Section Release Dates"),
                "long_description":
                _("Specify the release dates for each Section in your course. Sections become visible to students on their release dates."
                  ),
                "is_checked":
                False,
                "action_url":
                "CourseOutline",
                "action_text":
                _("Edit Course Outline"),
                "action_external":
                False
            }, {
                "short_description":
                _("Designate a Subsection as Graded"),
                "long_description":
                _("Set a Subsection to be graded as a specific assignment type. Assignments within graded Subsections count toward a student's final grade."
                  ),
                "is_checked":
                False,
                "action_url":
                "CourseOutline",
                "action_text":
                _("Edit Course Outline"),
                "action_external":
                False
            }, {
                "short_description":
                _("Reordering Course Content"),
                "long_description":
                _("Use drag and drop to reorder the content in your course."),
                "is_checked":
                False,
                "action_url":
                "CourseOutline",
                "action_text":
                _("Edit Course Outline"),
                "action_external":
                False
            }, {
                "short_description":
                _("Renaming Sections"),
                "long_description":
                _("Rename Sections by clicking the Section name from the Course Outline."
                  ),
                "is_checked":
                False,
                "action_url":
                "CourseOutline",
                "action_text":
                _("Edit Course Outline"),
                "action_external":
                False
            }, {
                "short_description":
                _("Deleting Course Content"),
                "long_description":
                _("Delete Sections, Subsections, or Units you don't need anymore. Be careful, as there is no Undo function."
                  ),
                "is_checked":
                False,
                "action_url":
                "CourseOutline",
                "action_text":
                _("Edit Course Outline"),
                "action_external":
                False
            }, {
                "short_description":
                _("Add an Instructor-Only Section to Your Outline"),
                "long_description":
                _("Some course authors find using a section for unsorted, in-progress work useful. To do this, create a section and set the release date to the distant future."
                  ),
                "is_checked":
                False,
                "action_url":
                "CourseOutline",
                "action_text":
                _("Edit Course Outline"),
                "action_external":
                False
            }]
        }, {
            "short_description":
            _("Explore edX's Support Tools"),
            "items": [{
                "short_description":
                _("Explore the Studio Help Forum"),
                "long_description":
                _("Access the Studio Help forum from the menu that appears when you click your user name in the top right corner of Studio."
                  ),
                "is_checked":
                False,
                "action_url":
                "http://help.edge.edx.org/",
                "action_text":
                _("Visit Studio Help"),
                "action_external":
                True
            }, {
                "short_description":
                _("Enroll in edX 101"),
                "long_description":
                _("Register for edX 101, edX's primer for course creation."),
                "is_checked":
                False,
                "action_url":
                "https://edge.edx.org/courses/edX/edX101/How_to_Create_an_edX_Course/about",
                "action_text":
                _("Register for edX 101"),
                "action_external":
                True
            }, {
                "short_description":
                _("Download the Studio Documentation"),
                "long_description":
                _("Download the searchable Studio reference documentation in PDF form."
                  ),
                "is_checked":
                False,
                "action_url":
                "http://files.edx.org/Getting_Started_with_Studio.pdf",
                "action_text":
                _("Download Documentation"),
                "action_external":
                True
            }]
        }, {
            "short_description":
            _("Draft Your Course About Page"),
            "items": [{
                "short_description":
                _("Draft a Course Description"),
                "long_description":
                _("Courses on edX have an About page that includes a course video, description, and more. Draft the text students will read before deciding to enroll in your course."
                  ),
                "is_checked":
                False,
                "action_url":
                "SettingsDetails",
                "action_text":
                _("Edit Course Schedule &amp; Details"),
                "action_external":
                False
            }, {
                "short_description":
                _("Add Staff Bios"),
                "long_description":
                _("Showing prospective students who their instructor will be is helpful. Include staff bios on the course About page."
                  ),
                "is_checked":
                False,
                "action_url":
                "SettingsDetails",
                "action_text":
                _("Edit Course Schedule &amp; Details"),
                "action_external":
                False
            }, {
                "short_description":
                _("Add Course FAQs"),
                "long_description":
                _("Include a short list of frequently asked questions about your course."
                  ),
                "is_checked":
                False,
                "action_url":
                "SettingsDetails",
                "action_text":
                _("Edit Course Schedule &amp; Details"),
                "action_external":
                False
            }, {
                "short_description":
                _("Add Course Prerequisites"),
                "long_description":
                _("Let students know what knowledge and/or skills they should have before they enroll in your course."
                  ),
                "is_checked":
                False,
                "action_url":
                "SettingsDetails",
                "action_text":
                _("Edit Course Schedule &amp; Details"),
                "action_external":
                False
            }]
        }])
    info_sidebar_name = String(
        display_name=_("Course Info Sidebar Name"),
        help=
        _("Enter the heading that you want students to see above your course handouts on the Course Info page. Your course handouts appear in the right panel of the page."
          ),
        scope=Scope.settings,
        default='Course Handouts')
    show_timezone = Boolean(
        help=
        "True if timezones should be shown on dates in the courseware. Deprecated in favor of due_date_display_format.",
        scope=Scope.settings,
        default=True)
    due_date_display_format = String(
        display_name=_("Due Date Display Format"),
        help=
        _("Enter the format due dates are displayed in. Due dates must be in MM-DD-YYYY, DD-MM-YYYY, YYYY-MM-DD, or YYYY-DD-MM format."
          ),
        scope=Scope.settings,
        default=None)
    enrollment_domain = String(
        display_name=_("External Login Domain"),
        help=_(
            "Enter the external login method students can use for the course."
        ),
        scope=Scope.settings)
    certificates_show_before_end = Boolean(
        display_name=_("Certificates Downloadable Before End"),
        help=
        _("Enter true or false. If true, students can download certificates before the course ends, if they've met certificate requirements."
          ),
        scope=Scope.settings,
        default=False,
        deprecated=True)

    certificates_display_behavior = String(
        display_name=_("Certificates Display Behavior"),
        help=
        _("Has three possible states: 'end', 'early_with_info', 'early_no_info'. 'end' is the default behavior, where certificates will only appear after a course has ended. 'early_with_info' will display all certificate information before a course has ended. 'early_no_info' will hide all certificate information unless a student has earned a certificate."
          ),
        scope=Scope.settings,
        default="end")
    course_image = String(
        display_name=_("Course About Page Image"),
        help=
        _("Edit the name of the course image file. You must upload this file on the Files & Uploads page. You can also set the course image on the Settings & Details page."
          ),
        scope=Scope.settings,
        # Ensure that courses imported from XML keep their image
        default="images_course_image.jpg")

    ## Course level Certificate Name overrides.
    cert_name_short = String(help=_(
        "Between quotation marks, enter the short name of the course to use on the certificate that students receive when they complete the course."
    ),
                             display_name=_("Certificate Name (Short)"),
                             scope=Scope.settings,
                             default="")
    cert_name_long = String(help=_(
        "Between quotation marks, enter the long name of the course to use on the certificate that students receive when they complete the course."
    ),
                            display_name=_("Certificate Name (Long)"),
                            scope=Scope.settings,
                            default="")

    # An extra property is used rather than the wiki_slug/number because
    # there are courses that change the number for different runs. This allows
    # courses to share the same css_class across runs even if they have
    # different numbers.
    #
    # TODO get rid of this as soon as possible or potentially build in a robust
    # way to add in course-specific styling. There needs to be a discussion
    # about the right way to do this, but arjun will address this ASAP. Also
    # note that the courseware template needs to change when this is removed.
    css_class = String(
        display_name=_("CSS Class for Course Reruns"),
        help=
        _("Allows courses to share the same css class across runs even if they have different numbers."
          ),
        scope=Scope.settings,
        default="",
        deprecated=True)

    # TODO: This is a quick kludge to allow CS50 (and other courses) to
    # specify their own discussion forums as external links by specifying a
    # "discussion_link" in their policy JSON file. This should later get
    # folded in with Syllabus, Course Info, and additional Custom tabs in a
    # more sensible framework later.
    discussion_link = String(
        display_name=_("Discussion Forum External Link"),
        help=
        _("Allows specification of an external link to replace discussion forums."
          ),
        scope=Scope.settings,
        deprecated=True)

    # TODO: same as above, intended to let internal CS50 hide the progress tab
    # until we get grade integration set up.
    # Explicit comparison to True because we always want to return a bool.
    hide_progress_tab = Boolean(display_name=_("Hide Progress Tab"),
                                help=_("Allows hiding of the progress tab."),
                                scope=Scope.settings,
                                deprecated=True)

    display_organization = String(
        display_name=_("Course Organization Display String"),
        help=
        _("Enter the course organization that you want to appear in the courseware. This setting overrides the organization that you entered when you created the course. To use the organization that you entered when you created the course, enter null."
          ),
        scope=Scope.settings)

    display_coursenumber = String(
        display_name=_("Course Number Display String"),
        help=
        _("Enter the course number that you want to appear in the courseware. This setting overrides the course number that you entered when you created the course. To use the course number that you entered when you created the course, enter null."
          ),
        scope=Scope.settings)

    max_student_enrollments_allowed = Integer(
        display_name=_("Course Maximum Student Enrollment"),
        help=
        _("Enter the maximum number of students that can enroll in the course. To allow an unlimited number of students, enter null."
          ),
        scope=Scope.settings)

    allow_public_wiki_access = Boolean(
        display_name=_("Allow Public Wiki Access"),
        help=
        _("Enter true or false. If true, edX users can view the course wiki even if they're not enrolled in the course."
          ),
        default=False,
        scope=Scope.settings)

    invitation_only = Boolean(
        display_name=_("Invitation Only"),
        help=
        "Whether to restrict enrollment to invitation by the course staff.",
        default=False,
        scope=Scope.settings)
class ScormXBlock(XBlock):
    """
    When a user uploads a Scorm package, the zip file is stored in:

        media/{org}/{course}/{block_type}/{block_id}/{sha1}{ext}

    This zip file is then extracted to the media/{scorm_location}/{block_id}.

    The scorm location is defined by the LOCATION xblock setting. If undefined, this is
    "scorm". This setting can be set e.g:

        XBLOCK_SETTINGS["ScormXBlock"] = {
            "LOCATION": "alternatevalue",
        }

    Note that neither the folder the folder nor the package file are deleted when the
    xblock is removed.
    """

    display_name = String(
        display_name=_("Display Name"),
        help=_("Display name for this module"),
        default="Scorm module",
        scope=Scope.settings,
    )
    index_page_path = String(
        display_name=_("Path to the index page in scorm file"),
        scope=Scope.settings)
    package_meta = Dict(scope=Scope.content)
    scorm_version = String(default="SCORM_12", scope=Scope.settings)

    # save completion_status for SCORM_2004
    lesson_status = String(scope=Scope.user_state, default="not attempted")
    success_status = String(scope=Scope.user_state, default="unknown")
    lesson_score = Float(scope=Scope.user_state, default=0)
    weight = Float(
        default=1,
        display_name=_("Weight"),
        help=_("Weight/Maximum grade"),
        scope=Scope.settings,
    )
    has_score = Boolean(
        display_name=_("Scored"),
        help=
        _("Select False if this component will not receive a numerical score from the Scorm"
          ),
        default=True,
        scope=Scope.settings,
    )

    # See the Scorm data model:
    # https://scorm.com/scorm-explained/technical-scorm/run-time/
    scorm_data = Dict(scope=Scope.user_state, default={})

    icon_class = String(default="video", scope=Scope.settings)
    width = Integer(
        display_name=_("Display width (px)"),
        help=_("Width of iframe (default: 100%)"),
        scope=Scope.settings,
    )
    height = Integer(
        display_name=_("Display height (px)"),
        help=_("Height of iframe"),
        default=450,
        scope=Scope.settings,
    )

    has_author_view = True

    def render_template(self, template_path, context):
        template_str = self.resource_string(template_path)
        template = Template(template_str)
        return template.render(Context(context))

    @staticmethod
    def resource_string(path):
        """Handy helper for getting static resources from our kit."""
        data = pkg_resources.resource_string(__name__, path)
        return data.decode("utf8")

    def author_view(self, context=None):
        context = context or {}
        if not self.index_page_path:
            context[
                "message"] = "Click 'Edit' to modify this module and upload a new SCORM package."
        return self.student_view(context=context)

    def student_view(self, context=None):
        logger.info("student_view index_page_url %s", self.index_page_url)
        student_context = {
            "index_page_url": self.index_page_url,
            "completion_status": self.get_completion_status(),
            "grade": self.get_grade(),
            "scorm_xblock": self,
        }
        student_context.update(context or {})
        template = self.render_template("static/html/scormxblock.html",
                                        student_context)
        frag = Fragment(template)
        frag.add_css(self.resource_string("static/css/scormxblock.css"))
        frag.add_javascript(
            self.resource_string("static/js/src/scormxblock.js"))
        frag.initialize_js("ScormXBlock",
                           json_args={"scorm_version": self.scorm_version})
        return frag

    def studio_view(self, context=None):
        # Note that we cannot use xblockutils's StudioEditableXBlockMixin because we
        # need to support package file uploads.
        studio_context = {
            "field_display_name": self.fields["display_name"],
            "field_has_score": self.fields["has_score"],
            "field_weight": self.fields["weight"],
            "field_width": self.fields["width"],
            "field_height": self.fields["height"],
            "scorm_xblock": self,
        }
        studio_context.update(context or {})
        template = self.render_template("static/html/studio.html",
                                        studio_context)
        frag = Fragment(template)
        frag.add_css(self.resource_string("static/css/scormxblock.css"))
        frag.add_javascript(self.resource_string("static/js/src/studio.js"))
        frag.initialize_js("ScormStudioXBlock")
        return frag

    @staticmethod
    def json_response(data):
        return Response(json.dumps(data),
                        content_type="application/json",
                        charset="utf8")

    @XBlock.handler
    def studio_submit(self, request, _suffix):
        self.display_name = request.params["display_name"]
        self.width = request.params["width"]
        self.height = request.params["height"]
        self.has_score = request.params["has_score"]
        self.weight = request.params["weight"]
        self.icon_class = "problem" if self.has_score == "True" else "video"

        response = {"result": "success", "errors": []}
        if not hasattr(request.params["file"], "file"):
            # File not uploaded
            return self.json_response(response)

        package_file = request.params["file"].file
        self.update_package_meta(package_file)

        # First, save scorm file in the storage for mobile clients
        if default_storage.exists(self.package_path):
            logger.info('Removing previously uploaded "%s"', self.package_path)
            default_storage.delete(self.package_path)
        default_storage.save(self.package_path, File(package_file))
        logger.info('Scorm "%s" file stored at "%s"', package_file,
                    self.package_path)

        # Then, extract zip file
        if default_storage.exists(self.extract_folder_base_path):
            logger.info('Removing previously unzipped "%s"',
                        self.extract_folder_base_path)
            recursive_delete(self.extract_folder_base_path)
        with zipfile.ZipFile(package_file, "r") as scorm_zipfile:
            tmp_dir = tempfile.mkdtemp()
            for zipinfo in scorm_zipfile.infolist():
                # Do not unzip folders, only files. In Python 3.6 we will have access to
                # the is_dir() method to verify whether a ZipInfo object points to a
                # directory.
                # https://docs.python.org/3.6/library/zipfile.html#zipfile.ZipInfo.is_dir
                if not zipinfo.filename.endswith("/"):
                    # Manually extract the file to avoid UnsupportedOperation seek
                    tmp_file = scorm_zipfile.extract(zipinfo, tmp_dir)
                    logger.info("Extracting SCORM file %s", zipinfo.filename)

                    # This is an extremely hacky solution.
                    # The problem is mimetypes.guess_type('*.js')
                    # eventually return type as bytes instead of str (i.e b'text/javascript' instead)
                    # Why I said eventually because at first start-up, everything is fine. But after
                    # using studio for uploading other files (video transcript for example), the problem
                    # appears

                    mimetypes.add_types('text/javascript', '.js')
                    default_storage.save(
                        os.path.join(self.extract_folder_path,
                                     zipinfo.filename), open(tmp_file, "rb"))
                    os.remove(tmp_file)
        try:
            self.update_package_fields()
        except ScormError as e:
            response["errors"].append(e.args[0])

        return self.json_response(response)

    @property
    def index_page_url(self):
        if not self.package_meta or not self.index_page_path:
            logger.info("index_page_url index_page_url is blank")
            return ""
        folder = self.extract_folder_path
        if default_storage.exists(
                os.path.join(self.extract_folder_base_path,
                             self.index_page_path)):
            # For backward-compatibility, we must handle the case when the xblock data
            # is stored in the base folder.
            folder = self.extract_folder_base_path
            logger.warning("Serving SCORM content from old-style path: %s",
                           folder)
        result = default_storage.url(os.path.join(folder,
                                                  self.index_page_path))
        logger.info("index_page_url index_page_url = %s", result)
        return result

    @property
    def package_path(self):
        """
        Get file path of storage.
        """
        return (
            "{loc.org}/{loc.course}/{loc.block_type}/{loc.block_id}/{sha1}{ext}"
        ).format(
            loc=self.location,
            sha1=self.package_meta["sha1"],
            ext=os.path.splitext(self.package_meta["name"])[1],
        )

    @property
    def extract_folder_path(self):
        """
        This path needs to depend on the content of the scorm package. Otherwise,
        served media files might become stale when the package is update.
        """
        return os.path.join(self.extract_folder_base_path,
                            self.package_meta["sha1"])

    @property
    def extract_folder_base_path(self):
        """
        Path to the folder where packages will be extracted.
        """
        return os.path.join(self.scorm_location(), self.location.block_id)

    @XBlock.json_handler
    def scorm_get_value(self, data, _suffix):
        name = data.get("name")
        if name in ["cmi.core.lesson_status", "cmi.completion_status"]:
            return {"value": self.lesson_status}
        if name == "cmi.success_status":
            return {"value": self.success_status}
        if name in ["cmi.core.score.raw", "cmi.score.raw"]:
            return {"value": self.lesson_score * 100}
        return {"value": self.scorm_data.get(name, "")}

    @XBlock.json_handler
    def scorm_set_value(self, data, _suffix):
        context = {"result": "success"}
        name = data.get("name")

        if name in ["cmi.core.lesson_status", "cmi.completion_status"]:
            self.lesson_status = data.get("value")
            if self.has_score and data.get("value") in [
                    "completed",
                    "failed",
                    "passed",
            ]:
                self.publish_grade()
                context.update({"lesson_score": self.lesson_score})
        elif name == "cmi.success_status":
            self.success_status = data.get("value")
            if self.has_score:
                if self.success_status == "unknown":
                    self.lesson_score = 0
                self.publish_grade()
                context.update({"lesson_score": self.lesson_score})
        elif name in ["cmi.core.score.raw", "cmi.score.raw"
                      ] and self.has_score:
            self.lesson_score = float(data.get("value", 0)) / 100.0
            self.publish_grade()
            context.update({"lesson_score": self.lesson_score})
        else:
            self.scorm_data[name] = data.get("value", "")

        context.update({"completion_status": self.get_completion_status()})
        return context

    def publish_grade(self):
        self.runtime.publish(
            self,
            "grade",
            {
                "value": self.get_grade(),
                "max_value": self.weight
            },
        )

    def get_grade(self):
        lesson_score = self.lesson_score
        if self.lesson_status == "failed" or (
                self.scorm_version == "SCORM_2004"
                and self.success_status in ["failed", "unknown"]):
            lesson_score = 0
        return lesson_score * self.weight

    def set_score(self, score):
        """
        Utility method used to rescore a problem.
        """
        self.lesson_score = score.raw_earned / self.weight

    def max_score(self):
        """
        Return the maximum score possible.
        """
        return self.weight if self.has_score else None

    def update_package_meta(self, package_file):
        self.package_meta["sha1"] = self.get_sha1(package_file)
        self.package_meta["name"] = package_file.name
        self.package_meta["last_updated"] = timezone.now().strftime(
            DateTime.DATETIME_FORMAT)
        self.package_meta["size"] = package_file.seek(0, 2)
        package_file.seek(0)

    def update_package_fields(self):
        """
        Update version and index page path fields.
        """
        self.index_page_path = ""
        imsmanifest_path = os.path.join(self.extract_folder_path,
                                        "imsmanifest.xml")
        try:
            imsmanifest_file = default_storage.open(imsmanifest_path)
        except IOError:
            raise ScormError(
                "Invalid package: could not find 'imsmanifest.xml' file at the root of the zip file"
            )
        else:
            tree = ET.parse(imsmanifest_file)
            imsmanifest_file.seek(0)
            self.index_page_path = "index.html"
            namespace = ""
            for _, node in ET.iterparse(imsmanifest_file, events=["start-ns"]):
                if node[0] == "":
                    namespace = node[1]
                    break
            root = tree.getroot()

            if namespace:
                resource = root.find(
                    "{{{0}}}resources/{{{0}}}resource".format(namespace))
                schemaversion = root.find(
                    "{{{0}}}metadata/{{{0}}}schemaversion".format(namespace))
            else:
                resource = root.find("resources/resource")
                schemaversion = root.find("metadata/schemaversion")

            if resource:
                self.index_page_path = resource.get("href")
            if (schemaversion is not None) and (re.match(
                    "^1.2$", schemaversion.text) is None):
                self.scorm_version = "SCORM_2004"
            else:
                self.scorm_version = "SCORM_12"

    def get_completion_status(self):
        completion_status = self.lesson_status
        if self.scorm_version == "SCORM_2004" and self.success_status != "unknown":
            completion_status = self.success_status
        return completion_status

    def scorm_location(self):
        """
        Unzipped files will be stored in a media folder with this name, and thus
        accessible at a url with that also includes this name.
        """
        default_scorm_location = "scorm"
        settings_service = self.runtime.service(self, "settings")
        if not settings_service:
            return default_scorm_location
        xblock_settings = settings_service.get_settings_bucket(self)
        return xblock_settings.get("LOCATION", default_scorm_location)

    @staticmethod
    def get_sha1(file_descriptor):
        """
        Get file hex digest (fingerprint).
        """
        block_size = 8 * 1024
        sha1 = hashlib.sha1()
        while True:
            block = file_descriptor.read(block_size)
            if not block:
                break
            sha1.update(block)
        file_descriptor.seek(0)
        return sha1.hexdigest()

    def student_view_data(self):
        """
        Inform REST api clients about original file location and it's "freshness".
        Make sure to include `student_view_data=openedxscorm` to URL params in the request.
        """
        if self.index_page_url:
            return {
                "last_modified": self.package_meta.get("last_updated", ""),
                "scorm_data": default_storage.url(self.package_path),
                "size": self.package_meta.get("size", 0),
                "index_page": self.index_page_path,
            }
        return {}

    @staticmethod
    def workbench_scenarios():
        """A canned scenario for display in the workbench."""
        return [
            (
                "ScormXBlock",
                """<vertical_demo>
                <openedxscorm/>
                </vertical_demo>
             """,
            ),
        ]
Esempio n. 28
0
class DashboardBlock(StudioEditableXBlockMixin, ExportMixin, XBlock):
    """
    A block to summarize self-assessment results.
    """
    display_name = String(
        display_name=_("Display Name"),
        help=_("Display name for this module"),
        scope=Scope.settings,
        default=_('Self-Assessment Summary'),
    )
    mentoring_ids = List(
        display_name=_("Mentoring Blocks"),
        help=_(
            "This should be an ordered list of the url_names of each mentoring block whose multiple choice question "
            "values are to be shown on this dashboard. The list should be in JSON format. Example: {example_here}"
        ).format(example_here='["2754b8afc03a439693b9887b6f1d9e36", "215028f7df3d4c68b14fb5fea4da7053"]'),
        scope=Scope.settings,
    )
    exclude_questions = Dict(
        display_name=_("Questions to be hidden"),
        help=_(
            "Optional rules to exclude specific questions both from displaying in dashboard and from the calculated "
            "average. Rules must start with the url_name of a mentoring block, followed by list of question numbers "
            "to exclude. Rule set must be in JSON format. Question numbers are one-based (the first question being "
            "number 1). Must be in JSON format. Examples: {examples_here}"
        ).format(
            examples_here='{"2754b8afc03a439693b9887b6f1d9e36":[1,2], "215028f7df3d4c68b14fb5fea4da7053":[1,5]}'
        ),
        scope=Scope.content,
        multiline_editor=True,
        resettable_editor=False,
    )
    color_rules = String(
        display_name=_("Color Coding Rules"),
        help=_(
            "Optional rules to assign colors to possible answer values and average values. "
            "One rule per line. First matching rule will be used. Light colors are recommended. "
            "Examples: {examples_here}"
        ).format(examples_here='"1: LightCoral", "0 <= x < 5: LightBlue", "LightGreen"'),
        scope=Scope.content,
        default="",
        multiline_editor=True,
        resettable_editor=False,
    )
    visual_rules = String(
        display_name=_("Visual Representation"),
        default="",
        help=_("Optional: Enter the JSON configuration of the visual representation desired (Advanced)."),
        scope=Scope.content,
        multiline_editor=True,
        resettable_editor=False,
    )
    visual_title = String(
        display_name=_("Visual Representation Title"),
        default=_("Visual Representation"),
        help=_("This text is not displayed visually but is exposed to screen reader users who may not see the image."),
        scope=Scope.content,
    )
    visual_desc = String(
        display_name=_("Visual Repr. Description"),
        default=_("The data represented in this image is available in the tables below."),
        help=_(
            "This longer description is not displayed visually but is exposed to screen reader "
            "users who may not see the image."
        ),
        scope=Scope.content,
    )
    average_labels = Dict(
        display_name=_("Label for average value"),
        help=_(
            "This settings allows overriding label for the calculated average per mentoring block. Must be in JSON "
            "format. Examples: {examples_here}."
        ).format(
            examples_here='{"2754b8afc03a439693b9887b6f1d9e36": "Avg.", "215028f7df3d4c68b14fb5fea4da7053": "Mean"}'
        ),
        scope=Scope.content,
    )
    show_numbers = Boolean(
        display_name=_("Display values"),
        default=True,
        help=_("Toggles if numeric values are displayed"),
        scope=Scope.content
    )
    header_html = String(
        display_name=_("Header HTML"),
        default="",
        help=_("Custom text to include at the beginning of the report."),
        multiline_editor="html",
        resettable_editor=False,
        scope=Scope.content,
    )
    footer_html = String(
        display_name=_("Footer HTML"),
        default="",
        help=_("Custom text to include at the end of the report."),
        multiline_editor="html",
        resettable_editor=False,
        scope=Scope.content,
    )

    editable_fields = (
        'display_name', 'mentoring_ids', 'exclude_questions', 'average_labels', 'show_numbers',
        'color_rules', 'visual_rules', 'visual_title', 'visual_desc', 'header_html', 'footer_html',
    )
    css_path = 'public/css/dashboard.css'
    js_path = 'public/js/review_blocks.js'

    def get_mentoring_blocks(self, mentoring_ids, ignore_errors=True):
        """
        Generator returning the specified mentoring blocks, in order.

        Will yield None for every invalid mentoring block ID, or if
        ignore_errors is False, will raise InvalidUrlName.
        """
        for url_name in mentoring_ids:
            try:
                mentoring_id = self.scope_ids.usage_id.course_key.make_usage_key('problem-builder', url_name)
                yield self.runtime.get_block(mentoring_id)
            except Exception:  # Catch-all b/c we could get XBlockNotFoundError, ItemNotFoundError, InvalidKeyError, ...
                # Maybe it's using the deprecated block type "mentoring":
                try:
                    mentoring_id = self.scope_ids.usage_id.course_key.make_usage_key('mentoring', url_name)
                    yield self.runtime.get_block(mentoring_id)
                except Exception:
                    if ignore_errors:
                        yield None
                    else:
                        raise InvalidUrlName(url_name)

    def parse_color_rules_str(self, color_rules_str, ignore_errors=True):
        """
        Parse the color rules. Returns a list of ColorRule objects.

        Color rules are like: "0 < x < 4: red" or "blue" (for a catch-all rule)
        """
        rules = []
        for lineno, line in enumerate(color_rules_str.splitlines()):
            line = line.strip()
            if line:
                try:
                    if ":" in line:
                        condition, value = line.split(':')
                        value = value.strip()
                        if condition.isnumeric():  # A condition just listed as an exact value
                            condition = "x == " + condition
                    else:
                        condition = "1"  # Always true
                        value = line
                    rules.append(ColorRule(condition, value))
                except ValueError:
                    if ignore_errors:
                        continue
                    raise ValueError(
                        _("Invalid color rule on line {line_number}").format(line_number=lineno + 1)
                    )
        return rules

    @lazy
    def color_rules_parsed(self):
        """
        Caching property to get parsed color rules. Returns a list of ColorRule objects.
        """
        return self.parse_color_rules_str(self.color_rules) if self.color_rules else []

    def _get_submission_key(self, usage_key):
        """
        Given the usage_key of an MCQ block, get the dict key needed to look it up with the
        submissions API.
        """
        return dict(
            student_id=self.runtime.anonymous_student_id,
            course_id=six.text_type(usage_key.course_key),
            item_id=six.text_type(usage_key),
            item_type=usage_key.block_type,
        )

    def color_for_value(self, value):
        """ Given a string value, get the color rule that matches, if any """
        if isinstance(value, six.string_types):
            if value.isnumeric():
                value = float(value)
            else:
                return None
        for rule in self.color_rules_parsed:
            if rule.matches(value):
                return rule.color_str
        return None

    def _get_problem_questions(self, mentoring_block):
        """ Generator returning only children of specified block that are MCQs """
        for child_id in mentoring_block.children:
            if child_isinstance(mentoring_block, child_id, MCQBlock):
                yield child_id

    @XBlock.supports("multi_device")  # Mark as mobile-friendly
    def student_view(self, context=None):  # pylint: disable=unused-argument
        """
        Standard view of this XBlock.
        """
        if not self.mentoring_ids:
            return Fragment(u"<h1>{}</h1><p>{}</p>".format(self.display_name, _("Not configured.")))

        blocks = []
        for mentoring_block in self.get_mentoring_blocks(self.mentoring_ids):
            if mentoring_block is None:
                continue
            block = {
                'display_name': mentoring_block.display_name,
                'mcqs': []
            }
            try:
                hide_questions = self.exclude_questions.get(mentoring_block.url_name, [])
            except Exception:  # pylint: disable=broad-except-clause
                log.exception("Cannot parse exclude_questions setting - probably malformed: %s", self.exclude_questions)
                hide_questions = []

            for question_number, child_id in enumerate(self._get_problem_questions(mentoring_block), 1):
                try:
                    if question_number in hide_questions:
                        continue
                except TypeError:
                    log.exception(
                        "Cannot check question number - expected list of ints got: %s",
                        hide_questions
                    )

                # Get the student's submitted answer to this MCQ from the submissions API:
                mcq_block = self.runtime.get_block(child_id)
                mcq_submission_key = self._get_submission_key(child_id)
                try:
                    value = sub_api.get_submissions(mcq_submission_key, limit=1)[0]["answer"]
                except IndexError:
                    value = None

                block['mcqs'].append({
                    "display_name": mcq_block.display_name_with_default,
                    "value": value,
                    "accessible_value": _("Score: {score}").format(score=value) if value else _("No value yet"),
                    "color": self.color_for_value(value) if value is not None else None,
                })
            # If the values are numeric, display an average:
            numeric_values = [
                float(mcq['value']) for mcq in block['mcqs']
                if mcq['value'] is not None and mcq['value'].isnumeric()
            ]
            if numeric_values:
                average_value = sum(numeric_values) / len(numeric_values)
                block['average'] = average_value
                # average block is shown only if average value exists, so accessible text for no data is not required
                block['accessible_average'] = _("Score: {score}").format(
                    score=floatformat(average_value)
                )
                block['average_label'] = self.average_labels.get(mentoring_block.url_name, _("Average"))
                block['has_average'] = True
                block['average_color'] = self.color_for_value(average_value)
            blocks.append(block)

        visual_repr = None
        if self.visual_rules:
            try:
                rules_parsed = json.loads(self.visual_rules)
            except ValueError:
                pass  # JSON errors should be shown as part of validation
            else:
                visual_repr = DashboardVisualData(
                    blocks, rules_parsed, self.color_for_value, self.visual_title, self.visual_desc
                )

        report_template = loader.render_django_template('templates/html/dashboard_report.html', {
            'title': self.display_name,
            'css': loader.load_unicode(self.css_path),
            'student_name': self._get_user_full_name(),
            'course_name': self._get_course_name(),
        })

        html = loader.render_django_template('templates/html/dashboard.html', {
            'blocks': blocks,
            'display_name': self.display_name,
            'visual_repr': visual_repr,
            'show_numbers': self.show_numbers,
            'header_html': self.header_html,
            'footer_html': self.footer_html,
        })

        fragment = Fragment(html)
        fragment.add_css_url(self.runtime.local_resource_url(self, self.css_path))
        fragment.add_javascript_url(self.runtime.local_resource_url(self, self.js_path))
        fragment.initialize_js(
            'PBDashboardBlock', {
                'reportTemplate': report_template,
                'reportContentSelector': '.dashboard-report'
            })
        return fragment

    def validate_field_data(self, validation, data):
        """
        Validate this block's field data.
        """
        super(DashboardBlock, self).validate_field_data(validation, data)

        def add_error(msg):
            validation.add(ValidationMessage(ValidationMessage.ERROR, msg))

        try:
            list(self.get_mentoring_blocks(data.mentoring_ids, ignore_errors=False))
        except InvalidUrlName as e:
            add_error(_(u'Invalid block url_name given: "{bad_url_name}"').format(bad_url_name=six.text_type(e)))

        if data.exclude_questions:
            for key, value in six.iteritems(data.exclude_questions):
                if not isinstance(value, list):
                    add_error(
                        _(u"'Questions to be hidden' is malformed: value for key {key} is {value}, "
                          u"expected list of integers")
                        .format(key=key, value=value)
                    )

                if key not in data.mentoring_ids:
                    add_error(
                        _(u"'Questions to be hidden' is malformed: mentoring url_name {url_name} "
                          u"is not added to Dashboard")
                        .format(url_name=key)
                    )

        if data.average_labels:
            for key, value in six.iteritems(data.average_labels):
                if not isinstance(value, six.string_types):
                    add_error(
                        _(u"'Label for average value' is malformed: value for key {key} is {value}, expected string")
                        .format(key=key, value=value)
                    )

                if key not in data.mentoring_ids:
                    add_error(
                        _(u"'Label for average value' is malformed: mentoring url_name {url_name} "
                          u"is not added to Dashboard")
                        .format(url_name=key)
                    )

        if data.color_rules:
            try:
                self.parse_color_rules_str(data.color_rules, ignore_errors=False)
            except ValueError as e:
                add_error(six.text_type(e))

        if data.visual_rules:
            try:
                rules = json.loads(data.visual_rules)
            except ValueError as e:
                add_error(_(u"Visual rules contains an error: {error}").format(error=e))
            else:
                if not isinstance(rules, dict):
                    add_error(_(u"Visual rules should be a JSON dictionary/object: {...}"))
Esempio n. 29
0
class VideoFields(object):
    """Fields for `VideoModule` and `VideoDescriptor`."""
    display_name = String(
        help=_("The name students see. This name appears in the course ribbon and as a header for the video."),
        display_name=_("Component Display Name"),
        default="Video",
        scope=Scope.settings
    )

    saved_video_position = RelativeTime(
        help=_("Current position in the video."),
        scope=Scope.user_state,
        default=datetime.timedelta(seconds=0)
    )
    # TODO: This should be moved to Scope.content, but this will
    # require data migration to support the old video module.
    youtube_id_1_0 = String(
        help=_("Optional, for older browsers: the YouTube ID for the normal speed video."),
        display_name=_("YouTube ID"),
        scope=Scope.settings,
        default="3_yD_cEKoCk"
    )
    youtube_id_0_75 = String(
        help=_("Optional, for older browsers: the YouTube ID for the .75x speed video."),
        display_name=_("YouTube ID for .75x speed"),
        scope=Scope.settings,
        default=""
    )
    youtube_id_1_25 = String(
        help=_("Optional, for older browsers: the YouTube ID for the 1.25x speed video."),
        display_name=_("YouTube ID for 1.25x speed"),
        scope=Scope.settings,
        default=""
    )
    youtube_id_1_5 = String(
        help=_("Optional, for older browsers: the YouTube ID for the 1.5x speed video."),
        display_name=_("YouTube ID for 1.5x speed"),
        scope=Scope.settings,
        default=""
    )
    start_time = RelativeTime(  # datetime.timedelta object
        help=_(
            "Time you want the video to start if you don't want the entire video to play. "
            "Not supported in the native mobile app: the full video file will play. "
            "Formatted as HH:MM:SS. The maximum value is 23:59:59."
        ),
        display_name=_("Video Start Time"),
        scope=Scope.settings,
        default=datetime.timedelta(seconds=0)
    )
    end_time = RelativeTime(  # datetime.timedelta object
        help=_(
            "Time you want the video to stop if you don't want the entire video to play. "
            "Not supported in the native mobile app: the full video file will play. "
            "Formatted as HH:MM:SS. The maximum value is 23:59:59."
        ),
        display_name=_("Video Stop Time"),
        scope=Scope.settings,
        default=datetime.timedelta(seconds=0)
    )
    #front-end code of video player checks logical validity of (start_time, end_time) pair.

    # `source` is deprecated field and should not be used in future.
    # `download_video` is used instead.
    source = String(
        help=_("The external URL to download the video."),
        display_name=_("Download Video"),
        scope=Scope.settings,
        default=""
    )
    download_video = Boolean(
        help=_("Allow students to download versions of this video in different formats if they cannot use the edX video player or do not have access to YouTube. You must add at least one non-YouTube URL in the Video File URLs field."),  # pylint: disable=line-too-long
        display_name=_("Video Download Allowed"),
        scope=Scope.settings,
        default=False
    )
    html5_sources = List(
        help=_("The URL or URLs where you've posted non-YouTube versions of the video. Each URL must end in .mpeg, .mp4, .ogg, or .webm and cannot be a YouTube URL. (For browser compatibility, we strongly recommend .mp4 and .webm format.) Students will be able to view the first listed video that's compatible with the student's computer. To allow students to download these videos, set Video Download Allowed to True."),  # pylint: disable=line-too-long
        display_name=_("Video File URLs"),
        scope=Scope.settings,
    )
    track = String(
        help=_("By default, students can download an .srt or .txt transcript when you set Download Transcript Allowed to True. If you want to provide a downloadable transcript in a different format, we recommend that you upload a handout by using the Upload a Handout field. If this isn't possible, you can post a transcript file on the Files & Uploads page or on the Internet, and then add the URL for the transcript here. Students see a link to download that transcript below the video."),  # pylint: disable=line-too-long
        display_name=_("Downloadable Transcript URL"),
        scope=Scope.settings,
        default=''
    )
    download_track = Boolean(
        help=_("Allow students to download the timed transcript. A link to download the file appears below the video. By default, the transcript is an .srt or .txt file. If you want to provide the transcript for download in a different format, upload a file by using the Upload Handout field."),  # pylint: disable=line-too-long
        display_name=_("Download Transcript Allowed"),
        scope=Scope.settings,
        default=False
    )
    sub = String(
        help=_("The default transcript for the video, from the Default Timed Transcript field on the Basic tab. This transcript should be in English. You don't have to change this setting."),  # pylint: disable=line-too-long
        display_name=_("Default Timed Transcript"),
        scope=Scope.settings,
        default=""
    )
    show_captions = Boolean(
        help=_("Specify whether the transcripts appear with the video by default."),
        display_name=_("Show Transcript"),
        scope=Scope.settings,
        default=True
    )
    # Data format: {'de': 'german_translation', 'uk': 'ukrainian_translation'}
    transcripts = Dict(
        help=_("Add transcripts in different languages. Click below to specify a language and upload an .srt transcript file for that language."),  # pylint: disable=line-too-long
        display_name=_("Transcript Languages"),
        scope=Scope.settings,
        default={}
    )
    transcript_language = String(
        help=_("Preferred language for transcript."),
        display_name=_("Preferred language for transcript"),
        scope=Scope.preferences,
        default="en"
    )
    transcript_download_format = String(
        help=_("Transcript file format to download by user."),
        scope=Scope.preferences,
        values=[
            # Translators: This is a type of file used for captioning in the video player.
            {"display_name": _("SubRip (.srt) file"), "value": "srt"},
            {"display_name": _("Text (.txt) file"), "value": "txt"}
        ],
        default='srt',
    )
    speed = Float(
        help=_("The last speed that the user specified for the video."),
        scope=Scope.user_state
    )
    global_speed = Float(
        help=_("The default speed for the video."),
        scope=Scope.preferences,
        default=1.0
    )
    youtube_is_available = Boolean(
        help=_("Specify whether YouTube is available for the user."),
        scope=Scope.user_info,
        default=True
    )
    handout = String(
        help=_("Upload a handout to accompany this video. Students can download the handout by clicking Download Handout under the video."),  # pylint: disable=line-too-long
        display_name=_("Upload Handout"),
        scope=Scope.settings,
    )
    only_on_web = Boolean(
        help=_(
            "Specify whether access to this video is limited to browsers only, or if it can be "
            "accessed from other applications including mobile apps."
        ),
        display_name=_("Video Available on Web Only"),
        scope=Scope.settings,
        default=False
    )
    edx_video_id = String(
        help=_("If you were assigned a Video ID by edX for the video to play in this component, enter the ID here. In this case, do not enter values in the Default Video URL, the Video File URLs, and the YouTube ID fields. If you were not assigned a Video ID, enter values in those other fields and ignore this field."),  # pylint: disable=line-too-long
        display_name=_("Video ID"),
        scope=Scope.settings,
        default="",
    )
    bumper_last_view_date = DateTime(
        display_name=_("Date of the last view of the bumper"),
        scope=Scope.preferences,
    )
    bumper_do_not_show_again = Boolean(
        display_name=_("Do not show bumper again"),
        scope=Scope.preferences,
        default=False,
    )
Esempio n. 30
0
class PollBlock(
        MakoTemplateBlockBase,
        XmlMixin,
        XModuleToXBlockMixin,
        HTMLSnippet,
        ResourceTemplates,
        XModuleMixin,
):  # pylint: disable=abstract-method
    """Poll Module"""
    # Name of poll to use in links to this poll
    display_name = String(help=_("The display name for this component."),
                          scope=Scope.settings)

    voted = Boolean(help=_("Whether this student has voted on the poll"),
                    scope=Scope.user_state,
                    default=False)
    poll_answer = String(help=_("Student answer"),
                         scope=Scope.user_state,
                         default='')
    poll_answers = Dict(help=_("Poll answers from all students"),
                        scope=Scope.user_state_summary)

    # List of answers, in the form {'id': 'some id', 'text': 'the answer text'}
    answers = List(help=_("Poll answers from xml"),
                   scope=Scope.content,
                   default=[])

    question = String(help=_("Poll question"), scope=Scope.content, default='')

    resources_dir = None
    uses_xmodule_styles_setup = True

    preview_view_js = {
        'js': [
            resource_string(__name__, 'js/src/javascript_loader.js'),
            resource_string(__name__, 'js/src/poll/poll.js'),
            resource_string(__name__, 'js/src/poll/poll_main.js')
        ],
        'xmodule_js':
        resource_string(__name__, 'js/src/xmodule.js'),
    }
    preview_view_css = {
        'scss': [resource_string(__name__, 'css/poll/display.scss')],
    }

    # There is no studio_view() for this XBlock but this is needed to make the
    # the static_content command happy.
    studio_view_js = {
        'js': [],
        'xmodule_js': resource_string(__name__, 'js/src/xmodule.js')
    }

    studio_view_css = {'scss': []}

    def handle_ajax(self, dispatch, data):  # lint-amnesty, pylint: disable=unused-argument
        """Ajax handler.

        Args:
            dispatch: string request slug
            data: dict request data parameters

        Returns:
            json string
        """
        if dispatch in self.poll_answers and not self.voted:
            # FIXME: fix this, when xblock will support mutable types.
            # Now we use this hack.
            temp_poll_answers = self.poll_answers
            temp_poll_answers[dispatch] += 1
            self.poll_answers = temp_poll_answers

            self.voted = True
            self.poll_answer = dispatch
            return json.dumps({
                'poll_answers': self.poll_answers,
                'total': sum(self.poll_answers.values()),
                'callback': {
                    'objectName': 'Conditional'
                }
            })
        elif dispatch == 'get_state':
            return json.dumps({
                'poll_answer': self.poll_answer,
                'poll_answers': self.poll_answers,
                'total': sum(self.poll_answers.values())
            })
        elif dispatch == 'reset_poll' and self.voted and \
                self.xml_attributes.get('reset', 'True').lower() != 'false':
            self.voted = False

            # FIXME: fix this, when xblock will support mutable types.
            # Now we use this hack.
            temp_poll_answers = self.poll_answers
            temp_poll_answers[self.poll_answer] -= 1
            self.poll_answers = temp_poll_answers

            self.poll_answer = ''
            return json.dumps({'status': 'success'})
        else:  # return error message
            return json.dumps({'error': 'Unknown Command!'})

    def student_view(self, _context):
        """
        Renders the student view.
        """
        fragment = Fragment()
        params = {
            'element_id': self.location.html_id(),
            'element_class': self.location.block_type,
            'ajax_url': self.ajax_url,
            'configuration_json': self.dump_poll(),
        }
        fragment.add_content(
            self.runtime.service(self,
                                 'mako').render_template('poll.html', params))
        add_webpack_to_fragment(fragment, 'PollBlockPreview')
        shim_xmodule_js(fragment, 'Poll')
        return fragment

    def dump_poll(self):
        """Dump poll information.

        Returns:
            string - Serialize json.
        """
        # FIXME: hack for resolving caching `default={}` during definition
        # poll_answers field
        if self.poll_answers is None:
            self.poll_answers = {}

        answers_to_json = OrderedDict()

        # FIXME: fix this, when xblock support mutable types.
        # Now we use this hack.
        temp_poll_answers = self.poll_answers

        # Fill self.poll_answers, prepare data for template context.
        for answer in self.answers:
            # Set default count for answer = 0.
            if answer['id'] not in temp_poll_answers:
                temp_poll_answers[answer['id']] = 0
            answers_to_json[answer['id']] = html.escape(answer['text'],
                                                        quote=False)
        self.poll_answers = temp_poll_answers

        return json.dumps({
            'answers':
            answers_to_json,
            'question':
            html.escape(self.question, quote=False),
            # to show answered poll after reload:
            'poll_answer':
            self.poll_answer,
            'poll_answers':
            self.poll_answers if self.voted else {},
            'total':
            sum(self.poll_answers.values()) if self.voted else 0,
            'reset':
            str(self.xml_attributes.get('reset', 'true')).lower()
        })

    _tag_name = 'poll_question'
    _child_tag_name = 'answer'

    @classmethod
    def definition_from_xml(cls, xml_object, system):
        """Pull out the data into dictionary.

        Args:
            xml_object: xml from file.
            system: `system` object.

        Returns:
            (definition, children) - tuple
            definition - dict:
                {
                    'answers': <List of answers>,
                    'question': <Question string>
                }
        """
        # Check for presense of required tags in xml.
        if len(xml_object.xpath(cls._child_tag_name)) == 0:
            raise ValueError("Poll_question definition must include \
                at least one 'answer' tag")

        xml_object_copy = deepcopy(xml_object)
        answers = []
        for element_answer in xml_object_copy.findall(cls._child_tag_name):
            answer_id = element_answer.get('id', None)
            if answer_id:
                answers.append({
                    'id': answer_id,
                    'text': stringify_children(element_answer)
                })
            xml_object_copy.remove(element_answer)

        definition = {
            'answers': answers,
            'question': stringify_children(xml_object_copy)
        }
        children = []

        return (definition, children)

    def definition_to_xml(self, resource_fs):
        """Return an xml element representing to this definition."""
        poll_str = HTML('<{tag_name}>{text}</{tag_name}>').format(
            tag_name=self._tag_name, text=self.question)
        xml_object = etree.fromstring(poll_str)
        xml_object.set('display_name', self.display_name)

        def add_child(xml_obj, answer):  # lint-amnesty, pylint: disable=unused-argument
            # Escape answer text before adding to xml tree.
            answer_text = str(answer['text'])
            child_str = Text('{tag_begin}{text}{tag_end}').format(
                tag_begin=HTML('<{tag_name} id="{id}">').format(
                    tag_name=self._child_tag_name, id=answer['id']),
                text=answer_text,
                tag_end=HTML('</{tag_name}>').format(
                    tag_name=self._child_tag_name))
            child_node = etree.fromstring(child_str)
            xml_object.append(child_node)

        for answer in self.answers:
            add_child(xml_object, answer)

        return xml_object