Beispiel #1
0
class ProblemBlock(XBlock):
    """A generalized container of InputBlocks and Checkers.

    """
    script = String(help="Python code to compute values",
                    scope=Scope.content,
                    default="")
    seed = Integer(help="Random seed for this student",
                   scope=Scope.user_state,
                   default=0)
    problem_attempted = Boolean(help="Has the student attempted this problem?",
                                scope=Scope.user_state,
                                default=False)
    has_children = True

    @classmethod
    def parse_xml(cls, node, runtime, keys, id_generator):
        block = runtime.construct_xblock_from_class(cls, keys)

        # Find <script> children, turn them into script content.
        for child in node:
            if child.tag == "script":
                block.script += child.text
            else:
                block.runtime.add_node_as_child(block, child, id_generator)

        return block

    def set_student_seed(self):
        """Set a random seed for the student so they each have different but repeatable data."""
        # Don't return zero, that's the default, and the sign that we should make a new seed.
        self.seed = int(time.time() * 1000) % 100 + 1

    def calc_context(self, context):
        """If we have a script, run it, and return the resulting context."""
        if self.script:
            # Seed the random number for the student
            if not self.seed:
                self.set_student_seed()
            random.seed(self.seed)
            script_vals = run_script(self.script)
            context = dict(context)
            context.update(script_vals)
        return context

    # The content controls how the Inputs attach to Graders
    def student_view(self, context=None):
        """Provide the default student view."""
        if context is None:
            context = {}

        context = self.calc_context(context)

        result = Fragment()
        named_child_frags = []
        # self.children is an attribute obtained from ChildrenModelMetaclass, so disable the
        # static pylint checking warning about this.
        for child_id in self.children:  # pylint: disable=E1101
            child = self.runtime.get_block(child_id)
            frag = self.runtime.render_child(child, "problem_view", context)
            result.add_frag_resources(frag)
            named_child_frags.append((child.name, frag))
        result.add_css("""
            .problem {
                border: solid 1px #888; padding: 3px;
            }
            """)
        result.add_content(
            self.runtime.render_template("problem.html",
                                         named_children=named_child_frags))
        result.add_javascript("""
            function ProblemBlock(runtime, element) {

                function callIfExists(obj, fn) {
                    if (typeof obj[fn] == 'function') {
                        return obj[fn].apply(obj, Array.prototype.slice.call(arguments, 2));
                    } else {
                        return undefined;
                    }
                }

                function handleCheckResults(results) {
                    $.each(results.submitResults || {}, function(input, result) {
                        callIfExists(runtime.childMap(element, input), 'handleSubmit', result);
                    });
                    $.each(results.checkResults || {}, function(checker, result) {
                        callIfExists(runtime.childMap(element, checker), 'handleCheck', result);
                    });
                }

                // To submit a problem, call all the named children's submit()
                // function, collect their return values, and post that object
                // to the check handler.
                $(element).find('.check').bind('click', function() {
                    var data = {};
                    var children = runtime.children(element);
                    for (var i = 0; i < children.length; i++) {
                        var child = children[i];
                        if (child.name !== undefined) {
                            data[child.name] = callIfExists(child, 'submit');
                        }
                    }
                    var handlerUrl = runtime.handlerUrl(element, 'check')
                    $.post(handlerUrl, JSON.stringify(data)).success(handleCheckResults);
                });

                $(element).find('.rerandomize').bind('click', function() {
                    var handlerUrl = runtime.handlerUrl(element, 'rerandomize');
                    $.post(handlerUrl, JSON.stringify({}));
                });
            }
            """)
        result.initialize_js('ProblemBlock')
        return result

    @XBlock.json_handler
    def check(self, submissions, suffix=''):  # pylint: disable=unused-argument
        """
        Processess the `submissions` with each provided Checker.

        First calls the submit() method on each InputBlock. Then, for each Checker,
        finds the values it needs and passes them to the appropriate `check()` method.

        Returns a dictionary of 'submitResults': {input_name: user_submitted_results},
        'checkResults': {checker_name: results_passed_through_checker}

        """
        self.problem_attempted = True
        context = self.calc_context({})

        child_map = {}
        # self.children is an attribute obtained from ChildrenModelMetaclass, so disable the
        # static pylint checking warning about this.
        for child_id in self.children:  # pylint: disable=E1101
            child = self.runtime.get_block(child_id)
            if child.name:
                child_map[child.name] = child

        # For each InputBlock, call the submit() method with the browser-sent
        # input data.
        submit_results = {}
        for input_name, submission in submissions.items():
            child = child_map[input_name]
            submit_results[input_name] = child.submit(submission)
            child.save()

        # For each Checker, find the values it wants, and pass them to its
        # check() method.
        checkers = list(self.runtime.querypath(self, "./checker"))
        check_results = {}
        for checker in checkers:
            arguments = checker.arguments
            kwargs = {}
            kwargs.update(arguments)
            for arg_name, arg_value in arguments.items():
                if arg_value.startswith("."):
                    values = list(self.runtime.querypath(self, arg_value))
                    # TODO: What is the specific promised semantic of the iterability
                    # of the value returned by querypath?
                    kwargs[arg_name] = values[0]
                elif arg_value.startswith("$"):
                    kwargs[arg_name] = context.get(arg_value[1:])
                elif arg_value.startswith("="):
                    kwargs[arg_name] = int(arg_value[1:])
                else:
                    raise ValueError(
                        "Couldn't interpret checker argument: %r" % arg_value)
            result = checker.check(**kwargs)
            if checker.name:
                check_results[checker.name] = result

        return {
            'submitResults': submit_results,
            'checkResults': check_results,
        }

    @XBlock.json_handler
    def rerandomize(self, unused, suffix=''):  # pylint: disable=unused-argument
        """Set a new random seed for the student."""
        self.set_student_seed()
        return {'status': 'ok'}

    @staticmethod
    def workbench_scenarios():
        """A few canned scenarios for display in the workbench."""
        return [
            ("problem with thumbs and textbox", """\
                <problem_demo>
                    <html_demo>
                        <p>You have three constraints to satisfy:</p>
                        <ol>
                            <li>The upvotes and downvotes must be equal.</li>
                            <li>You must enter the number of upvotes into the text field.</li>
                            <li>The number of upvotes must be $numvotes.</li>
                        </ol>
                    </html_demo>

                    <thumbs name='thumb'/>
                    <textinput_demo name='vote_count' input_type='int'/>

                    <script>
                        # Compute the random answer.
                        import random
                        numvotes = random.randrange(2,5)
                    </script>
                    <equality_demo name='votes_equal' left='./thumb/@upvotes' right='./thumb/@downvotes'>
                        Upvotes match downvotes
                    </equality_demo>
                    <equality_demo name='votes_named' left='./thumb/@upvotes' right='./vote_count/@student_input'>
                        Number of upvotes matches entered string
                    </equality_demo>
                    <equality_demo name='votes_specified' left='./thumb/@upvotes' right='$numvotes'>
                        Number of upvotes is $numvotes
                    </equality_demo>
                </problem_demo>
             """),
            ("three problems 2", """
                <vertical_demo>
                    <attempts_scoreboard_demo/>
                    <problem_demo>
                        <html_demo><p>What is $a+$b?</p></html_demo>
                        <textinput_demo name="sum_input" input_type="int" />
                        <equality_demo name="sum_checker" left="./sum_input/@student_input" right="$c" />
                        <script>
                            import random
                            a = random.randint(2, 5)
                            b = random.randint(1, 4)
                            c = a + b
                        </script>
                    </problem_demo>

                    <sidebar_demo>
                        <problem_demo>
                            <html_demo><p>What is $a &#215; $b?</p></html_demo>
                            <textinput_demo name="sum_input" input_type="int" />
                            <equality_demo name="sum_checker" left="./sum_input/@student_input" right="$c" />
                            <script>
                                import random
                                a = random.randint(2, 6)
                                b = random.randint(3, 7)
                                c = a * b
                            </script>
                        </problem_demo>
                    </sidebar_demo>

                    <problem_demo>
                        <html_demo><p>What is $a+$b?</p></html_demo>
                        <textinput_demo name="sum_input" input_type="int" />
                        <equality_demo name="sum_checker" left="./sum_input/@student_input" right="$c" />
                        <script>
                            import random
                            a = random.randint(3, 5)
                            b = random.randint(2, 6)
                            c = a + b
                        </script>
                    </problem_demo>
                </vertical_demo>
             """),
        ]
class VideoFields(object):
    """Fields for `VideoModule` and `VideoDescriptor`."""
    display_name = String(display_name="Display Name",
                          help="Display name for this module.",
                          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="This is the Youtube ID reference for the normal speed video.",
        display_name="Youtube ID",
        scope=Scope.settings,
        default="OEoXaMPEzfM")
    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="Start time for the video (HH:MM:SS). Max value is 23:59:59.",
        display_name="Start Time",
        scope=Scope.settings,
        default=datetime.timedelta(seconds=0))
    end_time = RelativeTime(  # datetime.timedelta object
        help="End time for the video (HH:MM:SS). Max value is 23:59:59.",
        display_name="End 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=
        "Show a link beneath the video to allow students to download the video. Note: You must add at least one video source below.",
        display_name="Video Download Allowed",
        scope=Scope.settings,
        default=False)
    html5_sources = List(
        help=
        "A list of filenames to be used with HTML5 video. The first supported filetype will be displayed.",
        display_name="Video Sources",
        scope=Scope.settings,
    )
    track = String(
        help=
        "The external URL to download the timed transcript track. This appears as a link beneath the video.",
        display_name="Download Transcript",
        scope=Scope.settings,
        default='')
    download_track = Boolean(
        help=
        "Show a link beneath the video to allow students to download the transcript. Note: You must add a link to the HTML5 Transcript field above.",
        display_name="Transcript Download Allowed",
        scope=Scope.settings,
        default=False)
    sub = String(
        help="The name of the timed transcript track (for non-Youtube videos).",
        display_name="Transcript (primary)",
        scope=Scope.settings,
        default="")
    show_captions = Boolean(
        help="This controls whether or not captions are shown by default.",
        display_name="Transcript Display",
        scope=Scope.settings,
        default=True)
    # Data format: {'de': 'german_translation', 'uk': 'ukrainian_translation'}
    transcripts = Dict(help="Add additional transcripts in other languages",
                       display_name="Transcript Translations",
                       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=[{
            "display_name": "SubRip (.srt) file",
            "value": "srt"
        }, {
            "display_name": "Text (.txt) file",
            "value": "txt"
        }],
        default='srt',
    )
    speed = Float(
        help="The last speed that was explicitly set by user for the video.",
        scope=Scope.user_state,
    )
    global_speed = Float(
        help=
        "Default speed in cases when speed wasn't explicitly for specific video",
        scope=Scope.preferences,
        default=1.0)
Beispiel #3
0
class AssetsDownloadMixin:
    """
    Mixin providing utility functions and handler to download captions and transcripts from Wistia.

    The utility mixin is heavily depending on the media ID property provided by the XBlock.
    """

    __slots__ = ("media_id", )

    access_token = String(
        default='',
        display_name=_('Wistia API key'),
        help=_(
            'The API key related to the account where the video uploaded to.'),
        scope=Scope.content,
    )

    show_captions_download = Boolean(
        default=False,
        display_name=_('Captions download button visible'),
        help=_('Show download captions button.'),
        scope=Scope.content,
    )

    show_transcripts_download = Boolean(
        default=True,
        display_name=_('Transcripts download button visible'),
        help=_('Show download transcripts button.'),
        scope=Scope.content,
    )

    asset_download_editable_fields = (
        'access_token',
        'show_captions_download',
        'show_transcripts_download',
    )

    def __send_request(self, url):
        """
        Send a request to Wistia API using the given access token if provided and return the
        response as a parsed JSON string.
        """

        return requests.get(url, params={
            "access_token": self.access_token
        }).json()

    @property
    def has_access_token(self):
        return bool(self.access_token)

    @staticmethod
    def __compress_captions(srt_files):
        """
        Compress files into a zip file.
        """

        zip_file = tempfile.NamedTemporaryFile(prefix="captions_",
                                               suffix=".zip",
                                               delete=False)

        with ZipFile(zip_file.name, mode="w",
                     compression=ZIP_DEFLATED) as compressed:
            for srt_file in srt_files:
                compressed.write(srt_file, srt_file.split("/")[-1])

        return Path(zip_file.name)

    @staticmethod
    def __save_caption(caption):
        """
        Save the caption to a temporary file and return its absolute path.
        """

        language = caption["language"]
        content = caption['text']

        srt_file = tempfile.NamedTemporaryFile(prefix="{}_".format(language),
                                               suffix=".srt",
                                               delete=False)

        srt_file.write(bytes(content, encoding="UTF-8"))
        srt_file.close()

        return srt_file.name

    @XBlock.handler
    def download_captions(self, request, suffix=""):
        """
        Handle captions download.

        Get captions text available for the media ID and save them in separate files. The saved
        captions will be compressed and prepared for download in the response.
        """

        response = self.__send_request(
            "https://api.wistia.com/v1/medias/{}/captions.json".format(
                self.media_id), )

        srt_files = map(self.__save_caption, response)
        zip_file = self.__compress_captions(srt_files)
        map(os.unlink, srt_files)

        return Response(
            body=zip_file.read_bytes(),
            headerlist=[
                ("Content-Type", "application/zip; charset=UTF-8"),
                ("Content-Disposition",
                 "attachment; filename=captions_{}.zip".format(
                     self.media_id, )),
            ],
        )
Beispiel #4
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."
    ),
                             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."
          ),
        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."
    ),
                   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."
    ),
                             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."
    ),
                 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."
    ),
                       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."
          ),
        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="",
    )
Beispiel #5
0
class HtmlBlock(object):
    """
    This will eventually subclass XBlock and merge HtmlModule and HtmlDescriptor
    into one. For now, it's a place to put the pieces that are already sharable
    between the two (field information and XBlock handlers).
    """
    display_name = String(
        display_name=_("Display Name"),
        help=_("The display name for this component."),
        scope=Scope.settings,
        # it'd be nice to have a useful default but it screws up other things; so,
        # use display_name_with_default for those
        default=_("Text"))
    data = String(help=_("Html contents to display for this module"),
                  default=u"",
                  scope=Scope.content)
    source_code = String(help=_(
        "Source code for LaTeX documents. This feature is not well-supported."
    ),
                         scope=Scope.settings)
    use_latex_compiler = Boolean(help=_("Enable LaTeX templates?"),
                                 default=False,
                                 scope=Scope.settings)
    editor = String(help=_(
        "Select Visual to enter content and have the editor automatically create the HTML. Select Raw to edit "
        "HTML directly. If you change this setting, you must save the component and then re-open it for editing."
    ),
                    display_name=_("Editor"),
                    default="visual",
                    values=[{
                        "display_name": _("Visual"),
                        "value": "visual"
                    }, {
                        "display_name": _("Raw"),
                        "value": "raw"
                    }],
                    scope=Scope.settings)

    ENABLE_HTML_XBLOCK_STUDENT_VIEW_DATA = 'ENABLE_HTML_XBLOCK_STUDENT_VIEW_DATA'

    @XBlock.supports("multi_device")
    def student_view(self, _context):
        """
        Return a fragment that contains the html for the student view
        """
        return Fragment(self.get_html())

    @XBlock.supports("multi_device")
    def public_view(self, context):
        """
        Returns a fragment that contains the html for the preview view
        """
        return self.student_view(context)

    def student_view_data(self, context=None):  # pylint: disable=unused-argument
        """
        Return a JSON representation of the student_view of this XBlock.
        """
        if getattr(settings, 'FEATURES',
                   {}).get(self.ENABLE_HTML_XBLOCK_STUDENT_VIEW_DATA, False):
            return {'enabled': True, 'html': self.get_html()}
        else:
            return {
                'enabled':
                False,
                'message':
                'To enable, set FEATURES["{}"]'.format(
                    self.ENABLE_HTML_XBLOCK_STUDENT_VIEW_DATA)
            }

    def get_html(self):
        """ Returns html required for rendering XModule. """

        # When we switch this to an XBlock, we can merge this with student_view,
        # but for now the XModule mixin requires that this method be defined.
        # pylint: disable=no-member
        if self.data is not None and getattr(
                self.system, 'anonymous_student_id', None) is not None:
            return self.data.replace("%%USER_ID%%",
                                     self.system.anonymous_student_id)
        return self.data
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,
    )
    course_edit_method = String(
        help="Method with which this course is edited.",
        default="Studio",
        scope=Scope.settings)
    giturl = String(
        help="url root for course data git repository",
        scope=Scope.settings,
    )
    xqa_key = String(help="DO NOT USE", scope=Scope.settings)
    annotation_storage_url = String(
        help="Location of Annotation backend",
        scope=Scope.settings,
        default="http://your_annotation_storage.com",
        display_name="Url for Annotation Storage")
    annotation_token_secret = String(
        help="Secret string for annotation storage",
        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(
        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)
    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."),
        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 by this course for the specified duration. "
        "Please do not share the API key with other courses and notify MathWorks immediately "
        "if you believe the key is exposed or compromised. To obtain a key for your course, "
        "or to report and 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(
        help=
        "The list of group configurations for partitioning students in content experiments.",
        default=[],
        scope=Scope.settings)
Beispiel #7
0
class VideoFields(object):
    """Fields for `VideoModule` and `VideoDescriptor`."""
    display_name = String(display_name="Display Name",
                          help="Display name for this module.",
                          default="Video",
                          scope=Scope.settings)
    position = Integer(help="Current position in the video",
                       scope=Scope.user_state,
                       default=0)
    show_captions = Boolean(
        help="This controls whether or not captions are shown by default.",
        display_name="Show Transcript",
        scope=Scope.settings,
        default=True)
    # 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="This is the Youtube ID reference for the normal speed video.",
        display_name="Youtube ID",
        scope=Scope.settings,
        default="OEoXaMPEzfM")
    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="Start time for the video (HH:MM:SS). Max value is 23:59:59.",
        display_name="Start Time",
        scope=Scope.settings,
        default=datetime.timedelta(seconds=0))
    end_time = RelativeTime(  # datetime.timedelta object
        help="End time for the video (HH:MM:SS). Max value is 23:59:59.",
        display_name="End 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 = String(
        help=
        "The external URL to download the video. This appears as a link beneath the video.",
        display_name="Download Video",
        scope=Scope.settings,
        default="")
    html5_sources = List(
        help=
        "A list of filenames to be used with HTML5 video. The first supported filetype will be displayed.",
        display_name="Video Sources",
        scope=Scope.settings,
    )
    track = String(
        help=
        "The external URL to download the timed transcript track. This appears as a link beneath the video.",
        display_name="Download Transcript",
        scope=Scope.settings,
        default="")
    sub = String(
        help="The name of the timed transcript track (for non-Youtube videos).",
        display_name="HTML5 Transcript",
        scope=Scope.settings,
        default="")
Beispiel #8
0
class PdfBlock(ScorableXBlockMixin, XBlock, XBlockWithSettingsMixin,
               ThemableXBlockMixin):
    '''
    Icon of the XBlock. Values : [other (default), video, problem]
    '''
    icon_class = "other"
    '''
    Fields
    '''
    display_name = String(
        display_name=_("Display Name"),
        default=_("PDF"),
        scope=Scope.settings,
        help=
        _("This name appears in the horizontal navigation at the top of the page."
          ))

    url = String(
        display_name=_("PDF URL"),
        default=_("http://tutorial.math.lamar.edu/pdf/Trig_Cheat_Sheet.pdf"),
        scope=Scope.content,
        help=_("The URL for your PDF."))

    allow_download = Boolean(display_name=_("PDF Download Allowed"),
                             default=True,
                             scope=Scope.content,
                             help=_("Display a download button for this PDF."))

    source_text = String(
        display_name=_("Source document button text"),
        default="",
        scope=Scope.content,
        help=_(
            "Add a download link for the source file of your PDF. "
            "Use it for example to provide the PowerPoint file used to create this PDF."
        ))

    source_url = String(
        display_name=_("Source document URL"),
        default="",
        scope=Scope.content,
        help=_(
            "Add a download link for the source file of your PDF. "
            "Use it for example to provide the PowerPoint file used to create this PDF."
        ))
    '''
    Util functions
    '''
    def load_resource(self, resource_path):
        """
        Gets the content of a resource
        """
        resource_content = pkg_resources.resource_string(
            __name__, resource_path)
        return resource_content.decode("utf8")

    def render_template(self, template_path, context={}):
        """
        Evaluate a template by resource path, applying the provided context
        """
        template_str = self.load_resource(template_path)
        return Template(template_str).render(Context(context))

    '''
    Main functions
    '''

    def student_view(self, context=None):
        """
        The primary view of the XBlock, shown to students
        when viewing courses.
        """
        context = {
            'display_name': self.display_name,
            'url': self.url,
            'allow_download': self.allow_download,
            'source_text': self.source_text,
            'source_url': self.source_url,
            '_i18n_service': self.i18n_service,
            'LMS_ROOT_URL': settings.LMS_ROOT_URL,
            'user': self.get_real_user(),
        }
        html = loader.render_django_template(
            'templates/html/pdf_view.html',
            context=context,
            i18n_service=self.i18n_service,
        )

        event_type = 'edx.pdf.loaded'
        event_data = {
            'url': self.url,
            'source_url': self.source_url,
        }
        self.runtime.publish(self, event_type, event_data)
        frag = Fragment(html)
        frag.add_javascript(self.load_resource("static/js/pdf_view.js"))
        frag.initialize_js('pdfXBlockInitView')
        return frag

    def studio_view(self, context=None):
        """
        The secondary view of the XBlock, shown to teachers
        when editing the XBlock.
        """
        context = {
            'display_name':
            self.display_name,
            'name_help':
            _("This name appears in the horizontal navigation at the top of the page."
              ),
            'url':
            self.url,
            'allow_download':
            self.allow_download,
            'source_text':
            self.source_text,
            'source_url':
            self.source_url
        }
        html = loader.render_django_template(
            'templates/html/pdf_edit.html',
            context=context,
            i18n_service=self.i18n_service,
        )
        frag = Fragment(html)
        frag.add_javascript(self.load_resource("static/js/pdf_edit.js"))
        frag.initialize_js('pdfXBlockInitEdit')
        return frag

    @XBlock.json_handler
    def on_download(self, data, suffix=''):
        """
        The download file event handler
        """
        event_type = 'edx.pdf.downloaded'
        event_data = {
            'url': self.url,
            'source_url': self.source_url,
        }
        self.runtime.publish(self, event_type, event_data)

    @XBlock.json_handler
    def save_pdf(self, data, suffix=''):
        """
        The saving handler.
        """
        self.display_name = data['display_name']
        self.url = data['url']
        self.allow_download = True if data[
            'allow_download'] == "True" else False  # Str to Bool translation
        self.source_text = data['source_text']
        self.source_url = data['source_url']

        return {
            'result': 'success',
        }

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

    def get_real_user(self):
        """returns session user"""
        try:
            return self.runtime.get_real_user(
                self.xmodule_runtime.anonymous_student_id)
        except Exception:
            return None
Beispiel #9
0
class pdfXBlock(XBlock):

    '''
    Icon of the XBlock. Values : [other (default), video, problem]
    '''
    icon_class = "other"

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

    url = String(display_name="PDF URL",
        default="http://tutorial.math.lamar.edu/pdf/Trig_Cheat_Sheet.pdf",
        scope=Scope.content,
        help="The URL for your PDF.")
    
    allow_download = Boolean(display_name="PDF Download Allowed",
        default=True,
        scope=Scope.content,
        help="Display a download button for this PDF.")
    
    source_text = String(display_name="Source document button text",
        default="",
        scope=Scope.content,
        help="Add a download link for the source file of your PDF. Use it for example to provide the PowerPoint file used to create this PDF.")
    
    source_url = String(display_name="Source document URL",
        default="",
        scope=Scope.content,
        help="Add a download link for the source file of your PDF. Use it for example to provide the PowerPoint file used to create this PDF.")

    '''
    Util functions
    '''
    def load_resource(self, resource_path):
        """
        Gets the content of a resource
        """
        resource_content = pkg_resources.resource_string(__name__, resource_path)
        return str(resource_content)

    def render_template(self, template_path, context={}):
        """
        Evaluate a template by resource path, applying the provided context
        """
        template_str = self.load_resource(template_path)
        return Template(template_str).render(Context(context))

    '''
    Main functions
    '''
    def student_view(self, context=None):
        """
        The primary view of the XBlock, shown to students
        when viewing courses.
        """
        
        context = {
            'display_name': self.display_name,
            'url': self.url,
            'allow_download': self.allow_download,
            'source_text': self.source_text,
            'source_url': self.source_url
        }
        html = self.render_template('static/html/pdf_view.html', context)
        
        frag = Fragment(html)
        frag.add_css(self.load_resource("static/css/pdf.css"))
        frag.add_javascript(self.load_resource("static/js/pdf_view.js"))
        frag.initialize_js('pdfXBlockInitView')
        return frag

    def studio_view(self, context=None):
        """
        The secondary view of the XBlock, shown to teachers
        when editing the XBlock.
        """
        context = {
            'display_name': self.display_name,
            'url': self.url,
            'allow_download': self.allow_download,
            'source_text': self.source_text,
            'source_url': self.source_url
        }
        html = self.render_template('static/html/pdf_edit.html', context)
        
        frag = Fragment(html)
        frag.add_javascript(self.load_resource("static/js/pdf_edit.js"))
        frag.initialize_js('pdfXBlockInitEdit')
        return frag

    @XBlock.json_handler
    def save_pdf(self, data, suffix=''):
        """
        The saving handler.
        """
        self.display_name = data['display_name']
        self.url = data['url']
        self.allow_download = True if data['allow_download'] == "True" else False # Str to Bool translation
        self.source_text = data['source_text']
        self.source_url = data['source_url']
        
        return {
            'result': 'success',
        }
class ImageExplorerBlock(XBlock):  # pylint: disable=no-init
    """
    XBlock that renders an image with tooltips
    """

    has_score = True
    has_author_view = True
    completion_mode = XBlockCompletionMode.COMPLETABLE

    display_name = String(
        display_name=_("Display Name"),
        help=
        _("This name appears in the horizontal navigation at the top of the page."
          ),
        scope=Scope.settings,
        default=_("Image Explorer"))

    _hotspot_coordinates_centered = Boolean(
        display_name=_("Hot Spots Coordinates Centered"),
        scope=Scope.settings,
        default=False,
    )

    opened_hotspots = List(
        help=_("Store hotspots opened by student, for completion"),
        default=[],
        scope=Scope.user_state,
    )

    data = String(help=_("XML contents to display for this module"),
                  scope=Scope.content,
                  default=textwrap.dedent("""\
        <image_explorer schema_version='2'>
            <background src="//upload.wikimedia.org/wikipedia/commons/thumb/a/ac/MIT_Dome_night1_Edit.jpg/800px-MIT_Dome_night1_Edit.jpg" />
            <description>
                <p>
                    Enjoy using the Image Explorer. Click around the MIT Dome and see what you find!
                </p>
            </description>
            <hotspots>
                <hotspot x='48.8125%' y='8.3162%' item-id='hotspotA'>
                    <feedback width='300' height='240'>
                        <header>
                            <p>
                                This is where many pranks take place. Below are some of the highlights:
                            </p>
                        </header>
                        <body>
                            <ul>
                                <li>Once there was a police car up here</li>
                                <li>Also there was a Fire Truck put up there</li>
                            </ul>
                        </body>
                    </feedback>
                </hotspot>
                <hotspot x='33.8125%' y='18.5831%' item-id="hotspotB">
                    <feedback width='440' height='400'>
                        <header>
                            <p>
                                Watch the Red Line subway go around the dome
                            </p>
                        </header>
                        <youtube video_id='dmoZXcuozFQ' width='400' height='300' />
                    </feedback>
                </hotspot>
            </hotspots>
        </image_explorer>
        """))

    def max_score(self):  # pylint: disable=no-self-use
        """
        Returns the maximum score that can be achieved (always 1.0 on this XBlock)
        """
        return 1.0

    @property
    def hotspot_coordinates_centered(self):
        """
        Returns true if the hotspot coordinates are centered
        """
        if self._hotspot_coordinates_centered:
            return True

        # hotspots are calculated from center for schema version > 1
        xmltree = etree.fromstring(self.data)
        schema_version = int(xmltree.attrib.get('schema_version', 1))

        return schema_version > 1

    def author_view(self, context=None):
        """
        Renders the Studio preview view.
        """
        return self.student_view(context, authoring=True)

    @XBlock.supports("multi_device")  # Mark as mobile-friendly
    def student_view(self, context, authoring=False):
        """
        Player view, displayed to the student
        """

        xmltree = etree.fromstring(self.data)

        description = self._get_description(xmltree)
        hotspots = self._get_hotspots(xmltree)
        background = self._get_background(xmltree)
        has_youtube = False
        has_ooyala = False

        for hotspot in hotspots:
            width = 'width:{0}px'.format(
                hotspot.feedback.width
            ) if hotspot.feedback.width else 'width:300px'
            height = 'height:{0}px'.format(
                hotspot.feedback.height) if hotspot.feedback.height else ''
            max_height = ''
            if not hotspot.feedback.height:
                max_height = 'max-height:{0}px'.format(hotspot.feedback.max_height) if \
                             hotspot.feedback.max_height else 'max-height:300px'

            hotspot.reveal_style = 'style="{0};{1};{2}"'.format(
                width, height, max_height)
            if hotspot.feedback.youtube:
                has_youtube = True

            if hotspot.feedback.ooyala:
                has_ooyala = True

        context = {
            'title': self.display_name,
            'hotspot_coordinates_centered': self.hotspot_coordinates_centered,
            'description_html': description,
            'hotspots': hotspots,
            'background': background,
            'ie_uid': uuid.uuid4().hex[:15],
        }

        fragment = Fragment()
        fragment.add_content(
            loader.render_django_template(
                '/templates/html/image_explorer.html',
                context=context,
                i18n_service=self.runtime.service(self, 'i18n')))
        hotspot_image_url = self.runtime.local_resource_url(
            self, 'public/images/hotspot-sprite.png')
        fragment.add_css(self.resource_string('public/css/image_explorer.css'))
        fragment.add_javascript_url(
            self.runtime.local_resource_url(self,
                                            'public/js/image_explorer.js'))
        if has_youtube:
            fragment.add_javascript_url('https://www.youtube.com/iframe_api')

        if has_ooyala:
            fragment.add_javascript_url(
                '//player.ooyala.com/core/10efd95b66124001b415aa2a4bee29c8?plugins=main,bm'
            )
            fragment.add_javascript_url(
                self.runtime.local_resource_url(self,
                                                'public/js/ooyala_player.js'))

        fragment.initialize_js(
            'ImageExplorerBlock', {
                'hotspot_image': hotspot_image_url,
                'authoring_view': 'true' if authoring else 'false'
            })

        return fragment

    def student_view_data(self, context=None):
        """
        Returns a JSON representation of the Image Explorer Xblock, that can be
        retrieved using Course Block API.
        """
        xmltree = etree.fromstring(self.data)

        description = self._get_description(xmltree, absolute_urls=True)
        background = self._get_background(xmltree)
        background['src'] = self._replace_static_from_url(background['src'])
        hotspots = self._get_hotspots(xmltree, absolute_urls=True)
        return {
            'description': description,
            'background': background,
            'hotspots': hotspots,
        }

    @XBlock.json_handler
    def publish_event(self, data, suffix=''):
        """
        Override XBlock method to publish event when an action is taken on the
        block. This is used to register student progress.
        """
        try:
            event_type = data.pop('event_type')
        except KeyError:
            return {
                'result': 'error',
                'message': self.ugettext('Missing event_type in JSON data')
            }

        data['user_id'] = self.scope_ids.user_id
        data['component_id'] = self._get_unique_id()
        self.runtime.publish(self, event_type, data)

        if event_type == 'xblock.image-explorer.hotspot.opened':
            self.register_progress(data['item_id'])

        return {'result': 'success'}

    def register_progress(self, hotspot_id):
        """
        Registers the completion of an hotspot, identified by id
        """
        xmltree = etree.fromstring(self.data)
        hotspots_ids = [h.item_id for h in self._get_hotspots(xmltree)]

        if not hotspots_ids \
                or hotspot_id not in hotspots_ids \
                or hotspot_id in self.opened_hotspots:
            return

        self.runtime.publish(self, 'progress', {})
        self.opened_hotspots.append(hotspot_id)
        log.debug(u'Opened hotspots so far for %s: %s', self._get_unique_id(),
                  self.opened_hotspots)

        opened_hotspots = [
            h for h in hotspots_ids if h in self.opened_hotspots
        ]
        percent_completion = float(len(opened_hotspots)) / len(hotspots_ids)
        self.runtime.publish(self, 'grade', {
            'value': percent_completion,
            'max_value': 1,
        })
        log.debug(u'Sending grade for %s: %s', self._get_unique_id(),
                  percent_completion)

    def _get_unique_id(self):
        try:
            unique_id = self.location.name
        except AttributeError:
            # workaround for xblock workbench
            unique_id = 'workbench-workaround-id'
        return unique_id

    def studio_view(self, context):
        """
        Editing view in Studio
        """
        fragment = Fragment()
        fragment.add_content(
            loader.render_django_template(
                '/templates/html/image_explorer_edit.html',
                context={'self': self},
                i18n_service=self.runtime.service(self, 'i18n')))
        fragment.add_javascript_url(
            self.runtime.local_resource_url(
                self, 'public/js/image_explorer_edit.js'))

        fragment.initialize_js('ImageExplorerEditBlock')

        return fragment

    @XBlock.json_handler
    def studio_submit(self, submissions, suffix=''):
        """
        Handle the action of the submit button when using the block from Studio
        """
        self.display_name = submissions['display_name']
        if submissions.get('hotspot_coordinates_centered', False):
            self._hotspot_coordinates_centered = True

        xml_content = submissions['data']

        try:
            etree.parse(StringIO(xml_content))
            self.data = xml_content
        except etree.XMLSyntaxError as err:
            # Python 2 and 3 compatibility fix
            # Switch to _, error_message = e.args
            try:
                error_message = err.message  # pylint: disable=exception-message-attribute
            except:  # pylint: disable=bare-except
                _, error_message = err.args

            return {
                'result': 'error',
                'message': error_message,
            }

        return {
            'result': 'success',
        }

    @staticmethod
    def _get_background(xmltree):
        """
        Parse the XML to get the information about the background image
        """
        background = xmltree.find('background')
        return AttrDict({
            'src': background.get('src'),
            'width': background.get('width'),
            'height': background.get('height')
        })

    def _replace_static_from_url(self, url):
        if not url:
            return url
        try:
            from static_replace import replace_static_urls
        except ImportError:
            return url

        url = '"{}"'.format(url)
        lms_relative_url = replace_static_urls(url, course_id=self.course_id)  # pylint: disable=no-member
        lms_relative_url = lms_relative_url.strip('"')
        return self._make_url_absolute(lms_relative_url)

    @staticmethod
    def _make_url_absolute(url):
        lms_base = settings.ENV_TOKENS.get('LMS_BASE')
        scheme = 'https' if settings.HTTPS == 'on' else 'http'
        lms_base = '{}://{}'.format(scheme, lms_base)
        return urllib.parse.urljoin(lms_base, url)

    def _inner_content(self, tag, absolute_urls=False):
        """
        Helper met
        """
        if tag is not None:
            tag_content = u''.join(html.tostring(e) for e in tag)
            if absolute_urls:
                return self._change_relative_url_to_absolute(tag_content)
            return tag_content
        return None

    def _get_description(self, xmltree, absolute_urls=False):
        """
        Parse the XML to get the description information
        """
        description = xmltree.find('description')
        if description is not None:
            description = self._inner_content(description, absolute_urls)
            return description
        return None

    def _change_relative_url_to_absolute(self, text):
        if text:
            relative_urls = Selector(
                text=text).css('::attr(href),::attr(src)').extract()
            for url in relative_urls:
                text = text.replace(url, self._replace_static_from_url(url))
        return text

    def _get_hotspots(self, xmltree, absolute_urls=False):
        """
        Parse the XML to get the hotspot information
        """
        hotspots_element = xmltree.find('hotspots')
        hotspot_elements = hotspots_element.findall('hotspot')
        hotspots = []
        for index, hotspot_element in enumerate(hotspot_elements):
            feedback_element = hotspot_element.find('feedback')

            feedback = AttrDict()
            feedback.width = feedback_element.get('width')
            feedback.height = feedback_element.get('height')
            feedback.max_height = feedback_element.get('max-height')
            feedback.header = self._inner_content(
                feedback_element.find('header'), absolute_urls)
            feedback.side = hotspot_element.get('side', 'auto')

            feedback.body = None
            body_element = feedback_element.find('body')
            if body_element is not None:
                feedback.type = 'text'
                feedback.body = self._inner_content(body_element,
                                                    absolute_urls)

            self._collect_video_elements(hotspot_element, feedback)

            hotspot = AttrDict()
            hotspot.item_id = hotspot_element.get('item-id')
            if hotspot.item_id is None:
                hotspot.item_id = 'hotspot' + str(index)
            hotspot.feedback = feedback

            hotspot.x = hotspot_element.get('x')
            if not hotspot.x.endswith('%'):
                hotspot.x += 'px'  # px is deprecated as it is not responsive

            hotspot.y = hotspot_element.get('y')
            if not hotspot.y.endswith('%'):
                hotspot.y += 'px'  # px is deprecated as it is not responsive

            hotspot.visited = hotspot.item_id in self.opened_hotspots

            hotspots.append(hotspot)

        return hotspots

    @staticmethod
    def _collect_video_elements(hotspot, feedback):
        """
        Parses and includes video elements contained in the hotspot
        """
        feedback_element = hotspot.find('feedback')

        feedback.youtube = None
        youtube_element = feedback_element.find('youtube')
        if youtube_element is not None:
            feedback.type = 'youtube'
            feedback.youtube = AttrDict()
            feedback.youtube.id = 'youtube-{}'.format(uuid.uuid4().hex)
            feedback.youtube.video_id = youtube_element.get('video_id')
            feedback.youtube.width = youtube_element.get('width')
            feedback.youtube.height = youtube_element.get('height')

        feedback.ooyala = None
        ooyala_element = feedback_element.find('ooyala')
        if ooyala_element is not None:
            feedback.type = 'ooyala'
            feedback.ooyala = AttrDict()
            feedback.ooyala.id = 'oo-{}'.format(uuid.uuid4().hex)
            feedback.ooyala.video_id = ooyala_element.get('video_id')
            feedback.ooyala.width = ooyala_element.get('width')
            feedback.ooyala.height = ooyala_element.get('height')

        # BC element could be anywhere in the hotspot
        bcove_element = hotspot.find(".//brightcove")
        if bcove_element is not None:
            feedback.bcove = AttrDict()
            feedback.bcove.id = 'bcove-{}'.format(uuid.uuid4().hex)
            feedback.bcove.video_id = bcove_element.get('video_id')
            feedback.bcove.account_id = bcove_element.get('account_id')
            feedback.bcove.width = bcove_element.get('width')
            feedback.bcove.height = bcove_element.get('height')

    @staticmethod
    def workbench_scenarios():
        """A canned scenario for display in the workbench."""
        return [("Image explorer scenario",
                 "<vertical_demo><image-explorer/></vertical_demo>")]

    def resource_string(self, path):  # pylint: disable=no-self-use
        """Handy helper for getting resources from our kit."""
        data = pkg_resources.resource_string(__name__, path)
        return data.decode("utf8")
Beispiel #11
0
class OoyalaPlayerBlock(XBlock):
    """
    XBlock providing a video player for videos hosted on Brightcove
    """
    display_name = String(
        display_name="Display Name",
        help=
        "This name appears in the horizontal navigation at the top of the page.",
        scope=Scope.settings,
        default="Ooyala Player")

    content_id = String(display_name="Content Id",
                        help="Identifier for the Content Id.",
                        scope=Scope.content,
                        default='54bjVpbTr-BnKKHR1pIZ3Wb6wSSyJQUP')

    transcript_file_id = String(
        display_name="3Play Transcript Id",
        help="Identifier for the 3Play Transcript File",
        scope=Scope.content,
        default='')

    transcript_project_id = String(
        display_name="3Play Transcript Project Id",
        help='Identifier for the 3Play Transcript Project',
        scope=Scope.content,
        default='')

    enable_player_token = Boolean(
        display_name="Enable Player Token",
        help='Set to True if a player token is required.',
        scope=Scope.content,
        default=False)

    partner_code = String(display_name="Partner Code",
                          help='Needed to generate a player token.',
                          scope=Scope.content,
                          default='')

    api_key = String(display_name="Api Key",
                     help='Needed to generate a player token.',
                     scope=Scope.content,
                     default='')

    api_secret_key = String(display_name="Api SecRet Key",
                            help='Needed to generate a player token.',
                            scope=Scope.content,
                            default='')

    expiration_time = Integer(
        display_name="Expiration Time",
        help='Expiration time in seconds. Needed to generate a player token.',
        scope=Scope.content,
        default=600)

    xml_config = String(help="XML Configuration",
                        default='<ooyala>\n</ooyala>',
                        scope=Scope.content)

    player_id = '8582dca2417b4e13bed27a4f0647c139'

    @property
    def player_token(self):
        if not self.enable_player_token:
            return ''

        return generate_player_token(self.partner_code, self.api_key,
                                     self.api_secret_key, self.content_id,
                                     self.expiration_time)

    @property
    def overlays(self):
        """
        Parse the xml config and return the overlays
        """

        overlays = []
        node = etree.parse(StringIO(self.xml_config)).getroot()
        overlays_node = node.find('overlays')
        video_id = 'ooyala-' + self._get_unique_id()
        if overlays_node is not None:
            for child_id, xml_child in enumerate(
                    overlays_node.findall('overlay')):
                overlay = OoyalaOverlay.init_overlay_from_node(
                    xml_child, video_id)
                overlays.append(overlay)

        return overlays

    @property
    def transcript_enabled(self):
        return bool(self.transcript_project_id and self.transcript_file_id)

    def student_view(self, context):
        """
        Player view, displayed to the student
        """

        dom_id = 'ooyala-' + self._get_unique_id()

        overlay_fragments = ""
        for overlay in self.overlays:
            overlay_fragments += overlay.render()

        context = {
            'title': self.display_name,
            'content_id': self.content_id,
            'transcript_file_id': self.transcript_file_id,
            'transcript_project_id': self.transcript_project_id,
            'player_id': self.player_id,
            'player_token': self.player_token,
            'dom_id': dom_id,
            'transcript_enabled': self.transcript_enabled,
            'overlay_fragments': overlay_fragments
        }

        fragment = Fragment()
        fragment.add_content(
            render_template('/templates/html/ooyala_player.html', context))
        fragment.add_css_url(
            self.runtime.local_resource_url(self,
                                            'public/css/ooyala_player.css'))
        fragment.add_css_url(
            self.runtime.local_resource_url(self,
                                            'public/css/vendor/jquery-ui.css'))

        # custom plugins styles
        fragment.add_css_url(
            self.runtime.local_resource_url(self,
                                            'public/css/speedplugin.css'))
        fragment.add_css_url(
            self.runtime.local_resource_url(self, 'public/css/transcript.css'))

        fragment.add_javascript_url(
            self.runtime.local_resource_url(self,
                                            'public/js/vendor/jquery-ui.js'))

        player_url = '//player.ooyala.com/v3/{0}?platform=html5-priority'.format(
            self.player_id)

        fragment.add_javascript_url(player_url)
        fragment.add_javascript_url(
            self.runtime.local_resource_url(
                self, 'public/js/vendor/speed_plugin.js'))
        fragment.add_javascript_url(
            self.runtime.local_resource_url(self,
                                            'public/js/vendor/popcorn.js'))
        fragment.add_javascript_url(
            self.runtime.local_resource_url(self,
                                            'public/js/vendor/underscore.js'))

        fragment.add_javascript(
            render_template(
                'public/js/ooyala_player.js', {
                    'self': self,
                    'overlay_fragments': overlay_fragments,
                    'dom_id': dom_id
                }))

        if self.transcript_enabled:
            transcript_js_url = textwrap.dedent('''\
            //static.3playmedia.com/p/projects/{0}/files/{1}/embed.js?
            plugin=transcript&settings=width:640,height:340,skin:frost,
            can_collapse:true,collapse_onload:true,can_print:true,can_download:true,
            scan_view:true&player_type=ooyala&player_id={2}
            '''.format(self.transcript_project_id, self.transcript_file_id,
                       self.player_id))

            fragment.add_javascript_url(transcript_js_url)

        fragment.initialize_js('OoyalaPlayerBlock')

        return fragment

    def _get_unique_id(self):
        try:
            unique_id = self.location.name
        except AttributeError:
            # workaround for xblock workbench
            unique_id = self.parent.replace('.', '-') + '-' + self.content_id
        return unique_id

    def studio_view(self, context):
        """
        Editing view in Studio
        """
        fragment = Fragment()
        fragment.add_content(
            render_template('/templates/html/ooyala_player_edit.html', {
                'self': self,
            }))
        fragment.add_javascript_url(
            self.runtime.local_resource_url(self,
                                            'public/js/ooyala_player_edit.js'))

        fragment.initialize_js('OoyalaPlayerEditBlock')

        return fragment

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

        xml_config = submissions['xml_config']
        try:
            etree.parse(StringIO(xml_config))
        except etree.XMLSyntaxError as e:
            response = {'result': 'error', 'message': e.message}
        else:
            response = {
                'result': 'success',
            }

            self.xml_config = xml_config
            self.display_name = submissions['display_name']
            self.content_id = submissions['content_id']
            self.transcript_file_id = submissions['transcript_file_id']
            self.transcript_project_id = submissions['transcript_project_id']
            self.enable_player_token = submissions['enable_player_token']
            self.partner_code = submissions['partner_code']
            self.api_key = submissions['api_key']
            self.api_secret_key = submissions['api_secret_key']
            self.expiration_time = submissions['expiration_time']

        return response

    @staticmethod
    def workbench_scenarios():
        """A canned scenario for display in the workbench."""
        return [("Ooyala scenario",
                 "<vertical_demo><ooyala-player/></vertical_demo>")]
Beispiel #12
0
class GroupProjectBaseFeedbackDisplayXBlock(BaseStageComponentXBlock,
                                            StudioEditableXBlockMixin,
                                            XBlockWithPreviewMixin,
                                            WorkgroupAwareXBlockMixin):
    DEFAULT_QUESTION_ID_VALUE = None

    NO_QUESTION_SELECTED = _(u"No question selected")
    QUESTION_NOT_FOUND = _(u"Selected question not found")
    QUESTION_ID_IS_NOT_UNIQUE = _(u"Question ID is not unique")

    question_id = String(display_name=_(u"Question ID"),
                         help=_(u"Question to be assessed"),
                         scope=Scope.content,
                         default=DEFAULT_QUESTION_ID_VALUE)

    show_mean = Boolean(
        display_name=_(u"Show Mean Value"),
        help=
        _(u"If True, converts review answers to numbers and calculates mean value"
          ),
        default=False,
        scope=Scope.content)

    editable_fields = ("question_id", "show_mean")
    has_author_view = True

    @property
    def activity_questions(self):
        raise NotImplementedError(MUST_BE_OVERRIDDEN)

    @property
    def display_name_with_default(self):
        if self.question:
            return _(u'Review Assessment for question "{question_title}"'
                     ).format(question_title=self.question.title)
        else:
            return _(u"Review Assessment")

    @lazy
    def question(self):
        matching_questions = [
            question for question in self.activity_questions
            if question.question_id == self.question_id
        ]
        if len(matching_questions) > 1:
            raise ValueError(self.QUESTION_ID_IS_NOT_UNIQUE)
        if not matching_questions:
            return None

        return matching_questions[0]

    @groupwork_protected_view
    def student_view(self, context):
        if self.question is None:
            return Fragment(messages.COMPONENT_MISCONFIGURED)

        raw_feedback = self.get_feedback()

        feedback = []
        for item in raw_feedback:
            feedback.append(html.escape(item['answer']))

        fragment = Fragment()
        title = self.question.assessment_title if self.question.assessment_title else self.question.title
        render_context = {
            'assessment': self,
            'question_title': title,
            'feedback': feedback
        }
        if self.show_mean:
            try:
                render_context['mean'] = "{0:.1f}".format(mean(feedback))
            except ValueError as exc:
                log.warn(exc)
                render_context['mean'] = _(u"N/A")

        render_context.update(context)
        fragment.add_content(
            loader.render_template(
                "templates/html/components/review_assessment.html",
                render_context))
        return fragment

    def validate(self):
        validation = super(GroupProjectBaseFeedbackDisplayXBlock,
                           self).validate()

        if not self.question_id:
            validation.add(
                ValidationMessage(ValidationMessage.ERROR,
                                  self.NO_QUESTION_SELECTED))

        if self.question_id and self.question is None:
            validation.add(
                ValidationMessage(ValidationMessage.ERROR,
                                  self.QUESTION_NOT_FOUND))

        return validation

    def author_view(self, context):
        if self.question:
            return self.student_view(context)

        fragment = Fragment()
        fragment.add_content(messages.QUESTION_NOT_SELECTED)
        return fragment

    def studio_view(self, context):
        # can't use values_provider as we need it to be bound to current block instance
        with FieldValuesContextManager(self, 'question_id',
                                       self.question_ids_values_provider):
            return super(GroupProjectBaseFeedbackDisplayXBlock,
                         self).studio_view(context)

    def question_ids_values_provider(self):
        not_selected = {
            "display_name": _(u"--- Not selected ---"),
            "value": self.DEFAULT_QUESTION_ID_VALUE
        }
        question_values = [{
            "display_name": question.title,
            "value": question.question_id
        } for question in self.activity_questions]
        return [not_selected] + question_values
Beispiel #13
0
class GroupProjectReviewQuestionXBlock(BaseStageComponentXBlock,
                                       StudioEditableXBlockMixin,
                                       XBlockWithPreviewMixin):
    CATEGORY = "gp-v2-review-question"
    STUDIO_LABEL = _(u"Review Question")

    @property
    def display_name_with_default(self):
        return self.title or _(u"Review Question")

    question_id = String(display_name=_(u"Question ID"),
                         default=UNIQUE_ID,
                         scope=Scope.content,
                         force_export=True)

    title = String(display_name=_(u"Question Text"),
                   default=_(u""),
                   scope=Scope.content)

    assessment_title = String(
        display_name=_(u"Assessment Question Text"),
        help=_(u"Overrides question title when displayed in assessment mode"),
        default=None,
        scope=Scope.content)

    question_content = String(display_name=_(u"Question Content"),
                              help=_(u"HTML control"),
                              default=_(u""),
                              scope=Scope.content,
                              multiline_editor="xml",
                              xml_node=True)

    required = Boolean(display_name=_(u"Required"),
                       default=False,
                       scope=Scope.content)

    grade = Boolean(
        display_name=_(u"Grading"),
        help=
        _(u"IF True, answers to this question will be used to calculate student grade for Group Project."
          ),
        default=False,
        scope=Scope.content)

    single_line = Boolean(
        display_name=_(u"Single Line"),
        help=_(
            u"If True question label and content will be displayed on single line, allowing for more compact layout."
            u"Only affects presentation."),
        default=False,
        scope=Scope.content)

    question_css_classes = String(
        display_name=_(u"CSS Classes"),
        help=
        _(u"CSS classes to be set on question element. Only affects presentation."
          ),
        scope=Scope.content)

    editable_fields = ("question_id", "title", "assessment_title",
                       "question_content", "required", "grade", "single_line",
                       "question_css_classes")
    has_author_view = True

    @lazy
    def stage(self):
        return self.get_parent()

    def render_content(self):
        try:
            answer_node = ElementTree.fromstring(self.question_content)
        except ElementTree.ParseError:
            message_tpl = "Exception when parsing question content for question {question_id}. Content is [{content}]."
            message_tpl.format(question_id=self.question_id,
                               content=self.question_content)
            log.exception(message_tpl)
            return ""

        answer_node.set('name', self.question_id)
        answer_node.set('id', self.question_id)
        current_class = answer_node.get('class')
        answer_classes = ['answer']
        if current_class:
            answer_classes.append(current_class)
        if self.single_line:
            answer_classes.append('side')
        if self.stage.is_closed:
            answer_node.set('disabled', 'disabled')
        else:
            answer_classes.append('editable')
        answer_node.set('class', ' '.join(answer_classes))

        return outer_html(answer_node)

    def student_view(self, context):
        question_classes = ["question"]
        if self.required:
            question_classes.append("required")
        if self.question_css_classes:
            question_classes.append(self.question_css_classes)

        fragment = Fragment()
        render_context = {
            'question': self,
            'question_classes': " ".join(question_classes),
            'question_content': self.render_content()
        }
        render_context.update(context)
        fragment.add_content(
            loader.render_template(
                "templates/html/components/review_question.html",
                render_context))
        return fragment

    def studio_view(self, context):
        fragment = super(GroupProjectReviewQuestionXBlock,
                         self).studio_view(context)

        # TODO: StudioEditableXBlockMixin should really support Codemirror XML editor
        add_resource(self, 'css', "public/css/components/question_edit.css",
                     fragment)
        add_resource(self, 'javascript',
                     "public/js/components/question_edit.js", fragment)
        fragment.initialize_js("GroupProjectQuestionEdit")
        return fragment

    def author_view(self, context):
        fragment = self.student_view(context)
        add_resource(self, 'css', "public/css/components/question_edit.css",
                     fragment)
        return fragment
Beispiel #14
0
class videojsXBlock(XBlock):
    '''
    Icon of the XBlock. Values : [other (default), video, problem]
    '''
    icon_class = "video"
    '''
    Fields
    '''
    display_name = String(
        display_name="Display Name",
        default="Video JS",
        scope=Scope.settings,
        help=
        "This name appears in the horizontal navigation at the top of the page."
    )

    url = String(display_name="Video URL",
                 default="http://vjs.zencdn.net/v/oceans.mp4",
                 scope=Scope.content,
                 help="The URL for your video.")

    allow_download = Boolean(display_name="Video Download Allowed",
                             default=True,
                             scope=Scope.content,
                             help="Allow students to download this video.")

    source_text = String(
        display_name="Source document button text",
        default="",
        scope=Scope.content,
        help=
        "Add a download link for the source file of your video. Use it for example to provide the PowerPoint or PDF file used for this video."
    )

    source_url = String(
        display_name="Source document URL",
        default="",
        scope=Scope.content,
        help=
        "Add a download link for the source file of your video. Use it for example to provide the PowerPoint or PDF file used for this video."
    )

    start_time = String(
        display_name="Start time",
        default="",
        scope=Scope.content,
        help=
        "The start and end time of your video. Equivalent to 'video.mp4#t=startTime,endTime' in the url."
    )

    end_time = String(
        display_name="End time",
        default="",
        scope=Scope.content,
        help=
        "The start and end time of your video. Equivalent to 'video.mp4#t=startTime,endTime' in the url."
    )

    sub_title_url = String(display_name="sub_title",
                           default="",
                           scope=Scope.content,
                           help="The link of subtitle.")
    '''
    Util functions
    '''
    def load_resource(self, resource_path):
        """
        Gets the content of a resource
        """
        resource_content = pkg_resources.resource_string(
            __name__, resource_path)
        return unicode(resource_content)

    def render_template(self, template_path, context={}):
        """
        Evaluate a template by resource path, applying the provided context
        """
        template_str = self.load_resource(template_path)
        return Template(template_str).render(Context(context))

    '''
    Main functions
    '''

    def student_view(self, context=None):
        """
        The primary view of the XBlock, shown to students
        when viewing courses.
        """
        fullUrl = self.url
        if self.start_time != "" and self.end_time != "":
            fullUrl += "#t=" + self.start_time + "," + self.end_time
        elif self.start_time != "":
            fullUrl += "#t=" + self.start_time
        elif self.end_time != "":
            fullUrl += "#t=0," + self.end_time

        context = {
            'display_name': self.display_name,
            'url': fullUrl,
            'allow_download': self.allow_download,
            'source_text': self.source_text,
            'source_url': self.source_url,
            'subtitle_url': self.sub_title_url,
            'id': time.time()
        }
        html = self.render_template('public/html/videojs_view.html', context)

        frag = Fragment(html)
        frag.add_css(self.load_resource("public/css/video-js.css"))
        frag.add_javascript(self.load_resource("public/js/video-js.min.js"))

        frag.add_css(self.load_resource("public/css/videojs.css"))
        # frag.add_css_url(self.runtime.local_resource_url(self, 'public/css/video-js.css'))
        # frag.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/video-js.min.js'))

        frag.add_javascript(self.load_resource("public/js/videojs_view.js"))
        frag.initialize_js('videojsXBlockInitView')
        return frag

    def studio_view(self, context=None):
        """
        The secondary view of the XBlock, shown to teachers
        when editing the XBlock.
        """
        context = {
            'display_name': self.display_name,
            'url': self.url,
            'allow_download': self.allow_download,
            'source_text': self.source_text,
            'source_url': self.source_url,
            'start_time': self.start_time,
            'end_time': self.end_time,
            'sub_title': self.sub_title_url
        }
        html = self.render_template('public/html/videojs_edit.html', context)

        frag = Fragment(html)
        frag.add_javascript(self.load_resource("public/js/videojs_edit.js"))
        # frag.add_javascript(self.load_resource("public/js/video-js.min.js"))
        # frag.add_css(self.load_resource("public/css/video-js.css"))
        frag.add_resource_url(
            self.runtime.local_resource_url(self, 'public/font/vjs.eot'))
        frag.add_resource_url(
            self.runtime.local_resource_url(self, 'public/font/vjs.svg'),
            'image/svg+xml')
        frag.add_resource_url(
            self.runtime.local_resource_url(self, 'public/font/vjs.ttf'))
        frag.add_resource_url(
            self.runtime.local_resource_url(self, 'public/font/vjs.woff'),
            'application/x-font-woff')

        frag.add_resource_url(
            self.runtime.local_resource_url(self, 'public/img/loading.gif'),
            'image/gif')
        frag.initialize_js('videojsXBlockInitStudio')
        return frag

    @XBlock.json_handler
    def save_videojs(self, data, suffix=''):
        """
        The saving handler.
        """
        self.display_name = data['display_name']
        self.url = data['url']
        self.allow_download = True if data[
            'allow_download'] == "True" else False  # Str to Bool translation
        self.source_text = data['source_text']
        self.source_url = data['source_url']
        self.start_time = ''.join(
            data['start_time'].split())  # Remove whitespace
        self.end_time = ''.join(data['end_time'].split())  # Remove whitespace
        self.sub_title_url = data['sub_title']

        return {
            'result': 'success',
        }

    @XBlock.json_handler
    def tracking_log(self, data, suffix=''):
        msg = data['msg']
        type = data['type']
        self.runtime.publish(self, type, msg)
        return {'result': 'success'}
Beispiel #15
0
class ProctoringFields(object):
    """
    Fields that are specific to Proctored or Timed Exams
    """
    is_time_limited = Boolean(
        display_name=_("Is Time Limited"),
        help=_("This setting indicates whether students have a limited time"
               " to view or interact with this courseware component."),
        default=False,
        scope=Scope.settings,
    )

    default_time_limit_minutes = Integer(
        display_name=_("Time Limit in Minutes"),
        help=
        _("The number of minutes available to students for viewing or interacting with this courseware component."
          ),
        default=None,
        scope=Scope.settings,
    )

    is_proctored_enabled = Boolean(
        display_name=_("Is Proctoring Enabled"),
        help=_(
            "This setting indicates whether this exam is a proctored exam."),
        default=False,
        scope=Scope.settings,
    )

    exam_review_rules = String(
        display_name=_("Software Secure Review Rules"),
        help=
        _("This setting indicates what rules the proctoring team should follow when viewing the videos."
          ),
        default='',
        scope=Scope.settings,
    )

    is_practice_exam = Boolean(
        display_name=_("Is Practice Exam"),
        help=
        _("This setting indicates whether this exam is for testing purposes only. Practice exams are not verified."
          ),
        default=False,
        scope=Scope.settings,
    )

    is_onboarding_exam = Boolean(
        display_name=_("Is Onboarding Exam"),
        help=_(
            "This setting indicates whether this exam is an onboarding exam."),
        default=False,
        scope=Scope.settings,
    )

    def _get_course(self):
        """
        Return course by course id.
        """
        return self.descriptor.runtime.modulestore.get_course(self.course_id)  # pylint: disable=no-member

    @property
    def is_timed_exam(self):
        """
        Alias the permutation of above fields that corresponds to un-proctored timed exams
        to the more clearly-named is_timed_exam
        """
        return not self.is_proctored_enabled and not self.is_practice_exam and self.is_time_limited

    @property
    def is_proctored_exam(self):
        """ Alias the is_proctored_enabled field to the more legible is_proctored_exam """
        return self.is_proctored_enabled

    @property
    def allow_proctoring_opt_out(self):
        """
        Returns true if the learner should be given the option to choose between
        taking a proctored exam, or opting out to take the exam without proctoring.
        """
        return self._get_course().allow_proctoring_opt_out

    @is_proctored_exam.setter
    def is_proctored_exam(self, value):
        """ Alias the is_proctored_enabled field to the more legible is_proctored_exam """
        self.is_proctored_enabled = value
class VideofrontXBlock(StudioEditableXBlockMixin, XBlock):
    """
    Play videos based on a modified videojs player. This XBlock supports
    subtitles and multiple resolutions.
    """

    # Used to load open edx-specific settings
    block_settings_key = 'videofront-xblock'

    has_author_view = True

    display_name = String(help=ugettext_lazy(
        "The name students see. This name appears in "
        "the course ribbon and as a header for the video."),
                          display_name=ugettext_lazy("Component Display Name"),
                          default=ugettext_lazy("New video"),
                          scope=Scope.settings)

    video_id = String(
        scope=Scope.settings,
        help=ugettext_lazy(
            'Fill this with the ID of the video found in the video uploads dashboard'
        ),
        default="",
        display_name=ugettext_lazy("Video ID"))

    allow_download = Boolean(
        help=ugettext_lazy("Allow students to download this video."),
        display_name=ugettext_lazy("Video download allowed"),
        scope=Scope.settings,
        default=True)

    editable_fields = (
        'display_name',
        'video_id',
        'allow_download',
    )

    liked = Boolean(default=False, scope=Scope.user_state)
    disliked = Boolean(default=False, scope=Scope.user_state)
    like_count = Integer(default=0, scope=Scope.user_state_summary)
    dislike_count = Integer(default=0, scope=Scope.user_state_summary)

    vid_reported = Boolean(default=False, scope=Scope.user_state)
    aud_reported = Boolean(default=False, scope=Scope.user_state)
    vid_rep_cnt = Integer(default=0, scope=Scope.user_state_summary)
    aud_rep_cnt = Integer(default=0, scope=Scope.user_state_summary)

    # Analytics data
    user_timeline = String(default="0", scope=Scope.user_state)
    total_timeline = String(default="0", scope=Scope.user_state_summary)
    user_watch_time = Integer(default=0, scope=Scope.user_state)
    total_watch_time = Integer(default=0, scope=Scope.user_state_summary)
    last_watch_date = Integer(default=0, scope=Scope.user_state)
    user_views = Integer(default=0, scope=Scope.user_state)
    total_views = Integer(default=0, scope=Scope.user_state_summary)
    video_downloads = Integer(default=0, scope=Scope.user_state_summary)
    transcript_downloads = Integer(default=0, scope=Scope.user_state_summary)
    most_used_controls = String(default="0,0,0,0,0,0,0,0,0,0,0",
                                scope=Scope.user_state_summary)

    # 0 - play/pause
    # 1 - volume change
    # 2 - rate change
    # 3 - seeking
    # 4 - subtitles
    # 5 - toggle transcript
    # 6 - like/dislike
    # 7 - report
    # 8 - pip
    # 9 - download video
    # 10 - download transcript

    def get_icon_class(self):
        """CSS class to be used in courseware sequence list."""
        return 'video'

    def build_fragment(self):
        # 1) Define context
        final_timeline, final_size = self.calculateTimeline(
            self.total_timeline.split(","))
        context = {
            'display_name':
            self.display_name,
            'like_count':
            self.like_count,
            'dislike_count':
            self.dislike_count,
            'liked':
            self.liked,
            'disliked':
            self.disliked,
            'reported':
            self.aud_reported or self.vid_reported,
            'aud_rep_cnt':
            self.aud_rep_cnt,
            'vid_rep_cnt':
            self.vid_rep_cnt,
            'total_timeline':
            final_timeline,
            'timeline_bar_width':
            60.0 / final_size,
            'total_views':
            self.total_views,
            'most_used_controls':
            self.calculateMostUsedControls(),
            'user_views':
            self.user_views,
            'last_watch_date':
            datetime.utcfromtimestamp(
                self.last_watch_date).strftime('%d-%m-%Y'),
            'video_downloads_cnt':
            self.video_downloads,
            'transcript_downloads_cnt':
            self.transcript_downloads,
        }
        # It is a common mistake to define video ids suffixed with empty spaces
        video_id = None if self.video_id is None else self.video_id.strip()
        context['video'], context[
            'messages'], poster_frames = self.get_video_context(video_id)
        context['video_downloads'] = self.get_video_downloads_context(
            context['video']) if self.allow_download else []
        context[
            'transcript_downloads'] = self.get_transcript_downloads_context(
                context['video']) if self.allow_download else []

        # 2) Render template
        template = Template(self.resource_string("public/html/xblock.html"))
        content = template.render(Context(context))

        # 3) Build fragment
        fragment = Fragment()
        fragment.add_content(content)
        fragment.add_css(self.resource_string('public/css/xblock.css'))
        fragment.add_css(
            self.resource_string(
                'public/css/vendor/videojs-resolution-switcher.css'))
        fragment.add_css(
            self.resource_string('public/css/vendor/videojs-seek-buttons.css'))
        fragment.add_css(
            self.resource_string(
                'public/css/vendor/videojs-vtt-thumbnails.css'))
        fragment.add_css_url(
            'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css'
        )
        fragment.add_css_url('https://vjs.zencdn.net/7.4.1/video-js.css')
        fragment.add_javascript_url('https://vjs.zencdn.net/7.4.1/video.js')
        fragment.add_javascript(self.resource_string('public/js/xblock.js'))
        fragment.add_javascript(
            self.resource_string(
                'public/js/vendor/videojs-resolution-switcher.js'))
        fragment.add_javascript(
            self.resource_string(
                'public/js/vendor/videojs-seek-buttons.min.js'))
        fragment.add_javascript(
            self.resource_string(
                'public/js/vendor/videojs-vtt-thumbnails.min.js'))
        return fragment, video_id, poster_frames

    def student_view(self, context=None):  # pylint: disable=W0613
        fragment, video_id, poster_frames = self.build_fragment()

        fragment.initialize_js('VideofrontXBlock',
                               json_args={
                                   'course_id':
                                   unicode(self.location.course_key)
                                   if hasattr(self, 'location') else '',
                                   'video_id':
                                   video_id,
                                   'poster_frames':
                                   poster_frames,
                                   'avg_watch_time':
                                   self.calc_total_watch_time()
                               })

        return fragment

    def author_view(self, context=None):  # pylint: disable=W0613
        fragment, video_id, poster_frames = self.build_fragment()
        fragment.add_css(
            self.resource_string('public/css/xblock_author_css.css'))

        fragment.initialize_js('VideofrontXBlock',
                               json_args={
                                   'course_id':
                                   unicode(self.location.course_key)
                                   if hasattr(self, 'location') else '',
                                   'video_id':
                                   video_id,
                                   'poster_frames':
                                   poster_frames,
                                   'avg_watch_time':
                                   self.calc_total_watch_time()
                               })

        return fragment

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

    @staticmethod
    def workbench_scenarios():
        """Useful for debugging this xblock in the workbench (from xblock-sdk)."""
        # Note that this XBlock is not compatible with the workbench because the workbench lacks requirejs.
        return [
            ("Videofront XBlock", """<videofront-xblock/>"""),
        ]

    def get_video_context(self, video_id):
        """
        The return values will be used in the view context.

        Returns:
            video (dict)
            messages (tuple): each message is of the form `(level, content)` where
            `level` is 'error', 'warning', etc. and `content` is the message that
            will be displayed to the user.
        """
        messages = []
        video = {}
        poster_frames = ""
        if not video_id:
            messages.append(
                ('warning',
                 ugettext_lazy(
                     "You need to define a valid Videofront video ID.")))
            return video, messages, poster_frames
        settings = self.runtime.service(self,
                                        "settings").get_settings_bucket(self)
        api_host = settings.get('HOST')
        api_token = settings.get('TOKEN')
        if not api_host:
            messages.append((
                'warning',
                ugettext_lazy(
                    "Undefined Videofront hostname. Contact your platform administrator."
                )))
            return video, messages, poster_frames
        if not api_token:
            messages.append((
                'warning',
                ugettext_lazy(
                    "Undefined Videofront auth token. Contact your platform administrator."
                )))
            return video, messages, poster_frames

        try:
            # TODO implement a cache to store the server responses: we don't
            # want to make a call to videofront for every video view.
            api_response = requests.get(
                '{}/api/v1/videos/{}/'.format(api_host, video_id),
                headers={'Authorization': 'Token ' + api_token})
        except requests.ConnectionError as e:
            messages.append((
                'error',
                ugettext_lazy(
                    "Could not reach Videofront server. Contact your platform administrator"
                )))
            logger.error("Could not connect to Videofront: %s", e)
            return video, messages, poster_frames

        if api_response.status_code >= 400:
            if api_response.status_code == 403:
                messages.append(
                    ('error', ugettext_lazy("Authentication error")))
            elif api_response.status_code == 404:
                messages.append(
                    ('warning', ugettext_lazy("Incorrect video id")))
            else:
                messages.append(
                    ('error', ugettext_lazy("An unknown error has occurred")))
                logger.error("Received error %d: %s", api_response.status_code,
                             api_response.content)
            return video, messages, poster_frames

        # Check processing status is correct
        video = json.loads(api_response.content)
        poster_frames = video['poster_frames']
        processing_status = video['processing']['status']
        if processing_status == 'processing':
            messages.append(
                ('info',
                 ugettext_lazy(
                     "Video is currently being processed ({:.2f}%)").format(
                         video['processing']['progress'])))
        elif processing_status == 'failed':
            messages.append((
                'warning',
                ugettext_lazy(
                    "Video processing failed: try again with a different video ID"
                )))

        return video, messages, poster_frames

    def get_video_downloads_context(self, video):
        """
        Args:
            video (dict): object as returned by `get_video_context`
        Returns:
            downloads (list): will be passed to the view context
        """
        download_labels = {
            'HD': 'High (720p)',
            'SD': 'Standard (480p)',
            'LD': 'Mobile (320p)',
        }

        # Sort download links by decreasing bitrates
        video_formats = video.get('formats', [])
        video_formats = video_formats[::-1]
        return [{
            'url': source['url'],
            'label': download_labels.get(source['name'], source['name'])
        } for source in video_formats]

    def get_transcript_downloads_context(self, video):
        """
        Args:
            video (dict): object as returned by `get_video_context`
        Returns:
            downloads (list): will be passed to the view context
        """

        # Sort download links by decreasing bitrates
        subtitles = video.get('subtitles', [])
        return [{
            'url': source['url'],
            'language': source['language']
        } for source in subtitles]

    def calculateTimeline(self, total_timeline, count=0):
        t_size = len(total_timeline)
        final_size = 1
        if t_size == 0 or self.total_views < 5:
            return [], final_size
        total_timeline = [float(x) for x in total_timeline]
        n_timeline = []
        usable_factors = []
        offset = 0
        if t_size >= 60:
            while (len(usable_factors) == 0):
                factors = self.calculateFactors(t_size + offset)
                usable_factors = [
                    f for f in factors
                    if t_size / f >= 60 and t_size / f <= 240
                ]
                offset -= 1
            final_factor = min(usable_factors)
            final_size = t_size / final_factor
            for i in range(0, t_size, final_factor):
                temp = 0
                for j in range(0, final_factor):
                    if i >= t_size:
                        break
                    temp += total_timeline[i]
                    i += 1
                n_timeline.append(temp)
        else:
            while (len(usable_factors) == 0):
                factors = self.calculateFactors(t_size + offset)
                usable_factors = [
                    f for f in factors
                    if t_size * f >= 60 and t_size * f <= 240
                ]
                offset -= 1
            final_factor = max(usable_factors)
            final_size = t_size * final_factor
            for t in total_timeline:
                for j in range(0, final_factor):
                    n_timeline.append(t)
        max_h = max(n_timeline)
        if max_h > 0:
            n_timeline = [int(x / max_h * 10) + 2 for x in n_timeline]
        if t_size >= 60:
            n_timeline = [[i * final_factor, x]
                          for i, x in enumerate(n_timeline)]
        else:
            n_timeline = [[i / final_factor, x]
                          for i, x in enumerate(n_timeline)]
        return n_timeline, final_size

    def calculateFactors(self, num):
        factors = []
        for n in range(1, num / 2 + 1):
            if num % n == 0:
                factors.append(n)
        factors.append(num)
        return factors

    def calculateMostUsedControls(self):
        controls = {
            0: 'Play/Pause',
            1: 'Volume Change',
            2: 'Playback Rate Change',
            3: 'Seeking',
            4: 'Subtitles Toggle',
            5: 'Transcript Toggle',
            6: 'Like/Dislike',
            7: 'Report',
            8: 'Picture in Picture',
            9: 'Video Download',
            10: 'Transcript Download'
        }
        most_used = self.most_used_controls.split(",")
        most_used_dict = {}
        for i in range(0, len(most_used)):
            most_used_dict[i] = int(most_used[i])
        final_list = []
        for i in range(0, len(most_used_dict)):
            curr_max_key = max(most_used_dict,
                               key=(lambda key: most_used_dict[key]))
            if most_used_dict[curr_max_key] > 0:
                final_list.append(controls[curr_max_key])
            most_used_dict.pop(curr_max_key)

        return final_list[0:4]

    def calc_total_watch_time(self):
        if self.total_views > 0:
            return self.total_watch_time / self.total_views
        else:
            return 0

    @XBlock.json_handler
    def like_dislike(self, data, suffix=''):  # pylint: disable=unused-argument
        """
        Update the user and global rating in response to user action
        """
        if data['voteType'] not in ('like', 'dislike'):
            log.error('error!')
            return

        if data['voteType'] == 'like':
            if self.liked:
                self.like_count -= 1
                self.liked = False
            else:
                self.like_count += 1
                self.liked = True
                if self.disliked:
                    self.dislike_count -= 1
                    self.disliked = False
        elif data['voteType'] == 'dislike':
            if self.disliked:
                self.dislike_count -= 1
                self.disliked = False
            else:
                self.dislike_count += 1
                self.disliked = True
                if self.liked:
                    self.like_count -= 1
                    self.liked = False

        return {
            'likes': self.like_count,
            'dislikes': self.dislike_count,
            'liked': self.liked,
            'disliked': self.disliked,
        }

    @XBlock.json_handler
    def report(self, data, suffix=''):  # pylint: disable=unused-argument
        """
        Update the user and global report status in response to user action
        """
        if data['voteType'] not in ('audio', 'video'):
            log.error('error!')
            return

        if data['voteType'] == 'audio':
            if not self.aud_reported:
                self.aud_rep_cnt += 1
                self.aud_reported = True
        elif data['voteType'] == 'video':
            if not self.vid_reported:
                self.vid_rep_cnt += 1
                self.vid_reported = True

        return {
            'aud_reported': self.aud_reported,
            'vid_reported': self.vid_reported,
        }

    @XBlock.json_handler
    def saveTimeline(self, data, suffix=''):  # pylint: disable=unused-argument
        """
        Update the watch data in timeline
        """

        timeline = data['timeline']
        old_timeline = self.user_timeline.split(",")
        new_timeline = timeline.split(",")
        total_timeline = self.total_timeline.split(",")
        for i in range(len(old_timeline), len(new_timeline)):
            old_timeline.append("0")
        for i in range(len(total_timeline), len(new_timeline)):
            total_timeline.append("0")
        for i in range(0, len(new_timeline)):
            if int(old_timeline[i]) < int(new_timeline[i]):
                total_timeline[i] = str(
                    int(total_timeline[i]) + int(new_timeline[i]))

        self.user_timeline = timeline
        self.total_timeline = ",".join(total_timeline)

        return

    @XBlock.json_handler
    def saveTotalWatchTime(self, data, suffix=''):  # pylint: disable=unused-argument
        """
        Return the watch data in timeline
        """

        self.total_views += 1
        self.user_views += 1
        self.total_watch_time += data['watchTime']
        self.user_watch_time = data['watchTime']
        self.last_watch_date = data['watchDate']

    @XBlock.json_handler
    def saveTranscriptDownloaded(self, data, suffix=''):  # pylint: disable=unused-argument
        """
        Return the watch data in timeline
        """

        self.transcript_downloads += 1

    @XBlock.json_handler
    def saveVideoDownloaded(self, data, suffix=''):  # pylint: disable=unused-argument
        """
        Return the watch data in timeline
        """

        self.video_downloads += 1

    @XBlock.json_handler
    def saveMostUsedControls(self, data, suffix=''):  # pylint: disable=unused-argument
        """
        Return the watch data in timeline
        """

        new_used_controls = data['controls'].split(",")
        most_used = self.most_used_controls.split(",")
        for i in range(len(most_used), len(new_used_controls)):
            most_used.append("0")
        for i in range(0, len(new_used_controls)):
            most_used[i] = str(int(most_used[i]) + int(new_used_controls[i]))

        self.most_used_controls = ",".join(most_used)

        return
class AdvancedHTMLXBlock(XBlock, PublishEventMixin):
    """
    Advanced HTML XBlock

    This XBlock allows internal CSS and external CSS(fetched through <link>)
    to be included in your course content

    The XBlock will allow course creator to edit raw HTML content using 
    CodeMirror 5.38 (studio_view)
    After the contents are saved, the XBlock will wrap these contents inside 
    an iframe preserving and conficing all CSS in it.(student_view)

    Each iframe is given a unique id and hence you should be able to use 
    multiple XBlocks on same page.

    Notes:
    While dragging XBlock in studio, it is possible that after dragging your 
    XBlock shows nothing, don't worry, all your data is still stored.
    This is because studio does not call student_view after dragging
    If you want to preview in this case, click on edit once and save/cance
    """

    display_name = String(default="Advanced HTML",
                          help="The display name of the XBlock")
    name = String(default="Advanced HTML XBlock")
    has_score = False
    icon_class = "other"
    unique_id = String(default="unique-id",
                       help="Unique ID of this xblock",
                       scope=Scope.user_state)
    # Even though this is default field, it is used
    # DONOT delete
    count = Integer(
        default=0,
        scope=Scope.user_state,
        help="A simple counter, to show something happening",
    )
    htmlcontent = String(default=defaultHTMLString,
                         scope=Scope.content,
                         help="Source code of HTML courseware")
    live_preview = Boolean(default=True,
                           scope=Scope.content,
                           help="Live Preview Flag")
    non_editable_metadata_fields = [
        "display_name", "has_score", "icon_class", "htmlcontent", "unique_id"
    ]

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

    def student_view(self, context=None):
        """
        The primary view of the AdvancedHTMLXBlock, shown to students
        when viewing courses.
        """
        if (self.count == 0):
            self.unique_id = str(uuid.uuid4())
            self.count = 1
        html = self.resource_string("static/html/advancedhtml.html")
        frag = Fragment(html.format(self=self))
        frag.add_css(
            self.resource_string("static/css/advancedhtml_student.css"))
        frag.add_javascript(
            self.resource_string("static/js/src/advancedhtml.js"))
        frag.initialize_js('AdvancedHTMLXBlock', {"unique-id": self.unique_id})
        return frag

    def studio_view(self, context=None):
        """
        The view that opens on clicking edit button in studio
        """
        html = self.resource_string("static/html/advancedhtml_edit.html")
        frag = Fragment(html.format(self=self))
        frag.add_css(self.resource_string("static/css/codemirror.css"))
        frag.add_css(self.resource_string("static/css/foldgutter.css"))
        frag.add_css(self.resource_string("static/css/bootstrap-grid.css"))
        frag.add_css(self.resource_string("static/css/advancedhtml.css"))
        # Load the main CodeMirror first
        frag.add_javascript(
            self.resource_string("static/js/src/codemirror/codemirror.js"))
        # Load the CodeMirror addons
        # Folder : addons/edit
        frag.add_javascript(
            self.resource_string("static/js/src/codemirror/closebrackets.js"))
        frag.add_javascript(
            self.resource_string("static/js/src/codemirror/closetag.js"))
        # Folder : addons/fold
        frag.add_javascript(
            self.resource_string("static/js/src/codemirror/foldcode.js"))
        frag.add_javascript(
            self.resource_string("static/js/src/codemirror/foldgutter.js"))
        frag.add_javascript(
            self.resource_string("static/js/src/codemirror/brace-fold.js"))
        frag.add_javascript(
            self.resource_string("static/js/src/codemirror/comment-fold.js"))
        frag.add_javascript(
            self.resource_string("static/js/src/codemirror/indent-fold.js"))
        frag.add_javascript(
            self.resource_string("static/js/src/codemirror/xml-fold.js"))
        # Load the CodeMirror modes
        frag.add_javascript(
            self.resource_string("static/js/src/codemirror/css.js"))
        frag.add_javascript(
            self.resource_string("static/js/src/codemirror/htmlmixed.js"))
        frag.add_javascript(
            self.resource_string("static/js/src/codemirror/javascript.js"))
        frag.add_javascript(
            self.resource_string("static/js/src/codemirror/xml.js"))
        # Finally load our JavaScript
        frag.add_javascript(
            self.resource_string("static/js/src/advancedhtml_edit.js"))

        frag.initialize_js('AdvancedHTMLXBlock_EditorInit',
                           {"live_preview": self.live_preview})
        return frag

    # Default XBlock function
    @XBlock.json_handler
    def increment_count(self, data, suffix=''):
        """
        An example handler, which increments the data.
        """
        # Just to show data coming in...
        assert data['hello'] == 'world'

        self.count += 1
        return {"count": self.count}

    @XBlock.json_handler
    def get_html_content(self, data, suffix=''):
        assert data['need_data'] == 'true'
        return {"htmlcontent": self.htmlcontent}

    @XBlock.json_handler
    def set_html_content(self, data, suffix=''):
        self.htmlcontent = data['set_data']
        self.display_name = data['set_display_name']
        self.live_preview = data['set_live_preview']
        return {"htmlcontent": self.htmlcontent}

    # TO-DO: change this to create the scenarios you'd like to see in the
    # workbench while developing your XBlock.
    @staticmethod
    def workbench_scenarios():
        """A canned scenario for display in the workbench."""
        return [
            ("AdvancedHTMLXBlock", """<advancedhtml/>
             """),
            ("Multiple AdvancedHTMLXBlock", """<vertical_demo>
                <advancedhtml/>
                <advancedhtml/>
                <advancedhtml/>
                </vertical_demo>
             """),
        ]
Beispiel #18
0
class FileThumbsBlock(XBlock):
    """
    An XBlock with thumbs-up/thumbs-down voting.

    Vote totals are stored for all students to see.  Each student is recorded
    as has-voted or not.

    This demonstrates multiple data scopes and ajax handlers.

    """

    upvotes = 0
    downvotes = 0
    voted = Boolean(help="Has this student voted?", default=False, scope=Scope.user_state)
    fs = Filesystem(help="File system", scope=Scope.user_state_summary)

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

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

        # Load the HTML fragment from within the package and fill in the template
        html_str = pkg_resources.resource_string(__name__, "static/html/thumbs.html")
        frag = Fragment(unicode(html_str))

        if not self.fs.exists(u"thumbsvotes.json"):
            with self.fs.open(u'thumbsvotes.json', 'wb') as file_output:
                json.dump({'up': 0, 'down': 0}, file_output)
                file_output.close()

        votes = json.load(self.fs.open(u"thumbsvotes.json"))
        self.upvotes = votes['up']
        self.downvotes = votes['down']

        # Load the CSS and JavaScript fragments from within the package
        css_str = pkg_resources.resource_string(__name__, "static/css/thumbs.css")
        frag.add_css(unicode(css_str))

        js_str = pkg_resources.resource_string(__name__,
                                               "static/js/src/thumbs.js")
        frag.add_javascript(unicode(js_str))

        with self.fs.open(u'uparrow.png', 'wb') as file_output:
            png.Writer(len(ARROW[0]), len(ARROW), greyscale=True, bitdepth=1).write(file_output, ARROW)

        with self.fs.open(u'downarrow.png', 'wb') as file_output:
            png.Writer(len(ARROW[0]), len(ARROW), greyscale=True, bitdepth=1).write(file_output, ARROW[::-1])

        frag.initialize_js('FileThumbsBlock', {'up': self.upvotes,
                                               'down': self.downvotes,
                                               'voted': self.voted,
                                               'uparrow': self.fs.get_url('uparrow.png'),
                                               'downarrow': self.fs.get_url('downarrow.png')})
        return frag

    problem_view = student_view

    @XBlock.json_handler
    def vote(self, data, suffix=''):  # pylint: disable=unused-argument
        """
        Update the vote count in response to a user action.
        """
        # Here is where we would prevent a student from voting twice, but then
        # we couldn't click more than once in the demo!
        #
        #     if self.voted:
        #         log.error("cheater!")
        #         return

        votes = json.load(self.fs.open(u"thumbsvotes.json"))
        self.upvotes = votes['up']
        self.downvotes = votes['down']

        if data['voteType'] not in ('up', 'down'):
            log.error('error!')
            return

        if data['voteType'] == 'up':
            self.upvotes += 1
        else:
            self.downvotes += 1

        with self.fs.open(u'thumbsvotes.json', 'wb') as file_output:
            json.dump({'up': self.upvotes, 'down': self.downvotes}, file_output)

        self.voted = True

        return {'up': self.upvotes, 'down': self.downvotes}

    @staticmethod
    def workbench_scenarios():
        """A canned scenario for display in the workbench."""
        return [
            ("filethumbs",
             """\
                <vertical_demo>
                    <filethumbs/>
                    <filethumbs/>
                    <filethumbs/>
                </vertical_demo>
             """)
        ]
Beispiel #19
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)
    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,
    )
    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",
    )
    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='',
    )
    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)
    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}"
          ).
        format(
            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)
Beispiel #20
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)
    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(2030, 1, 1, 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)
    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":
            "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(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(
        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)
Beispiel #21
0
class CombinedOpenEndedFields(object):
    display_name = String(
        display_name="Display Name",
        help=
        "This name appears in the horizontal navigation at the top of the page.",
        default="Open Response Assessment",
        scope=Scope.settings)
    current_task_number = Integer(help="Current task that the student is on.",
                                  default=0,
                                  scope=Scope.user_state)
    old_task_states = List(help=(
        "A list of lists of state dictionaries for student states that are saved."
        "This field is only populated if the instructor changes tasks after"
        "the module is created and students have attempted it (for example changes a self assessed problem to "
        "self and peer assessed."),
                           scope=Scope.user_state)
    task_states = List(
        help="List of state dictionaries of each task within this module.",
        scope=Scope.user_state)
    state = String(
        help="Which step within the current task that the student is on.",
        default="initial",
        scope=Scope.user_state)
    graded = Boolean(
        display_name="Graded",
        help=
        'Defines whether the student gets credit for this problem. Credit is based on peer grades of this problem.',
        default=False,
        scope=Scope.settings)
    student_attempts = Integer(
        help="Number of attempts taken by the student on this problem",
        default=0,
        scope=Scope.user_state)
    ready_to_reset = Boolean(
        help="If the problem is ready to be reset or not.",
        default=False,
        scope=Scope.user_state)
    max_attempts = Integer(
        display_name="Maximum Attempts",
        help="The number of times the student can try to answer this problem.",
        default=1,
        scope=Scope.settings,
        values={"min": 1})
    accept_file_upload = Boolean(
        display_name="Allow File Uploads",
        help="Whether or not the student can submit files as a response.",
        default=False,
        scope=Scope.settings)
    skip_spelling_checks = Boolean(
        display_name="Disable Quality Filter",
        help=
        "If False, the Quality Filter is enabled and submissions with poor spelling, short length, or poor grammar will not be peer reviewed.",
        default=False,
        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,
    )
    graceperiod = Timedelta(
        help=
        "Amount of time after the due date that submissions will be accepted",
        scope=Scope.settings)
    version = VersionInteger(help="Current version number",
                             default=DEFAULT_VERSION,
                             scope=Scope.settings)
    data = String(help="XML data for the problem",
                  scope=Scope.content,
                  default=DEFAULT_DATA)
    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)
    min_to_calibrate = Integer(
        display_name="Minimum Peer Grading Calibrations",
        help=
        "The minimum number of calibration essays each student will need to complete for peer grading.",
        default=3,
        scope=Scope.settings,
        values={
            "min": 1,
            "max": 20,
            "step": "1"
        })
    max_to_calibrate = Integer(
        display_name="Maximum Peer Grading Calibrations",
        help=
        "The maximum number of calibration essays each student will need to complete for peer grading.",
        default=6,
        scope=Scope.settings,
        values={
            "min": 1,
            "max": 20,
            "step": "1"
        })
    peer_grader_count = Integer(
        display_name="Peer Graders per Response",
        help="The number of peers who will grade each submission.",
        default=3,
        scope=Scope.settings,
        values={
            "min": 1,
            "step": "1",
            "max": 5
        })
    required_peer_grading = Integer(
        display_name="Required Peer Grading",
        help=
        "The number of other students each student making a submission will have to grade.",
        default=3,
        scope=Scope.settings,
        values={
            "min": 1,
            "step": "1",
            "max": 5
        })
    peer_grade_finished_submissions_when_none_pending = Boolean(
        display_name='Allow "overgrading" of peer submissions',
        help=
        ("EXPERIMENTAL FEATURE.  Allow students to peer grade submissions that already have the requisite number of graders, "
         "but ONLY WHEN all submissions they are eligible to grade already have enough graders.  "
         "This is intended for use when settings for `Required Peer Grading` > `Peer Graders per Response`"
         ),
        default=False,
        scope=Scope.settings,
    )
    markdown = String(help="Markdown source of this module",
                      default=textwrap.dedent("""\
                    [prompt]
                        <h3>Censorship in the Libraries</h3>

                        <p>'All of us can think of a book that we hope none of our children or any other children have taken off the shelf. But if I have the right to remove that book from the shelf -- that work I abhor -- then you also have exactly the same right and so does everyone else. And then we have no books left on the shelf for any of us.' --Katherine Paterson, Author
                        </p>

                        <p>
                        Write a persuasive essay to a newspaper reflecting your views on censorship in libraries. Do you believe that certain materials, such as books, music, movies, magazines, etc., should be removed from the shelves if they are found offensive? Support your position with convincing arguments from your own experience, observations, and/or reading.
                        </p>
                    [prompt]
                    [rubric]
                    + Ideas
                    - Difficult for the reader to discern the main idea.  Too brief or too repetitive to establish or maintain a focus.
                    - Attempts a main idea.  Sometimes loses focus or ineffectively displays focus.
                    - Presents a unifying theme or main idea, but may include minor tangents.  Stays somewhat focused on topic and task.
                    - Presents a unifying theme or main idea without going off on tangents.  Stays completely focused on topic and task.
                    + Content
                    - Includes little information with few or no details or unrelated details.  Unsuccessful in attempts to explore any facets of the topic.
                    - Includes little information and few or no details.  Explores only one or two facets of the topic.
                    - Includes sufficient information and supporting details. (Details may not be fully developed; ideas may be listed.)  Explores some facets of the topic.
                    - Includes in-depth information and exceptional supporting details that are fully developed.  Explores all facets of the topic.
                    + Organization
                    - Ideas organized illogically, transitions weak, and response difficult to follow.
                    - Attempts to logically organize ideas.  Attempts to progress in an order that enhances meaning, and demonstrates use of transitions.
                    - Ideas organized logically.  Progresses in an order that enhances meaning.  Includes smooth transitions.
                    + Style
                    - Contains limited vocabulary, with many words used incorrectly.  Demonstrates problems with sentence patterns.
                    - Contains basic vocabulary, with words that are predictable and common.  Contains mostly simple sentences (although there may be an attempt at more varied sentence patterns).
                    - Includes vocabulary to make explanations detailed and precise.  Includes varied sentence patterns, including complex sentences.
                    + Voice
                    - Demonstrates language and tone that may be inappropriate to task and reader.
                    - Demonstrates an attempt to adjust language and tone to task and reader.
                    - Demonstrates effective adjustment of language and tone to task and reader.
                    [rubric]
                    [tasks]
                    (Self), ({4-12}AI), ({9-12}Peer)
                    [tasks]

        """),
                      scope=Scope.settings)
class AdaptiveTestXBlock(XBlock):
    """
    An adaptive-learning testing xblock. This Xblock allows instructors to 
    selected one of many avlaiable tests (currently Kolb and Dominancia Cerebral)
    and provide an output of the student's learning style via a survey. Improvements
    to this Xblock include Course Modification (see TODOs).
    """

    # Scopes. Persistent variables
    # See scopes definition for user_state (per user) and user_state_summary (global), among others.
    testNumber = Integer(
        default=0,
        scope=Scope.user_state_summary,
        help="Test number (0: Not avaliable, 1: Kolb, 2: Dominancia",
    )
    # TestResult contains object: { result: string }
    testResult = JSONField(
        default="",
        scope=Scope.user_state,
        help="String identifying student learning style, according to test",
    )
    # TestResults[] contains per item:
    # { test: number, result: object, user_id: string, user_full_name: string }
    testResults = JSONField(
        default=[],
        scope=Scope.user_state_summary,
        help="Array containing student information and results",
    )
    testSolved = Boolean(
        default=False,
        scope=Scope.user_state,
        help="Flag if the user already solved the test",
    )

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

    def student_view(self, context=None):
        """
        The primary view of the StudentAdaptiveTestXBlock, shown to students
        when viewing courses.
        """
        html = self.resource_string("static/html/student_adaptive_test.html")
        frag = Fragment(html.format(self=self))

        frag.add_css(self.resource_string("static/css/adaptive_test.css"))

        frag.add_javascript(
            self.resource_string("static/js/src/jquery-1.12.4.js"))
        frag.add_javascript(self.resource_string("static/js/src/jquery-ui.js"))
        frag.add_javascript(
            self.resource_string("static/js/src/student_adaptive_test.js"))

        frag.initialize_js('StudentAdaptiveTestXBlock')
        return frag

    def studio_view(self, context=None):
        """
        The primary view of the StudioAdaptiveTestXBlock, shown to students
        when viewing courses.
        """
        if len(self.testResults) > 0:
            html = self.resource_string("static/html/studio_analytics.html")
            frag = Fragment(html.format(self=self))
            frag.add_javascript(
                self.resource_string("static/js/src/studio_analytics.js"))

            frag.add_css(self.resource_string("static/css/adaptive_test.css"))
            frag.initialize_js('StudioAnalyticsXBlock')  # Notice

        else:
            html = self.resource_string(
                "static/html/studio_adaptive_test.html")
            frag = Fragment(html.format(self=self))
            frag.add_javascript(
                self.resource_string("static/js/src/studio_adaptive_test.js"))

            frag.add_css(self.resource_string("static/css/adaptive_test.css"))
            frag.initialize_js('StudioAdaptiveTestXBlock')  # Notice

        return frag

    @XBlock.json_handler
    def select_test(self, data, suffix=''):
        """
        Instructor's selected test handler. JS returned data is saved into global testNumber
        """
        self.testNumber = data

        return True

    @XBlock.json_handler
    def load_test(self, data, suffix=''):
        """
        Handler that returns the test currently used
        """
        # Returns results in case they exist
        if self.testSolved:
            return {'test': self.testNumber, 'test_result': self.testResult}
        else:
            return {'test': self.testNumber}

    @XBlock.json_handler
    def submit_test(self, data, suffix=''):
        """
        An example handler, which increments the data.
        """
        collectedTest = data
        user_test_result = {}

        # Something should be modified in the course
        # EDXCUT: https://github.com/mitodl/edxcut showed to be an option.
        # Testing was unabled to use it correctly.
        # TODO: Take collectedTest and make modifications into the course content

        user_test_result["result"] = collectedTest
        user_test_result["test"] = self.testNumber

        user_test_result['user_id'] = self.scope_ids.user_id

        user_service = self.runtime.service(self, 'user')
        xb_user = user_service.get_current_user()
        user_test_result['user_full_name'] = xb_user.full_name

        self.testResults.append(user_test_result)

        self.testResult = collectedTest
        self.testSolved = True

        return True

    @XBlock.json_handler
    def load_analytics(self, data, suffix=''):
        """
        An example handler, which increments the data.
        """
        return self.testResults

    # Workbench scenarios. Ignore, unless you know how to use them.
    @staticmethod
    def workbench_scenarios():
        """A canned scenario for display in the workbench."""
        return [
            ("AdaptiveTestXBlock", """<adaptive_test/>
             """),
        ]
Beispiel #23
0
class MRQBlock(SubmittingXBlockMixin, StudentViewUserStateMixin,
               QuestionnaireAbstractBlock, ExpandStaticURLMixin):
    """
    An XBlock used to ask multiple-response questions
    """
    CATEGORY = 'pb-mrq'
    STUDIO_LABEL = _(u"Multiple Response Question")
    USER_STATE_FIELDS = [
        'student_choices',
    ]

    student_choices = List(
        # Last submissions by the student
        default=[],
        scope=Scope.user_state)
    required_choices = List(
        display_name=_("Required Choices"),
        help=
        _("Specify the value[s] that students must select for this MRQ to be considered correct."
          ),
        scope=Scope.content,
        list_values_provider=QuestionnaireAbstractBlock.choice_values_provider,
        list_style='set',  # Underered, unique items. Affects the UI editor.
        default=[],
    )
    ignored_choices = List(
        display_name=_("Ignored Choices"),
        help=_(
            "Specify the value[s] that are neither correct nor incorrect. "
            "Any values not listed as required or ignored will be considered wrong."
        ),
        scope=Scope.content,
        list_values_provider=QuestionnaireAbstractBlock.choice_values_provider,
        list_style='set',  # Underered, unique items. Affects the UI editor.
        default=[],
    )
    message = String(display_name=_("Message"),
                     help=_("General feedback provided when submitting"),
                     scope=Scope.content,
                     default="")
    hide_results = Boolean(display_name="Hide results",
                           scope=Scope.content,
                           default=False)
    editable_fields = (
        'question',
        'required_choices',
        'ignored_choices',
        'message',
        'display_name',
        'show_title',
        'weight',
        'hide_results',
    )

    def describe_choice_correctness(self, choice_value):
        if choice_value in self.required_choices:
            return self._(u"Required")
        elif choice_value in self.ignored_choices:
            return self._(u"Ignored")
        return self._(u"Not Acceptable")

    def get_results(self, previous_result):
        """
        Get the results a student has already submitted.
        """
        result = self.calculate_results(previous_result['submissions'])
        result['completed'] = True
        return result

    def get_last_result(self):
        if self.student_choices:
            return self.get_results({'submissions': self.student_choices})
        else:
            return {}

    def submit(self, submissions):
        log.debug(u'Received MRQ submissions: "%s"', submissions)

        result = self.calculate_results(submissions)
        self.student_choices = submissions

        log.debug(u'MRQ submissions result: %s', result)
        return result

    def calculate_results(self, submissions):
        score = 0
        results = []
        tips = None

        if not self.hide_results:
            tips = self.get_tips()

        for choice in self.custom_choices:
            choice_completed = True
            choice_tips_html = []
            choice_selected = choice.value in submissions

            if choice.value in self.required_choices:
                if not choice_selected:
                    choice_completed = False
            elif choice_selected and choice.value not in self.ignored_choices:
                choice_completed = False

            if choice_completed:
                score += 1

            choice_result = {
                'value': choice.value,
                'selected': choice_selected,
                'content': choice.content
            }
            # Only include tips/results in returned response if we want to display them
            if not self.hide_results:
                # choice_tips_html list is being set only when 'self.hide_results' is False, to optimize,
                # execute the loop only when 'self.hide_results' is set to False
                for tip in tips:
                    if choice.value in tip.values:
                        choice_tips_html.append(
                            tip.render('mentoring_view').content)
                        break

                loader = ResourceLoader(__name__)
                choice_result['completed'] = choice_completed
                choice_result['tips'] = loader.render_template(
                    'templates/html/tip_choice_group.html', {
                        'tips_html': choice_tips_html,
                    })

            results.append(choice_result)

        status = 'incorrect' if score <= 0 else 'correct' if score >= len(
            results) else 'partial'

        if sub_api:
            # Send the answer as a concatenated list to the submissions API
            answer = [
                choice['content'] for choice in results if choice['selected']
            ]
            sub_api.create_submission(self.student_item_key, ', '.join(answer))

        return {
            'submissions': submissions,
            'status': status,
            'choices': results,
            'message': self.message_formatted,
            'weight': self.weight,
            'score': (float(score) / len(results)) if results else 0,
        }

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

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

        def choice_name(choice_value):
            for choice in self.human_readable_choices:
                if choice["value"] == choice_value:
                    return choice["display_name"]
            return choice_value

        all_values = set(self.all_choice_values)
        required = set(data.required_choices)
        ignored = set(data.ignored_choices)

        if len(required) < len(data.required_choices):
            add_error(self._(u"Duplicate required choices set"))
        if len(ignored) < len(data.ignored_choices):
            add_error(self._(u"Duplicate ignored choices set"))
        for val in required.intersection(ignored):
            add_error(
                self._(u"A choice is listed as both required and ignored: {}").
                format(choice_name(val)))
        for val in (required - all_values):
            add_error(
                self._(u"A choice value listed as required does not exist: {}"
                       ).format(choice_name(val)))
        for val in (ignored - all_values):
            add_error(
                self._(u"A choice value listed as ignored does not exist: {}").
                format(choice_name(val)))

    def student_view_data(self, context=None):
        """
        Returns a JSON representation of the student_view of this XBlock,
        retrievable from the Course Block API.
        """
        return {
            'id':
            self.name,
            'block_id':
            unicode(self.scope_ids.usage_id),
            'display_name':
            self.display_name,
            'title':
            self.display_name,
            'type':
            self.CATEGORY,
            'weight':
            self.weight,
            'question':
            self.expand_static_url(self.question),
            'message':
            self.message,
            'choices': [{
                'value':
                choice['value'],
                'content':
                self.expand_static_url(choice['display_name'])
            } for choice in self.human_readable_choices],
            'hide_results':
            self.hide_results,
            'tips':
            [tip.student_view_data()
             for tip in self.get_tips()] if not self.hide_results else [],
        }
Beispiel #24
0
class VideoXBlock(
        SettingsMixin, TranscriptsMixin, PlaybackStateMixin, LocationMixin,
        StudioEditableXBlockMixin, ContentStoreMixin, WorkbenchMixin, XBlock
):
    """
    Main VideoXBlock class, responsible for saving video settings and rendering it for students.

    VideoXBlock only provide a storage facilities for fields data, but not
    decide what fields to show to user. `BaseVideoPlayer` and it's subclassess
    declare what fields are required for proper configuration of a video.
    See `BaseVideoPlayer.basic_fields` and `BaseVideoPlayer.advanced_fields`.
    """

    icon_class = "video"

    display_name = String(
        default=_('Video'),
        display_name=_('Component Display Name'),
        help=_('The name students see. This name appears in the course ribbon and as a header for the video.'),
        scope=Scope.content,
    )

    href = String(
        default='',
        display_name=_('Video URL'),
        help=_('URL of the video page. E.g. https://example.wistia.com/medias/12345abcde'),
        scope=Scope.content
    )

    download_video_allowed = Boolean(
        default=False,
        scope=Scope.content,
        display_name=_('Video Download Allowed'),
        help=_(
            "Allow students to download this video if they cannot use the edX video player."
            " A link to download the file appears below the video."
        ),
        resettable_editor=False
    )

    download_video_url = String(
        default='',
        display_name=_('Video file URL'),
        help=_("The URL where you've posted non hosted versions of the video. URL must end in .mpeg, .mp4, .ogg, or"
               " .webm. (For browser compatibility, we strongly recommend .mp4 and .webm format.) To allow students to"
               " download these videos, set Video Download Allowed to True."),
        scope=Scope.content
    )

    account_id = String(
        default='',
        display_name=_('Account Id'),
        help=_('Your Brightcove account id'),
        scope=Scope.content,
    )

    player_id = String(
        default='default',
        display_name=_('Player Id'),
        help=_('Your Brightcove player id. Use "Luna" theme for all your players. You can choose one of your players'
               ' from a <a href="https://studio.brightcove.com/products/videocloud/players" target="_blank">list</a>.'),
        scope=Scope.content,
    )

    player_name = String(
        default=PlayerName.DUMMY,
        scope=Scope.content
    )

    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.content,
        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.content,
        default=datetime.timedelta(seconds=0)
    )

    handout = String(
        default='',
        scope=Scope.content,
        display_name=_('Upload handout'),
        help=_('You can upload handout file for students')
    )

    download_transcript_allowed = Boolean(
        default=False,
        scope=Scope.content,
        display_name=_('Download Transcript Allowed'),
        help=_(
            "Allow students to download the timed transcript. A link to download the file appears below the video."
            " By default, the transcript is an .vtt or .srt file. If you want to provide the transcript for download"
            " in a different format, upload a file by using the Upload Handout field."
        ),
        resettable_editor=False
    )

    default_transcripts = String(
        default='',
        scope=Scope.content,
        display_name=_('Default Timed Transcript'),
        help=_(
            'Default transcripts are uploaded automatically from a video platform '
            'to the list of available transcripts.<br/>'
            '<b>Note: valid "Video API Token" should be given in order to make auto fetching possible.</b><br/>'
            'Advice: disable transcripts displaying on your video service to avoid transcripts overlapping.'
        ),
        resettable_editor=False
    )

    token = String(
        default='',
        display_name=_('Video API Token'),
        help=_('You can generate a client token following official documentation of your video platform\'s API.'),
        scope=Scope.content,
        resettable_editor=False
    )

    metadata = Dict(
        default={},
        display_name=_('Metadata'),
        help=_('This field stores different metadata, e.g. authentication data. '
               'If new metadata item is designed, this is to add an appropriate key to backend\'s '
               '`metadata_fields` property.'),
        scope=Scope.content
    )

    @property
    def editable_fields(self):
        """
        Return list of xblock's editable fields used by StudioEditableXBlockMixin.clean_studio_edits().
        """
        return self.get_player().editable_fields

    @staticmethod
    def get_brightcove_js_url(account_id, player_id):
        """
        Return url to brightcove player js file, considering `account_id` and `player_id`.

        Arguments:
            account_id (str): Account id fetched from video xblock.
            player_id (str): Player id fetched from video xblock.
        Returns:
            Url to brightcove player js (str).
        """
        return "https://players.brightcove.net/{account_id}/{player_id}_default/index.min.js".format(
            account_id=account_id,
            player_id=player_id
        )

    @staticmethod
    def add_validation_message(validation, message_text):
        """
        Add error message on xblock fields validation.

        Attributes:
            validation (xblock.validation.Validation): Object containing validation information for an xblock instance.
            message_text (str): Message text per se.
        """
        validation.add(ValidationMessage(ValidationMessage.ERROR, message_text))

    def validate_account_id_data(self, validation, data):
        """
        Validate account id value which is mandatory.

        Attributes:
            validation (xblock.validation.Validation): Object containing validation information for an xblock instance.
            data (xblock.internal.VideoXBlockWithMixins): Object containing data on xblock.
        """
        account_id_is_empty = data.account_id in ['', b'']  # pylint: disable=unsubscriptable-object
        # Validate provided account id
        if account_id_is_empty:
            # Account Id field is mandatory
            self.add_validation_message(
                validation,
                _("Account ID can not be empty. Please provide a valid Brightcove Account ID.")
            )
            return

        try:
            response = requests.head(VideoXBlock.get_brightcove_js_url(data.account_id, data.player_id))
            if not response.ok:
                self.add_validation_message(validation, _(
                    "Invalid Account ID or Player ID, please recheck."
                ))

        except requests.ConnectionError:
            self.add_validation_message(
                validation,
                _("Can't validate submitted account ID at the moment. "
                  "Please try to save settings one more time.")
            )

    def validate_href_data(self, validation, data):
        """
        Validate href value.

        Attributes:
            validation (xblock.validation.Validation): Object containing validation information for an xblock instance.
            data (xblock.internal.VideoXBlockWithMixins): Object containing data on xblock.
        """
        is_not_provided_href = \
            data.href == self.fields['href'].default  # pylint: disable=unsubscriptable-object
        is_matched_href = False
        for _player_name, player_class in BaseVideoPlayer.load_classes():
            if player_class.match(data.href):
                is_matched_href = True
        # Validate provided video href value
        if not (is_not_provided_href or is_matched_href):
            self.add_validation_message(
                validation,
                _("Incorrect or unsupported video URL, please recheck.")
            )

    def validate_field_data(self, validation, data):
        """
        Validate data submitted via xblock edit pop-up.

        Reference:
            https://github.com/edx/xblock-utils/blob/v1.0.3/xblockutils/studio_editable.py#L245

        Attributes:
            validation (xblock.validation.Validation): Object containing validation information for an xblock instance.
            data (xblock.internal.VideoXBlockWithMixins): Object containing data on xblock.
        """
        is_brightcove = str(self.player_name) == PlayerName.BRIGHTCOVE

        if is_brightcove:
            self.validate_account_id_data(validation, data)

        self.validate_href_data(validation, data)

    def get_download_video_url(self):
        """
        Return direct video url for download if download is allowed.

        Else return `False` which will hide "download video" button.
        """
        return self.download_video_allowed and self.get_player().download_video_url

    def student_view(self, _context=None):
        """
        The primary view of the `VideoXBlock`, shown to students when viewing courses.
        """
        player_url = self.runtime.handler_url(self, 'render_player')
        download_transcript_handler_url = self.runtime.handler_url(self, 'download_transcript')
        transcript_download_link = self.get_transcript_download_link()
        full_transcript_download_link = ''

        if transcript_download_link:
            full_transcript_download_link = download_transcript_handler_url + transcript_download_link

        context = {
            'player_url': player_url,
            'display_name': self.display_name,
            'usage_id': self.usage_id,
            'handout': self.handout,
            'transcripts': list(self.route_transcripts()),
            'download_transcript_allowed': self.download_transcript_allowed,
            'transcripts_streaming_enabled': self.threeplaymedia_streaming,
            'download_video_url': self.get_download_video_url(),
            'handout_file_name': self.get_file_name_from_path(self.handout),
            'transcript_download_link': full_transcript_download_link,
            'version': __version__
        }
        log.debug("[student_view_context]: transcripts %s", context['transcripts'])
        frag = Fragment(
            render_resource(
                'static/html/student_view.html',
                **context
            )
        )
        frag.add_javascript(resource_string("static/js/student-view/video-xblock.js"))
        frag.add_css(resource_string("static/css/student-view.css"))
        frag.initialize_js('VideoXBlockStudentViewInit')
        return frag

    def _update_default_transcripts(self, player, transcripts):
        """
        Private method to fetch/update default transcripts.
        """
        log.debug("Default transcripts updating...")
        # Prepare parameters necessary to make requests to API.
        video_id = player.media_id(self.href)
        kwargs = {'video_id': video_id}
        for k in self.metadata:
            kwargs[k] = self.metadata[k]
        # For a Brightcove player only
        is_not_default_account_id = \
            self.account_id is not self.fields['account_id'].default  # pylint: disable=unsubscriptable-object
        if is_not_default_account_id:
            kwargs['account_id'] = self.account_id

        # Fetch captions list (available/default transcripts list) from video platform API
        try:
            default_transcripts, transcripts_autoupload_message = player.get_default_transcripts(**kwargs)
        except ApiClientError:
            default_transcripts, transcripts_autoupload_message = [], _('Failed to fetch default transcripts.')
        log.debug("Autofetch message: '{}'".format(transcripts_autoupload_message))
        # Default transcripts should contain transcripts of distinct languages only
        distinct_default_transcripts = player.clean_default_transcripts(default_transcripts)
        # Needed for frontend
        initial_default_transcripts = distinct_default_transcripts
        # Exclude enabled transcripts from the list of available ones, and remove duplicates
        filtered_default_transcripts = player.filter_default_transcripts(distinct_default_transcripts, transcripts)
        self.default_transcripts = filtered_default_transcripts
        if self.default_transcripts:
            self.default_transcripts.sort(key=lambda l: l['label'])

        return initial_default_transcripts, transcripts_autoupload_message

    def studio_view(self, _context):
        """
        Render a form for XBlock editing.
        """
        fragment = Fragment()
        player = self.get_player()
        languages = [{'label': label, 'code': lang} for lang, label in ALL_LANGUAGES]
        languages.sort(key=lambda l: l['label'])
        transcripts = self.get_enabled_transcripts()
        download_transcript_handler_url = self.runtime.handler_url(self, 'download_transcript')
        auth_error_message = ''
        # Authenticate to API of the player video platform and update metadata with auth information.
        # Note that there is no need to authenticate to Youtube API,
        # whilst for Wistia, a sample authorised request is to be made to ensure authentication succeeded,
        # since it is needed for the auth status message generation and the player's state update with auth status.
        if self.token:
            _auth_data, auth_error_message = self.authenticate_video_api(self.token.encode(encoding='utf-8'))

        initial_default_transcripts, transcripts_autoupload_message = self._update_default_transcripts(
            player, transcripts
        )
        log.debug("Fetched default transcripts: {}".format(initial_default_transcripts))

        # Prepare basic_fields and advanced_fields for them to be rendered
        basic_fields = self.prepare_studio_editor_fields(player.basic_fields)
        advanced_fields = self.prepare_studio_editor_fields(player.advanced_fields)
        context = {
            'advanced_fields': advanced_fields,
            'auth_error_message': auth_error_message,
            'basic_fields': basic_fields,
            'courseKey': self.course_key,
            'languages': languages,
            'player_name': self.player_name,  # for players identification
            'players': PlayerName,
            'sources': list(TranscriptSource.to_dict().items()),
            # transcripts context:
            'transcripts': filter_transcripts_by_source(
                transcripts, sources=[TranscriptSource.THREE_PLAY_MEDIA], exclude=True
            ),
            'transcripts_fields': self.prepare_studio_editor_fields(player.trans_fields),
            'three_pm_fields': self.prepare_studio_editor_fields(player.three_pm_fields),
            'transcripts_type': '3PM' if self.threeplaymedia_streaming else 'manual',
            'default_transcripts': self.default_transcripts,
            'enabled_default_transcripts': filter_transcripts_by_source(transcripts),
            'enabled_managed_transcripts': self.get_enabled_managed_transcripts(),
            'initial_default_transcripts': initial_default_transcripts,
            'transcripts_autoupload_message': transcripts_autoupload_message,
            'download_transcript_handler_url': download_transcript_handler_url,
        }

        fragment.content = render_template('studio-edit.html', **context)
        fragment.add_css(resource_string("static/css/student-view.css"))
        fragment.add_css(resource_string("static/css/transcripts-upload.css"))
        fragment.add_css(resource_string("static/css/studio-edit.css"))
        fragment.add_css(resource_string("static/css/studio-edit-accordion.css"))
        fragment.add_javascript(resource_string("static/js/runtime-handlers.js"))
        fragment.add_javascript(resource_string("static/js/studio-edit/utils.js"))
        fragment.add_javascript(resource_string("static/js/studio-edit/studio-edit.js"))
        fragment.add_javascript(resource_string("static/js/studio-edit/transcripts-autoload.js"))
        fragment.add_javascript(resource_string("static/js/studio-edit/transcripts-manual-upload.js"))
        fragment.initialize_js('StudioEditableXBlock')
        return fragment

    @XBlock.handler
    def render_player(self, _request, _suffix=''):
        """
        View `student_view` loads this handler as an iframe to display actual video player.

        Arguments:
            _request (webob.Request): Request to handle. Imposed by `XBlock.handler`.
            _suffix (string): Slug used for routing. Imposed by `XBlock.handler`.
        Returns:
            Rendered html string as a Response (webob.Response).
        """
        player = self.get_player()
        save_state_url = self.runtime.handler_url(self, 'save_player_state')
        transcripts = render_resource(
            'static/html/transcripts.html',
            transcripts=list(self.route_transcripts())
        ).strip()
        return player.get_player_html(
            url=self.href, account_id=self.account_id, player_id=self.player_id,
            video_id=player.media_id(self.href),
            video_player_id='video_player_{}'.format(self.block_id),
            save_state_url=save_state_url,
            player_state=self.player_state,
            start_time=int(self.start_time.total_seconds()),  # pylint: disable=no-member
            end_time=int(self.end_time.total_seconds()),  # pylint: disable=no-member
            brightcove_js_url=VideoXBlock.get_brightcove_js_url(self.account_id, self.player_id),
            transcripts=transcripts,
        )

    @XBlock.json_handler
    def publish_event(self, data, _suffix=''):
        """
        Handler to publish XBlock event from frontend. Called by JavaScript of `student_view`.

        Arguments:
            data (dict): Data from frontend on the event.
            _suffix (string): Slug used for routing. Imposed by `XBlock.json_handler`.
        Returns:
            Data on result (dict).
        """
        try:
            event_type = data.pop('eventType')
        except KeyError:
            return {'result': 'error', 'message': 'Missing eventType in JSON data'}

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

    def clean_studio_edits(self, data):
        """
        Given POST data dictionary 'data', clean the data before validating it.

        Try to detect player by submitted video url. If fails, it defaults to 'dummy-player'.
        Also, populate xblock's default values from settings.

        Arguments:
            data (dict): POST data.
        """
        data['player_name'] = self.fields['player_name'].default  # pylint: disable=unsubscriptable-object
        for player_name, player_class in BaseVideoPlayer.load_classes():
            if player_name == PlayerName.DUMMY:
                continue
            if player_class.match(data['href']):
                data['player_name'] = player_name
                log.debug("Submitted player[{}] with data: {}".format(player_name, data))
                break

    def get_player(self):
        """
        Helper method to load video player by entry-point label.

        Returns:
            Current player object (instance of a platform-specific player class).
        """
        player = BaseVideoPlayer.load_class(self.player_name)
        return player(self)

    def _get_field_help(self, field_name, field):
        """
        Get help text for field.

        First try to load override from video backend, then check field definition
        and lastly fall back to empty string.
        """
        backend_fields_help = self.get_player().fields_help
        if field_name in backend_fields_help:
            return backend_fields_help[field_name]
        elif field.help:
            return field.help
        return ''

    def initialize_studio_field_info(self, field_name, field, field_type=None):
        """
        Initialize studio editor's field info.

        Arguments:
            field_name (str): Name of a video XBlock field whose info is to be made.
            field (xblock.fields): Video XBlock field object.
            field_type (str): Type of field.
        Returns:
            info (dict): Information on a field.
        """
        info = super(VideoXBlock, self)._make_field_info(field_name, field)
        # workaround for '' account_id value when unset - should use default
        if not info['is_set']:
            info['value'] = info['default']
        info['help'] = self._get_field_help(field_name, field)
        if field_type:
            info['type'] = field_type
        if field_name == 'handout':
            info['file_name'] = self.get_file_name_from_path(self.handout)
            info['value'] = self.get_path_for(self.handout)
        return info

    def populate_default_value(self, field):
        """
        Populate unset default values from settings file.
        """
        for key, value in list(self.settings.items()):
            # if field value is empty and there is json-settings default:
            if field.name == key and getattr(field, 'default', None) in ['', b'', 'default']:
                setattr(field, '_default', value)  # pylint: disable=literal-used-as-attribute

        return field

    def _make_field_info(self, field_name, field):
        """
        Override and extend data of built-in method.

        Create the information that the template needs to render a form field for this field.
        Reference:
            https://github.com/edx/xblock-utils/blob/v1.0.3/xblockutils/studio_editable.py#L96

        Arguments:
            field_name (str): Name of a video XBlock field whose info is to be made.
            field (xblock.fields): Video XBlock field object.
        Returns:
            info (dict): Information on a field to be rendered in the studio editor modal.
        """
        if field_name in ('start_time', 'end_time'):
            # RelativeTime field isn't supported by default.
            info = {
                'name': field_name,
                'display_name': field.display_name if field.display_name else "",
                'is_set': field.is_set_on(self),
                'default': field.default,
                'value': field.read_from(self),
                'has_values': False,
                'allow_reset': field.runtime_options.get('resettable_editor', True),
                'list_values': None,
                'has_list_values': False,
                'type': 'string',
            }
        elif field_name in ('handout', 'transcripts', 'default_transcripts', 'token'):
            info = self.initialize_studio_field_info(field_name, field, field_type=field_name)
        else:
            info = self.initialize_studio_field_info(field_name, field)
        return info

    def prepare_studio_editor_fields(self, field_names):
        """
        Order xblock fields in studio editor modal.

        Arguments:
            field_names (tuple): Names of Xblock fields.
        Returns:
            prepared_fields (list): XBlock fields prepared to be rendered in a studio edit modal.
        """
        prepared_fields = []
        for field_name in field_names:
            # set default from json XBLOCK_SETTINGS config:
            populated_field = self.populate_default_value(
                self.fields[field_name]  # pylint:disable=unsubscriptable-object
            )
            # make extra field configuration for frontend rendering:
            field_info = self._make_field_info(field_name, populated_field)
            prepared_fields.append(field_info)

        return prepared_fields

    def get_file_name_from_path(self, field):
        """
        Helper for getting filename from string with path to MongoDB storage.

        Example of string:
            asset-v1-RaccoonGang+1+2018+type@asset+block@<filename>

        Arguments:
            field (str): The path to file.
        Returns:
            The name of file with an extension.
        """
        return field.split('@')[-1]

    def get_path_for(self, file_field):
        """
        Return downloaded asset url with slash in start of it.

        Url, retrieved after storing of the file field in MongoDB, looks like this:
            'asset-v1-RaccoonGang+1+2018+type@asset+block@<filename>'

        Arguments:
            file_field (str): name a file is stored in MongoDB under.
        Returns:
            Full path of a downloaded asset.
        """
        if file_field:
            return os.path.join('/', file_field)
        return ''

    @XBlock.json_handler
    def dispatch(self, request, suffix):
        """
        Dispatch request to XBlock's player.

        Arguments:
            request (xblock.django.request.DjangoWebobRequest): Incoming request data.
            suffix (str): Slug used for routing. Imposed by `XBlock.json_handler`.
        Returns:
             Depending on player's `dispatch()` entry point, either info on video / Brightcove account or None value
             (when performing some action via Brightcove API) may be returned.
        """
        return self.get_player().dispatch(request, suffix)

    @XBlock.handler
    def ui_dispatch(self, _request, suffix):
        """
        Dispatcher for a requests sent by dynamic Front-end components.

        Typical use case: Front-end wants to check with backend if it's ok to show certain part of UI.

        Arguments:
            _request (xblock.django.request.DjangoWebobRequest): Incoming request data. Not used.
            suffix (str): Slug used for routing.
        Returns:
             Response object, containing response data.
        """
        resp = {
            'success': True,
            'data': {}
        }
        if suffix == 'get-metadata':
            resp['data'] = {'metadata': self.metadata}
        elif suffix == 'can-show-backend-settings':
            player = self.get_player()
            if str(self.player_name) == PlayerName.BRIGHTCOVE:
                resp['data'] = player.can_show_settings()
            else:
                resp['data'] = {'canShow': False}

        response = Response(json.dumps(resp), content_type='application/json', charset='utf8')
        return response

    def authenticate_video_api(self, token):
        """
        Authenticate to a video platform's API.

        Arguments:
            token (str): token provided by a user before the save button was clicked (for handlers).
        Returns:
            error_message (dict): Status message for template rendering.
            auth_data (dict): Tokens and credentials, necessary to perform authorised API requests.
        """
        # TODO move auth fields validation and kwargs population to specific backends
        # Handles a case where no token was provided by a user
        kwargs = {'token': token}

        # Handles a case where no account_id was provided by a user
        if str(self.player_name) == PlayerName.BRIGHTCOVE:
            if self.account_id == self.fields['account_id'].default:  # pylint: disable=unsubscriptable-object
                error_message = 'In order to authenticate to a video platform\'s API, please provide an Account Id.'
                return {}, error_message
            kwargs['account_id'] = self.account_id

        player = self.get_player()
        if str(self.player_name) == PlayerName.BRIGHTCOVE and self.metadata.get('client_id'):
            auth_data = {
                'client_secret': self.metadata.get('client_secret'),
                'client_id': self.metadata.get('client_id'),
            }
            error_message = ''
        else:
            auth_data, error_message = player.authenticate_api(**kwargs)

        # Metadata is to be updated on each authentication effort.
        self.update_metadata_authentication(auth_data=auth_data, player=player)
        return auth_data, error_message

    @XBlock.json_handler
    def authenticate_video_api_handler(self, data, _suffix=''):
        """
        Xblock handler to authenticate to a video platform's API. Called by JavaScript of `studio_view`.

        Arguments:
            data (dict): Data from frontend, necessary for authentication (tokens, account id, etc).
            _suffix (str): Slug used for routing. Imposed by `XBlock.json_handler`.
        Returns:
            response (dict): Status messages key-value pairs.
        """
        # Fetch a token provided by a user before the save button was clicked.
        token = str(data)

        is_default_token = token == self.fields['token'].default  # pylint: disable=unsubscriptable-object
        is_youtube_player = str(self.player_name) != PlayerName.YOUTUBE  # pylint: disable=unsubscriptable-object
        if not token or (is_default_token and is_youtube_player):
            return {
                'error_message': "In order to authenticate to a video platform's API, "
                                 "please provide a Video API Token."
                }

        _auth_data, error_message = self.authenticate_video_api(token)
        if error_message:
            return {'error_message': error_message}
        return {'success_message': 'Successfully authenticated to the video platform.'}

    def update_metadata_authentication(self, auth_data, player):
        """
        Update video xblock's metadata field with video platform's API authentication data.

        Arguments:
            auth_data (dict): Data containing credentials necessary for authentication.
            player (object): Object of a platform-specific player class.
        """
        # In case of successful authentication:
        for key in auth_data:
            if key not in player.metadata_fields:
                # Only backend-specific parameters are to be stored
                continue
            self.metadata[key] = auth_data[key]
        # If the last authentication effort was not successful, metadata should be updated as well.
        # Since video xblock metadata may store various information, this is to update the auth data only.
        if not auth_data:
            self.metadata['token'] = ''  # Wistia API
            self.metadata['access_token'] = ''  # Brightcove API
            self.metadata['client_id'] = ''  # Brightcove API
            self.metadata['client_secret'] = ''  # Brightcove API

    @XBlock.json_handler
    def upload_default_transcript_handler(self, data, _suffix=''):
        """
        Upload a transcript, fetched from a video platform's API, to video xblock.

        Arguments:
            data (dict): Data from frontend on a default transcript to be fetched from a video platform.
            _suffix (str): Slug used for routing. Imposed by `XBlock.json_handler`.
        Returns:
            response (dict): Data on a default transcript, fetched from a video platform.

        """
        log.debug("Uploading default transcript with data: {}".format(data))
        player = self.get_player()
        video_id = player.media_id(self.href)
        lang_code = str(data.get('lang'))
        lang_label = str(data.get('label'))
        source = str(data.get('source', ''))
        sub_url = str(data.get('url'))

        reference_name = create_reference_name(lang_label, video_id, source)

        # Fetch text of single default transcript:
        unicode_subs_text = player.download_default_transcript(sub_url, lang_code)
        if not unicode_subs_text:
            return {'failure_message': _("Couldn't upload transcript text.")}

        if not player.default_transcripts_in_vtt:
            prepared_subs = self.convert_caps_to_vtt(caps=unicode_subs_text)
        else:
            prepared_subs = unicode_subs_text

        file_name, external_url = self.create_transcript_file(
            trans_str=prepared_subs, reference_name=reference_name
        )

        # Exceptions are handled on the frontend
        success_message = 'Successfully uploaded "{}".'.format(file_name)
        response = {
            'success_message': success_message,
            'lang': lang_code,
            'url': external_url,
            'label': lang_label,
            'source': source,
        }
        log.debug("Uploaded default transcript: {}".format(response))
        return response

    def get_enabled_transcripts(self):
        """
        Get transcripts from different sources depending on current usage mode.
        """
        if self.threeplaymedia_streaming:
            transcripts = normalize_transcripts(list(self.fetch_available_3pm_transcripts()))
        else:
            transcripts = self.get_enabled_managed_transcripts()
        log.debug("Getting enabled transcripts: %s", transcripts)
        return transcripts

    def get_enabled_managed_transcripts(self):
        """
        Get currently enabled in player `managed` ("manual" & "default") transcripts.

        Please, for term definitions refer to the module docstring.
        :return: (list) transcripts that enabled but not directly streamed.
        """
        try:
            transcripts = json.loads(self.transcripts) if self.transcripts else []
            return normalize_transcripts(transcripts)
        except ValueError:
            log.exception("JSON parser can't handle 'self.transcripts' field value: {}".format(self.transcripts))
            return []

    def index_dictionary(self):
        """
        Part of edx-platform search index API.

        Is invoked during course [re]index operation.
        Takes enabled transcripts' content and puts it to search index.
        """
        xblock_body = super(VideoXBlock, self).index_dictionary()
        video_body = {"display_name": self.display_name}

        content = None
        enabled_transcripts = self.route_transcripts()
        for transcript in enabled_transcripts:
            asset_file_name = transcript['url'].split('@')[-1]
            try:
                if transcript['source'] in [TranscriptSource.MANUAL, TranscriptSource.DEFAULT]:
                    asset_location = self.static_content.compute_location(self.course_key, asset_file_name)
                    asset = self.contentstore().find(asset_location)  # pylint: disable=not-callable
                    content = asset.data
                elif transcript['source'] == TranscriptSource.THREE_PLAY_MEDIA:
                    external_transcript = self.fetch_single_3pm_translation({
                        'id': transcript['id'], 'language_id': transcript['lang_id']
                    })
                    content = external_transcript and external_transcript.content
            except IOError:
                log.exception("Transcript indexing failure: can't fetch external transcript[{}]".format(transcript))
            except (ValueError, KeyError, TypeError, AttributeError):
                log.exception(
                    "Transcript indexing failure: can't parse transcript for indexing: [{}]".format(transcript)
                )
            else:
                if content:
                    content_ = self.vtt_to_text(content)
                    video_body.update({transcript['lang']: content_})
            finally:
                content = None

        if "content" in xblock_body:
            xblock_body["content"].update(video_body)
        else:
            xblock_body["content"] = video_body
        xblock_body["content_type"] = "Video"

        return xblock_body
Beispiel #25
0
class SwipeBlock(
        QuestionMixin,
        StudioEditableXBlockMixin,
        StudentViewUserStateMixin,
        XBlockWithPreviewMixin,
        XBlock,
):
    """
    An XBlock used to ask binary-choice questions with a swiping interface
    """
    CATEGORY = 'pb-swipe'
    STUDIO_LABEL = _(u"Swipeable Binary Choice Question")
    USER_STATE_FIELDS = ['student_choice']

    text = String(
        display_name=_("Text"),
        help=
        _("Text to display on this card. The student must determine if this statement is true or false."
          ),
        scope=Scope.content,
        default="",
        multiline_editor=True,
    )

    img_url = String(
        display_name=_("Image"),
        help=_("Specify the URL of an image associated with this question."),
        scope=Scope.content,
        default="")

    correct = Boolean(
        display_name=_("Correct Choice"),
        help=_("Specifies whether the card is correct."),
        scope=Scope.content,
        default=False,
    )

    feedback_correct = String(
        display_name=_("Correct Answer Feedback"),
        help=_("Feedback to display when student answers correctly."),
        scope=Scope.content,
    )

    feedback_incorrect = String(
        display_name=_("Incorrect Answer Feedback"),
        help=_("Feedback to display when student answers incorrectly."),
        scope=Scope.content,
    )

    student_choice = Boolean(scope=Scope.user_state,
                             help=_("Last input submitted by the student."))

    editable_fields = ('display_name', 'text', 'img_url', 'correct',
                       'feedback_correct', 'feedback_incorrect')

    def calculate_results(self, submission):
        correct = submission == self.correct
        return {
            'submission':
            submission,
            'status':
            'correct' if correct else 'incorrect',
            'score':
            1 if correct else 0,
            'feedback':
            self.feedback_correct if correct else self.feedback_incorrect,
        }

    def get_results(self, previous_result):
        return self.calculate_results(previous_result['submission'])

    def get_last_result(self):
        return self.get_results({'submission': self.student_choice
                                 }) if self.student_choice else {}

    def submit(self, submission):
        log.debug(u'Received Swipe submission: "%s"', submission)
        # We expect to receive a boolean indicating the swipe the student made (left -> false, right -> true)
        self.student_choice = submission['value']
        result = self.calculate_results(self.student_choice)
        log.debug(u'Swipe submission result: %s', result)
        return result

    def student_view_data(self, context=None):
        """
        Returns a JSON representation of the student_view of this XBlock,
        retrievable from the Course Block API.
        """
        return {
            'id': self.name,
            'block_id': six.text_type(self.scope_ids.usage_id),
            'display_name': self.display_name_with_default,
            'type': self.CATEGORY,
            'text': self.text,
            'img_url': self.expand_static_url(self.img_url),
            'correct': self.correct,
            'feedback': {
                'correct': self.feedback_correct,
                'incorrect': self.feedback_incorrect,
            },
        }

    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('"{}"'.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.
            from .platform_dependencies import replace_static_urls
            if replace_static_urls:
                url = replace_static_urls(
                    '"{}"'.format(url), None,
                    course_id=self.runtime.course_id)[1:-1]
        return url

    def mentoring_view(self, context=None):
        """ Render the swipe image, text & whether it's correct within a mentoring block question. """
        return Fragment((u'<img src="{img_url}" style="max-width: 100%;" />'
                         u'<p class="swipe-text">"{text}"</p>').format(
                             img_url=self.expand_static_url(self.img_url),
                             text=self.text,
                         ))

    def student_view(self, context=None):
        """ Normal view of this XBlock, identical to mentoring_view """
        return self.mentoring_view(context)
Beispiel #26
0
class OpenAssessmentBlock(MessageMixin,
                          SubmissionMixin,
                          PeerAssessmentMixin,
                          SelfAssessmentMixin,
                          StaffAssessmentMixin,
                          StudioMixin,
                          GradeMixin,
                          LeaderboardMixin,
                          StaffAreaMixin,
                          WorkflowMixin,
                          TeamWorkflowMixin,
                          StudentTrainingMixin,
                          LmsCompatibilityMixin,
                          CourseItemsListingMixin,
                          ConfigMixin,
                          TeamMixin,
                          OpenAssessmentTemplatesMixin,
                          XBlock):
    """Displays a prompt and provides an area where students can compose a response."""

    VALID_ASSESSMENT_TYPES = [
        "student-training",
        "peer-assessment",
        "self-assessment",
        "staff-assessment",
    ]

    VALID_ASSESSMENT_TYPES_FOR_TEAMS = [  # pylint: disable=invalid-name
        'staff-assessment',
    ]

    public_dir = 'static'

    submission_start = String(
        default=DEFAULT_START, scope=Scope.settings,
        help="ISO-8601 formatted string representing the submission start date."
    )

    submission_due = String(
        default=DEFAULT_DUE, scope=Scope.settings,
        help="ISO-8601 formatted string representing the submission due date."
    )

    text_response_raw = String(
        help="Specify whether learners must include a text based response to this problem's prompt.",
        default="required",
        scope=Scope.settings
    )

    file_upload_response_raw = String(
        help="Specify whether learners are able to upload files as a part of their response.",
        default=None,
        scope=Scope.settings
    )

    allow_file_upload = Boolean(
        default=False,
        scope=Scope.content,
        help="Do not use. For backwards compatibility only."
    )

    file_upload_type_raw = String(
        default=None,
        scope=Scope.content,
        help="File upload to be included with submission (can be 'image', 'pdf-and-image', or 'custom')."
    )

    white_listed_file_types = List(
        default=[],
        scope=Scope.content,
        help="Custom list of file types allowed with submission."
    )

    allow_multiple_files = Boolean(
        default=True,
        scope=Scope.settings,
        help="Allow multiple files uploaded with submission (if file upload enabled)."
    )

    allow_latex = Boolean(
        default=False,
        scope=Scope.settings,
        help="Latex rendering allowed with submission."
    )

    title = String(
        default="Open Response Assessment",
        scope=Scope.content,
        help="A title to display to a student (plain text)."
    )

    leaderboard_show = Integer(
        default=0,
        scope=Scope.content,
        help="The number of leaderboard results to display (0 if none)"
    )

    prompt = String(
        default=DEFAULT_PROMPT,
        scope=Scope.content,
        help="The prompts to display to a student."
    )

    prompts_type = String(
        default='text',
        scope=Scope.content,
        help="The type of prompt. html or text"
    )

    rubric_criteria = List(
        default=DEFAULT_RUBRIC_CRITERIA,
        scope=Scope.content,
        help="The different parts of grading for students giving feedback."
    )

    rubric_feedback_prompt = String(
        default=DEFAULT_RUBRIC_FEEDBACK_PROMPT,
        scope=Scope.content,
        help="The rubric feedback prompt displayed to the student"
    )

    rubric_feedback_default_text = String(
        default=DEFAULT_RUBRIC_FEEDBACK_TEXT,
        scope=Scope.content,
        help="The default rubric feedback text displayed to the student"
    )

    rubric_assessments = List(
        default=DEFAULT_ASSESSMENT_MODULES,
        scope=Scope.content,
        help="The requested set of assessments and the order in which to apply them."
    )

    submission_uuid = String(
        default=None,
        scope=Scope.user_state,
        help="The student's submission that others will be assessing."
    )

    has_saved = Boolean(
        default=False,
        scope=Scope.user_state,
        help="Indicates whether the user has saved a response."
    )

    saved_response = String(
        default=u"",
        scope=Scope.user_state,
        help="Saved response submission for the current user."
    )

    saved_files_descriptions = String(
        default=u"",
        scope=Scope.user_state,
        help="Saved descriptions for each uploaded file."
    )

    saved_files_names = String(
        default=u"",
        scope=Scope.user_state,
        help="Saved original names for each uploaded file."
    )

    saved_files_sizes = String(
        default=u"",
        scope=Scope.user_state,
        help="Filesize of each uploaded file in bytes."
    )

    no_peers = Boolean(
        default=False,
        scope=Scope.user_state,
        help="Indicates whether or not there are peers to grade."
    )

    teams_enabled = Boolean(
        default=False,
        scope=Scope.settings,
        help="Whether team submissions are enabled for this case study.",
    )

    selected_teamset_id = String(
        default=u"",
        scope=Scope.settings,
        help="The id of the selected teamset.",
    )

    @property
    def course_id(self):
        return str(self.xmodule_runtime.course_id)  # pylint: disable=no-member

    @property
    def text_response(self):
        """
        Backward compatibility for existing blocks that were created without text_response
        or file_upload_response fields. These blocks will be treated as required text.
        """
        if not self.file_upload_response_raw and not self.text_response_raw:
            return 'required'
        return self.text_response_raw

    @text_response.setter
    def text_response(self, value):
        """
        Setter for text_response_raw
        """
        self.text_response_raw = value if value else None

    @property
    def file_upload_response(self):
        """
        Backward compatibility for existing block before that were created without
        'text_response' and 'file_upload_response_raw' fields.
        """
        if not self.file_upload_response_raw and (self.file_upload_type_raw is not None or self.allow_file_upload):
            return 'optional'
        return self.file_upload_response_raw

    @file_upload_response.setter
    def file_upload_response(self, value):
        """
        Setter for file_upload_response_raw
        """
        self.file_upload_response_raw = value if value else None

    @property
    def file_upload_type(self):
        """
        Backward compatibility for existing block before the change from allow_file_upload to file_upload_type_raw.

        This property will use new file_upload_type_raw field when available, otherwise will fall back to
        allow_file_upload field for old blocks.
        """
        if self.file_upload_type_raw is not None:
            return self.file_upload_type_raw
        if self.allow_file_upload:
            return 'image'
        return None

    @file_upload_type.setter
    def file_upload_type(self, value):
        """
        Setter for file_upload_type_raw
        """
        self.file_upload_type_raw = value

    @property
    def white_listed_file_types_string(self):
        """
        Join the white listed file types into comma delimited string
        """
        if self.white_listed_file_types:
            return ','.join(self.white_listed_file_types)
        return ''

    @white_listed_file_types_string.setter
    def white_listed_file_types_string(self, value):
        """
        Convert comma delimited white list string into list with some clean up
        """
        self.white_listed_file_types = [file_type.strip().strip('.').lower()
                                        for file_type in value.split(',')] if value else None

    def get_anonymous_user_id(self, username, course_id):
        """
        Get the anonymous user id from Xblock user service.

        Args:
            username(str): user's name entered by staff to get info.
            course_id(str): course id.

        Returns:
            A unique id for (user, course) pair
        """
        return self.runtime.service(self, 'user').get_anonymous_user_id(username, course_id)

    def is_user_state_service_available(self):
        """
        Check if the user state service is present in runtime.
        """
        try:
            self.runtime.service(self, 'user_state')
            return True
        except NoSuchServiceError:
            return False

    def get_user_state(self, username):
        """
        Get the student module state for the given username for current ORA block.

        Arguments:
            username(str): username against which the state is required in the current block.

        Returns:
            user state, if found, else empty dict
        """
        if self.is_user_state_service_available():
            user_state_service = self.runtime.service(self, 'user_state')
            return user_state_service.get_state_as_dict(username, str(self.location))  # pylint: disable=no-member
        return {}

    def should_use_user_state(self, upload_urls):
        """
        Return a boolean if the user state is used for additional data checks.

        User state is utilized when all of the following are true:
        1. user state service is available(which is only part of courseware)
        2. The waffle flag/switch is enabled
        3. the file upload is required or optional
        4. the file data from submission is missing information
        """
        return not any(upload_urls) \
            and self.is_user_state_service_available() \
            and self.user_state_upload_data_enabled \
            and self.file_upload_response

    def should_get_all_files_urls(self, upload_urls):
        """
        Returns a boolean to decide if all the file submitted by a learner in a block should be obtained.

        Following conditions should be true for boolean to be true:
        1. The waffle flag/switch is enabled
        2. the file upload is required or optional
        3. the file data from submission is missing information

        Arguments:
            upload_urls(list): A list of (file url, description, name) tuple, if info present, else empty list
        """
        return not any(upload_urls) \
            and self.is_fetch_all_urls_waffle_enabled \
            and self.file_upload_response

    def get_student_item_dict_from_username_or_email(self, username_or_email):
        """
        Get the item dict for a given username or email in the parent course of block.
        """
        anonymous_user_id = self.get_anonymous_user_id(username_or_email, self.course_id)
        return self.get_student_item_dict(anonymous_user_id=anonymous_user_id)

    def get_anonymous_user_id_from_xmodule_runtime(self):
        if hasattr(self, "xmodule_runtime"):
            return self.xmodule_runtime.anonymous_student_id  # pylint:disable=E1101
        return None

    def get_student_item_dict(self, anonymous_user_id=None):
        """Create a student_item_dict from our surrounding context.

        See also: submissions.api for details.

        Args:
            anonymous_user_id(str): A unique anonymous_user_id for (user, course) pair.
        Returns:
            (dict): The student item associated with this XBlock instance. This
                includes the student id, item id, and course id.
        """

        item_id = str(self.scope_ids.usage_id)

        # This is not the real way course_ids should work, but this is a
        # temporary expediency for LMS integration
        if hasattr(self, "xmodule_runtime"):
            course_id = self.course_id
            if anonymous_user_id:
                student_id = anonymous_user_id
            else:
                student_id = self.xmodule_runtime.anonymous_student_id  # pylint:disable=E1101
        else:
            course_id = "edX/Enchantment_101/April_1"
            if self.scope_ids.user_id is None:
                student_id = None
            else:
                student_id = str(self.scope_ids.user_id)

        student_item_dict = dict(
            student_id=student_id,
            item_id=item_id,
            course_id=course_id,
            item_type='openassessment'
        )
        return student_item_dict

    def add_javascript_files(self, fragment, item):
        """
        Add all the JavaScript files from a directory to the specified fragment
        """
        if pkg_resources.resource_isdir(__name__, item):
            for child_item in pkg_resources.resource_listdir(__name__, item):
                path = os.path.join(item, child_item)
                if not pkg_resources.resource_isdir(__name__, path):
                    fragment.add_javascript_url(self.runtime.local_resource_url(self, path))
        else:
            fragment.add_javascript_url(self.runtime.local_resource_url(self, item))

    @togglable_mobile_support
    def student_view(self, context=None):  # pylint: disable=unused-argument
        """The main view of OpenAssessmentBlock, displayed when viewing courses.

        The main view which displays the general layout for Open Ended
        Assessment Questions. The contents of the XBlock are determined
        dynamically based on the assessment workflow configured by the author.

        Args:
            context: Not used for this view.

        Returns:
            (Fragment): The HTML Fragment for this XBlock, which determines the
            general frame of the Open Ended Assessment Question.
        """
        # On page load, update the workflow status.
        # We need to do this here because peers may have graded us, in which
        # case we may have a score available.

        try:
            self.update_workflow_status()
        except AssessmentWorkflowError:
            # Log the exception, but continue loading the page
            logger.exception('An error occurred while updating the workflow on page load.')

        ui_models = self._create_ui_models()
        # All data we intend to pass to the front end.
        context_dict = {
            "title": self.title,
            "prompts": self.prompts,
            "prompts_type": self.prompts_type,
            "rubric_assessments": ui_models,
            "show_staff_area": self.is_course_staff and not self.in_studio_preview,
        }
        template = get_template("openassessmentblock/oa_base.html")
        return self._create_fragment(template, context_dict, initialize_js_func='OpenAssessmentBlock')

    def ora_blocks_listing_view(self, context=None):
        """This view is used in the Open Response Assessment tab in the LMS Instructor Dashboard
        to display all available course ORA blocks.

        Args:
            context: contains two items:
                "ora_items" - all course items with names and parents, example:
                    [{"parent_name": "Vertical name",
                      "name": "ORA Display Name",
                      "url_grade_available_responses": "/grade_available_responses_view",
                      "staff_assessment": false,
                      "parent_id": "vertical_block_id",
                      "url_base": "/student_view",
                      "id": "openassessment_block_id"
                     }, ...]
                "ora_item_view_enabled" - enabled LMS API endpoint to serve XBlock view or not

        Returns:
            (Fragment): The HTML Fragment for this XBlock.
        """
        ora_items = context.get('ora_items', []) if context else []
        ora_item_view_enabled = context.get('ora_item_view_enabled', False) if context else False
        context_dict = {
            "ora_items": json.dumps(ora_items),
            "ora_item_view_enabled": ora_item_view_enabled
        }

        template = get_template('openassessmentblock/instructor_dashboard/oa_listing.html')

        min_postfix = '.min' if settings.DEBUG else ''

        return self._create_fragment(
            template,
            context_dict,
            initialize_js_func='CourseOpenResponsesListingBlock',
            additional_css=["static/css/lib/backgrid/backgrid%s.css" % min_postfix],
            additional_js=["static/js/lib/backgrid/backgrid%s.js" % min_postfix]
        )

    def grade_available_responses_view(self, context=None):  # pylint: disable=unused-argument
        """Grade Available Responses view.

        Auxiliary view which displays the staff grading area
        (used in the Open Response Assessment tab in the Instructor Dashboard of LMS)

        Args:
            context: Not used for this view.

        Returns:
            (Fragment): The HTML Fragment for this XBlock.
        """
        student_item = self.get_student_item_dict()
        staff_assessment_required = "staff-assessment" in self.assessment_steps

        context_dict = {
            "title": self.title,
            'staff_assessment_required': staff_assessment_required
        }

        if staff_assessment_required:
            context_dict.update(
                self.get_staff_assessment_statistics_context(student_item["course_id"], student_item["item_id"])
            )

        template = get_template('openassessmentblock/instructor_dashboard/oa_grade_available_responses.html')

        return self._create_fragment(template, context_dict, initialize_js_func='StaffAssessmentBlock')

    def _create_fragment(self, template, context_dict, initialize_js_func, additional_css=None, additional_js=None):
        """
        Creates a fragment for display.

        """
        fragment = Fragment(template.render(context_dict))

        if additional_css is None:
            additional_css = []
        if additional_js is None:
            additional_js = []

        i18n_service = self.runtime.service(self, 'i18n')
        if hasattr(i18n_service, 'get_language_bidi') and i18n_service.get_language_bidi():
            css_url = "static/css/openassessment-rtl.css"
        else:
            css_url = "static/css/openassessment-ltr.css"

        # TODO: load CSS and JavaScript as URLs once they can be served by the CDN
        for css in additional_css:
            fragment.add_css(load(css))
        fragment.add_css(load(css_url))

        # minified additional_js should be already included in 'make javascript'
        fragment.add_javascript(load("static/js/openassessment-lms.js"))

        js_context_dict = {
            "ALLOWED_IMAGE_MIME_TYPES": self.ALLOWED_IMAGE_MIME_TYPES,
            "ALLOWED_FILE_MIME_TYPES": self.ALLOWED_FILE_MIME_TYPES,
            "FILE_EXT_BLACK_LIST": self.FILE_EXT_BLACK_LIST,
            "FILE_TYPE_WHITE_LIST": self.white_listed_file_types,
            "MAXIMUM_FILE_UPLOAD_COUNT": self.MAX_FILES_COUNT,
            "TEAM_ASSIGNMENT": self.is_team_assignment()
        }
        fragment.initialize_js(initialize_js_func, js_context_dict)
        return fragment

    @property
    def is_admin(self):
        """
        Check whether the user has global staff permissions.

        Returns:
            bool
        """
        if hasattr(self, 'xmodule_runtime'):
            return getattr(self.xmodule_runtime, 'user_is_admin', False)  # pylint: disable=no-member
        return False

    @property
    def is_course_staff(self):
        """
        Check whether the user has course staff permissions for this XBlock.

        Returns:
            bool
        """
        if hasattr(self, 'xmodule_runtime'):
            return getattr(self.xmodule_runtime, 'user_is_staff', False)  # pylint: disable=no-member
        return False

    @property
    def is_beta_tester(self):
        """
        Check whether the user is a beta tester.

        Returns:
            bool
        """
        if hasattr(self, 'xmodule_runtime'):
            return getattr(self.xmodule_runtime, 'user_is_beta_tester', False)  # pylint: disable=no-member
        return False

    @property
    def in_studio_preview(self):
        """
        Check whether we are in Studio preview mode.

        Returns:
            bool

        """
        # When we're running in Studio Preview mode, the XBlock won't provide us with a user ID.
        # (Note that `self.xmodule_runtime` will still provide an anonymous
        # student ID, so we can't rely on that)
        return self.scope_ids.user_id is None

    def _create_ui_models(self):
        """Combine UI attributes and XBlock configuration into a UI model.

        This method takes all configuration for this XBlock instance and appends
        UI attributes to create a UI Model for rendering all assessment modules.
        This allows a clean separation of static UI attributes from persistent
        XBlock configuration.

        """
        ui_models = [UI_MODELS["submission"]]
        staff_assessment_required = False
        for assessment in self.valid_assessments:
            if assessment["name"] == "staff-assessment":
                if not assessment["required"]:
                    continue
                staff_assessment_required = True
            ui_model = UI_MODELS.get(assessment["name"])
            if ui_model:
                ui_models.append(dict(assessment, **ui_model))

        if not staff_assessment_required and self.staff_assessment_exists(self.submission_uuid):
            ui_models.append(UI_MODELS["staff-assessment"])

        ui_models.append(UI_MODELS["grade"])

        if self.leaderboard_show > 0 and not self.teams_enabled:
            ui_models.append(UI_MODELS["leaderboard"])

        return ui_models

    @staticmethod
    def workbench_scenarios():
        """A canned scenario for display in the workbench.

        These scenarios are only intended to be used for Workbench XBlock
        Development.

        """
        return [
            (
                "OpenAssessmentBlock File Upload: Images",
                load('static/xml/file_upload_image_only.xml')
            ),
            (
                "OpenAssessmentBlock File Upload: PDF and Images",
                load('static/xml/file_upload_pdf_and_image.xml')
            ),
            (
                "OpenAssessmentBlock File Upload: Custom File Types",
                load('static/xml/file_upload_custom.xml')
            ),
            (
                "OpenAssessmentBlock File Upload: allow_file_upload compatibility",
                load('static/xml/file_upload_compat.xml')
            ),
            (
                "OpenAssessmentBlock Unicode",
                load('static/xml/unicode.xml')
            ),
            (
                "OpenAssessmentBlock Poverty Rubric",
                load('static/xml/poverty_rubric_example.xml')
            ),
            (
                "OpenAssessmentBlock Leaderboard",
                load('static/xml/leaderboard.xml')
            ),
            (
                "OpenAssessmentBlock Leaderboard with Custom File Type",
                load('static/xml/leaderboard_custom.xml')
            ),
            (
                "OpenAssessmentBlock (Peer Only) Rubric",
                load('static/xml/poverty_peer_only_example.xml')
            ),
            (
                "OpenAssessmentBlock (Self Only) Rubric",
                load('static/xml/poverty_self_only_example.xml')
            ),
            (
                "OpenAssessmentBlock Censorship Rubric",
                load('static/xml/censorship_rubric_example.xml')
            ),
            (
                "OpenAssessmentBlock Promptless Rubric",
                load('static/xml/promptless_rubric_example.xml')
            ),
        ]

    @classmethod
    def parse_xml(cls, node, runtime, keys, id_generator):
        """Instantiate XBlock object from runtime XML definition.

        Inherited by XBlock core.

        """
        config = parse_from_xml(node)
        block = runtime.construct_xblock_from_class(cls, keys)

        xblock_validator = validator(block, block._, strict_post_release=False)
        xblock_validator(
            create_rubric_dict(config['prompts'], config['rubric_criteria']),
            config['rubric_assessments'],
            submission_start=config['submission_start'],
            submission_due=config['submission_due'],
            leaderboard_show=config['leaderboard_show']
        )

        block.rubric_criteria = config['rubric_criteria']
        block.rubric_feedback_prompt = config['rubric_feedback_prompt']
        block.rubric_feedback_default_text = config['rubric_feedback_default_text']
        block.rubric_assessments = config['rubric_assessments']
        block.submission_start = config['submission_start']
        block.submission_due = config['submission_due']
        block.title = config['title']
        block.prompts = config['prompts']
        block.prompts_type = config['prompts_type']
        block.text_response = config['text_response']
        block.file_upload_response = config['file_upload_response']
        block.allow_file_upload = config['allow_file_upload']
        block.file_upload_type = config['file_upload_type']
        block.white_listed_file_types_string = config['white_listed_file_types']
        block.allow_multiple_files = config['allow_multiple_files']
        block.allow_latex = config['allow_latex']
        block.leaderboard_show = config['leaderboard_show']
        block.group_access = config['group_access']
        block.teams_enabled = config['teams_enabled']
        block.selected_teamset_id = config['selected_teamset_id']
        return block

    @property
    def _(self):
        i18nService = self.runtime.service(self, 'i18n')  # pylint: disable=invalid-name
        return i18nService.ugettext

    @property
    def prompts(self):
        """
        Return the prompts.

        Initially a block had a single prompt which was saved as a simple
        string in the prompt field. Now prompts are saved as a serialized
        list of dicts in the same field. If prompt field contains valid json,
        parse and return it. Otherwise, assume it is a simple string prompt
        and return it in a list of dict.

        Returns:
            list of dict
        """
        return create_prompts_list(self.prompt)

    @prompts.setter
    def prompts(self, value):
        """
        Serialize the prompts and save to prompt field.

        Args:
            value (list of dict): The prompts to set.
        """

        if value is None:
            self.prompt = None
        elif len(value) == 1:
            # For backwards compatibility. To be removed after all code
            # is migrated to use prompts property instead of prompt field.
            self.prompt = value[0]['description']
        else:
            self.prompt = json.dumps(value)

    @property
    def valid_assessments(self):
        """
        Return a list of assessment dictionaries that we recognize.
        This allows us to gracefully handle situations in which unrecognized
        assessment types are stored in the XBlock field (e.g. because
        we roll back code after releasing a feature).

        Returns:
            list

        """
        assessment_types = self.VALID_ASSESSMENT_TYPES
        if self.teams_enabled:
            assessment_types = self.VALID_ASSESSMENT_TYPES_FOR_TEAMS

        _valid_assessments = [
            asmnt for asmnt in self.rubric_assessments
            if asmnt.get('name') in assessment_types
        ]
        return update_assessments_format(copy.deepcopy(_valid_assessments))

    @property
    def assessment_steps(self):
        """
        Return a list of assessment steps by name.
        Also filter out assessments that have required set to false and do not
        contain a staff grade override.

        Returns:
            list

        """
        assessment_steps = []
        for assessment in self.valid_assessments:
            if assessment['name'] == 'staff-assessment' and assessment["required"] is False:
                if not self.staff_assessment_exists(self.submission_uuid):
                    continue
            assessment_steps.append(assessment['name'])
        return assessment_steps

    @lazy
    def rubric_criteria_with_labels(self):
        """
        Backwards compatibility: We used to treat "name" as both a user-facing label
        and a unique identifier for criteria and options.
        Now we treat "name" as a unique identifier, and we've added an additional "label"
        field that we display to the user.
        If criteria/options in the problem definition do NOT have a "label" field
        (because they were created before this change),
        we create a new label that has the same value as "name".

        The result of this call is cached, so it should NOT be used in a runtime
        that can modify the XBlock settings (in the LMS, settings are read-only).

        Returns:
            list of criteria dictionaries

        """
        criteria = copy.deepcopy(self.rubric_criteria)
        for criterion in criteria:
            if 'label' not in criterion:
                criterion['label'] = criterion['name']
            for option in criterion['options']:
                if 'label' not in option:
                    option['label'] = option['name']
        return criteria

    def render_assessment(self, path, context_dict=None):
        """Render an Assessment Module's HTML

        Given the name of an assessment module, find it in the list of
        configured modules, and ask for its rendered HTML.

        Args:
            path (str): The path to the template used to render this HTML
                section.
            context_dict (dict): A dictionary of context variables used to
                populate this HTML section.

        Returns:
            (Response): A Response Object with the generated HTML fragment. This
                is intended for AJAX calls to load dynamically into a larger
                document.
        """
        if not context_dict:
            context_dict = {}

        template = get_template(path)
        return Response(template.render(context_dict), content_type='application/html', charset='UTF-8')

    def add_xml_to_node(self, node):
        """
        Serialize the XBlock to XML for exporting.
        """
        serialize_content_to_xml(self, node)

    def render_error(self, error_msg):
        """
        Render an error message.

        Args:
            error_msg (unicode): The error message to display.

        Returns:
            Response: A response object with an HTML body.
        """
        context = {'error_msg': error_msg}
        template = get_template('openassessmentblock/oa_error.html')
        return Response(template.render(context), content_type='application/html', charset='UTF-8')

    def is_closed(self, step=None, course_staff=None):
        """
        Checks if the question is closed.

        Determines if the start date is in the future or the end date has
            passed.  Optionally limited to a particular step in the workflow.

        Start/due dates do NOT apply to course staff, since course staff may need to get to
        the peer grading step AFTER the submission deadline has passed.
        This may not be necessary when we implement a grading interface specifically for course staff.

        Keyword Arguments:
            step (str): The step in the workflow to check.  Options are:
                None: check whether the problem as a whole is open.
                "submission": check whether the submission section is open.
                "peer-assessment": check whether the peer-assessment section is open.
                "self-assessment": check whether the self-assessment section is open.

            course_staff (bool): Whether to treat the user as course staff (disable start/due dates).
                If not specified, default to the current user's status.

        Returns:
            tuple of the form (is_closed, reason, start_date, due_date), where
                is_closed (bool): indicates whether the step is closed.
                reason (str or None): specifies the reason the step is closed ("start" or "due")
                start_date (datetime): is the start date of the step/problem.
                due_date (datetime): is the due date of the step/problem.

        Examples:
            >>> is_closed()
            False, None, datetime.datetime(2014, 3, 27, 22, 7, 38, 788861),
            datetime.datetime(2015, 3, 27, 22, 7, 38, 788861)
            >>> is_closed(step="submission")
            True, "due", datetime.datetime(2014, 3, 27, 22, 7, 38, 788861),
            datetime.datetime(2015, 3, 27, 22, 7, 38, 788861)
            >>> is_closed(step="self-assessment")
            True, "start", datetime.datetime(2014, 3, 27, 22, 7, 38, 788861),
            datetime.datetime(2015, 3, 27, 22, 7, 38, 788861)

        """
        submission_range = (self.submission_start, self.submission_due)
        assessment_ranges = [
            (asmnt.get('start'), asmnt.get('due'))
            for asmnt in self.valid_assessments
        ]

        # Resolve unspecified dates and date strings to datetimes
        start, due, date_ranges = resolve_dates(
            self.start, self.due, [submission_range] + assessment_ranges, self._
        )

        open_range = (start, due)
        assessment_steps = self.assessment_steps
        if step == 'submission':
            open_range = date_ranges[0]
        elif step in assessment_steps:
            step_index = assessment_steps.index(step)
            open_range = date_ranges[1 + step_index]

        # Course staff always have access to the problem
        if course_staff is None:
            course_staff = self.is_course_staff
        if course_staff:
            return False, None, DISTANT_PAST, DISTANT_FUTURE

        if self.is_beta_tester:
            beta_start = self._adjust_start_date_for_beta_testers(open_range[0])
            open_range = (beta_start, open_range[1])

        # Check if we are in the open date range
        now = dt.datetime.utcnow().replace(tzinfo=pytz.utc)

        if now < open_range[0]:
            return True, "start", open_range[0], open_range[1]
        elif now >= open_range[1]:
            return True, "due", open_range[0], open_range[1]
        return False, None, open_range[0], open_range[1]

    def get_waiting_details(self, status_details):
        """
        Returns waiting status (boolean value) based on the given status_details.

        Args:
            status_details (dict): A dictionary containing the details of each
                assessment module status. This will contain keys such as
                "peer", "ai", and "staff", referring to dictionaries, which in
                turn will have the key "graded". If this key has a value set,
                these assessment modules have been graded.

        Returns:
            True if waiting for a grade from peer, ai, or staff assessment, else False.

        Examples:
            >>> now = dt.datetime.utcnow().replace(tzinfo=pytz.utc)
            >>> status_details = {
            >>>     'peer': {
            >>>         'completed': None,
            >>>         'graded': now
            >>>     },
            >>>     'ai': {
            >>>         'completed': now,
            >>>         'graded': None
            >>>     }
            >>> }
            >>> self.get_waiting_details(status_details)
            True
        """
        steps = ["peer", "ai", "staff"]  # These are the steps that can be submitter-complete, but lack a grade
        for step in steps:
            if step in status_details and not status_details[step]["graded"]:
                return True
        return False

    def is_released(self, step=None):
        """
        Check if a question has been released.

        Keyword Arguments:
            step (str): The step in the workflow to check.
                None: check whether the problem as a whole is open.
                "submission": check whether the submission section is open.
                "peer-assessment": check whether the peer-assessment section is open.
                "self-assessment": check whether the self-assessment section is open.

        Returns:
            bool
        """
        # By default, assume that we're published, in case the runtime doesn't support publish date.
        if hasattr(self.runtime, 'modulestore'):
            is_published = self.runtime.modulestore.has_published_version(self)
        else:
            is_published = True
        is_closed, reason, __, __ = self.is_closed(step=step)  # pylint: disable=redeclared-assigned-name
        is_released = is_published and (not is_closed or reason == 'due')
        if self.start:
            is_released = is_released and dt.datetime.now(pytz.UTC) > parse_date_value(self.start, self._)
        return is_released

    def get_assessment_module(self, mixin_name):
        """
        Get a configured assessment module by name.

        Args:
            mixin_name (str): The name of the mixin (e.g. "self-assessment" or "peer-assessment")

        Returns:
            dict

        Example:
            >>> self.get_assessment_module('peer-assessment')
            {
                "name": "peer-assessment",
                "start": None,
                "due": None,
                "must_grade": 5,
                "must_be_graded_by": 3,
            }
        """
        for assessment in self.valid_assessments:
            if assessment["name"] == mixin_name:
                return assessment
        return None

    def publish_assessment_event(self, event_name, assessment, **kwargs):
        """
        Emit an analytics event for the peer assessment.

        Args:
            event_name (str): An identifier for this event type.
            assessment (dict): The serialized assessment model.

        Returns:
            None

        """
        parts_list = []
        for part in assessment["parts"]:
            # Some assessment parts do not include point values,
            # only written feedback.  In this case, the assessment
            # part won't have an associated option.
            option_dict = None
            if part["option"] is not None:
                option_dict = {
                    "name": part["option"]["name"],
                    "points": part["option"]["points"],
                }

            # All assessment parts are associated with criteria
            criterion_dict = {
                "name": part["criterion"]["name"],
                "points_possible": part["criterion"]["points_possible"]
            }

            parts_list.append({
                "option": option_dict,
                "criterion": criterion_dict,
                "feedback": part["feedback"]
            })

        event_data = {
            "feedback": assessment["feedback"],
            "rubric": {
                "content_hash": assessment["rubric"]["content_hash"],
            },
            "scorer_id": assessment["scorer_id"],
            "score_type": assessment["score_type"],
            "scored_at": assessment["scored_at"],
            "submission_uuid": assessment["submission_uuid"],
            "parts": parts_list
        }

        for key in kwargs:
            event_data[key] = kwargs[key]

        self.runtime.publish(
            self, event_name,
            event_data
        )

    @XBlock.json_handler
    def publish_event(self, data, suffix=''):  # pylint: disable=unused-argument
        """
        Publish the given data to an event.

        Expects key 'event_name' to be present in the data dictionary.
        """

        try:
            event_name = data['event_name']
        except KeyError:
            logger.exception("Could not find the name of the event to be triggered.")
            return {'success': False}

        # Remove the name so we don't publish as part of the data.
        del data['event_name']

        self.runtime.publish(self, event_name, data)
        return {'success': True}

    def get_real_user(self, anonymous_user_id):
        """
        Return the user associated with anonymous_user_id
        Args:
            anonymous_user_id (str): the anonymous user id of the user

        Returns: the user model for the user if it can be identified.
            If the xblock service to converts to a real user fails,
            returns None and logs the error.

        """
        if hasattr(self, "xmodule_runtime"):
            if self.xmodule_runtime.get_real_user is None:  # pylint: disable=no-member
                return None
            user = self.xmodule_runtime.get_real_user(anonymous_user_id)  # pylint: disable=no-member
            if user:
                return user
            logger.exception(
                u"XBlock service could not find user for anonymous_user_id '{}'".format(anonymous_user_id)
            )
        return None

    def get_username(self, anonymous_user_id):
        """
        Return the username of the user associated with anonymous_user_id
        Args:
            anonymous_user_id (str): the anonymous user id of the user

        Returns: the username if it can be identified.

        """
        user = self.get_real_user(anonymous_user_id)
        if user:
            return user.username
        return None

    def _adjust_start_date_for_beta_testers(self, start):
        """
        Returns the start date for a Beta tester.
        """
        if hasattr(self, "xmodule_runtime"):
            days_early_for_beta = getattr(self.xmodule_runtime, 'days_early_for_beta', 0)  # pylint: disable=no-member
            if days_early_for_beta is not None:
                delta = dt.timedelta(days_early_for_beta)
                effective = start - delta
                return effective

        return start

    def get_xblock_id(self):
        """
        Returns the xblock id
        """
        return str(self.scope_ids.usage_id)

    def _clean_data(self, data):
        cleaner = Cleaner(tags=[], strip=True)
        cleaned_text = " ".join(re.split(r"\s+", cleaner.clean(data), flags=re.UNICODE)).strip()
        return cleaned_text

    def index_dictionary(self):
        """
        Return dictionary prepared with module content and type for indexing.
        """

        # return key/value fields in a Python dict object
        # values may be numeric / string or dict
        # default implementation is an empty dict
        xblock_body = super(OpenAssessmentBlock, self).index_dictionary()

        # Check whether there is only one prompt or more than one
        # If there is single prompt, self.prompt would be simply a string
        # otherwise self.prompt would have json embedded in the string.
        try:
            prompt = {
                "prompt_{}".format(prompt_i): self._clean_data(prompt.get("description", ""))
                for prompt_i, prompt in enumerate(json.loads(self.prompt))
            }
        except ValueError:
            prompt = {
                "prompt": self._clean_data(self.prompt)
            }

        content = {
            "display_name": self.display_name,
            "title": self.title,
            **prompt
        }

        if "content" in xblock_body:
            xblock_body["content"].update(content)
        else:
            xblock_body["content"] = content

        xblock_body["content_type"] = "ORA"

        return xblock_body
class UploaderDownloaderXBlock(XBlock):
    """
    Upload and list uploaded files on S3
    """

    display_name = String(
        display_name="S3 Uploader Downloader",
        help=
        "This name appears in the horizontal navigation at the top of the page.",
        scope=Scope.settings,
        default="S3 Uploader Downloder")

    general_title = String(default="",
                           scope=Scope.content,
                           help="General Title")
    s3_mid_folder = String(default="", scope=Scope.content)
    uploadable_by_students = Boolean(default=False, scope=Scope.settings)
    size_limit = Integer(default=10,
                         scope=Scope.content,
                         help="Number of recordings on one page")
    paginate = Integer(default=20, scope=Scope.content)
    s3_bucket = String(default='public-sgu', scope=Scope.settings)

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

    def studio_view(self, context):
        """
        Create a fragment used to display the edit view in the Studio.
        """
        context['general_title'] = self.general_title or ''
        context['s3_mid_folder'] = self.s3_mid_folder or ''
        context["uploadable_by_students"] = self.uploadable_by_students
        context['size_limit'] = self.size_limit
        context['paginate'] = self.paginate

        if (context["uploadable_by_students"] == True):
            context["check_uploadable_by_students"] = "checked"
            context["check_uploadable_by_staff"] = ""
        else:
            context["check_uploadable_by_students"] = ""
            context["check_uploadable_by_staff"] = "checked"

        fragment = Fragment()

        fragment.add_content(
            loader.render_template(
                "static/html/s3uploader_downloader_edit.html", context))

        fragment.add_javascript(
            self.resource_string(
                "static/js/src/s3uploader_downloader_edit.js"))
        fragment.initialize_js('s3UploaderDownloaderEditBlock')
        return fragment

    @XBlock.json_handler
    def studio_submit(self, data, suffix=''):
        """
        Called when submitting the form in Studio.
        """
        self.general_title = data.get('general_title')
        self.s3_mid_folder = data.get('s3_mid_folder')
        self.uploadable_by_students = data.get('uploadable_by_students')
        self.size_limit = data.get('size_limit')
        self.paginate = data.get('paginate')

        return {'result': 'success'}

    def get_course(self):

        return self.scope_ids.usage_id.course_key

    def get_course_level(self):
        course = self.scope_ids.usage_id.course_key
        course_level = course.course
        return course_level

    def student_view(self, context=None):
        """
        The primary view of the UploaderDownloaderXBlock, shown to students
        when viewing courses.
        """
        from .models import FileUploadAndUrl
        from xmodule.modulestore.django import modulestore
        unit_location = modulestore().get_parent_location(self.location)
        unit_id = unit_location.name
        course_level = self.get_course_level()
        data = FileUploadAndUrl.objects.filter(unit_id=unit_id,
                                               course_level=course_level)
        context.update({
            "self":
            self,
            "data":
            data,
            "paginate":
            self.paginate,
            "aws_key":
            settings.AWS_ACCESS_KEY_ID,
            "unit_id":
            unit_id,
            "s3_mid_folder":
            self.s3_mid_folder,
            "course_level":
            course_level,
            "general_title":
            self.general_title,
            "size_limit":
            self.size_limit,
            "bin_icon":
            self.runtime.local_resource_url(self, 'static/img/bin.png'),
            "gear_icon":
            self.runtime.local_resource_url(self, 'static/img/gear.png')
        })

        frag = Fragment()
        frag.add_content(
            loader.render_template("static/html/list_download.html", context))
        frag.add_css_url(
            "https://cdn.datatables.net/1.10.15/css/jquery.dataTables.min.css")
        frag.add_javascript_url(
            "https://cdn.datatables.net/1.10.15/js/jquery.dataTables.min.js")
        frag.add_css(self.resource_string("static/css/s3uploader.css"))

        display_to_students = self.runtime.user_is_staff or self.uploadable_by_students
        if "username" in context and display_to_students:
            frag.add_content(
                self.resource_string("static/html/s3uploader.html"))

            css_context = dict(
                continue_gif=self.runtime.local_resource_url(
                    self, 'static/img/continue.gif'),
                edit=self.runtime.local_resource_url(self,
                                                     'static/img/edit.gif'),
                loading=self.runtime.local_resource_url(
                    self, 'static/img/loading.gif'),
                pause=self.runtime.local_resource_url(self,
                                                      'static/img/pause.gif'),
                processing=self.runtime.local_resource_url(
                    self, 'static/img/processing.gif'),
                retry=self.runtime.local_resource_url(self,
                                                      'static/img/retry.gif'),
                trash=self.runtime.local_resource_url(self,
                                                      'static/img/trash.gif'),
            )
            css = loader.render_template(
                'static/css/fine-uploader-gallery.css', css_context)
            frag.add_css(css)

            frag.add_css(
                self.resource_string("static/css/fine-uploader.min.css"))
            frag.add_css(self.resource_string("static/css/bootstrap.min.css"))
            frag.add_css(
                self.resource_string("static/css/bootstrap-grid.min.css"))
            frag.add_css(
                self.resource_string("static/css/bootstrap-reboot.min.css"))

            frag.add_javascript_url(
                "https://npmcdn.com/[email protected]/dist/js/tether.min.js")
            frag.add_javascript_url(
                "https://cdnjs.cloudflare.com/ajax/libs/js-cookie/2.1.3/js.cookie.min.js"
            )

            frag.add_javascript(
                self.resource_string("static/js/src/bootstrap.min.js"))
            frag.add_javascript(
                self.resource_string("static/js/src/dnd.min.js"))
            frag.add_javascript(
                self.resource_string(
                    "static/js/src/s3.fine-uploader.core.min.js"))
            frag.add_javascript(
                self.resource_string("static/js/src/s3.fine-uploader.js"))
            frag.add_javascript(
                self.resource_string(
                    "static/js/src/s3.jquery.fine-uploader.min.js"))
        frag.add_javascript(
            loader.render_template("static/js/src/s3uploader_downloader.js",
                                   context))
        frag.initialize_js('UploaderDownloaderXBlock')
        return frag

    @XBlock.json_handler
    def sign_content(self, data, suffix=''):
        """ Handle S3 uploader POST requests here. For files <=5MiB this is a simple
        request to sign the policy document. For files >5MiB this is a request
        to sign the headers to start a multipart encoded request.
        """
        if data.get('success', None):
            return self.make_response(200)
        else:
            request_payload = data
            headers = request_payload.get('headers', None)
            if headers:
                # The presence of the 'headers' property in the request payload
                # means this is a request to sign a REST/multipart request
                # and NOT a policy document
                response_data = self.sign_headers(headers)
            else:
                if not self.is_valid_policy(request_payload):
                    raise JsonHandlerError(400, 'Points must be an integer')
                response_data = self.sign_policy_document(request_payload)
            response_payload = json.dumps(response_data)
            response_payload = json.loads(response_payload)
            return response_payload

    @XBlock.json_handler
    def add_file_details(self, data, suffix=''):
        from .models import FileAndUrl
        file_name = data.get('file_name', None)
        file_title = data.get('file_title', None)
        description = data.get('description', None)
        uploaded_by = data.get('uploaded_by', None)
        unit_id = data.get('unit_id', None)
        course_level = self.get_course_level()
        folder_name = self.s3_mid_folder
        is_url = False
        if folder_name is None:
            folder_name = course_level
        else:
            folder_name = course_level + '/' + folder_name
        fileuploader = FileAndUrl()
        fileuploader.create_record(file_name, file_title, description,
                                   uploaded_by, unit_id, course_level,
                                   folder_name, is_url)
        return

    @XBlock.json_handler
    def edit_file_details(self, data, suffix=''):
        from .models import FileAndUrl
        file_id = data.get('file_id', None)
        file_title = data.get('file_title', None)
        description = data.get('description', None)
        is_url = False
        fileuploader = FileAndUrl()
        fileuploader.update_record(file_id, None, file_title, description,
                                   is_url)
        return

    @XBlock.json_handler
    def delete_file(self, data, suffix=''):
        """ Handle file deletion requests. For this, we use the Amazon Python SDK,
        boto.
        """
        from .models import FileAndUrl
        boto.set_stream_logger('boto')
        S3 = S3Connection(settings.AWS_ACCESS_KEY_ID,
                          settings.AWS_SECRET_ACCESS_KEY)
        if boto:
            file_id = data.get('file_id', None)
            bucket_name = self.s3_bucket
            aws_bucket = S3.get_bucket(bucket_name, validate=False)

            fileuploader = FileAndUrl()
            log.info(u"fileuploader.get_file_path(file_id)%s",
                     fileuploader.get_file_path(file_id))
            #Delete for S3
            file_key = Key(aws_bucket, fileuploader.get_file_path(file_id))
            file_key.delete()
            #Delete from db
            fileuploader.delete_record(file_id)

            return
        else:
            return

    @XBlock.json_handler
    def download_file(self, data, suffix=''):
        """ Handle file deletion requests. For this, we use the Amazon Python SDK,
        boto.
        """
        from .models import FileAndUrl
        S3 = S3Connection(settings.AWS_ACCESS_KEY_ID,
                          settings.AWS_SECRET_ACCESS_KEY)
        file_id = data.get('file_id', None)
        fileuploader = FileAndUrl()
        url = S3.generate_url(60,
                              'GET',
                              self.s3_bucket,
                              fileuploader.get_file_path(file_id),
                              response_headers={
                                  'response-content-type':
                                  'application/octet-stream'
                              })
        return url

    @XBlock.json_handler
    def add_url_details(self, data, suffix=''):
        from .models import FileAndUrl
        addUrl = data.get('addUrl', None)
        addUrlName = data.get('addUrlName', None)
        addUrlDescription = data.get('addUrlDescription', None)
        uploaded_by = data.get('uploaded_by', None)
        unit_id = data.get('unit_id', None)
        course = self.get_course()
        folder_name = None
        course_level = self.get_course_level()
        is_url = True

        urlClass = FileAndUrl()
        urlClass.create_record(addUrl, addUrlName, addUrlDescription,
                               uploaded_by, unit_id, course_level, folder_name,
                               is_url)
        return

    @XBlock.json_handler
    def edit_url_details(self, data, suffix=''):
        from .models import FileAndUrl
        url_id = data.get('url_id', None)
        url_src = data.get('url_src', None)
        url_title = data.get('url_title', None)
        url_description = data.get('url_description', None)
        is_url = True

        fileAndUrl = FileAndUrl()
        fileAndUrl.update_record(url_id, url_src, url_title, url_description,
                                 is_url)
        return

    @XBlock.json_handler
    def delete_url_row(self, data, suffix=''):
        from .models import FileAndUrl
        row_id = data.get('row_id', None)
        fileAndUrl = FileAndUrl()
        fileAndUrl.delete_record(row_id)
        return

    def make_response(self, status=200, content=None):
        """ Construct an HTTP response. Fine Uploader expects 'application/json'.
        """
        response = HttpResponse()
        response.status_code = status
        response['Content-Type'] = "application/json"
        response.content = content
        return response

    def sign_headers(self, headers):
        """ Sign and return the headers for a chunked upload. """
        return {
            'signature':
            base64.b64encode(
                hmac.new(settings.AWS_SECRET_ACCESS_KEY, headers,
                         hashlib.sha1).digest())
        }

    def sign_policy_document(self, policy_document):
        """ Sign and return the policy doucument for a simple upload.
        http://aws.amazon.com/articles/1434/#signyours3postform
        """
        policy = base64.b64encode(json.dumps(policy_document))
        signature = base64.b64encode(
            hmac.new(str(settings.AWS_SECRET_ACCESS_KEY), policy,
                     hashlib.sha1).digest())
        return {'policy': policy, 'signature': signature}

    def is_valid_policy(self, policy_document):
        """ Verify the policy document has not been tampered with client-side
        before sending it off. 
        """
        bucket = ''
        parsed_max_size = 0

        for condition in policy_document['conditions']:
            if isinstance(condition,
                          list) and condition[0] == 'content-length-range':
                parsed_max_size = condition[2]
            else:
                if condition.get('bucket', None):
                    bucket = condition['bucket']

        sys_values = bucket.lower() == str(
            self.s3_bucket).lower() and int(parsed_max_size) == int(
                1000000 * self.size_limit)
        return sys_values
Beispiel #28
0
class StattutorXBlock(XBlock):
    """
    A XBlock providing a CTAT backed StatTutor.
    """
    # pylint: disable=too-many-instance-attributes
    # All of the instance variables are required.

    display_name = String(
        help="Display name of the component",
        default="StatTutor",
        scope=Scope.content)  # required to prevent garbage name at the top

    # **** xBlock tag variables ****
    # The width must be at least 900 in order to accommodate some dynamically
    # loaded images and some of the interactive elements without causing
    # side scrolling scrollbars to appear.  They are set here instead of
    # hard coding them into ctatxblock.html to make it easier for EdX
    # administrators to modify them if they wish without having to scour
    # all of the code for where they are set.
    width = 900  # Width of the StatTutor frame.
    height = 750  # Height of the StatTutor frame.

    # **** Grading variables ****
    # All of the variable in this section are required to get grading to work
    # according to EdX's documentation.
    has_score = Boolean(default=True, scope=Scope.content)
    icon_class = String(default="problem", scope=Scope.content)
    score = Integer(help="Current count of correctly completed student steps",
                    scope=Scope.user_state,
                    default=0)
    max_problem_steps = Integer(help="Total number of steps",
                                scope=Scope.user_state,
                                default=1)
    max_possible_score = 1

    def create_problem_dict(pkg_resources):
        """ Generate and store a dictionary of the available problems. """
        problems = {}
        for pf_dir in pkg_resources.resource_listdir(__name__,
                                                     'public/problem_files/'):
            pdir = 'public/problem_files/{}'.format(pf_dir)
            if pkg_resources.resource_isdir(__name__, pdir):
                pdir_files = [
                    f for f in pkg_resources.resource_listdir(__name__, pdir)
                ]
                brds = [brd for brd in pdir_files if '.brd' in brd]
                desc = [dsc for dsc in pdir_files if '.xml' in dsc]
                if len(brds) > 0 and len(desc) > 0:
                    problems[pf_dir] = {
                        'name': pf_dir,
                        'brd': pdir + '/' + brds[0],
                        'description': pdir + '/' + desc[0]
                    }
        return problems

    def max_score(self):
        """ The maximum raw score of the problem. """
        # For some unknown reason, errors are thrown if the return value is
        # hard coded.
        return self.max_possible_score

    attempted = Boolean(help="True if at least one step has been completed",
                        scope=Scope.user_state,
                        default=False)
    completed = Boolean(
        help="True if all of the required steps are correctly completed",
        scope=Scope.user_state,
        default=False)
    weight = Float(
        display_name="Problem Weight",
        help=("Defines the number of points each problem is worth. "
              "If the value is not set, the problem is worth the sum of the "
              "option point values."),
        values={
            "min": 0,
            "step": .1
        },
        scope=Scope.settings
    )  # weight needs to be set to something, errors will be thrown if it does
    # not exist.

    # **** Basic interface variables ****

    src = "public/html/StatTutor.html"  # this is static in StatTutor
    # src can not be hard coded into static/html/ctatxblock.html because of the
    # relative path issues discussed elsewhere in this file.

    problems = create_problem_dict(pkg_resources)

    problem = String(help="The selected problem from problems",
                     default="m1_survey",
                     scope=Scope.settings)

    # **** CTATConfiguration variables ****
    # These should be the only variables needed to set up logging.

    # log_url should be the url of the logging service.
    # This should probably be hard coded or at least made to be one
    # of a few predefined log servers.
    log_url = String(help="URL of the logging service, used to indicate " +
                     "where the server is that will receive the log messages",
                     default="",
                     scope=Scope.settings)

    # Flat to enable or disable logging
    logging_enabled = Boolean(help="Enable logging.",
                              default=True,
                              scope=Scope.settings)

    # **** User Information ****
    # This section includes variables necessary for storing partial
    # student answers so that they can come back and work on a problem
    # without worrying about loosing progress.
    saveandrestore = String(help="Internal data blob used by the tracer",
                            default="",
                            scope=Scope.user_state)

    # **** Utility functions and methods ****

    @staticmethod
    def resource_string(path):
        """ Read in the contents of a resource file. """
        data = pkg_resources.resource_string(__name__, path)
        return data.decode("utf8")

    @staticmethod
    def strip_local(url):
        """ Returns the given url with //localhost:port removed. """
        return re.sub(r'//localhost(:\d*)?', '', url)

    def get_local_resource_url(self, url):
        """ Wrapper for self.runtime.local_resource_url. """
        # It has been observed that self.runtime.local_resource_url(self, url)
        # prepends "//localhost:(port)" which makes accessing the Xblock in EdX
        # from a remote machine fail completely.
        return self.strip_local(self.runtime.local_resource_url(self, url))

    # -------------------------------------------------------------------
    # Here we construct the tutor html page from various resources. This
    # is where all things go to hell. We can't use jsrender because the
    # XBlock API call add_resource doesn't support non-registered mime-
    # types and it doesn't support the addition of an id for the script
    # tag.
    # More information on this poor excuse for an API at:
    #
    # http://edx.readthedocs.org/projects/xblock/en/latest/fragment.html
    #
    # The XBlock developers seem to be confused as to what a relative url
    # is but have no problem accusing outside developers of not
    # understanding the concept. Also of course major documentation missing
    # or unclear on how to add static resources to XBLock html pages and
    # CSS files:
    #
    # https://groups.google.com/forum/#!topic/edx-code/MXWBNkE6gjU
    #
    # -------------------------------------------------------------------

    def student_view(self, dummy_context=None):
        """
        Create a Fragment used to display a CTAT StatTutor xBlock to a student.

        Args:
          dummy_context: unused but required as a XBlock.student_view.
        Returns:
          a Fragment object containing the HTML to display.
        """
        # read in template html
        html = self.resource_string("static/html/ctatxblock.html")
        brd = self.problems[self.problem]['brd']
        description = self.problems[self.problem]['description']
        frag = Fragment(
            html.format(
                # Until the iframe srcdoc attribute is universally supported
                # a valid xblock generated url has to be passed into
                # ctatxblock.html.  Internet Explorer does not support srcdoc.
                tutor_html=self.get_local_resource_url(self.src),
                width=self.width,
                height=self.height))
        config = self.resource_string("static/js/CTATConfig.js")
        usage_id = self.scope_ids.usage_id
        sdk_usage = isinstance(usage_id, basestring)
        frag.add_javascript(
            config.format(
                # meta
                guid=str(uuid.uuid4()),
                student_id=self.runtime.anonymous_student_id if hasattr(
                    self.runtime, 'anonymous_student_id') else 'bogus-sdk-id',
                # class
                course=unicode(usage_id.course) if not sdk_usage else usage_id,
                org=unicode(usage_id.org) if not sdk_usage else usage_id,
                run=unicode(usage_id.run) if not sdk_usage else usage_id,
                # dataset
                course_key=unicode(usage_id.course_key)
                if not sdk_usage else usage_id,
                problem_name=self.problem,
                block_type=unicode(usage_id.block_type)
                if not sdk_usage else usage_id,
                # runtime
                logtype=self.logging_enabled,
                log_url=self.log_url,
                question_file=self.get_local_resource_url(brd),
                saved_state_len=len(self.saveandrestore),
                completed=self.completed,
                usage_id=unicode(self.scope_ids.usage_id),
                problem_description=self.get_local_resource_url(description)))
        # Add the xml2json library here because someone has a problem if it lives somewhere else instead
        frag.add_javascript(self.resource_string("static/js/xml2json.min.js"))
        # Add javascript initialization code
        frag.add_javascript(
            self.resource_string("static/js/Initialize_CTATXBlock.js"))
        # Execute javascript initialization code
        frag.initialize_js('Initialize_CTATXBlock')
        return frag

    @XBlock.json_handler
    def ctat_log(self, data, dummy_suffix=''):

        # Send a problem_check event on each student submission for the OLI event handler.

        corrects = 0

        if data.get('event') is not None:
            grade = data.get('event')
            problem_check_event_data = {
                'grade': grade.get('grade'),
                'max_grade': 1.0,
                'problem_question_name': data.get('problem_question_name')
            }
            try:
                self.runtime.publish(self, 'problem_check',
                                     problem_check_event_data)
            except Exception as err:
                log.error(
                    'Error on sending problem_check event from StatTutor Xblock'
                )
        """Publish log messages from a CTAT tutor to EdX log."""

        data_present = 'event_type' in data and 'action' in data and 'message' in data
        if not data_present:
            return {
                'result': 'fail',
                'error': 'Log request message is missing required fields.'
            }

        data.pop('event_type')
        # pylint: disable=broad-except
        try:
            data['user_id'] = self.runtime.user_id
            data['component_id'] = unicode(self.scope_ids.usage_id)
            self.runtime.publish(self, "ctatlog", data)
        # General mechanism to catch a very broad category of errors.
        except Exception as err:
            return {'result': 'fail', 'error': unicode(err)}
        # pylint: enable=broad-except
        return {'result': 'success'}

    @XBlock.json_handler
    def ctat_grade(self, data, dummy_suffix=''):
        """
        Handles updating the grade based on post request from the tutor.

        Args:
          self: the StatTutor XBlock.
          data: A JSON object.
          dummy_suffix: unused but required as a XBlock.json_handler.
        Returns:
          A JSON object reporting the success or failure.
        """
        self.attempted = True
        corrects = 0

        if 'value' in data:
            try:
                corrects = int(data.get('value'))
            except ValueError as int_err:
                return {
                    'result': 'fail',
                    'error': 'Bad grading values:' + unicode(int_err)
                }

        if 'max_value' in data:
            try:
                max_val = int(data.get('max_value'))
            except ValueError as int_err:
                return {
                    'result': 'fail',
                    'error': 'Bad grading values:' + unicode(int_err)
                }
            if max_val > 0:
                # only update if a valid number
                self.max_problem_steps = max_val

        # Send a problem_check event on each student submission for the OLI event handler.
        #if corrects > self.score:
        #    correct = 1
        #else:
        #    correct = 0
        #problem_check_event_data = {'grade': correct, 'max_grade': 1.0, 'problem_question_name' : self.problem+'_'+data.get('last_selection'),}
        #try:
        #    self.runtime.publish(self, 'problem_check', problem_check_event_data)
        #except Exception as err:
        #    log.error(
        #        'Error on sending problem_check event from StatTutor Xblock',
        #    )

        # only change score if it increases.
        # this is done because corrects should only ever increase and
        # it deals with issues EdX has with grading, in particular
        # the multiple identical database entries issue.
        if corrects > self.score:
            self.score = corrects
            self.completed = self.score >= self.max_problem_steps
            scaled = float(self.score) / float(self.max_problem_steps)
            # trying with max of 1. because basing it on max_problem_steps
            # seems to cause EdX to incorrectly report the grade.
            event_data = {'value': scaled, 'max_value': 1.0}
            # pylint: disable=broad-except
            # The errors that should be checked are django errors, but there
            # type is not known at this point. This exception is designed
            # partially to learn what the possible errors are.
            try:
                self.runtime.publish(self, 'grade', event_data)
            except Exception as err:
                return {'result': 'fail', 'Error': err.message}
            return {
                'result': 'success',
                'finished': self.completed,
                'score': scaled
            }
            # pylint: enable=broad-except
        return {
            'result': 'no-change',
            'finished': self.completed,
            'score': float(self.score) / float(self.max_problem_steps)
        }

    def studio_view(self, dummy_context=None):
        """ Generate the Studio page contents. """
        html = self.resource_string("static/html/ctatstudio.html")
        problem_dirs = [
            '<option value="{0}"{1}>{0}</option>'.format(
                d, ' selected' if d == self.problem else '')
            for d in self.problems.keys()
        ]
        problem_dirs.sort()
        frag = Fragment(
            html.format(problems=''.join(problem_dirs),
                        logging='checked' if self.logging_enabled else '',
                        logserver=self.log_url))
        studio_js = self.resource_string("static/js/ctatstudio.js")
        frag.add_javascript(unicode(studio_js))
        frag.initialize_js('CTATXBlockStudio')
        return frag

    @XBlock.json_handler
    def studio_submit(self, data, dummy_suffix=''):
        """
        Called when submitting the form in Studio.

        Args:
          self: the StatTutor XBlock.
          data: a JSON object encoding the form data from
                static/html/ctatstudio.html
          dummy_suffix: unused but required as a XBlock.json_handler.
        Returns:
          A JSON object reporting the success of the operation.
        """
        status = 'success'
        messages = []
        statmodule = bleach.clean(data.get('statmodule'), strip=True)
        logging = bleach.clean(data.get('logging'), strip=True)
        if statmodule in self.problems.keys():
            self.problem = statmodule
        else:
            status = 'failure'
            messages.append("invalid module")
        if logging.lower() == "true":
            self.logging_enabled = True
        else:
            self.logging_enabled = False
        ret = {'result': status}
        if len(messages) > 0:
            ret['message'] = "; ".join(messages)
        return ret

    @XBlock.json_handler
    def ctat_save_problem_state(self, data, dummy_suffix=''):
        """Called from CTATLMS.saveProblemState.
        This saves the current state of the tutor after each correct action.

        Args:
          self: the StatTutor XBlock.
          data: A JSON object with a 'state' field with a payload of the blob
                of 64 bit encoded data that represents the current
                state of the tutor.
          dummy_suffix: unused but required as a XBlock.json_handler.
        Returns:
          A JSON object with a 'result' field with a payload indicating the
          success status.
        """
        if data.get('state') is not None:
            self.saveandrestore = bleach.clean(data.get('state'))
            return {'result': 'success'}
        return {'result': 'failure'}

    @XBlock.json_handler
    def ctat_get_problem_state(self, dummy_data, dummy_suffix=''):
        """
        Return the stored problem state to reconstruct a student's progress.

        Args:
          self: the StatTutor XBlock.
          dummy_data: unused but required as a XBlock.json_handler.
          dummy_suffix: unused but required as a XBlock.json_handler.
        Returns:
          A JSON object with a 'result' and a 'state' field.
        """
        return {'result': 'success', 'state': self.saveandrestore}

    @staticmethod
    def workbench_scenarios():
        """ Prescribed XBlock method for displaying this in the workbench. """
        return [
            ("StattutorXBlock", """<vertical_demo>
                <stattutor width="900" height="750"/>
                </vertical_demo>
             """),
        ]
class OpenAssessmentBlock(XBlock, MessageMixin, SubmissionMixin,
                          PeerAssessmentMixin, SelfAssessmentMixin,
                          StudioMixin, GradeMixin, LeaderboardMixin,
                          StaffInfoMixin, WorkflowMixin, StudentTrainingMixin,
                          LmsCompatibilityMixin):
    """Displays a prompt and provides an area where students can compose a response."""

    submission_start = String(
        default=DEFAULT_START,
        scope=Scope.settings,
        help="ISO-8601 formatted string representing the submission start date."
    )

    submission_due = String(
        default=DEFAULT_DUE,
        scope=Scope.settings,
        help="ISO-8601 formatted string representing the submission due date.")

    allow_file_upload = Boolean(default=False,
                                scope=Scope.content,
                                help="File upload allowed with submission.")

    allow_latex = Boolean(default=False,
                          scope=Scope.settings,
                          help="Latex rendering allowed with submission.")

    title = String(default="",
                   scope=Scope.content,
                   help="A title to display to a student (plain text).")

    leaderboard_show = Integer(
        default=0,
        scope=Scope.content,
        help="The number of leaderboard results to display (0 if none)")

    prompt = String(default=DEFAULT_PROMPT,
                    scope=Scope.content,
                    help="A prompt to display to a student (plain text).")

    rubric_criteria = List(
        default=DEFAULT_RUBRIC_CRITERIA,
        scope=Scope.content,
        help="The different parts of grading for students giving feedback.")

    rubric_feedback_prompt = String(
        default=DEFAULT_RUBRIC_FEEDBACK_PROMPT,
        scope=Scope.content,
        help="The rubric feedback prompt displayed to the student")

    rubric_feedback_default_text = String(
        default=DEFAULT_RUBRIC_FEEDBACK_TEXT,
        scope=Scope.content,
        help="The default rubric feedback text displayed to the student")

    rubric_assessments = List(
        default=DEFAULT_ASSESSMENT_MODULES,
        scope=Scope.content,
        help=
        "The requested set of assessments and the order in which to apply them."
    )

    course_id = String(
        default=u"TestCourse",
        scope=Scope.content,
        help=
        "The course_id associated with this prompt (until we can get it from runtime)."
    )

    submission_uuid = String(
        default=None,
        scope=Scope.user_state,
        help="The student's submission that others will be assessing.")

    has_saved = Boolean(
        default=False,
        scope=Scope.user_state,
        help="Indicates whether the user has saved a response.")

    saved_response = String(
        default=u"",
        scope=Scope.user_state,
        help="Saved response submission for the current user.")

    no_peers = Boolean(
        default=False,
        scope=Scope.user_state,
        help="Indicates whether or not there are peers to grade.")

    def get_student_item_dict(self):
        """Create a student_item_dict from our surrounding context.

        See also: submissions.api for details.

        Returns:
            (dict): The student item associated with this XBlock instance. This
                includes the student id, item id, and course id.
        """

        item_id = self._serialize_opaque_key(self.scope_ids.usage_id)

        # This is not the real way course_ids should work, but this is a
        # temporary expediency for LMS integration
        if hasattr(self, "xmodule_runtime"):
            course_id = self._serialize_opaque_key(
                self.xmodule_runtime.course_id)  # pylint:disable=E1101
            student_id = self.xmodule_runtime.anonymous_student_id  # pylint:disable=E1101
        else:
            course_id = "edX/Enchantment_101/April_1"
            if self.scope_ids.user_id is None:
                student_id = None
            else:
                student_id = unicode(self.scope_ids.user_id)

        student_item_dict = dict(student_id=student_id,
                                 item_id=item_id,
                                 course_id=course_id,
                                 item_type='openassessment')
        return student_item_dict

    def student_view(self, context=None):
        """The main view of OpenAssessmentBlock, displayed when viewing courses.

        The main view which displays the general layout for Open Ended
        Assessment Questions. The contents of the XBlock are determined
        dynamically based on the assessment workflow configured by the author.

        Args:
            context: Not used for this view.

        Returns:
            (Fragment): The HTML Fragment for this XBlock, which determines the
            general frame of the Open Ended Assessment Question.
        """
        # On page load, update the workflow status.
        # We need to do this here because peers may have graded us, in which
        # case we may have a score available.

        try:
            self.update_workflow_status()
        except AssessmentWorkflowError:
            # Log the exception, but continue loading the page
            logger.exception(
                'An error occurred while updating the workflow on page load.')

        ui_models = self._create_ui_models()
        # All data we intend to pass to the front end.
        context_dict = {
            "title":
            self.title,
            "question":
            self.prompt,
            "rubric_assessments":
            ui_models,
            "show_staff_debug_info":
            self.is_course_staff and not self.in_studio_preview,
        }
        template = get_template("openassessmentblock/oa_base.html")
        context = Context(context_dict)
        frag = Fragment(template.render(context))

        i18n_service = self.runtime.service(self, 'i18n')
        if hasattr(i18n_service,
                   'get_language_bidi') and i18n_service.get_language_bidi():
            frag.add_css(load("static/css/openassessment-rtl.css"))
        else:
            frag.add_css(load("static/css/openassessment-ltr.css"))

        frag.add_javascript(load("static/js/openassessment-lms.min.js"))
        frag.initialize_js('OpenAssessmentBlock')
        return frag

    @property
    def is_admin(self):
        """
        Check whether the user has global staff permissions.

        Returns:
            bool
        """
        if hasattr(self, 'xmodule_runtime'):
            return getattr(self.xmodule_runtime, 'user_is_admin', False)
        else:
            return False

    @property
    def is_course_staff(self):
        """
        Check whether the user has course staff permissions for this XBlock.

        Returns:
            bool
        """
        if hasattr(self, 'xmodule_runtime'):
            return getattr(self.xmodule_runtime, 'user_is_staff', False)
        else:
            return False

    @property
    def in_studio_preview(self):
        """
        Check whether we are in Studio preview mode.

        Returns:
            bool

        """
        # When we're running in Studio Preview mode, the XBlock won't provide us with a user ID.
        # (Note that `self.xmodule_runtime` will still provide an anonymous
        # student ID, so we can't rely on that)
        return self.scope_ids.user_id is None

    def _create_ui_models(self):
        """Combine UI attributes and XBlock configuration into a UI model.

        This method takes all configuration for this XBlock instance and appends
        UI attributes to create a UI Model for rendering all assessment modules.
        This allows a clean separation of static UI attributes from persistent
        XBlock configuration.

        """
        ui_models = [UI_MODELS["submission"]]
        for assessment in self.valid_assessments:
            ui_model = UI_MODELS.get(assessment["name"])
            if ui_model:
                ui_models.append(dict(assessment, **ui_model))
        ui_models.append(UI_MODELS["grade"])

        if self.leaderboard_show > 0:
            ui_models.append(UI_MODELS["leaderboard"])

        return ui_models

    @staticmethod
    def workbench_scenarios():
        """A canned scenario for display in the workbench.

        These scenarios are only intended to be used for Workbench XBlock
        Development.

        """
        return [
            ("OpenAssessmentBlock Unicode", load('static/xml/unicode.xml')),
            ("OpenAssessmentBlock Example Based Rubric",
             load('static/xml/example_based_example.xml')),
            ("OpenAssessmentBlock Poverty Rubric",
             load('static/xml/poverty_rubric_example.xml')),
            ("OpenAssessmentBlock Leaderboard",
             load('static/xml/leaderboard.xml')),
            ("OpenAssessmentBlock (Peer Only) Rubric",
             load('static/xml/poverty_peer_only_example.xml')),
            ("OpenAssessmentBlock (Self Only) Rubric",
             load('static/xml/poverty_self_only_example.xml')),
            ("OpenAssessmentBlock Censorship Rubric",
             load('static/xml/censorship_rubric_example.xml')),
            ("OpenAssessmentBlock Promptless Rubric",
             load('static/xml/promptless_rubric_example.xml')),
        ]

    @classmethod
    def parse_xml(cls, node, runtime, keys, id_generator):
        """Instantiate XBlock object from runtime XML definition.

        Inherited by XBlock core.

        """
        config = parse_from_xml(node)
        block = runtime.construct_xblock_from_class(cls, keys)

        xblock_validator = validator(block, block._, strict_post_release=False)
        xblock_validator(create_rubric_dict(config['prompt'],
                                            config['rubric_criteria']),
                         config['rubric_assessments'],
                         submission_start=config['submission_start'],
                         submission_due=config['submission_due'],
                         leaderboard_show=config['leaderboard_show'])

        block.rubric_criteria = config['rubric_criteria']
        block.rubric_feedback_prompt = config['rubric_feedback_prompt']
        block.rubric_feedback_default_text = config[
            'rubric_feedback_default_text']
        block.rubric_assessments = config['rubric_assessments']
        block.submission_start = config['submission_start']
        block.submission_due = config['submission_due']
        block.title = config['title']
        block.prompt = config['prompt']
        block.allow_file_upload = config['allow_file_upload']
        block.allow_latex = config['allow_latex']
        block.leaderboard_show = config['leaderboard_show']

        return block

    @property
    def _(self):
        i18nService = self.runtime.service(self, 'i18n')
        return i18nService.ugettext

    @property
    def valid_assessments(self):
        """
        Return a list of assessment dictionaries that we recognize.
        This allows us to gracefully handle situations in which unrecognized
        assessment types are stored in the XBlock field (e.g. because
        we roll back code after releasing a feature).

        Returns:
            list

        """
        return [
            asmnt for asmnt in self.rubric_assessments
            if asmnt.get('name') in VALID_ASSESSMENT_TYPES
        ]

    @property
    def assessment_steps(self):
        return [asmnt['name'] for asmnt in self.valid_assessments]

    @lazy
    def rubric_criteria_with_labels(self):
        """
        Backwards compatibility: We used to treat "name" as both a user-facing label
        and a unique identifier for criteria and options.
        Now we treat "name" as a unique identifier, and we've added an additional "label"
        field that we display to the user.
        If criteria/options in the problem definition do NOT have a "label" field
        (because they were created before this change),
        we create a new label that has the same value as "name".

        The result of this call is cached, so it should NOT be used in a runtime
        that can modify the XBlock settings (in the LMS, settings are read-only).

        Returns:
            list of criteria dictionaries

        """
        criteria = copy.deepcopy(self.rubric_criteria)
        for criterion in criteria:
            if 'label' not in criterion:
                criterion['label'] = criterion['name']
            for option in criterion['options']:
                if 'label' not in option:
                    option['label'] = option['name']
        return criteria

    def render_assessment(self, path, context_dict=None):
        """Render an Assessment Module's HTML

        Given the name of an assessment module, find it in the list of
        configured modules, and ask for its rendered HTML.

        Args:
            path (str): The path to the template used to render this HTML
                section.
            context_dict (dict): A dictionary of context variables used to
                populate this HTML section.

        Returns:
            (Response): A Response Object with the generated HTML fragment. This
                is intended for AJAX calls to load dynamically into a larger
                document.
        """
        if not context_dict:
            context_dict = {}

        template = get_template(path)
        context = Context(context_dict)
        return Response(template.render(context),
                        content_type='application/html',
                        charset='UTF-8')

    def add_xml_to_node(self, node):
        """
        Serialize the XBlock to XML for exporting.
        """
        serialize_content_to_xml(self, node)

    def render_error(self, error_msg):
        """
        Render an error message.

        Args:
            error_msg (unicode): The error message to display.

        Returns:
            Response: A response object with an HTML body.
        """
        context = Context({'error_msg': error_msg})
        template = get_template('openassessmentblock/oa_error.html')
        return Response(template.render(context),
                        content_type='application/html',
                        charset='UTF-8')

    def is_closed(self, step=None, course_staff=None):
        """
        Checks if the question is closed.

        Determines if the start date is in the future or the end date has
            passed.  Optionally limited to a particular step in the workflow.

        Start/due dates do NOT apply to course staff, since course staff may need to get to
        the peer grading step AFTER the submission deadline has passed.
        This may not be necessary when we implement a grading interface specifically for course staff.

        Keyword Arguments:
            step (str): The step in the workflow to check.  Options are:
                None: check whether the problem as a whole is open.
                "submission": check whether the submission section is open.
                "peer-assessment": check whether the peer-assessment section is open.
                "self-assessment": check whether the self-assessment section is open.

            course_staff (bool): Whether to treat the user as course staff (disable start/due dates).
                If not specified, default to the current user's status.

        Returns:
            tuple of the form (is_closed, reason, start_date, due_date), where
                is_closed (bool): indicates whether the step is closed.
                reason (str or None): specifies the reason the step is closed ("start" or "due")
                start_date (datetime): is the start date of the step/problem.
                due_date (datetime): is the due date of the step/problem.

        Examples:
            >>> is_closed()
            False, None, datetime.datetime(2014, 3, 27, 22, 7, 38, 788861), datetime.datetime(2015, 3, 27, 22, 7, 38, 788861)
            >>> is_closed(step="submission")
            True, "due", datetime.datetime(2014, 3, 27, 22, 7, 38, 788861), datetime.datetime(2015, 3, 27, 22, 7, 38, 788861)
            >>> is_closed(step="self-assessment")
            True, "start", datetime.datetime(2014, 3, 27, 22, 7, 38, 788861), datetime.datetime(2015, 3, 27, 22, 7, 38, 788861)

        """
        submission_range = (self.submission_start, self.submission_due)
        assessment_ranges = [(asmnt.get('start'), asmnt.get('due'))
                             for asmnt in self.valid_assessments]

        # Resolve unspecified dates and date strings to datetimes
        start, due, date_ranges = resolve_dates(
            self.start, self.due, [submission_range] + assessment_ranges,
            self._)

        open_range = (start, due)
        assessment_steps = self.assessment_steps
        if step == 'submission':
            open_range = date_ranges[0]
        elif step in assessment_steps:
            step_index = assessment_steps.index(step)
            open_range = date_ranges[1 + step_index]

        # Course staff always have access to the problem
        if course_staff is None:
            course_staff = self.is_course_staff
        if course_staff:
            return False, None, DISTANT_PAST, DISTANT_FUTURE

        # Check if we are in the open date range
        now = dt.datetime.utcnow().replace(tzinfo=pytz.utc)

        if now < open_range[0]:
            return True, "start", open_range[0], open_range[1]
        elif now >= open_range[1]:
            return True, "due", open_range[0], open_range[1]
        else:
            return False, None, open_range[0], open_range[1]

    def get_waiting_details(self, status_details):
        """
        Returns the specific waiting status based on the given status_details.
        This status can currently be peer, example-based, or both. This is
        determined by checking that status details to see if all assessment
        modules have been graded.

        Args:
            status_details (dict): A dictionary containing the details of each
                assessment module status. This will contain keys such as
                "peer" and "ai", referring to dictionaries, which in turn will
                have the key "graded". If this key has a value set, these
                assessment modules have been graded.

        Returns:
            A string of "peer", "exampled-based", or "all" to indicate which
            assessment modules in the workflow are waiting on assessments.
            Returns None if no module is waiting on an assessment.

        Examples:
            >>> now = dt.datetime.utcnow().replace(tzinfo=pytz.utc)
            >>> status_details = {
            >>>     'peer': {
            >>>         'completed': None,
            >>>         'graded': now
            >>>     },
            >>>     'ai': {
            >>>         'completed': now,
            >>>         'graded': None
            >>>     }
            >>> }
            >>> self.get_waiting_details(status_details)
            "peer"
        """
        waiting = None
        peer_waiting = "peer" in status_details and not status_details["peer"][
            "graded"]
        ai_waiting = "ai" in status_details and not status_details["ai"][
            "graded"]
        if peer_waiting and ai_waiting:
            waiting = "all"
        elif peer_waiting:
            waiting = "peer"
        elif ai_waiting:
            waiting = "example-based"
        return waiting

    def is_released(self, step=None):
        """
        Check if a question has been released.

        Keyword Arguments:
            step (str): The step in the workflow to check.
                None: check whether the problem as a whole is open.
                "submission": check whether the submission section is open.
                "peer-assessment": check whether the peer-assessment section is open.
                "self-assessment": check whether the self-assessment section is open.

        Returns:
            bool
        """
        # By default, assume that we're published, in case the runtime doesn't support publish date.
        if hasattr(self.runtime, 'modulestore'):
            is_published = self.runtime.modulestore.has_published_version(self)
        else:
            is_published = True
        is_closed, reason, __, __ = self.is_closed(step=step)
        return is_published and (not is_closed or reason == 'due')

    def get_assessment_module(self, mixin_name):
        """
        Get a configured assessment module by name.

        Args:
            mixin_name (str): The name of the mixin (e.g. "self-assessment" or "peer-assessment")

        Returns:
            dict

        Example:
            >>> self.get_assessment_module('peer-assessment')
            {
                "name": "peer-assessment",
                "start": None,
                "due": None,
                "must_grade": 5,
                "must_be_graded_by": 3,
            }
        """
        for assessment in self.valid_assessments:
            if assessment["name"] == mixin_name:
                return assessment

    def publish_assessment_event(self, event_name, assessment):
        """
        Emit an analytics event for the peer assessment.

        Args:
            event_name (str): An identifier for this event type.
            assessment (dict): The serialized assessment model.

        Returns:
            None

        """
        parts_list = []
        for part in assessment["parts"]:
            # Some assessment parts do not include point values,
            # only written feedback.  In this case, the assessment
            # part won't have an associated option.
            option_dict = None
            if part["option"] is not None:
                option_dict = {
                    "name": part["option"]["name"],
                    "points": part["option"]["points"],
                }

            # All assessment parts are associated with criteria
            criterion_dict = {
                "name": part["criterion"]["name"],
                "points_possible": part["criterion"]["points_possible"]
            }

            parts_list.append({
                "option": option_dict,
                "criterion": criterion_dict,
                "feedback": part["feedback"]
            })

        self.runtime.publish(
            self, event_name, {
                "feedback": assessment["feedback"],
                "rubric": {
                    "content_hash": assessment["rubric"]["content_hash"],
                },
                "scorer_id": assessment["scorer_id"],
                "score_type": assessment["score_type"],
                "scored_at": assessment["scored_at"],
                "submission_uuid": assessment["submission_uuid"],
                "parts": parts_list
            })

    def _serialize_opaque_key(self, key):
        """
        Gracefully handle opaque keys, both before and after the transition.
        https://github.com/edx/edx-platform/wiki/Opaque-Keys

        Currently uses `to_deprecated_string()` to ensure that new keys
        are backwards-compatible with keys we store in ORA2 database models.

        Args:
            key (unicode or OpaqueKey subclass): The key to serialize.

        Returns:
            unicode

        """
        if hasattr(key, 'to_deprecated_string'):
            return key.to_deprecated_string()
        else:
            return unicode(key)
Beispiel #30
0
class EqualityCheckerBlock(CheckerBlock):
    """An XBlock that checks the equality of two student data fields."""

    # Content: the problem will hook us up with our data.
    content = String(help="Message describing the equality test",
                     scope=Scope.content,
                     default="Equality test")

    # Student data
    left = Any(scope=Scope.user_state)
    right = Any(scope=Scope.user_state)
    attempted = Boolean(scope=Scope.user_state)

    def problem_view(self, context=None):
        """Renders the problem view.

        The view is specific to whether or not this problem was attempted, and, if so,
        if it was answered correctly.

        """
        correct = self.left == self.right

        # TODO: I originally named this class="data", but that conflicted with
        # the CSS on the page! :(  We might have to do something to namespace
        # things.
        # TODO: Should we have a way to spit out JSON islands full of data?
        # Note the horror of mixed Python-Javascript data below...
        content = string.Template(self.content).substitute(**context)
        result = Fragment(u"""
            <span class="mydata" data-attempted='{self.attempted}' data-correct='{correct}'>
                {content}
                <span class='indicator'></span>
            </span>
            """.format(self=self, content=content, correct=correct))
        # TODO: This is a runtime-specific URL.  But if each XBlock ships their
        # own copy of underscore.js, we won't be able to uniquify them.
        # Perhaps runtimes can offer a palette of popular libraries so that
        # XBlocks can refer to them in XBlock-standard ways?
        result.add_javascript_url(
            self.runtime.resource_url("js/vendor/underscore-min.js"))

        # TODO: The image tag here needs a magic URL, not a hard-coded one.
        format_data = {
            'correct':
            self.runtime.local_resource_url(self,
                                            'public/images/correct-icon.png'),
            'incorrect':
            self.runtime.local_resource_url(
                self, 'public/images/incorrect-icon.png'),
        }
        result.add_resource(
            u"""
            <script type="text/template" id="xblock-equality-template">
                <% if (attempted !== "True") {{ %>
                    (Not attempted)
                <% }} else if (correct === "True") {{ %>
                    <img src="{correct}">
                <% }} else {{ %>
                    <img src="{incorrect}">
                <% }} %>
            </script>
            """.format(**format_data), "text/html")

        result.add_javascript("""
            function EqualityCheckerBlock(runtime, element) {
                var template = _.template($("#xblock-equality-template").html());
                function render() {
                    var data = $("span.mydata", element).data();
                    $("span.indicator", element).html(template(data));
                }
                render();
                return {
                    handleCheck: function(result) {
                        $("span.mydata", element)
                              .data("correct", result ? "True" : "False")
                              .data("attempted", "True");
                        render();
                    }
                }
            }
            """)

        result.initialize_js('EqualityCheckerBlock')
        return result

    def check(self, left, right):  # pylint: disable=W0221
        self.attempted = True
        self.left = left
        self.right = right

        event_data = {'value': 1 if left == right else 0, 'max_value': 1}
        self.runtime.publish(self, 'grade', event_data)

        return left == right