Beispiel #1
0
class PollBase(XBlock, ResourceMixin, PublishEventMixin):
    """
    Base class for Poll-like XBlocks.
    """
    has_author_view = True

    event_namespace = 'xblock.pollbase'
    private_results = Boolean(default=False, help=_("Whether or not to display results to the user."))
    max_submissions = Integer(default=1, help=_("The maximum number of times a user may send a submission."))
    submissions_count = Integer(
        default=0, help=_("Number of times the user has sent a submission."), scope=Scope.user_state
    )
    feedback = String(default='', help=_("Text to display after the user votes."))

    def send_vote_event(self, choice_data):
        # Let the LMS know the user has answered the poll.
        self.runtime.publish(self, 'progress', {})
        # The SDK doesn't set url_name.
        event_dict = {'url_name': getattr(self, 'url_name', '')}
        event_dict.update(choice_data)
        self.publish_event_from_dict(
            self.event_namespace + '.submitted',
            event_dict,
        )

    @staticmethod
    def any_image(field):
        """
        Find out if any answer has an image, since it affects layout.
        """
        return any(value['img'] for key, value in field)

    @staticmethod
    def markdown_items(items):
        """
        Convert all items' labels into markdown.
        """
        return [(key, {'label': markdown(value['label']), 'img': value['img'], 'img_alt': value.get('img_alt')})
                for key, value in items]

    def _get_block_id(self):
        """
        Return unique ID of this block. Useful for HTML ID attributes.

        Works both in LMS/Studio and workbench runtimes:
        - In LMS/Studio, use the location.html_id method.
        - In the workbench, use the usage_id.
        """
        if hasattr(self, 'location'):
            return self.location.html_id()  # pylint: disable=no-member

        return unicode(self.scope_ids.usage_id)

    def img_alt_mandatory(self):
        """
        Determine whether alt attributes for images are configured to be mandatory.  Defaults to True.
        """
        settings_service = self.runtime.service(self, "settings")
        if not settings_service:
            return True
        xblock_settings = settings_service.get_settings_bucket(self)
        return xblock_settings.get('IMG_ALT_MANDATORY', True)

    def gather_items(self, data, result, noun, field, image=True):
        """
        Gathers a set of label-img pairs from a data dict and puts them in order.
        """
        items = []
        if field not in data or not isinstance(data[field], list):
            source_items = []
            result['success'] = False
            error_message = self.ugettext(
                # Translators: {field} is either "answers" or "questions".
                "'{field}' is not present, or not a JSON array."
            ).format(field=field)
            result['errors'].append(error_message)
        else:
            source_items = data[field]

        # Make sure all components are present and clean them.
        for item in source_items:
            if not isinstance(item, dict):
                result['success'] = False
                error_message = self.ugettext(
                    # Translators: {noun} is either "Answer" or "Question". {item} identifies the answer or question.
                    "{noun} {item} not a javascript object!"
                ).format(noun=noun, item=item)
                result['errors'].append(error_message)
                continue
            key = item.get('key', '').strip()
            if not key:
                result['success'] = False
                error_message = self.ugettext(
                    # Translators: {noun} is either "Answer" or "Question". {item} identifies the answer or question.
                    "{noun} {item} contains no key."
                ).format(noun=noun, item=item)
                result['errors'].append(error_message)
            image_link = item.get('img', '').strip()
            image_alt = item.get('img_alt', '').strip()
            label = item.get('label', '').strip()
            if not label:
                if image and not image_link:
                    result['success'] = False
                    error_message = self.ugettext(
                        # Translators: {noun} is either "Answer" or "Question".
                        # {noun_lower} is the lowercase version of {noun}.
                        "{noun} has no text or img. Please make sure all {noun_lower}s have one or the other, or both."
                    ).format(noun=noun, noun_lower=noun.lower())
                    result['errors'].append(error_message)
                elif not image:
                    result['success'] = False
                    # If there's a bug in the code or the user just forgot to relabel a question,
                    # votes could be accidentally lost if we assume the omission was an
                    # intended deletion.
                    error_message = self.ugettext(
                        # Translators: {noun} is either "Answer" or "Question".
                        # {noun_lower} is the lowercase version of {noun}.
                        "{noun} was added with no label. All {noun_lower}s must have labels. Please check the form. "
                        "Check the form and explicitly delete {noun_lower}s if not needed."
                    ).format(noun=noun, noun_lower=noun.lower())
                    result['errors'].append(error_message)
            if image_link and not image_alt and self.img_alt_mandatory():
                result['success'] = False
                result['errors'].append(
                    self.ugettext(
                        "All images must have an alternative text describing the image in a way "
                        "that would allow someone to answer the poll if the image did not load."
                    )
                )
            if image:
                items.append((key, {'label': label, 'img': image_link, 'img_alt': image_alt}))
            else:
                items.append([key, label])

        if not items:
            error_message = self.ugettext(
                # Translators: "{noun_lower} is either "answer" or "question".
                "You must include at least one {noun_lower}."
            ).format(noun_lower=noun.lower())
            result['errors'].append(error_message)
            result['success'] = False

        return items

    def can_vote(self):
        """
        Checks to see if the user is permitted to vote. This may not be the case if they used up their max_submissions.
        """
        return self.max_submissions == 0 or self.submissions_count < self.max_submissions

    def can_view_private_results(self):
        """
        Checks to see if the user has permissions to view private results.
        This only works inside the LMS.
        """
        if not hasattr(self.runtime, 'user_is_staff'):
            return False

        # Course staff users have permission to view results.
        if self.runtime.user_is_staff:
            return True

        # Check if user is member of a group that is explicitly granted
        # permission to view the results through django configuration.
        if not HAS_GROUP_PROFILE:
            return False
        group_names = getattr(settings, 'XBLOCK_POLL_EXTRA_VIEW_GROUPS', [])
        if not group_names:
            return False
        user = self.runtime.get_real_user(self.runtime.anonymous_student_id)
        group_ids = user.groups.values_list('id', flat=True)
        return GroupProfile.objects.filter(group_id__in=group_ids, name__in=group_names).exists()

    @staticmethod
    def get_max_submissions(ugettext, data, result, private_results):
        """
        Gets the value of 'max_submissions' from studio submitted AJAX data, and checks for conflicts
        with private_results, which may not be False when max_submissions is not 1, since that would mean
        the student could change their answer based on other students' answers.
        """
        try:
            max_submissions = int(data['max_submissions'])
        except (ValueError, KeyError):
            max_submissions = 1
            result['success'] = False
            result['errors'].append(ugettext('Maximum Submissions missing or not an integer.'))

        # Better to send an error than to confuse the user by thinking this would work.
        if (max_submissions != 1) and not private_results:
            result['success'] = False
            result['errors'].append(ugettext("Private results may not be False when Maximum Submissions is not 1."))
        return max_submissions

    @classmethod
    def static_replace_json_handler(cls, func):
        """A JSON handler that replace all static pseudo-URLs by the actual paths.

        The object returned by func is JSON-serialised, and the resulting string is passed to
        replace_static_urls() to perform regex-based URL replacing.

        We would prefer to explicitly call an API function on single image URLs, but such a function
        is not exposed by the LMS API, so we have to fall back to this slightly hacky implementation.
        """

        @cls.json_handler
        @functools.wraps(func)
        def wrapper(self, request_json, suffix=''):
            response = json.dumps(func(self, request_json, suffix))
            response = replace_static_urls(response, course_id=self.runtime.course_id)
            return Response(response, content_type='application/json')

        if HAS_STATIC_REPLACE:
            # Only use URL translation if it is available
            return wrapper
        # Otherwise fall back to a standard JSON handler
        return cls.json_handler(func)
Beispiel #2
0
class TemplateBuilderContainerBlock(StudioContainerWithNestedXBlocksMixin, XBlock, StudioEditableXBlockMixin):
    display_name = String(
        display_name = _("Title"),
        help =_("This block is a container of problem template builder xblocks developed by GCS"),
        scope = Scope.settings,
        default = _("GCS Problem Builder")
        )

    library_mode = Boolean(
        display_name=_("Library Mode"),
        default=False,
        help=_("If True, use this container xBlock as a library, i.e. it will randomly pick the number of components set at field 'Count' and render to studnets."),
        scope=Scope.settings,
    )

    count = Integer(
        display_name = _("Count"),
        default = 1,
        help=_("Enter the number of components to display to each student."),
        scope = Scope.settings,
        )

    random_samples = List(
        default=[],
        scope= Scope.user_state
    )

    editable_fields = ('display_name', 'library_mode', 'count')

    @property
    def allowed_nested_blocks(self):
        '''
        Define nested XBlock list
        '''
        return [MathProblemTemplateBuilderXBlock]

    def validate_field_data(self, validation, data):
        """""
        Ask this xblock to validate itself.
        XBlock subclass are expected to override this method. Any overiding method should call super() to collect 
        validation results from its superclass, and then add any additional results as necesary.
        """""
        super(TemplateBuilderContainerBlock, self).validate_field_data(validation, data)
        def add_error(msg):
            validation.add(ValidationMessage(ValidationMessage.ERROR, msg))

    def author_edit_view(self, context = None):
        frag = super(TemplateBuilderContainerBlock, self).author_edit_view(context)
        return frag

    def student_view(self, context):
        children_contents = []
        fragment = Fragment()
        # print "Type of self.children: {}".format(type(self.children))
        # print "self.children = {}".format(self.children)

        # # Process library mode
        # if not self.library_mode:
        #     # Get all child components
        #     self.random_samples = self.children
        # else:
        #     # Randomly pick Count samples from the list of all child components to render to specific student on specific course.
        #     #
        #     # refer: https://stackoverflow.com/questions/15511349/select-50-items-from-list-at-random-to-write-to-file
        #
        #     # Check Count vs Total child components
        #     number_of_childs = len(self.children)
        #     if self.count > number_of_childs:
        #         self.count = number_of_childs
        #     # Pick self.count samples from the childrent list
        #     self.random_samples = random.sample(self.children, self.count)
        # print "Type of self.random_samples = {}".format(type(self.random_samples))
        # print "self.random_samples = {}".format(self.random_samples)

        # for child_id in self.random_samples:
        for child_id in self.children:
            child = self.runtime.get_block(child_id)
            child_fragment = self._render_child_fragment(child, context, 'student_view')
            fragment.add_frag_resources(child_fragment)
            children_contents.append(child_fragment.content)

        render_context = {
            'block': self,
            'children_contents': children_contents
        }
        render_context.update(context)
        fragment.add_content(self.loader.render_template(self.CHILD_PREVIEW_TEMPLATE, render_context))

        return fragment
Beispiel #3
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).",
        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).",
        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 #4
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 (unicode): 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 ['', u'']  # pylint: disable=unsubscriptable-object
        # Validate provided account id
        if account_id_is_empty:
            # Account Id field is mandatory
            self.add_validation_message(
                validation,
                _(u"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,
                    _(u"Invalid Account ID or Player ID, please recheck."))

        except requests.ConnectionError:
            self.add_validation_message(
                validation,
                _(u"Can't validate submitted account ID at the moment. "
                  u"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,
                _(u"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':
            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=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)
        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 self.settings.items():
            # if field value is empty and there is json-settings default:
            if field.name == key and getattr(field, 'default',
                                             None) in ['', u'', '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')
        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(u'lang'))
        lang_label = str(data.get(u'label'))
        source = str(data.get(u'source', ''))
        sub_url = str(data.get(u'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[u'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[u'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 #5
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='{ecb.attempted}' data-correct='{correct}'>
                {content}
                <span class='indicator'></span>
            </span>
            """.format(ecb=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(
            u"""
            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
Beispiel #6
0
class LTIFields(object):
    """
    Fields to define and obtain LTI tool from provider are set here,
    except credentials, which should be set in course settings::

    `lti_id` is id to connect tool with credentials in course settings. It should not contain :: (double semicolon)
    `launch_url` is launch URL of tool.
    `custom_parameters` are additional parameters to navigate to proper book and book page.

    For example, for Vitalsource provider, `launch_url` should be
    *https://bc-staging.vitalsource.com/books/book*,
    and to get to proper book and book page, you should set custom parameters as::

        vbid=put_book_id_here
        book_location=page/put_page_number_here

    Default non-empty URL for `launch_url` is needed due to oauthlib demand (URL scheme should be presented)::

    https://github.com/idan/oauthlib/blob/master/oauthlib/oauth1/rfc5849/signature.py#L136
    """
    display_name = String(
        display_name=_("Display Name"),
        help=_(
            "Enter the name that students see for this component.  "
            "Analytics reports may also use the display name to identify this component."
        ),
        scope=Scope.settings,
        default="LTI",
    )
    lti_id = String(
        display_name=_("LTI ID"),
        help=_(
            "Enter the LTI ID for the external LTI provider.  "
            "This value must be the same LTI ID that you entered in the "
            "LTI Passports setting on the Advanced Settings page."
            "<br />See {docs_anchor_open}the edX LTI documentation{anchor_close} for more details on this setting."
        ).format(docs_anchor_open=DOCS_ANCHOR_TAG_OPEN, anchor_close="</a>"),
        default='',
        scope=Scope.settings)
    launch_url = String(
        display_name=_("LTI URL"),
        help=
        _("Enter the URL of the external tool that this component launches. "
          "This setting is only used when Hide External Tool is set to False."
          "<br />See {docs_anchor_open}the edX LTI documentation{anchor_close} for more details on this setting."
          ).format(docs_anchor_open=DOCS_ANCHOR_TAG_OPEN, anchor_close="</a>"),
        default='http://www.example.com',
        scope=Scope.settings)
    custom_parameters = List(
        display_name=_("Custom Parameters"),
        help=
        _("Add the key/value pair for any custom parameters, such as the page your e-book should open to or "
          "the background color for this component."
          "<br />See {docs_anchor_open}the edX LTI documentation{anchor_close} for more details on this setting."
          ).format(docs_anchor_open=DOCS_ANCHOR_TAG_OPEN, anchor_close="</a>"),
        scope=Scope.settings)
    open_in_a_new_page = Boolean(
        display_name=_("Open in New Page"),
        help=
        _("Select True if you want students to click a link that opens the LTI tool in a new window. "
          "Select False if you want the LTI content to open in an IFrame in the current page. "
          "This setting is only used when Hide External Tool is set to False.  "
          ),
        default=True,
        scope=Scope.settings)
    has_score = Boolean(
        display_name=_("Scored"),
        help=
        _("Select True if this component will receive a numerical score from the external LTI system."
          ),
        default=False,
        scope=Scope.settings)
    weight = Float(
        display_name=_("Weight"),
        help=_("Enter the number of points possible for this component.  "
               "The default value is 1.0.  "
               "This setting is only used when Scored is set to True."),
        default=1.0,
        scope=Scope.settings,
        values={"min": 0},
    )
    module_score = Float(help=_(
        "The score kept in the xblock KVS -- duplicate of the published score in django DB"
    ),
                         default=None,
                         scope=Scope.user_state)
    score_comment = String(
        help=_("Comment as returned from grader, LTI2.0 spec"),
        default="",
        scope=Scope.user_state)
    hide_launch = Boolean(
        display_name=_("Hide External Tool"),
        help=
        _("Select True if you want to use this component as a placeholder for syncing with an external grading  "
          "system rather than launch an external tool.  "
          "This setting hides the Launch button and any IFrames for this component."
          ),
        default=False,
        scope=Scope.settings)

    # Users will be presented with a message indicating that their e-mail/username would be sent to a third
    # party application. When "Open in New Page" is not selected, the tool automatically appears without any user action.
    ask_to_send_username = Boolean(
        display_name=_("Request user's username"),
        # Translators: This is used to request the user's username for a third party service.
        # Usernames can only be requested if "Open in New Page" is set to True.
        help=
        _("Select True to request the user's username. You must also set Open in New Page to True to get the user's information."
          ),
        default=False,
        scope=Scope.settings)
    ask_to_send_email = Boolean(
        display_name=_("Request user's email"),
        # Translators: This is used to request the user's email for a third party service.
        # Emails can only be requested if "Open in New Page" is set to True.
        help=
        _("Select True to request the user's email address. You must also set Open in New Page to True to get the user's information."
          ),
        default=False,
        scope=Scope.settings)

    description = String(
        display_name=_("LTI Application Information"),
        help=
        _("Enter a description of the third party application. If requesting username and/or email, use this text box to inform users "
          "why their username and/or email will be forwarded to a third party application."
          ),
        default="",
        scope=Scope.settings)

    button_text = String(
        display_name=_("Button Text"),
        help=
        _("Enter the text on the button used to launch the third party application."
          ),
        default="",
        scope=Scope.settings)

    accept_grades_past_due = Boolean(
        display_name=_("Accept grades past deadline"),
        help=
        _("Select True to allow third party systems to post grades past the deadline."
          ),
        default=True,
        scope=Scope.settings)
Beispiel #7
0
class LibraryRoot(XBlock):
    """
    The LibraryRoot is the root XBlock of a content library. All other blocks in
    the library are its children. It contains metadata such as the library's
    display_name.
    """
    display_name = String(
        help=_("Enter the name of the library as it should appear in Studio."),
        default="Library",
        display_name=_("Library Display Name"),
        scope=Scope.settings)
    advanced_modules = List(
        display_name=_("Advanced Module List"),
        help=_(
            "Enter the names of the advanced components to use in your library."
        ),
        scope=Scope.settings,
        xml_node=True,
    )
    show_children_previews = Boolean(
        display_name="Hide children preview",
        help="Choose if preview of library contents is shown",
        scope=Scope.user_state,
        default=True)
    has_children = True
    has_author_view = True

    def __unicode__(self):
        return u"Library: {}".format(self.display_name)

    def __str__(self):
        return unicode(self).encode('utf-8')

    def author_view(self, context):
        """
        Renders the Studio preview view.
        """
        fragment = Fragment()
        self.render_children(context,
                             fragment,
                             can_reorder=False,
                             can_add=True)
        return fragment

    def render_children(self,
                        context,
                        fragment,
                        can_reorder=False,
                        can_add=False):  # pylint: disable=unused-argument
        """
        Renders the children of the module with HTML appropriate for Studio. Reordering is not supported.
        """
        contents = []

        paging = context.get('paging', None)

        children_count = len(self.children)  # pylint: disable=no-member
        item_start, item_end = 0, children_count

        # TODO sort children
        if paging:
            page_number = paging.get('page_number', 0)
            raw_page_size = paging.get('page_size', None)
            page_size = raw_page_size if raw_page_size is not None else children_count
            item_start, item_end = page_size * page_number, page_size * (
                page_number + 1)

        children_to_show = self.children[item_start:item_end]  # pylint: disable=no-member

        force_render = context.get('force_render', None)

        for child_key in children_to_show:  # pylint: disable=E1101
            # Children must have a separate context from the library itself. Make a copy.
            child_context = context.copy()
            child_context['show_preview'] = self.show_children_previews
            child_context['can_edit_visibility'] = False
            child = self.runtime.get_block(child_key)
            child_view_name = StudioEditableModule.get_preview_view_name(child)

            if unicode(child.location) == force_render:
                child_context['show_preview'] = True

            if child_context['show_preview']:
                rendered_child = self.runtime.render_child(
                    child, child_view_name, child_context)
            else:
                rendered_child = self.runtime.render_child_placeholder(
                    child, child_view_name, child_context)
            fragment.add_frag_resources(rendered_child)

            contents.append({
                'id': unicode(child.location),
                'content': rendered_child.content,
            })

        fragment.add_content(
            self.runtime.render_template(
                "studio_render_paged_children_view.html", {
                    'items': contents,
                    'xblock_context': context,
                    'can_add': can_add,
                    'first_displayed': item_start,
                    'total_children': children_count,
                    'displayed_children': len(children_to_show),
                    'previews': self.show_children_previews
                }))

    @property
    def display_org_with_default(self):
        """
        Org display names are not implemented. This just provides API compatibility with CourseDescriptor.
        Always returns the raw 'org' field from the key.
        """
        return self.scope_ids.usage_id.course_key.org

    @property
    def display_number_with_default(self):
        """
        Display numbers are not implemented. This just provides API compatibility with CourseDescriptor.
        Always returns the raw 'library' field from the key.
        """
        return self.scope_ids.usage_id.course_key.library

    @XBlock.json_handler
    def trigger_previews(self, request_body, suffix):  # pylint: disable=unused-argument
        """ Enable or disable previews in studio for library children. """
        self.show_children_previews = request_body.get(
            'showChildrenPreviews', self.show_children_previews)
        return {'showChildrenPreviews': self.show_children_previews}
class OpenAssessmentBlock(MessageMixin, SubmissionMixin, PeerAssessmentMixin,
                          SelfAssessmentMixin, StaffAssessmentMixin,
                          StudioMixin, GradeMixin, LeaderboardMixin,
                          StaffAreaMixin, WorkflowMixin, StudentTrainingMixin,
                          LmsCompatibilityMixin, CourseItemsListingMixin,
                          WaffleMixin, 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_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.",
    )

    @property
    def course_id(self):
        return text_type(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 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 get_student_item_dict_from_username(self, username):
        """
        Get the item dict for a given username in the parent course of block.
        """
        anonymous_user_id = self.get_anonymous_user_id(username,
                                                       self.course_id)
        return self.get_student_item_dict(anonymous_user_id=anonymous_user_id)

    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 = text_type(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 = text_type(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))

    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"

        if settings.DEBUG:
            for css in additional_css:
                fragment.add_css_url(self.runtime.local_resource_url(
                    self, css))
            fragment.add_css_url(self.runtime.local_resource_url(
                self, css_url))

            for js in additional_js:  # pylint: disable=invalid-name
                self.add_javascript_files(fragment, js)
            self.add_javascript_files(fragment, "static/js/src/oa_shared.js")
            self.add_javascript_files(fragment, "static/js/src/oa_server.js")
            self.add_javascript_files(fragment, "static/js/src/lms")
        else:
            # 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.min.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.teams_enabled and self.team_submissions_enabled()
        }
        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
                else:
                    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:
            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_latex = config['allow_latex']
        block.leaderboard_show = config['leaderboard_show']
        block.group_access = config['group_access']

        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 [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)
        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)
        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

    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"):
            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 text_type(self.scope_ids.usage_id)
Beispiel #9
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="",
    )
    bumper_last_view_date = DateTime(
        display_name=_("Date of the last view of the bumper"),
        scope=Scope.preferences,
    )
    bumper_do_not_show_again = Boolean(
        display_name=_("Do not show bumper again"),
        scope=Scope.preferences,
        default=False,
    )
Beispiel #10
0
class CourseFields(object):
    lti_passports = List(
        display_name=_("LTI Passports"),
        help=
        _("Enter the passports for course LTI tools in the following format: \"id\":\"client_key:client_secret\"."
          ),
        scope=Scope.settings)
    textbooks = TextbookList(
        help="List of pairs of (title, url) for textbooks used in this course",
        default=[],
        scope=Scope.content)

    wiki_slug = String(help="Slug that points to the wiki for this course",
                       scope=Scope.content)
    enrollment_start = Date(
        help="Date that enrollment for this class is opened",
        scope=Scope.settings)
    enrollment_end = Date(help="Date that enrollment for this class is closed",
                          scope=Scope.settings)
    start = Date(help="Start time when this module is visible",
                 default=DEFAULT_START_DATE,
                 scope=Scope.settings)
    end = Date(help="Date that this class ends", scope=Scope.settings)
    advertised_start = String(
        display_name=_("Course Advertised Start Date"),
        help=
        _("Enter the date you want to advertise as the course start date, if this date is different from the set start date. To advertise the set start date, enter null."
          ),
        scope=Scope.settings)
    grading_policy = Dict(help="Grading policy definition for this class",
                          default={
                              "GRADER": [{
                                  "type": "Homework",
                                  "min_count": 12,
                                  "drop_count": 2,
                                  "short_label": "HW",
                                  "weight": 0.15
                              }, {
                                  "type": "Lab",
                                  "min_count": 12,
                                  "drop_count": 2,
                                  "weight": 0.15
                              }, {
                                  "type": "Midterm Exam",
                                  "short_label": "Midterm",
                                  "min_count": 1,
                                  "drop_count": 0,
                                  "weight": 0.3
                              }, {
                                  "type": "Final Exam",
                                  "short_label": "Final",
                                  "min_count": 1,
                                  "drop_count": 0,
                                  "weight": 0.4
                              }],
                              "GRADE_CUTOFFS": {
                                  "Pass": 0.5
                              }
                          },
                          scope=Scope.content)
    show_calculator = Boolean(
        display_name=_("Show Calculator"),
        help=
        _("Enter true or false. When true, students can see the calculator in the course."
          ),
        default=False,
        scope=Scope.settings)
    display_name = String(help=_(
        "Enter the name of the course as it should appear in the edX.org course list."
    ),
                          default="Empty",
                          display_name=_("Course Display Name"),
                          scope=Scope.settings)

    #nicky added here
    course_kinds = String(help=_('Input the kinds of the course'),
                          default="",
                          display_name=_("The Kinds of Course"),
                          scope=Scope.settings)

    course_level = String(help=_(
        'The level of the course.Primary is the lowest grade,intermediate is the middle grade,senior is the highest grade.'
    ),
                          default="",
                          display_name=_("The Level of Course"),
                          scope=Scope.settings)

    course_introduce = Boolean(help=_(
        'Enter true or false. when True:the course has been introduced course.'
    ),
                               default=False,
                               display_name=_('Course Introduced'),
                               scope=Scope.settings)

    course_edit_method = String(
        display_name=_("Course Editor"),
        help=
        _("Enter the method by which this course is edited (\"XML\" or \"Studio\")."
          ),
        default="Studio",
        scope=Scope.settings,
        deprecated=
        True  # Deprecated because someone would not edit this value within Studio.
    )
    show_chat = Boolean(
        display_name=_("Show Chat Widget"),
        help=
        _("Enter true or false. When true, students can see the chat widget in the course."
          ),
        default=False,
        scope=Scope.settings)
    tabs = CourseTabList(help="List of tabs to enable in this course",
                         scope=Scope.settings,
                         default=[])
    end_of_course_survey_url = String(
        display_name=_("Course Survey URL"),
        help=
        _("Enter the URL for the end-of-course survey. If your course does not have a survey, enter null."
          ),
        scope=Scope.settings)
    discussion_blackouts = List(
        display_name=_("Discussion Blackout Dates"),
        help=
        _("Enter pairs of dates between which students cannot post to discussion forums, formatted as \"YYYY-MM-DD-YYYY-MM-DD\". To specify times as well as dates, format the pairs as \"YYYY-MM-DDTHH:MM-YYYY-MM-DDTHH:MM\" (be sure to include the \"T\" between the date and time)."
          ),
        scope=Scope.settings)
    discussion_topics = Dict(
        display_name=_("Discussion Topic Mapping"),
        help=
        _("Enter discussion categories in the following format: \"CategoryName\": {\"id\": \"i4x-InstitutionName-CourseNumber-course-CourseRun\"}. For example, one discussion category may be \"Lydian Mode\": {\"id\": \"i4x-UniversityX-MUS101-course-2014_T1\"}."
          ),
        scope=Scope.settings)
    discussion_sort_alpha = Boolean(
        display_name=_("Discussion Sorting Alphabetical"),
        scope=Scope.settings,
        default=False,
        help=
        _("Enter true or false. If true, discussion categories and subcategories are sorted alphabetically. If false, they are sorted chronologically."
          ))
    announcement = Date(display_name=_("Course Announcement Date"),
                        help=_("Enter the date to announce your course."),
                        scope=Scope.settings)
    cohort_config = Dict(display_name=_("Cohort Configuration"),
                         help=_("Cohorts are not currently supported by edX."),
                         scope=Scope.settings)
    is_new = Boolean(
        display_name=_("Course Is New"),
        help=
        _("Enter true or false. If true, the course appears in the list of new courses on edx.org, and a New! badge temporarily appears next to the course image."
          ),
        scope=Scope.settings)
    no_grade = Boolean(
        display_name=_("Course Not Graded"),
        help=_("Enter true or false. If true, the course will not be graded."),
        default=False,
        scope=Scope.settings)
    disable_progress_graph = Boolean(
        display_name=_("Disable Progress Graph"),
        help=
        _("Enter true or false. If true, students cannot view the progress graph."
          ),
        default=False,
        scope=Scope.settings)
    pdf_textbooks = List(
        display_name=_("PDF Textbooks"),
        help=_("List of dictionaries containing pdf_textbook configuration"),
        scope=Scope.settings)
    html_textbooks = List(
        display_name=_("HTML Textbooks"),
        help=
        _("For HTML textbooks that appear as separate tabs in the courseware, enter the name of the tab (usually the name of the book) as well as the URLs and titles of all the chapters in the book."
          ),
        scope=Scope.settings)
    remote_gradebook = Dict(
        display_name=_("Remote Gradebook"),
        help=
        _("Enter the remote gradebook mapping. Only use this setting when REMOTE_GRADEBOOK_URL has been specified."
          ),
        scope=Scope.settings)
    allow_anonymous = Boolean(
        display_name=_("Allow Anonymous Discussion Posts"),
        help=
        _("Enter true or false. If true, students can create discussion posts that are anonymous to all users."
          ),
        scope=Scope.settings,
        default=True)
    allow_anonymous_to_peers = Boolean(
        display_name=_("Allow Anonymous Discussion Posts to Peers"),
        help=
        _("Enter true or false. If true, students can create discussion posts that are anonymous to other students. This setting does not make posts anonymous to course staff."
          ),
        scope=Scope.settings,
        default=False)
    advanced_modules = List(
        display_name=_("Advanced Module List"),
        help=_(
            "Enter the names of the advanced components to use in your course."
        ),
        scope=Scope.settings)

    has_children = True
    checklists = List(
        scope=Scope.settings,
        default=[{
            "short_description":
            _("Getting Started With Studio"),
            "items": [{
                "short_description":
                _("Add Course Team Members"),
                "long_description":
                _("Grant your collaborators permission to edit your course so you can work together."
                  ),
                "is_checked":
                False,
                "action_url":
                "ManageUsers",
                "action_text":
                _("Edit Course Team"),
                "action_external":
                False
            }, {
                "short_description":
                _("Set Important Dates for Your Course"),
                "long_description":
                _("Establish your course's student enrollment and launch dates on the Schedule and Details page."
                  ),
                "is_checked":
                False,
                "action_url":
                "SettingsDetails",
                "action_text":
                _("Edit Course Details &amp; Schedule"),
                "action_external":
                False
            }, {
                "short_description":
                _("Draft Your Course's Grading Policy"),
                "long_description":
                _("Set up your assignment types and grading policy even if you haven't created all your assignments."
                  ),
                "is_checked":
                False,
                "action_url":
                "SettingsGrading",
                "action_text":
                _("Edit Grading Settings"),
                "action_external":
                False
            }, {
                "short_description":
                _("Explore the Other Studio Checklists"),
                "long_description":
                _("Discover other available course authoring tools, and find help when you need it."
                  ),
                "is_checked":
                False,
                "action_url":
                "",
                "action_text":
                "",
                "action_external":
                False
            }]
        }, {
            "short_description":
            _("Draft a Rough Course Outline"),
            "items": [{
                "short_description":
                _("Create Your First Section and Subsection"),
                "long_description":
                _("Use your course outline to build your first Section and Subsection."
                  ),
                "is_checked":
                False,
                "action_url":
                "CourseOutline",
                "action_text":
                _("Edit Course Outline"),
                "action_external":
                False
            }, {
                "short_description":
                _("Set Section Release Dates"),
                "long_description":
                _("Specify the release dates for each Section in your course. Sections become visible to students on their release dates."
                  ),
                "is_checked":
                False,
                "action_url":
                "CourseOutline",
                "action_text":
                _("Edit Course Outline"),
                "action_external":
                False
            }, {
                "short_description":
                _("Designate a Subsection as Graded"),
                "long_description":
                _("Set a Subsection to be graded as a specific assignment type. Assignments within graded Subsections count toward a student's final grade."
                  ),
                "is_checked":
                False,
                "action_url":
                "CourseOutline",
                "action_text":
                _("Edit Course Outline"),
                "action_external":
                False
            }, {
                "short_description":
                _("Reordering Course Content"),
                "long_description":
                _("Use drag and drop to reorder the content in your course."),
                "is_checked":
                False,
                "action_url":
                "CourseOutline",
                "action_text":
                _("Edit Course Outline"),
                "action_external":
                False
            }, {
                "short_description":
                _("Renaming Sections"),
                "long_description":
                _("Rename Sections by clicking the Section name from the Course Outline."
                  ),
                "is_checked":
                False,
                "action_url":
                "CourseOutline",
                "action_text":
                _("Edit Course Outline"),
                "action_external":
                False
            }, {
                "short_description":
                _("Deleting Course Content"),
                "long_description":
                _("Delete Sections, Subsections, or Units you don't need anymore. Be careful, as there is no Undo function."
                  ),
                "is_checked":
                False,
                "action_url":
                "CourseOutline",
                "action_text":
                _("Edit Course Outline"),
                "action_external":
                False
            }, {
                "short_description":
                _("Add an Instructor-Only Section to Your Outline"),
                "long_description":
                _("Some course authors find using a section for unsorted, in-progress work useful. To do this, create a section and set the release date to the distant future."
                  ),
                "is_checked":
                False,
                "action_url":
                "CourseOutline",
                "action_text":
                _("Edit Course Outline"),
                "action_external":
                False
            }]
        }, {
            "short_description":
            _("Explore edX's Support Tools"),
            "items": [{
                "short_description":
                _("Explore the Studio Help Forum"),
                "long_description":
                _("Access the Studio Help forum from the menu that appears when you click your user name in the top right corner of Studio."
                  ),
                "is_checked":
                False,
                "action_url":
                "http://help.edge.edx.org/",
                "action_text":
                _("Visit Studio Help"),
                "action_external":
                True
            }, {
                "short_description":
                _("Enroll in edX 101"),
                "long_description":
                _("Register for edX 101, edX's primer for course creation."),
                "is_checked":
                False,
                "action_url":
                "https://edge.edx.org/courses/edX/edX101/How_to_Create_an_edX_Course/about",
                "action_text":
                _("Register for edX 101"),
                "action_external":
                True
            }, {
                "short_description":
                _("Download the Studio Documentation"),
                "long_description":
                _("Download the searchable Studio reference documentation in PDF form."
                  ),
                "is_checked":
                False,
                "action_url":
                "http://files.edx.org/Getting_Started_with_Studio.pdf",
                "action_text":
                _("Download Documentation"),
                "action_external":
                True
            }]
        }, {
            "short_description":
            _("Draft Your Course About Page"),
            "items": [{
                "short_description":
                _("Draft a Course Description"),
                "long_description":
                _("Courses on edX have an About page that includes a course video, description, and more. Draft the text students will read before deciding to enroll in your course."
                  ),
                "is_checked":
                False,
                "action_url":
                "SettingsDetails",
                "action_text":
                _("Edit Course Schedule &amp; Details"),
                "action_external":
                False
            }, {
                "short_description":
                _("Add Staff Bios"),
                "long_description":
                _("Showing prospective students who their instructor will be is helpful. Include staff bios on the course About page."
                  ),
                "is_checked":
                False,
                "action_url":
                "SettingsDetails",
                "action_text":
                _("Edit Course Schedule &amp; Details"),
                "action_external":
                False
            }, {
                "short_description":
                _("Add Course FAQs"),
                "long_description":
                _("Include a short list of frequently asked questions about your course."
                  ),
                "is_checked":
                False,
                "action_url":
                "SettingsDetails",
                "action_text":
                _("Edit Course Schedule &amp; Details"),
                "action_external":
                False
            }, {
                "short_description":
                _("Add Course Prerequisites"),
                "long_description":
                _("Let students know what knowledge and/or skills they should have before they enroll in your course."
                  ),
                "is_checked":
                False,
                "action_url":
                "SettingsDetails",
                "action_text":
                _("Edit Course Schedule &amp; Details"),
                "action_external":
                False
            }]
        }])
    info_sidebar_name = String(
        display_name=_("Course Info Sidebar Name"),
        help=
        _("Enter the heading that you want students to see above your course handouts on the Course Info page. Your course handouts appear in the right panel of the page."
          ),
        scope=Scope.settings,
        default='Course Handouts')
    show_timezone = Boolean(
        help=
        "True if timezones should be shown on dates in the courseware. Deprecated in favor of due_date_display_format.",
        scope=Scope.settings,
        default=True)
    due_date_display_format = String(
        display_name=_("Due Date Display Format"),
        help=
        _("Enter the format due dates are displayed in. Due dates must be in MM-DD-YYYY, DD-MM-YYYY, YYYY-MM-DD, or YYYY-DD-MM format."
          ),
        scope=Scope.settings,
        default=None)
    enrollment_domain = String(
        display_name=_("External Login Domain"),
        help=_(
            "Enter the external login method students can use for the course."
        ),
        scope=Scope.settings)
    certificates_show_before_end = Boolean(
        display_name=_("Certificates Downloadable Before End"),
        help=
        _("Enter true or false. If true, students can download certificates before the course ends, if they've met certificate requirements."
          ),
        scope=Scope.settings,
        default=False,
        deprecated=True)

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

    course_video = String(
        display_name=_("Course Introduce Video"),
        help=
        _("Edit the name of the course video file. You must upload this file on the Files & Uploads page."
          ),
        scope=Scope.settings,
        default="course_video_course.mp4")

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

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

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

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

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

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

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

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

    invitation_only = Boolean(
        display_name=_("Invitation Only"),
        help=
        "Whether to restrict enrollment to invitation by the course staff.",
        default=False,
        scope=Scope.settings)
Beispiel #11
0
class DmCloud(XBlock):
    """
    XBlock providing a video player for videos hosted on DMCloud
    This Xblock show video in iframe using DMCloud Api and video_id
    """

    URL_TIMEOUT = 3600 * 24 * 7

    # To make sure that js files are called in proper order we use numerical
    # index. We do that to avoid issues that occurs in tests.

    #display name already defined by xblock - we just redefined it to update translation
    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=_("New video"),
                          scope=Scope.settings)

    id_video = String(
        scope=Scope.settings,
        help=_('Fill this with the ID of the video found on DM Cloud'),
        default="",
        display_name=_('Video ID'))

    allow_download_video = Boolean(
        help=_("Allow students to download this video."),
        display_name=_("Video Download Allowed"),
        scope=Scope.settings,
        default=False)

    saved_video_position = Integer(help="Current position in the video.",
                                   scope=Scope.user_state,
                                   default=0)

    player = String(display_name=_("Player"),
                    help=_("Player used to display the video"),
                    scope=Scope.settings,
                    values=[
                        {
                            'name': "HTML5",
                            'val': "HTML5"
                        },
                        {
                            'name': "Dailymotion",
                            'val': "Dailymotion"
                        },
                    ],
                    default="HTML5")

    def __init__(self, *args, **kwargs):
        super(DmCloud, self).__init__(*args, **kwargs)
        self._cloudkey = None
        self._univ = None

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

    @property
    def univ(self):
        """
        Get the university from the organisation code filled during the course creation.
        """
        if self._univ is None:
            try:
                self._univ = University.objects.get(code=self.location.org)
            except University.DoesNotExist:
                log.error("university not found: %s", self.location.org)
                raise
        return self._univ

    @property
    def cloudkey(self):
        """
        Initialize cloudKey to get video url with video_id.
        """
        if self._cloudkey is None:
            self._cloudkey = CloudKey(self.univ.dm_user_id,
                                      self.univ.dm_api_key)
        return self._cloudkey

    def resource_string(self, path):
        """Gets the content of a resource"""
        data = pkg_resources.resource_string(__name__, path)
        return data.decode("utf8")

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

    def student_view(self, context=None):
        """
        Player view, displayed to the student
        """
        ###
        # VIDEO MODULE
        #
        #### => work and load dailymotion cloud player
        stream_url = ""
        stream_url_hd = ""
        download_url_ld = ""
        download_url_std = ""
        download_url_hd = ""
        thumbnail_url = ""
        subs_url = {}
        auth_key = ""
        auth_key_secure = ""
        msg = {}

        if self.id_video != "":
            try:
                assets = self.cloudkey.media.get_assets(id=self.id_video)
                if self.player == "Dailymotion":
                    auth_key = self.get_dailymotion_auth_key(False)
                    auth_key_secure = self.get_dailymotion_auth_key(True)
                else:
                    #assets['jpeg_thumbnail_source']['stream_url']
                    #mp4_h264_aac
                    #mp4_h264_aac_ld
                    #mp4_h264_aac_hq -> 480
                    #mp4_h264_aac_hd -> 720
                    #jpeg_thumbnail_medium
                    thumbnail_url = self.cloudkey.media.get_stream_url(
                        id=self.id_video, asset_name='jpeg_thumbnail_source')
                    stream_url = self.get_stream_url()
                    if assets.get('mp4_h264_aac_hd'):
                        stream_url_hd = self.get_stream_url('mp4_h264_aac_hd',
                                                            download=False)
                    elif assets.get('mp4_h264_aac_hq'):
                        stream_url_hd = self.get_stream_url('mp4_h264_aac_hq',
                                                            download=False)
                    subs_url = self.get_subs_url()
                if self.allow_download_video:
                    download_url_ld = self.get_stream_url('mp4_h264_aac_ld',
                                                          download=True)
                    download_url_std = self.get_stream_url(download=True)
                    if assets.get('mp4_h264_aac_hd'):
                        download_url_hd = self.get_stream_url(
                            'mp4_h264_aac_hd', download=True)
                    elif assets.get('mp4_h264_aac_hq'):
                        download_url_hd = self.get_stream_url(
                            'mp4_h264_aac_hq', download=True)
            except InvalidParameter:
                msg = {
                    "level": "warning",
                    "message": _("Your video ID is invalid")
                }
            except AuthenticationError:
                msg = {
                    "level": "warning",
                    "message": _("Your university ID is invalid")
                }
            except Exception as e:
                # we use Exception to catch everything because if one fails, all xblock on page fail
                # and become uneditable
                msg = {
                    "level": "error",
                    "message": u'Unexpected error : %r' % e
                }
                log.error(msg['message'])
        else:
            msg = {
                "level": "warning",
                "message": _("You must provide a video ID")
            }
        #create url for videojs to add it directly in the template
        dmjsurl = self.runtime.local_resource_url(
            self, "public/js/src/dmplayer-sdk.js")

        frag = Fragment()
        frag.add_content(
            self.render_template(
                "templates/html/dmcloud.html",
                {
                    'self':
                    self,
                    'msg':
                    msg,
                    'id':
                    self.location.html_id(),
                    #dmplayer
                    'auth_key':
                    auth_key,
                    'auth_key_secure':
                    auth_key_secure,
                    'video_id':
                    self.id_video,
                    'user_id':
                    self.univ.dm_user_id,
                    'dmjsurl':
                    dmjsurl,
                    #end dmplayer
                    'download_url_ld':
                    download_url_ld,
                    'download_url_std':
                    download_url_std,
                    'download_url_hd':
                    download_url_hd,
                    'stream_url':
                    stream_url,
                    'stream_url_hd':
                    stream_url_hd,
                    'subs_url':
                    subs_url,
                    'thumbnail_url':
                    thumbnail_url,
                    "transcript_url":
                    self.runtime.handler_url(self, 'transcript',
                                             'translation').rstrip('/?'),
                }))

        #load locally to work with more than one instance on page
        frag.add_css_url(
            self.runtime.local_resource_url(self, "public/css/dmcloud.css"))

        if self.player == "Dailymotion":
            frag.add_javascript(
                self.resource_string("public/js/src/dmcloud-dm.js"))
            frag.initialize_js('DmCloudPlayer')
        else:
            frag.add_css_url(
                self.runtime.local_resource_url(
                    self, "public/video-js-4.10.2/video-js.min.css"))
            frag.add_javascript_url(
                self.runtime.local_resource_url(
                    self, "public/video-js-4.10.2/video.dev.js"))
            frag.add_javascript(
                self.resource_string("public/js/src/dmcloud-video.js"))
            frag.initialize_js('DmCloudVideo')

        return frag

    def get_dailymotion_auth_key(self, secure):
        embed_url = self.get_embed_url(secure)
        auth_index = embed_url.find("auth=")
        auth_keys = embed_url[auth_index + 5:len(embed_url)]
        return auth_keys

    def get_embed_url(self, secure):
        return self.cloudkey.media._get_url(base_path="/player/embed",
                                            id=self.id_video,
                                            expires=time.time() +
                                            self.URL_TIMEOUT,
                                            secure=secure)

    def get_stream_url(self, asset_name='mp4_h264_aac', download=False):
        return self.cloudkey.media.get_stream_url(id=self.id_video,
                                                  asset_name=asset_name,
                                                  download=download,
                                                  expires=time.time() +
                                                  self.URL_TIMEOUT)

    def get_subs_url(self):
        try:
            return self.cloudkey.media.get_subs_urls(id=self.id_video,
                                                     type="srt")
        except SerializerError:
            return ""

    def studio_view(self, context=None):
        """
        Editing view in Studio
        """
        frag = Fragment()
        frag.add_content(
            self.render_template("/templates/html/dmcloud-studio.html",
                                 {'self': self}))
        frag.add_css(self.resource_string("public/css/dmcloud.css"))
        frag.add_javascript(self.resource_string("public/js/src/dmcloud.js"))
        frag.initialize_js('DmCloud')
        return frag

    @XBlock.json_handler
    def studio_submit(self, submissions, suffix=''):
        if submissions['id_video'] == "":
            response = {
                'result': 'error',
                'message': 'You should give a video ID'
            }
        else:
            log.info(u'Received submissions: %s', submissions)
            self.display_name = submissions['display_name']
            self.id_video = submissions['id_video'].strip()
            self.allow_download_video = submissions['allow_download_video']
            self.player = submissions['dmcloud_player']
            response = {
                'result': 'success',
            }
        return response

    @XBlock.json_handler
    def save_user_state(self, submissions, suffix=''):
        self.saved_video_position = submissions['saved_video_position']
        response = {
            'result': 'success',
        }
        return response

    @XBlock.handler
    def transcript(self, request, dispatch):
        if request.GET.get('url'):
            r = requests.get(request.GET.get('url'))
            response = Response("%s" % r.content)
            response.content_type = 'text/plain'
        else:
            response = Response(status=404)
        return response

    @staticmethod
    def workbench_scenarios():
        """A canned scenario for display in the workbench."""
        return [
            ("DmCloud", """<vertical_demo>
                <dmcloud/>
                </vertical_demo>
             """),
        ]
Beispiel #12
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.
            try:
                from static_replace import replace_static_urls  # pylint: disable=import-error
                url = replace_static_urls(
                    '"{}"'.format(url), None,
                    course_id=self.runtime.course_id)[1:-1]
            except ImportError:
                pass
        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 #13
0
class ScormXBlock(XBlock):
    """
    When a user uploads a Scorm package, the zip file is stored in:
        media/{org}/{course}/{block_type}/{block_id}/{sha1}{ext}
    This zip file is then extracted to the media/{scorm_location}/{block_id}.
    The scorm location is defined by the LOCATION xblock setting. If undefined, this is
    "scorm". This setting can be set e.g:
        XBLOCK_SETTINGS["ScormXBlock"] = {
            "LOCATION": "alternatevalue",
        }
    Note that neither the folder the folder nor the package file are deleted when the
    xblock is removed.
    """
    has_custom_completion = True
    completion_mode = XBlockCompletionMode.COMPLETABLE

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

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

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

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

    has_author_view = True

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

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

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

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

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

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

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

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

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

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

        # Then, extract zip file
        if scorm_storage_instance.exists(self.extract_folder_base_path):
            logger.info(
                'Removing previously unzipped "%s"', self.extract_folder_base_path
            )
            recursive_delete(self.extract_folder_base_path)

        def unzip_member(_scorm_storage_instance,uncompressed_file,extract_folder_path, filename):
            logger.info('Started uploading file {fname}'.format(fname=filename))
            _scorm_storage_instance.save(
                os.path.join(extract_folder_path, filename),
                uncompressed_file,
            )
            logger.info('End uploadubg file {fname}'.format(fname=filename))

        with zipfile.ZipFile(package_file, "r") as scorm_zipfile:
            futures = []
            with concurrent.futures.ThreadPoolExecutor() as executor:
                logger.info("started concurrent.futures.ThreadPoolExecutor")
                for zipinfo in scorm_zipfile.infolist():
                    fp = tempfile.TemporaryFile()
                    fp.write(scorm_zipfile.open(zipinfo.filename).read())
                    logger.info("started uploadig file {fname}".format(fname=zipinfo.filename))
                    # Do not unzip folders, only files. In Python 3.6 we will have access to
                    # the is_dir() method to verify whether a ZipInfo object points to a
                    # directory.
                    # https://docs.python.org/3.6/library/zipfile.html#zipfile.ZipInfo.is_dir
                    if not zipinfo.filename.endswith("/"):
                        futures.append(
                            executor.submit(
                                unzip_member,
                                scorm_storage_instance,
                                fp,
                                self.extract_folder_path,
                                zipinfo.filename,
                            )
                        )
                logger.info("end concurrent.futures.ThreadPoolExecutor")

        try:
            self.update_package_fields()
        except ScormError as e:
            response["errors"].append(e.args[0])

        return self.json_response(response)

    @property
    def index_page_url(self):
        if not self.package_meta or not self.index_page_path:
            return ""
        folder = self.extract_folder_path
        if scorm_storage_instance.exists(
            os.path.join(self.extract_folder_base_path, self.index_page_path)
        ):
            # For backward-compatibility, we must handle the case when the xblock data
            # is stored in the base folder.
            folder = self.extract_folder_base_path
            logger.warning("Serving SCORM content from old-style path: %s", folder)
        url = scorm_storage_instance.url(os.path.join(folder, self.index_page_path))
        if SCORM_MEDIA_BASE_URL:
            splitted_url = list(urlparse(url))
            url = "{base_url}{path}".format(base_url=SCORM_MEDIA_BASE_URL, path=splitted_url[2])

        return url

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

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

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

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

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

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

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

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


    def publish_completion(self):
        """
        Mark scorm xbloxk as completed if user has completed the scorm course unit.

        it will work along with the edX completion tool: https://github.com/edx/completion
        """
        if not completion_waffle.waffle().is_enabled(completion_waffle.ENABLE_COMPLETION_TRACKING):
            return

        if XBlockCompletionMode.get_mode(self) != XBlockCompletionMode.COMPLETABLE:
            return

        completion_value = 0.0
        if not self.has_score:
            # component does not have any score
            if self.get_completion_status() == "completed":
                completion_value = 1.0
        else:
            if self.get_completion_status() in ["passed", "failed"]:
                completion_value = 1.0

        data = {
            "completion": completion_value
        }
        self.runtime.publish(self, "completion", data)


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

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

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

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

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

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

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

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

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

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

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

    @staticmethod
    def workbench_scenarios():
        """A canned scenario for display in the workbench."""
        return [
            (
                "ScormXBlock",
                """<vertical_demo>
                <openedxscorm/>
                </vertical_demo>
             """,
            ),
        ]
class UcDockerXBlock(XBlock):

    logger = Util.uc_logger()

    is_new = Boolean(default=True, scope=Scope.user_state, help="is new")
    private_key = String(default="",
                         scope=Scope.user_state,
                         help="SHH Private Key")
    public_key = String(default="",
                        scope=Scope.user_state,
                        help="SHH Public Key")
    git_password = String(default="",
                          scope=Scope.user_state,
                          help="Git password")
    git_id = Integer(default="", scope=Scope.user_state, help="Git id")
    git_user_token = String(default="",
                            scope=Scope.user_state,
                            help="Git private token")
    dockers = List(default=[], scope=Scope.user_state, help="dockers")

    labs = List(default=[], scope=Scope.content, help="labs")

    # config
    CONFIG = Config.CONFIG
    git_host = CONFIG["GIT"]["HOST"]
    git_port = CONFIG["GIT"]["PORT"]
    git_admin_token = CONFIG["GIT"]["ADMIN_TOKEN"]
    docker_host = CONFIG["DOCKER"]["HOST"]
    docker_url = CONFIG["DOCKER"]["REMOTE_API"]["URL"]
    docker_namespace = CONFIG["DOCKER"]["NAMESPACE"]
    docker_mem = CONFIG["DOCKER"]["MEM_LIMIT"]
    ca = CONFIG["DOCKER"]["REMOTE_API"]["CA"]
    cert = CONFIG["DOCKER"]["REMOTE_API"]["CERT"]
    key = CONFIG["DOCKER"]["REMOTE_API"]["KEY"]
    version = CONFIG["DOCKER"]["REMOTE_API"]["VERSION"]
    git_teacher_token = CONFIG["GIT"]["TEACHER"]["TOKEN"]

    principal_name = CONFIG["LDAP"]["PRINCIPAL_NAME"]
    ldap_password = CONFIG["LDAP"]["PASSWORD"]
    ldap_url = CONFIG["LDAP"]["LDAP_URL"]
    base_dn = CONFIG["LDAP"]["BASE_DN"]

    docker_helper = DockerRawHelper(docker_host, docker_url, ca, cert, key)

    def student_view(self, context=None):

        # runtime error
        if not hasattr(self.runtime, "anonymous_student_id"):
            return self.message_view(
                "Error in uc_docker (get anonymous student id)",
                "Cannot get anonymous_student_id in runtime", context)

        # preview in studio
        if self.runtime.anonymous_student_id == "student":

            result, message = GitLabUtil.get_user_projects(
                self.git_host, self.git_port, self.git_teacher_token)
            if not result:
                return self.message_view(
                    "Error in uc_docker (get git projects)",
                    "Cannot get user's projects in git", context)

            context_dict = {"labs": self.labs, "message": ""}
            fragment = Fragment()
            fragment.add_content(
                Util.render_template('static/html/uc_lab.html', context_dict))
            fragment.add_css(Util.load_resource("static/css/uc_docker.css"))
            fragment.add_javascript(
                Util.load_resource("static/js/src/uc_lab.js"))
            fragment.initialize_js("UcDockerXBlock")
            return fragment

        # student view in open-edx
        if self.is_new:
            # create git account when first visiting
            student = self.runtime.get_real_user(
                self.runtime.anonymous_student_id)
            email = student.email
            name = student.first_name + " " + student.last_name
            username = student.username
            self.git_password = Util.create_random_password()
            self.save()
            # first_name, last_name are empty
            if name == " ":
                name = username
            self.logger.info("password is " + self.git_password)

            # create ldap account
            l = ldap.initialize(self.ldap_url)
            l.bind(self.principal_name, self.ldap_password)
            dn = "uid=" + username + "," + self.base_dn

            attrs = {}
            attrs['objectclass'] = ['top', 'inetOrgPerson', 'eduPerson']
            attrs['cn'] = str(username)
            attrs['sn'] = str(username)
            attrs['givenName'] = str(username)
            attrs['uid'] = str(username)
            attrs['userPassword'] = str(self.git_password)
            attrs['description'] = 'ldap user for shibboleth'
            attrs['eduPersonPrincipalName'] = str(email)

            # Convert our dict to nice syntax for the add-function using modlist-module
            ldif = modlist.addModlist(attrs)
            l.add_s(dn, ldif)
            l.unbind_s()
            self.logger.info("create ldap account " + username + "," + dn)

            self.logger.info(self.git_host + "," + str(self.git_port) + "," +
                             self.git_admin_token + "," + name + "," +
                             username + "," + email + "," + self.git_password)
            result, message = GitLabUtil.create_account(
                self.git_host, self.git_port, self.git_admin_token, name,
                username, email, self.git_password)
            self.logger.info("create_account result:")
            self.logger.info(result)
            self.logger.info(message)
            if not result:
                return self.message_view(
                    "Error in uc_docker (create git account)", message,
                    context)

            result, message = GitLabUtil.login(self.git_host, self.git_port,
                                               username, self.git_password)
            self.logger.info("login result:")
            self.logger.info(result)
            self.logger.info(message)
            if not result:
                return self.message_view(
                    "Error in uc_docker (login git account)", message, context)

            try:
                message = json.loads(message)
                self.git_id = message["id"]
                self.git_user_token = message["private_token"]
                self.save()

            except Exception, ex:
                return self.message_view(
                    "Error in uc_docker (load json string)", message, context)

            try:
                self.private_key, self.public_key = Util.gen_ssh_keys(email)
                self.logger.info("private_key:" + self.private_key)
                self.save()
                conn = pymongo.Connection('localhost', 27017)
                db = conn.test
                token = db.token
                token.insert({
                    "username": username,
                    "token": message["private_token"],
                    "password": self.git_password,
                    "private_key": self.private_key,
                    "public_key": self.public_key
                })
                conn.disconnect()

            except Exception, ex:
                return self.message_view("Error in uc_docker (gen ssh key)",
                                         ex, context)

            result, message = GitLabUtil.add_ssh_key(self.git_host,
                                                     self.git_port,
                                                     self.git_user_token,
                                                     "uClassroom default",
                                                     self.public_key)
            self.logger.info("add_ssh_key result:")
            self.logger.info(result)
            self.logger.info(message)
            if not result:
                return self.message_view(
                    "Error in uc_docker (add git ssh key)", message, context)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        if data.visual_rules:
            try:
                rules = json.loads(data.visual_rules)
            except ValueError as e:
                add_error(
                    _(u"Visual rules contains an error: {error}").format(
                        error=e))
            else:
                if not isinstance(rules, dict):
                    add_error(
                        _(u"Visual rules should be a JSON dictionary/object: {...}"
                          ))
Beispiel #16
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, if a self assessed problem is "
          "changed 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)
    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)
Beispiel #17
0
class LmsBlockMixin(XBlockMixin):
    """
    Mixin that defines fields common to all blocks used in the LMS
    """
    hide_from_toc = Boolean(
        help=_("Whether to display this module in the table of contents"),
        default=False,
        scope=Scope.settings)
    format = String(
        # Translators: "TOC" stands for "Table of Contents"
        help=_("What format this module is in (used for deciding which "
               "grader to apply, and what to show in the TOC)"),
        scope=Scope.settings,
    )
    chrome = String(
        display_name=_("Course Chrome"),
        # Translators: DO NOT translate the words in quotes here, they are
        # specific words for the acceptable values.
        help=_(
            "Enter the chrome, or navigation tools, to use for the XBlock in the LMS. Valid values are: \n"
            "\"chromeless\" -- to not use tabs or the accordion; \n"
            "\"tabs\" -- to use tabs only; \n"
            "\"accordion\" -- to use the accordion only; or \n"
            "\"tabs,accordion\" -- to use tabs and the accordion."),
        scope=Scope.settings,
        default=None,
    )
    default_tab = String(
        display_name=_("Default Tab"),
        help=
        _("Enter the tab that is selected in the XBlock. If not set, the Course tab is selected."
          ),
        scope=Scope.settings,
        default=None,
    )
    source_file = String(display_name=_("LaTeX Source File Name"),
                         help=_("Enter the source file name for LaTeX."),
                         scope=Scope.settings,
                         deprecated=True)
    visible_to_staff_only = Boolean(
        help=
        _("If true, can be seen only by course staff, regardless of start date."
          ),
        default=False,
        scope=Scope.settings,
    )
    group_access = GroupAccessDict(
        help=
        _("A dictionary that maps which groups can be shown this block. The keys "
          "are group configuration ids and the values are a list of group IDs. "
          "If there is no key for a group configuration or if the set of group IDs "
          "is empty then the block is considered visible to all. Note that this "
          "field is ignored if the block is visible_to_staff_only."),
        default={},
        scope=Scope.settings,
    )

    @lazy
    def merged_group_access(self):
        """
        This computes access to a block's group_access rules in the context of its position
        within the courseware structure, in the form of a lazily-computed attribute.
        Each block's group_access rule is merged recursively with its parent's, guaranteeing
        that any rule in a parent block will be enforced on descendants, even if a descendant
        also defined its own access rules.  The return value is always a dict, with the same
        structure as that of the group_access field.

        When merging access rules results in a case where all groups are denied access in a
        user partition (which effectively denies access to that block for all students),
        the special value False will be returned for that user partition key.
        """
        parent = self.get_parent()
        if not parent:
            return self.group_access or {}

        merged_access = parent.merged_group_access.copy()
        if self.group_access is not None:
            for partition_id, group_ids in self.group_access.items():  # pylint: disable=no-member
                if group_ids:  # skip if the "local" group_access for this partition is None or empty.
                    if partition_id in merged_access:
                        if merged_access[partition_id] is False:
                            # special case - means somewhere up the hierarchy, merged access rules have eliminated
                            # all group_ids from this partition, so there's no possible intersection.
                            continue
                        # otherwise, if the parent defines group access rules for this partition,
                        # intersect with the local ones.
                        merged_access[partition_id] = list(
                            set(merged_access[partition_id]).intersection(
                                group_ids)) or False
                    else:
                        # add the group access rules for this partition to the merged set of rules.
                        merged_access[partition_id] = group_ids
        return merged_access

    # Specified here so we can see what the value set at the course-level is.
    user_partitions = UserPartitionList(help=_(
        "The list of group configurations for partitioning students in content experiments."
    ),
                                        default=[],
                                        scope=Scope.settings)

    def _get_user_partition(self, user_partition_id):
        """
        Returns the user partition with the specified id. Note that this method can return
        an inactive user partition. Raises `NoSuchUserPartitionError` if the lookup fails.
        """
        for user_partition in self.runtime.service(
                self, 'partitions').course_partitions:
            if user_partition.id == user_partition_id:
                return user_partition

        raise NoSuchUserPartitionError(
            u"could not find a UserPartition with ID [{}]".format(
                user_partition_id))

    def _has_nonsensical_access_settings(self):
        """
        Checks if a block's group access settings do not make sense.

        By nonsensical access settings, we mean a component's access
        settings which contradict its parent's access in that they
        restrict access to the component to a group that already
        will not be able to see that content.
        Note:  This contradiction can occur when a component
        restricts access to the same partition but a different group
        than its parent, or when there is a parent access
        restriction but the component attempts to allow access to
        all learners.

        Returns:
            bool: True if the block's access settings contradict its
            parent's access settings.
        """
        parent = self.get_parent()
        if not parent:
            return False

        parent_group_access = parent.group_access
        component_group_access = self.group_access

        for user_partition_id, parent_group_ids in parent_group_access.iteritems(
        ):
            component_group_ids = component_group_access.get(user_partition_id)  # pylint: disable=no-member
            if component_group_ids:
                return parent_group_ids and not set(
                    component_group_ids).issubset(set(parent_group_ids))
            else:
                return not component_group_access
        return False

    def validate(self):
        """
        Validates the state of this xblock instance.
        """
        _ = self.runtime.service(self, "i18n").ugettext
        validation = super(LmsBlockMixin, self).validate()
        has_invalid_user_partitions = False
        has_invalid_groups = False
        block_is_unit = is_unit(self)

        for user_partition_id, group_ids in self.group_access.iteritems():  # pylint: disable=no-member
            try:
                user_partition = self._get_user_partition(user_partition_id)
            except NoSuchUserPartitionError:
                has_invalid_user_partitions = True
            else:
                # Skip the validation check if the partition has been disabled
                if user_partition.active:
                    for group_id in group_ids:
                        try:
                            user_partition.get_group(group_id)
                        except NoSuchUserPartitionGroupError:
                            has_invalid_groups = True

        if has_invalid_user_partitions:
            validation.add(
                ValidationMessage(
                    ValidationMessage.ERROR,
                    (INVALID_USER_PARTITION_VALIDATION_UNIT if block_is_unit
                     else INVALID_USER_PARTITION_VALIDATION_COMPONENT)))

        if has_invalid_groups:
            validation.add(
                ValidationMessage(
                    ValidationMessage.ERROR,
                    (INVALID_USER_PARTITION_GROUP_VALIDATION_UNIT
                     if block_is_unit else
                     INVALID_USER_PARTITION_GROUP_VALIDATION_COMPONENT)))

        if self._has_nonsensical_access_settings():
            validation.add(
                ValidationMessage(ValidationMessage.ERROR,
                                  NONSENSICAL_ACCESS_RESTRICTION))

        return validation

    @XBlock.json_handler
    def publish_completion(self, data, suffix=''):  # pylint: disable=unused-argument
        """
        Publish completion data from the front end.
        """
        completion_service = self.runtime.service(self, 'completion')
        if completion_service is None:
            raise JsonHandlerError(500, u"No completion service found")
        elif not completion_service.completion_tracking_enabled():
            raise JsonHandlerError(
                404,
                u"Completion tracking is not enabled and API calls are unexpected"
            )
        if not completion_service.can_mark_block_complete_on_view(self):
            raise JsonHandlerError(
                400, u"Block not configured for completion on view.")
        self.runtime.publish(self, "completion", data)
        return {'result': 'ok'}
Beispiel #18
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."
    )
    '''
    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
        }
        html = self.render_template('static/html/videojs_view.html', context)

        frag = Fragment(html)
        frag.add_css(self.load_resource("static/css/video-js.min.css"))
        frag.add_css(self.load_resource("static/css/videojs.css"))
        frag.add_javascript(self.load_resource("static/js/video-js.js"))
        frag.add_javascript(self.load_resource("static/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
        }
        html = self.render_template('static/html/videojs_edit.html', context)

        frag = Fragment(html)
        frag.add_javascript(self.load_resource("static/js/videojs_edit.js"))
        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

        return {
            'result': 'success',
        }

    @staticmethod
    def workbench_scenarios():
        return [
            ("videojs demo", "<videojs />")  #the name should be "<videojs />"
        ]
Beispiel #19
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
Beispiel #20
0
class LEARCTATXBLOCKCLASS(XBlock):
    """
    A XBlock providing CTAT tutors.
    """

    ### xBlock tag variables
    width = Integer(help="Width of the StatTutor frame.",
                    default=690,
                    scope=Scope.content)
    height = Integer(help="Height of the StatTutor frame.",
                     default=550,
                     scope=Scope.content)

    ### Grading variables
    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)

    def max_score(self):
        """ The maximum raw score of the problem. """
        return 1  #self.max_problem_steps

    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

    ### Basic interface variables
    src = String(help="The source html file for CTAT interface.",
                 default="public/test1.html",
                 scope=Scope.settings)
    brd = String(help="The behavior graph.",
                 default="public/problem_files/additionTest.brd",
                 scope=Scope.settings)

    ### CTATConfiguration variables
    log_name = String(help="Problem name to log",
                      default="CTATEdXProblem",
                      scope=Scope.settings)
    log_dataset = String(help="Dataset name to log",
                         default="edxdataset",
                         scope=Scope.settings)
    log_level1 = String(help="Level name to log",
                        default="unit1",
                        scope=Scope.settings)
    log_type1 = String(help="Level type to log",
                       default="unit",
                       scope=Scope.settings)
    log_level2 = String(help="Level name to log",
                        default="unit2",
                        scope=Scope.settings)
    log_type2 = String(help="Level type to log",
                       default="unit",
                       scope=Scope.settings)
    log_url = String(help="URL of the logging service",
                     default="http://pslc-qa.andrew.cmu.edu/log/server",
                     scope=Scope.settings)
    logtype = String(help="How should data be logged",
                     default="clienttologserver",
                     scope=Scope.settings)
    log_diskdir = String(
        help="Directory for log files relative to the tutoring service",
        default=".",
        scope=Scope.settings)
    log_port = String(help="Port used by the tutoring service",
                      default="8080",
                      scope=Scope.settings)
    log_remoteurl = String(
        help="Location of the tutoring service (localhost or domain name)",
        default="localhost",
        scope=Scope.settings)

    ctat_connection = String(help="",
                             default="javascript",
                             scope=Scope.settings)

    ### user information
    saveandrestore = String(help="Internal data blob used by the tracer",
                            default="",
                            scope=Scope.user_state)
    skillstring = String(help="Internal data blob used by the tracer",
                         default="",
                         scope=Scope.user_info)

    def logdebug(self, aMessage):
        global dbgopen, tmp_file
        if (dbgopen == False):
            tmp_file = open("/tmp/edx-tmp-log-ctat.txt", "a", 0)
            dbgopen = True
        tmp_file.write(aMessage + "\n")

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

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

    def get_local_resource_url(self, url):
        """ Wrapper for self.runtime.local_resource_url. """
        return self.strip_local(self.runtime.local_resource_url(self, url))

    # -------------------------------------------------------------------
    # TO-DO: change this view to display your data your own way.
    # -------------------------------------------------------------------

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

        Returns a Fragment object containing the HTML to display
        """
        # read in template html
        html = self.resource_string("static/html/ctatxblock.html")
        frag = Fragment(
            html.format(tutor_html=self.get_local_resource_url(self.src)))
        config = self.resource_string("static/js/CTATConfig.js")
        frag.add_javascript(
            config.format(
                self=self,
                tutor_html=self.get_local_resource_url(self.src),
                question_file=self.get_local_resource_url(self.brd),
                student_id=self.runtime.anonymous_student_id if hasattr(
                    self.runtime, 'anonymous_student_id') else 'bogus-sdk-id',
                guid=str(uuid.uuid4())))
        frag.add_javascript(
            self.resource_string("static/js/Initialize_CTATXBlock.js"))
        frag.initialize_js('Initialize_CTATXBlock')
        return frag

    @XBlock.json_handler
    def ctat_grade(self, data, suffix=''):
        #self.logdebug ("ctat_grade ()")
        #print('ctat_grade:',data,suffix)
        self.attempted = True
        self.score = int(data.get('value'))
        self.max_problem_steps = int(data.get('max_value'))
        self.completed = self.score >= self.max_problem_steps
        scaled = float(self.score) / float(self.max_problem_steps)
        # trying with max of 1.
        event_data = {'value': scaled, 'max_value': 1}
        self.runtime.publish(self, 'grade', event_data)
        return {
            'result': 'success',
            'finished': self.completed,
            'score': scaled
        }

    # -------------------------------------------------------------------
    # TO-DO: change this view to display your data your own way.
    # -------------------------------------------------------------------
    def studio_view(self, context=None):
        html = self.resource_string("static/html/ctatstudio.html")
        frag = Fragment(html.format(self=self))
        js = self.resource_string("static/js/ctatstudio.js")
        frag.add_javascript(unicode(js))
        frag.initialize_js('CTATXBlockStudio')
        return frag

    @XBlock.json_handler
    def studio_submit(self, data, suffix=''):
        """
        Called when submitting the form in Studio.
        """
        self.src = data.get('src')
        self.brd = data.get('brd')
        self.width = data.get('width')
        self.height = data.get('height')
        return {'result': 'success'}

    @XBlock.json_handler
    def ctat_save_problem_state(self, data, suffix=''):
        """Called from CTATLMS.saveProblemState."""
        if data.get('state') is not None:
            self.saveandrestore = data.get('state')
            return {'result': 'success'}
        return {'result': 'failure'}

    @XBlock.json_handler
    def ctat_get_problem_state(self, data, suffix=''):
        return {'result': 'success', 'state': self.saveandrestore}

    @XBlock.json_handler
    def ctat_set_variable(self, data, suffix=''):
        self.logdebug("ctat_set_variable ()")

        for key in data:
            #value = base64.b64decode(data[key])
            value = data[key]
            self.logdebug("Setting ({}) to ({})".format(key, value))
            if (key == "href"):
                self.href = value
            elif (key == "ctatmodule"):
                self.ctatmodule = value
            elif (key == "problem"):
                self.problem = value
            elif (key == "dataset"):
                self.dataset = value
            elif (key == "level1"):
                self.level1 = value
            elif (key == "type1"):
                self.type1 = value
            elif (key == "level2"):
                self.level2 = value
            elif (key == "type2"):
                self.type2 = value
            elif (key == "logurl"):
                self.logurl = value
            elif (key == "logtype"):
                self.logtype = value
            elif (key == "diskdir"):
                self.diskdir = value
            elif (key == "port"):
                self.port = value
            elif (key == "remoteurl"):
                self.remoteurl = value
            elif (key == "connection"):
                self.connection = value
            #elif (key=="src"):
            #   self.src = value
            elif (key == "saveandrestore"):
                self.logdebug("Received saveandrestore request")
                self.saveandrestore = value
            #elif (key=="skillstring"):
            #  self.skillstring = value

        return {'result': 'success'}

    # -------------------------------------------------------------------
    # TO-DO: change this to create the scenarios you'd like to see in the
    # workbench while developing your XBlock.
    # -------------------------------------------------------------------
    @staticmethod
    def workbench_scenarios():
        return [
            ("LEARCTATXBLOCKCLASS", """<vertical_demo>
                <learctatxblock width="" height=""/>
                </vertical_demo>
             """),
        ]
Beispiel #21
0
class DoneXBlock(XBlock):
    """
    Show a toggle which lets students mark things as done.
    """

    done = Boolean(scope=Scope.user_state,
                   help="Is the student done?",
                   default=False)

    align = String(scope=Scope.settings,
                   help="Align left/right/center",
                   default="left")

    has_score = True

    # pylint: disable=unused-argument
    @XBlock.json_handler
    def toggle_button(self, data, suffix=''):
        """
        Ajax call when the button is clicked. Input is a JSON dictionary
        with one boolean field: `done`. This will save this in the
        XBlock field, and then issue an appropriate grade.
        """
        if 'done' in data:
            self.done = data['done']
            if data['done']:
                grade = 1
            else:
                grade = 0
            grade_event = {'value': grade, 'max_value': 1}
            self.runtime.publish(self, 'grade', grade_event)
            # This should move to self.runtime.publish, once that pipeline
            # is finished for XBlocks.
            self.runtime.publish(self, "edx.done.toggled", {'done': self.done})

        return {'state': self.done}

    def student_view(self, context=None):  # pylint: disable=unused-argument
        """
        The primary view of the DoneXBlock, shown to students
        when viewing courses.
        """
        html_resource = resource_string("static/html/done.html")
        html = html_resource.format(done=self.done, id=uuid.uuid1(0))
        (unchecked_png,
         checked_png) = (self.runtime.local_resource_url(self, x)
                         for x in ('public/check-empty.png',
                                   'public/check-full.png'))

        frag = Fragment(html)
        frag.add_css(resource_string("static/css/done.css"))
        frag.add_javascript(resource_string("static/js/src/done.js"))
        frag.initialize_js(
            "DoneXBlock", {
                'state': self.done,
                'unchecked': unchecked_png,
                'checked': checked_png,
                'align': self.align.lower()
            })
        return frag

    def studio_view(self, _context=None):  # pylint: disable=unused-argument
        '''
        Minimal view with no configuration options giving some help text.
        '''
        html = resource_string("static/html/studioview.html")
        frag = Fragment(html)
        return frag

    @staticmethod
    def workbench_scenarios():
        """A canned scenario for display in the workbench."""
        return [
            ("DoneXBlock", """<vertical_demo>
                  <done align="left"> </done>
                  <done align="right"> </done>
                  <done align="center"> </done>
                </vertical_demo>
             """),
        ]

    # Everything below is stolen from
    # https://github.com/edx/edx-ora2/blob/master/apps/openassessment/
    #        xblock/lms_mixin.py
    # It's needed to keep the LMS+Studio happy.
    # It should be included as a mixin.

    display_name = String(default="Completion",
                          scope=Scope.settings,
                          help="Display name")

    start = DateTime(
        default=None,
        scope=Scope.settings,
        help="ISO-8601 formatted string representing the start date "
        "of this assignment. We ignore this.")

    due = DateTime(default=None,
                   scope=Scope.settings,
                   help="ISO-8601 formatted string representing the due date "
                   "of this assignment. We ignore this.")

    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)

    def has_dynamic_children(self):
        """Do we dynamically determine our children? No, we don't have any.
        """
        return False

    def max_score(self):
        """The maximum raw score of our problem.
        """
        return 1
class RocketChatXBlock(XBlock, XBlockWithSettingsMixin,
                       StudioEditableXBlockMixin):
    """
    This class allows to embed a chat window inside a unit course
    and set the necessary variables to config the rocketChat enviroment
    """
    display_name = String(display_name=_("Display Name"),
                          scope=Scope.settings,
                          default="Rocket Chat")
    email = String(
        default="",
        scope=Scope.user_state,
        help="Email in rocketChat",
    )
    rocket_chat_role = String(default="user",
                              scope=Scope.user_state,
                              help="The defined role in rocketChat")

    default_channel = String(
        display_name=_("Specific Channel"),
        scope=Scope.content,
        help=
        _("This field allows to select the channel that would be accesible in the unit"
          ),
        values_provider=lambda self: self.get_groups(),
    )

    ui_is_block = Boolean(
        default=False,
        scope=Scope.user_state,
        help="This is the flag for the initial channel",
    )

    selected_view = String(
        display_name=_("Select Channel"),
        default=_("Main View"),
        scope=Scope.content,
        help=
        _("This field allows to select the channel that would be accesible in the unit"
          ),
        values_provider=lambda self: self.channels_enabled(),
    )

    team_channel = String(default="", scope=Scope.user_state)

    emoji = String(
        display_name=_("Emoji to grade with"),
        default="",
        scope=Scope.settings,
        help=_("Select the emoji which you want to grade"),
    )

    oldest = DateTime(
        display_name=_("Date From"),
        default=None,
        scope=Scope.settings,
        help=
        _("ISO-8601 formatted string representing the start date of this assignment."
          ))

    latest = DateTime(
        display_name=_("Date To"),
        default=None,
        scope=Scope.settings,
        help=
        _("ISO-8601 formatted string representing the due date of this assignment."
          ))

    target_reaction = Integer(
        display_name=_("Target Reaction Count"),
        default=5,
        scope=Scope.settings,
        help=_("Target value in order to achieve a defined grade."))

    graded_activity = Boolean(
        display_name=_("Graded Activity"),
        default=False,
        scope=Scope.settings,
    )

    points = Float(
        display_name=_("Score"),
        help=_("Defines the number of points each problem is worth. "),
        values={
            "min": 0,
            "step": .1
        },
        default=1,
        scope=Scope.settings)

    grade = Float(scope=Scope.user_state, default=0)

    count_messages = Integer(display_name=_("Last Messages"),
                             default=1000,
                             scope=Scope.settings,
                             help=_("The amount of messages to retrieve"))

    has_score = graded_activity
    team_view = True
    _api_teams = None
    _api_rocket_chat = None

    VIEWS = ["Main View", "Team Discussion", "Specific Channel"]

    # Possible editable fields
    editable_fields = ('selected_view', 'default_channel', 'graded_activity',
                       'emoji', 'target_reaction', 'oldest', 'latest',
                       'points', 'count_messages')

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

    @XBlock.supports('multi_device')  # Mark as mobile-friendly
    def student_view(self, context=None):
        """
        The primary view of the RocketChatXBlock, shown to students
        when viewing courses.
        """
        in_studio_runtime = hasattr(self.xmodule_runtime, 'is_author_mode')  # pylint: disable=no-member

        if in_studio_runtime:
            return self.author_view(context)

        context = {
            "response":
            self.init(),
            "user_data":
            self.user_data,
            "ui_is_block":
            self.ui_is_block,
            "public_url_service":
            self.server_data["public_url_service"],
            "key":
            hashlib.sha1("{}_{}".format(
                ROCKET_CHAT_DATA, self.user_data["username"])).hexdigest()
        }

        frag = Fragment(
            LOADER.render_template('static/html/rocketc.html', context))
        frag.add_css(self.resource_string("static/css/rocketc.css"))
        frag.add_javascript(self.resource_string("static/js/src/rocketc.js"))
        frag.initialize_js('RocketChatXBlock')
        return frag

    def author_view(self, context=None):
        """  Returns author view fragment on Studio """
        # pylint: disable=unused-argument
        frag = Fragment(u"Studio Runtime RocketChatXBlock")
        frag.add_javascript(self.resource_string("static/js/src/rocketc.js"))
        frag.initialize_js('RocketChatXBlock')

        return frag

    def studio_view(self, context=None):
        """  Returns edit studio view fragment """
        frag = super(RocketChatXBlock, self).studio_view(context)
        frag.add_content(
            LOADER.render_template('static/html/studio.html', context))
        frag.add_css(self.resource_string("static/css/rocketc.css"))
        frag.add_javascript(
            self.resource_string("static/js/src/studio_view.js"))
        frag.initialize_js('StudioViewEdit')
        return frag

    # 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 [
            ("RocketChatXBlock", """<rocketc/>
             """),
            ("Multiple RocketChatXBlock", """<vertical_demo>
                <rocketc/>
                <rocketc/>
                <rocketc/>
                </vertical_demo>
             """),
        ]

    @property
    def api_rocket_chat(self):
        """
        Creates an ApiRocketChat object
        """
        if not self._api_rocket_chat:
            try:
                user = self.xblock_settings["admin_user"]
                password = self.xblock_settings["admin_pass"]
            except KeyError:
                LOG.exception("The admin's settings has not been found")
                raise
            self._api_rocket_chat = ApiRocketChat(  # pylint: disable=attribute-defined-outside-init
                user=user,
                password=password,
                server_url=self.server_data["private_url_service"],
            )

            LOG.info("Api rocketChat initialize: %s ", self._api_rocket_chat)

        return self._api_rocket_chat

    @property
    def api_teams(self):
        """
        Creates an ApiTeams object
        """
        if not self._api_teams:
            try:
                client_id = self.xblock_settings["client_id"]
                client_secret = self.xblock_settings["client_secret"]
            except KeyError as xblock_settings_error:
                LOG.error('Get rocketchat xblock settings error: %s',
                          xblock_settings_error)
                raise

            server_url = settings.LMS_ROOT_URL

            self._api_teams = ApiTeams(  # pylint: disable=attribute-defined-outside-init
                client_id, client_secret, server_url)

            LOG.info("Api Teams initialize: %s ", self._api_teams)

        return self._api_teams

    @property
    def server_data(self):
        """
        This method allows to get private and public url from xblock settings
        """
        xblock_settings = self.xblock_settings
        server_data = {}
        server_data["private_url_service"] = xblock_settings[
            "private_url_service"]
        server_data["public_url_service"] = xblock_settings[
            "public_url_service"]
        return server_data

    @property
    def user_data(self):
        """
        This method initializes the user's parameters
        """
        runtime = self.runtime  # pylint: disable=no-member
        user = runtime.service(self, 'user').get_current_user()
        user_data = {}
        user_data["email"] = user.emails[0]
        user_data["role"] = runtime.get_user_role()
        user_data["course_id"] = runtime.course_id
        user_data["username"] = user.opt_attrs['edx-platform.username']
        user_data["anonymous_student_id"] = runtime.anonymous_student_id
        return user_data

    @property
    def xblock_settings(self):
        """
        This method allows to get the xblock settings
        """
        return self.get_xblock_settings()

    @property
    def course_id(self):
        """
        This method allows to get the course_id
        """
        try:
            return re.sub('[^A-Za-z0-9]+', '',
                          unicode(self.xmodule_runtime.course_id))
        except AttributeError:
            course_id = unicode(self.runtime.course_id)
            return re.sub('[^A-Za-z0-9]+', '',
                          course_id.split("+branch", 1)[0])

    def channels_enabled(self):
        """
        This method returns a list with the channel options
        """
        if self._teams_is_enabled():
            return self.VIEWS
        view = list(self.VIEWS)
        view.remove(self.VIEWS[1])
        return view

    def get_groups(self):
        """
        This method lists the existing groups
        """
        # the following instructions get all the groups
        # except the team groups and specific teams
        course = self.course_id
        kwargs = generate_query_dict(course)
        groups = [
            group.split("__", 1)[0]
            for group in self.api_rocket_chat.get_groups(**kwargs)
        ]

        kwargs = generate_query_dict(course, specific_team=True)
        # This instruction adds the string "(Team Group)" if the group is in team_groups
        groups += [
            '{}-{}'.format("(Team Group)",
                           team_group.split("__", 1)[0])
            for team_group in self.api_rocket_chat.get_groups(**kwargs)
        ]

        groups.append("")
        return sorted(groups)

    def init(self):
        """
        This method initializes the user's variables and
        log in to rocketchat account
        """

        user_data = self.user_data
        key = hashlib.sha1("{}-{}-{}-{}-{}".format(
            ROCKET_CHAT_DATA,
            user_data["username"],
            self.selected_view,
            self.default_channel,
            self._get_team(user_data["username"]),
        )).hexdigest()
        response = cache.get(key)

        if response:
            return response

        response = self._login(user_data)
        if response['success']:
            response = response['data']
            user_id = response['userId']
            auth_token = response['authToken']
            response['default_group'] = self._join_user_to_groups(
                user_id, user_data, auth_token)
            response['team_view'] = self.team_view
            self._update_user(user_id, user_data)
            self._grading_discussions(response['default_group'])
            cache.set(key, response, CACHE_TIMEOUT)
            return response

        return response['errorType']

    def _login(self, user_data):
        """
        This method allows to get the user's authToken and id
        or creates a user to login in RocketChat
        """
        key = hashlib.sha1("{}_{}".format(ROCKET_CHAT_DATA,
                                          user_data["username"])).hexdigest()
        data = cache.get(key)

        if data:
            return data

        api = self.api_rocket_chat

        rocket_chat_user = api.search_rocket_chat_user(user_data["username"])
        LOG.info("Login method: result search user: %s",
                 rocket_chat_user["success"])

        if rocket_chat_user["success"]:
            data = api.create_token(user_data["username"])
        else:
            response = api.create_user(user_data["anonymous_student_id"],
                                       user_data["email"],
                                       user_data["username"])
            LOG.info("Login method: result create user : %s", response)

            data = api.create_token(user_data["username"])

        LOG.info("Login method: result create token: %s", data)
        cache.set(key, data, CACHE_TIMEOUT)
        return data

    def _add_user_to_course_group(self, user_id):
        """
        This method add the user to the default course channel
        """
        group_name = "{}__{}".format("General", self.course_id)
        self._add_user_to_group(
            user_id,
            group_name,
            members=[self.user_data["username"]],
            custom_fields=generate_custom_fields(course=self.course_id),
            create=True)

    def _add_user_to_specific_group(self, group_name, user_id):
        """
        This method allows to add a user to a given group.
        """
        group = "{}__{}".format(group_name, self.course_id)
        result = self._add_user_to_group(user_id, group)
        return result, group

    def _add_user_to_team_group(self, user_id, username, auth_token):
        """
        Add the user to team's group in rocketChat
        """
        team = self._get_team(username)

        if team is None:
            self._remove_user_from_group(self.team_channel, user_id,
                                         auth_token)
            return False
        team_name, topic_id = generate_team_variables(team)
        group_name = "{}__{}__{}".format(team_name, topic_id, self.course_id)

        if self.team_channel != group_name:
            self._remove_user_from_group(self.team_channel, user_id,
                                         auth_token)

        self.team_channel = group_name

        return self._add_user_to_group(user_id,
                                       group_name,
                                       members=[username],
                                       custom_fields=generate_custom_fields(
                                           self.course_id, team),
                                       create=True)

    def _get_team(self, username):
        """
        This method gets the user's team
        """
        course_team_membership = CourseTeamMembership.objects.filter(
            user=User.objects.get(username=username), )

        if course_team_membership:
            return course_team_membership[0].team

        return None

    def _join_user_to_groups(self, user_id, user_data, auth_token):
        """
        This method add the user to the different channels
        """
        default_channel = self.default_channel

        if self.selected_view == self.VIEWS[1] and self._teams_is_enabled():
            self.team_view = self._add_user_to_team_group(
                user_id, user_data["username"], auth_token)
            self.ui_is_block = self.team_view
            return self.team_channel

        elif self.selected_view == self.VIEWS[2] and default_channel:
            if default_channel.startswith("(Team Group)"):
                return self._join_user_to_specific_team_group(
                    user_id, user_data, default_channel)

            self.ui_is_block, default_channel = self._add_user_to_specific_group(
                default_channel, user_id)
            return default_channel
        else:
            self.ui_is_block = False
            self._add_user_to_course_group(user_id)
        return None

    def _add_user_to_group(self, user_id, group_name, **kwargs):
        """
        This method allows to add a user to any channel, returns True if it's successful
        """
        group = self.api_rocket_chat.search_rocket_chat_group(group_name)
        response = {}

        if group["success"]:
            response = self.api_rocket_chat.add_user_to_group(
                user_id, group['group']['_id'])
        elif kwargs.get("create", False):
            response = self.api_rocket_chat.create_group(
                group_name, kwargs.get("members", []),
                **kwargs.get("custom_fields", {}))

        return response.get("success", False)

    def _join_user_to_specific_team_group(self, user_id, user_data,
                                          default_channel):
        team = self._get_team(user_data["username"])
        if team is None:
            self.team_view = False
            return None
        default_channel = self._create_team_group_name(
            team, default_channel.replace("(Team Group)-", ""), self.course_id)
        self.ui_is_block = self._add_user_to_group(
            user_id,
            default_channel,
            members=[user_data["username"]],
            custom_fields=generate_custom_fields(self.course_id, team),
            create=True)
        return default_channel

    def _teams_is_enabled(self):
        """
        This method verifies if teams are available
        """
        from .openedx_dependencies import modulestore  # pylint: disable=relative-import
        try:
            course_id = self.runtime.course_id  # pylint: disable=no-member
        except AttributeError:
            return False

        course = modulestore().get_course(course_id, depth=0)
        teams_configuration = course.teams_configuration
        LOG.info("Team is enabled result: %s", teams_configuration)
        if "topics" in teams_configuration and teams_configuration["topics"]:
            return True

        return False

    def _update_user(self, user_id, user_data):
        """
        This method updates the email and photo's profile
        """
        api = self.api_rocket_chat
        if user_data["email"] != self.email:
            self.email = api.update_user(user_id, user_data["email"])

        api.set_avatar(user_data["username"], self._user_image_url())

    def _user_image_url(self):
        """Returns an image url for the current user"""
        from .openedx_dependencies import get_profile_image_urls_for_user  # pylint: disable=relative-import
        current_user = User.objects.get(username=self.user_data["username"])
        profile_image_url = get_profile_image_urls_for_user(
            current_user)["full"]

        if profile_image_url.startswith("http"):
            return profile_image_url

        base_url = settings.LMS_ROOT_URL
        image_url = "{}{}".format(base_url, profile_image_url)
        LOG.info("User image url: %s ", image_url)
        return image_url

    @XBlock.json_handler
    def create_group(self, data, suffix=""):
        """
        This method allows to create a group
        """
        # pylint: disable=unused-argument
        group_name = data.get("groupName", None)

        if group_name == "" or group_name is None:
            return {"success": False, "error": "Group Name is not valid"}

        group_name = re.sub(r'\W+', '', group_name)
        specific_team = False
        members = []
        team = None

        if data.get("asTeam"):
            group_name = "{}__{}".format(group_name, self.course_id)
            specific_team = True

        elif data.get("teamGroup"):
            team = self._get_team(self.user_data["username"])
            members = list(self.get_team_members(team))
            group_name = self._create_team_group_name(team, group_name,
                                                      self.course_id)

        else:
            group_name = "{}__{}".format(group_name, self.course_id)

        custom_fields = generate_custom_fields(self.course_id, team,
                                               specific_team)
        group = self.api_rocket_chat.create_group(group_name, members,
                                                  **custom_fields)

        if "group" in group:
            group_id = group["group"]["_id"]

            self.api_rocket_chat.set_group_description(group_id,
                                                       data.get("description"))
            self.api_rocket_chat.set_group_topic(group_id, data.get("topic"))

        LOG.info("Method Public Create Group: %s", group)
        return group

    @staticmethod
    def _create_team_group_name(team, group_name, course):
        """
        This method returns the formated name for given team and group name,
        in order to ensure a unique name in the rocketchat server.
        **Example**
            ** group_name = "Test"
            ** team = {
                "name": "team1",
                "topic": "animals"
                ......
            }
            ** course = "coursev1:edx-c101-2019T2"

            returns "Test(animals/team1)__coursev1:edx-c101-2019T2"
        """
        team_name, team_topic = generate_team_variables(team)
        return "{}({}/{})__{}".format(group_name, team_topic, team_name,
                                      course)

    def _remove_user_from_group(self, group_name, user_id, auth_token=None):
        """
        This method removes a user form a team
        """
        if not group_name:
            return False
        api = self.api_rocket_chat

        if group_name.startswith("Team-") and auth_token is not None:
            regex = group_name.replace('Team-', '', 1)
            query = {"name": {"$regex": regex}}
            kwargs = {"query": json.dumps(query)}
            groups = api.list_all_groups(user_id, auth_token, **kwargs)

            if groups["success"]:
                groups = groups["groups"]
                for group in groups:
                    api.kick_user_from_group(user_id, group["_id"])
                return {"success": True}

        group = api.search_rocket_chat_group(group_name)
        if group["success"]:
            group = group["group"]
            response = api.kick_user_from_group(user_id, group["_id"])
            return response
        return {"success": False, "error": "Channel not found"}

    def get_team_members(self, team):
        """
        This method allows to get the members of a team
        """
        if team:
            team_id = team["id"]
            members = self.api_teams.get_members(team_id)
            if members:
                for member in members:
                    yield member["user"]["username"]

    @XBlock.json_handler
    def leave_group(self, data, suffix=""):
        """
        This method allows to leave a group
        """
        # pylint: disable=unused-argument

        username = self.user_data["username"]
        user = self.api_rocket_chat.search_rocket_chat_user(username)
        group_name = data["groupName"]

        if not user["success"]:
            return {"success": False, "error": "User is not valid"}

        if group_name == "" or group_name is None:
            return {"success": False, "error": "Group Name is not valid"}

        team = self._get_team(self.user_data["username"])
        team_name, topic_id = generate_team_variables(team)

        if group_name == team_name or group_name == "General":
            return {
                "success": False,
                "error": "You Can Not Leave a Main Group"
            }
        group_name = "{}__{}__{}__{}".format(group_name, team_name, topic_id,
                                             self.course_id)

        return self._remove_user_from_group(group_name, user["user"]["_id"])

    @XBlock.json_handler
    def get_list_of_groups(self, data, suffix=""):
        """Returns a list with the group names"""
        # pylint: disable=unused-argument
        user_id = data.get("userId", None)
        auth_token = data.get("authToken", None)

        if not user_id or not auth_token:
            LOG.warn("Invalid data for method get_list_of_groups: %s", data)
            return None

        groups = list(self._get_list_groups(user_id, auth_token))
        return groups

    def _get_list_groups(self, user_id, auth_token, **kwargs):
        """
        This method allows to get a list of group names
        """
        api = self.api_rocket_chat
        groups = api.list_all_groups(user_id, auth_token, **kwargs)
        team, topic = generate_team_variables(
            self._get_team(self.user_data["username"]))
        if groups["success"]:
            for group in groups["groups"]:
                fields = group.get("customFields", {})
                if (team == fields.get("team") and topic == fields.get("topic")
                        and self.course_id == fields.get("course")):
                    yield group["name"]

    def _get_user_messages(self, group_name, latest="", oldest="", count=100):
        """
        Gets the messages from a user's private group.
        """
        api = self.api_rocket_chat
        rocket_chat_group = api.search_rocket_chat_group(group_name)

        if not rocket_chat_group['success']:
            return []

        group_id = rocket_chat_group['group']['_id']
        messages = api.get_groups_history(room_id=group_id,
                                          latest=latest,
                                          oldest=oldest,
                                          count=count)

        if not messages["success"]:
            return []

        return [
            message for message in messages["messages"]
            if message["u"]["username"] == self.user_data["username"]
        ]

    def _filter_by_reaction_and_user_role(self, messages, reaction):
        """
        Returns generator with filtered messages by a given reaction
        """
        for message in messages:
            if reaction not in message.get("reactions", {}):
                continue
            usernames = message["reactions"][reaction]["usernames"]

            for username in usernames:
                if self._validate_user_role(username):
                    yield message
                    break

    def _validate_user_role(self, username):
        """
        Returns True if the user is privileged in teams discussions for
        this course.
        """
        from .openedx_dependencies import CourseStaffRole  # pylint: disable=relative-import

        user = User.objects.get(username=username)

        if user.is_staff:
            return True
        if CourseStaffRole(self.user_data["course_id"]).has_user(user):
            return True
        return False

    def _grading_discussions(self, graded_group):
        """
        This method allows to grade contributions to Rocket.Chat given a reaction.
        """
        if not self.graded_activity or self.grade == self.points:
            return
        messages = self._get_user_messages(graded_group, self.latest,
                                           self.oldest, self.count_messages)
        messages = list(
            self._filter_by_reaction_and_user_role(messages, self.emoji))
        if len(messages) >= self.target_reaction:
            self.grade = self.points
            self.runtime.publish(self, 'grade', {
                'value': self.grade,
                'max_value': self.points
            })

    def max_score(self):
        if self.graded_activity:
            return self.points

    @XBlock.handler
    def logout_user(self, request=None, suffix=None):
        """
        This method allows to invalidate the user token
        """
        # pylint: disable=unused-argument
        key = request.GET.get("beacon_rc")
        data = cache.get(key)
        if data:
            api = self.api_rocket_chat
            user_data = data.get("data")
            login_token = user_data.get("authToken")
            user_id = user_data.get("userId")
            response = api.logout_user(user_id, login_token)

            if response.get("status") == "success":
                cache.delete(key)
                return Response(status=202)

        return Response(status=404)
class embedurlXBlock(XBlock):
    '''
    Icon of the XBlock. Values : [other (default), video, problem]
    '''
    icon_class = "other"
    '''
    Fields
    '''
    display_name = String(
        display_name="Display Name",
        default="Embed URL",
        scope=Scope.settings,
        help=
        "This name appears in the horizontal navigation at the top of the page."
    )

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

    new_window_button = Boolean(display_name="Enable Open In New Window",
                                default=False,
                                scope=Scope.content,
                                help="Display Open New Window Button.")

    min_height = String(display_name="Mininum height",
                        default="450",
                        scope=Scope.content,
                        help="Add Minimum Height for Doc")
    '''
    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.
        """
        if self.min_height == '':
            self.min_height = "450"

        context = {
            'display_name': self.display_name,
            'url': self.url,
            'new_window_button': self.new_window_button,
            'min_height': self.min_height,
        }
        html = self.render_template('static/html/embedurl_view.html', context)

        frag = Fragment(html)
        frag.add_css(self.load_resource("static/css/embedurl.css"))
        frag.add_javascript(self.load_resource("static/js/embedurl_view.js"))
        frag.initialize_js('embedurlXBlockInitView')
        return frag

    def studio_view(self, context=None):
        """
        The secondary view of the XBlock, shown to teachers
        when editing the XBlock.
        """
        if self.min_height == '':
            self.min_height = "450"

        context = {
            'display_name': self.display_name,
            'url': self.url,
            'new_window_button': self.new_window_button,
            'min_height': self.min_height,
        }
        html = self.render_template('static/html/embedurl_edit.html', context)

        frag = Fragment(html)
        frag.add_javascript(self.load_resource("static/js/embedurl_edit.js"))
        frag.initialize_js('embedurlXBlockInitEdit')
        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.new_window_button = True if data[
            'new_window_button'] == "True" else False  # Str to Bool translation
        self.min_height = data['min_height']

        return {
            'result': 'success',
        }

    def student_view_data(self):
        """
        Inform REST api clients about original file location and it's "freshness".
        Make sure to include `student_view_data=scorm` to URL params in the request.
        """
        return {
            'last_modified': self.new_window_button,
            'scorm_data': self.url
        }
Beispiel #24
0
class CapaFields(object):
    """
    Define the possible fields for a Capa problem
    """
    display_name = String(
        display_name="Display Name",
        help=
        "This name appears in the horizontal navigation at the top of the page.",
        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="Blank Advanced Problem")
    attempts = Integer(
        help="Number of attempts taken by the student on this problem",
        default=0,
        scope=Scope.user_state)
    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)
    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)
    showanswer = String(
        display_name="Show Answer",
        help=("Defines when to show the answer to the problem. "
              "A default value can be set in Advanced Settings."),
        scope=Scope.settings,
        default="finished",
        values=[{
            "display_name": "Always",
            "value": "always"
        }, {
            "display_name": "Answered",
            "value": "answered"
        }, {
            "display_name": "Attempted",
            "value": "attempted"
        }, {
            "display_name": "Closed",
            "value": "closed"
        }, {
            "display_name": "Finished",
            "value": "finished"
        }, {
            "display_name": "Past Due",
            "value": "past_due"
        }, {
            "display_name": "Never",
            "value": "never"
        }])
    force_save_button = Boolean(
        help="Whether to force the save button to appear on the page",
        scope=Scope.settings,
        default=False)
    rerandomize = Randomization(
        display_name="Randomization",
        help=
        "Defines how often inputs are randomized when a student loads the problem. "
        "This setting only applies to problems that can have randomly generated numeric values. "
        "A default value can be set in Advanced Settings.",
        default="never",
        scope=Scope.settings,
        values=[{
            "display_name": "Always",
            "value": "always"
        }, {
            "display_name": "On Reset",
            "value": "onreset"
        }, {
            "display_name": "Never",
            "value": "never"
        }, {
            "display_name": "Per Student",
            "value": "per_student"
        }])
    data = String(help="XML data for the problem",
                  scope=Scope.content,
                  default="<problem></problem>")
    correct_map = Dict(
        help="Dictionary with the correctness of current student answers",
        scope=Scope.user_state,
        default={})
    input_state = Dict(
        help="Dictionary for maintaining the state of inputtypes",
        scope=Scope.user_state)
    student_answers = Dict(
        help="Dictionary with the current student responses",
        scope=Scope.user_state)
    done = Boolean(help="Whether the student has answered the problem",
                   scope=Scope.user_state)
    seed = Integer(help="Random seed for this student", scope=Scope.user_state)
    weight = Float(
        display_name="Problem Weight",
        help=
        ("Defines the number of points each problem is worth. "
         "If the value is not set, each response field in the problem is worth one point."
         ),
        values={
            "min": 0,
            "step": .1
        },
        scope=Scope.settings)
    markdown = String(help="Markdown source of this module",
                      default=None,
                      scope=Scope.settings)
    source_code = String(
        help=
        "Source code for LaTeX and Word problems. This feature is not well-supported.",
        scope=Scope.settings)
    text_customization = Dict(
        help="String customization substitutions for particular locations",
        scope=Scope.settings
        # TODO: someday it should be possible to not duplicate this definition here
        # and in inheritance.py
    )
    use_latex_compiler = Boolean(help="Enable LaTeX templates?",
                                 default=False,
                                 scope=Scope.settings)
Beispiel #25
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(u"""
            .problem {
                border: solid 1px #888; padding: 3px;
            }
            """)
        result.add_content(self.runtime.render_template(
            "problem.html",
            named_children=named_child_frags
        ))
        result.add_javascript(u"""
            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(u"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>
             """),
        ]
Beispiel #26
0
class PDFXBlock(XBlock):
    """
    PDF XBlock.
    """

    loader = ResourceLoader(__name__)

    # Icon of the XBlock. Values : [other (default), video, problem]

    icon_class = 'other'

    # Enable view as specific student

    show_in_read_only_mode = True

    # 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=_('https://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.'
        ))

    def load_resource(self, resource_path):  # pylint: disable=no-self-use
        """
        Gets the content of a resource
        """

        resource_content = pkg_resources.resource_string(
            __name__, resource_path)
        return resource_content.decode('utf-8')

    def render_template(self, path, context=None):
        """
        Evaluate a template by resource path, applying the provided context
        """

        return self.loader.render_django_template(
            os.path.join('static/html', path),
            context=Context(context or {}),
            i18n_service=self.runtime.service(self, 'i18n'))

    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('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('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=''):  # pylint: disable=unused-argument
        """
        The saving handler.
        """
        self.display_name = data['display_name']
        self.url = data['url']
        self.allow_download = data[
            'allow_download'] == 'True'  # Basic str to translation
        self.source_text = data['source_text']
        self.source_url = data['source_url']

        return {'result': 'success'}
class QuizBlock(ResourceMixin, QuizResultMixin, ExportDataBlock,
                XBlockWithTranslationServiceMixin):
    """
    An XBlock which can be used to add diagnostic quiz
    """
    BUZZFEED_QUIZ_VALUE = "BFQ"
    BUZZFEED_QUIZ_LABEL = _("BuzzFeed-style")
    DIAGNOSTIC_QUIZ_VALUE = "DG"
    DIAGNOSTIC_QUIZ_LABEL = _("Diagnostic-style")
    DEFAULT_GROUP = _('Default Group')

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

    title = String(default='', scope=Scope.content, help=_("Title of quiz"))

    description = String(default="",
                         scope=Scope.content,
                         help=_("Description of quiz"))

    questions = List(
        default=[],
        help=_("This will hold list of question with respective choices"),
        scope=Scope.content,
    )

    student_choices = Dict(
        default={},
        help=_("This will hold user provided answers of questions"),
        scope=Scope.user_state,
    )

    quiz_type = String(default="", scope=Scope.content, help=_("Type of quiz"))

    results = List(default=[], scope=Scope.content, help=_("List of results"))

    student_result = String(default='',
                            scope=Scope.user_state,
                            help=_("Calculated feedback of each user"))

    types = List(default=[
        {
            "value": BUZZFEED_QUIZ_VALUE,
            "label": BUZZFEED_QUIZ_LABEL
        },
        {
            "value": DIAGNOSTIC_QUIZ_VALUE,
            "label": DIAGNOSTIC_QUIZ_LABEL
        },
    ],
                 scope=Scope.content,
                 help=_("List of results"))

    groups = List(default=[DEFAULT_GROUP],
                  scope=Scope.content,
                  help=_("List of results"))

    current_step = Integer(
        default=0,
        scope=Scope.user_state,
        help=_("To control which question should be shown to student"))

    weight = Float(display_name=_("Weight"),
                   help=_("Defines the maximum total grade of this question."),
                   default=1,
                   scope=Scope.content,
                   enforce_type=True)

    completed = Boolean(default=False,
                        scope=Scope.user_state,
                        help=_("Has the student completed this quiz"))

    @property
    def display_name_with_default(self):
        return self.title

    @property
    def additional_publish_event_data(self):
        return {
            'user_id': self.scope_ids.user_id,
            'block_id': self.get_block_id(),
            'component_id': self.scope_ids.usage_id
        }

    has_score = True

    def get_fragment(self, context, view='studio', json_args=None):
        """
        return fragment after loading css/js/html either for studio OR student view
        :param context: context for templates
        :param view: view_type i;e studio/student
        :return: fragment after loading all assets
        """
        """
            Return fragment after adding required css/js/html
        """
        fragment = Fragment()
        self.add_templates(fragment, context, view)
        self.add_css(fragment, view)
        self.add_js(fragment, view)
        self.initialize_js_classes(fragment, view, json_args)
        return fragment

    def append_choice(self, questions):
        """
        append student choice with each question if available
        :param questions: list of questions
        :return:
        """
        """

        """
        for question in questions:
            if self.quiz_type == self.DIAGNOSTIC_QUIZ_VALUE:
                question['student_choice'] = float(self.student_choices.get(question['id'])) if \
                    self.student_choices.get(question['id']) else ''
            else:
                question['student_choice'] = self.student_choices.get(
                    question['id'], '')

    def get_block_id(self):
        """
        Return ID of `block`
        """
        usage_id = self.scope_ids.usage_id
        # Try accessing block ID. If usage_id does not have it, return usage_id itself
        return six.text_type(getattr(usage_id, 'block_id', usage_id))

    def get_question(self, question_id):
        """
        Return Question object for given question id
        """
        question = {}
        for question in self.questions:
            if question['id'] == question_id:
                question = question
                break

        return question

    def get_buzzfeed_answer(self, choices, student_choice):
        """
        Return buzzfeed quiz answer label from question choices using student choice
        """
        choice_name = ''
        for choice in choices:
            if choice['category_id'] == student_choice:
                choice_name = choice['name']
                break

        return choice_name

    def get_diagnostic_answer(self, choices, student_choice):
        """
        Return diagnostic quiz answer label from question choices using student choice
        """
        choice_name = ''
        for choice in choices:
            if str(choice['value']) == student_choice:
                choice_name = choice['name']
                break

        return choice_name

    @XBlock.supports("multi_device")  # Mark as mobile-friendly
    def student_view(self, context=None):
        """
        it will loads student view
        :param context: context
        :return: fragment
        """

        context = {
            'questions': copy.deepcopy(self.questions),
            'self': self,
            'block_id': "xblock-{}".format(self.get_block_id()),
            'user_is_staff': self.user_is_staff()
        }

        if self.student_choices:
            self.append_choice(context['questions'])

        # return final result to show if user already completed the quiz
        if self.questions and self.current_step:
            if len(self.questions) == self.current_step:
                context['result'] = self.get_result()

        return self.get_fragment(context, 'student', {
            'quiz_type': self.quiz_type,
            'quiz_title': self.title
        })

    def student_view_data(self, context=None):
        """
        Returns a JSON representation of the Diagnostic Feedback Xblock, that
        can be retrieved using Course Block API.
        """
        return {
            'quiz_type': self.quiz_type,
            'quiz_title': self.title,
            'questions': self.questions,
            'description': self.description,
        }

    @XBlock.handler
    def student_view_user_state(self, data, suffix=''):
        """
        Returns a JSON representation of the student data for Diagnostic Feedback Xblock
        """
        response = {
            'student_choices': self.student_choices,
            'student_result': self.student_result,
            'current_step': self.current_step,
            'completed': self.completed,
        }

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

    def get_attached_groups(self):
        # return already attached groups
        groups = []
        for r in self.results:
            if r['group'] not in groups:
                groups.append(r['group'])

        return groups

    def studio_view(self, context):
        """
        it will loads studio view
        :param context: context
        :return: fragment
        """
        block_id = "xblock-{}".format(self.get_block_id())
        course_key = getattr(self.scope_ids.usage_id, 'course_key', None)

        context['self'] = self
        context['block_id'] = block_id

        try:
            from xmodule.contentstore.content import StaticContent
            base_asset_url = StaticContent.get_base_url_path_for_course_assets(
                course_key)
        except Exception:
            base_asset_url = ''

        return self.get_fragment(
            context, 'studio', {
                'base_asset_url':
                base_asset_url,
                'quiz_type':
                self.quiz_type,
                'block_id':
                block_id,
                'results':
                self.results,
                'BUZZFEED_QUIZ_VALUE':
                self.BUZZFEED_QUIZ_VALUE,
                'DIAGNOSTIC_QUIZ_VALUE':
                self.DIAGNOSTIC_QUIZ_VALUE,
                'DEFAULT_GROUP':
                self.DEFAULT_GROUP,
                'questions':
                self.questions,
                'groups':
                self.groups,
                'attachedGroups':
                self.get_attached_groups(),
                'categoryTpl':
                loader.load_unicode('templates/underscore/category.html'),
                'rangeTpl':
                loader.load_unicode('templates/underscore/range.html'),
                'questionTpl':
                loader.load_unicode('templates/underscore/question.html'),
                'choiceTpl':
                loader.load_unicode('templates/underscore/choice.html')
            })

    @XBlock.json_handler
    def save_data(self, data, suffix=''):
        """
        ajax handler to save data after applying required validation & filtration
        :param data: step data to save
        :param suffix:
        :return: response dict
        """

        success = True
        response_message = ""
        step = data.get('step', '')

        if not step:
            success = False
            response_message = self._('missing step number')
        else:
            try:
                is_valid_data, response_message = Validator.validate(
                    self, data)
                if is_valid_data:
                    response_message = MainHelper.save_filtered_data(
                        self, data)
                else:
                    success = False

            except Exception as ex:
                success = False
                response_message += ex.message if ex.message else str(ex)

        return {'step': step, 'success': success, 'msg': response_message}

    @XBlock.json_handler
    def save_choice(self, data, suffix=''):
        """
        save student choice for a question after validations
        :param data: answer data
        :param suffix:
        :return: response dict
        """
        # Import is placed here to avoid model import at project startup.
        try:
            from submissions import api as submissions_api
        except ImportError:
            log.info("Cannot import submissions_api")
            submissions_api = None

        student_result = ""
        response_message = ""

        try:
            success, response_message = Validator.validate_student_answer(
                self, data)
            if success:
                question_id = data['question_id']

                # save student answer
                self.student_choices[question_id] = data['student_choice']
                if (self.current_step) < int(data['currentStep']):
                    self.current_step = int(data['currentStep'])

                # calculate feedback result if user answering last question
                if data['isLast']:
                    student_result = self.get_result()

                    if not self.completed:
                        # Save the latest score and make quiz completed
                        self.runtime.publish(self, 'grade', {
                            'value': 1.0,
                            'max_value': 1.0
                        })
                        self.completed = True

                    if submissions_api:
                        log.info("have sub_api instance")
                        # Also send to the submissions API:
                        item_key = self.student_item_key
                        item_key['item_id'] = self.get_block_id()
                        submission_data = self.create_submission_data()
                        submission_data['final_result'] = student_result
                        submissions_api.create_submission(
                            item_key, json.dumps(submission_data))

                response_message = self._("Your response is saved")
        except Exception as ex:
            success = False
            response_message += str(ex)
        return {
            'success': success,
            'student_result': student_result,
            'response_msg': response_message
        }

    @XBlock.json_handler
    def start_over_quiz(self, data, suffix=''):
        """
        reset student_choices, student_result, current_step for current user
        :param data: empty dict
        :param suffix:
        :return: response dict
        """

        success = True
        response_message = self._("student data cleared")

        self.student_choices = {}
        self.student_result = ""
        self.current_step = 0

        return {'success': success, 'msg': response_message}

    @XBlock.json_handler
    def add_group(self, data, suffix=''):
        """
        Add new group in self.groups list
        """

        success = True
        grp_name = data.get('name', '')
        if grp_name not in self.groups:
            msg = self._('Group added successfully.')
            self.groups.append(grp_name)

        else:
            msg = self._('Group already exist.')
            success = False

        return {'success': success, 'msg': msg, 'group_name': grp_name}

    @XBlock.json_handler
    def publish_event(self, data, suffix=''):
        """
        Publish data for analytics purposes
        """
        event_type = data.pop('event_type')
        data['time'] = datetime.now()

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

    def create_submission_data(self):
        """
        Return a complete submission data as quiz completed
        """
        submission = {}
        for question in self.questions:
            question_id = question['id']
            question_data = self.get_question(question_id)
            if self.quiz_type == self.BUZZFEED_QUIZ_VALUE:
                question_answer = self.get_buzzfeed_answer(
                    question_data['choices'],
                    self.student_choices[question_id])
            else:
                question_answer = self.get_diagnostic_answer(
                    question_data['choices'],
                    self.student_choices[question_id])

            submission[question_id] = {
                'question_text': question['text'],
                'answer': question_answer
            }

        return submission
Beispiel #28
0
class MentoringBlock(XBlockWithLightChildren, StepParentMixin):
    """
    An XBlock providing mentoring capabilities

    Composed of text, answers input fields, and a set of MRQ/MCQ with advices.
    A set of conditions on the provided answers and MCQ/MRQ choices will determine if the
    student is a) provided mentoring advices and asked to alter his answer, or b) is given the
    ok to continue.
    """

    @staticmethod
    def is_default_xml_content(value):
        return _is_default_xml_content(value)

    attempted = Boolean(help="Has the student attempted this mentoring step?",
                        default=False, scope=Scope.user_state)
    completed = Boolean(help="Has the student completed this mentoring step?",
                        default=False, scope=Scope.user_state)
    next_step = String(help="url_name of the next step the student must complete (global to all blocks)",
                       default='mentoring_first', scope=Scope.preferences)
    followed_by = String(help="url_name of the step after the current mentoring block in workflow",
                         default=None, scope=Scope.content)
    url_name = String(help="Name of the current step, used for URL building",
                      default='mentoring-default', scope=Scope.content)
    enforce_dependency = Boolean(help="Should the next step be the current block to complete?",
                                 default=False, scope=Scope.content, enforce_type=True)
    display_submit = Boolean(help="Allow submission of the current block?", default=True,
                             scope=Scope.content, enforce_type=True)
    xml_content = String(help="XML content", default=DEFAULT_XML_CONTENT, scope=Scope.content)
    weight = Float(help="Defines the maximum total grade of the block.",
                   default=1, scope=Scope.content, enforce_type=True)
    num_attempts = Integer(help="Number of attempts a user has answered for this questions",
                           default=0, scope=Scope.user_state, enforce_type=True)
    max_attempts = Integer(help="Number of max attempts for this questions", default=0,
                           scope=Scope.content, enforce_type=True)
    mode = String(help="Mode of the mentoring. 'standard' or 'assessment'",
                  default='standard', scope=Scope.content)
    step = Integer(help="Keep track of the student assessment progress.",
                   default=0, scope=Scope.user_state, enforce_type=True)
    student_results = List(help="Store results of student choices.", default=[],
                           scope=Scope.user_state)
    extended_feedback = Boolean(help="Show extended feedback details when all attempts are used up.",
                                default=False, Scope=Scope.content)
    display_name = String(help="Display name of the component", default="Mentoring XBlock",
                          scope=Scope.settings)
    icon_class = 'problem'
    has_score = True

    MENTORING_MODES = ('standard', 'assessment')

    FLOATING_BLOCKS = (TitleBlock, MentoringMessageBlock, SharedHeaderBlock)

    FIELDS_TO_INIT = ('xml_content',)

    @property
    def is_assessment(self):
        return self.mode == 'assessment'

    def get_question_number(self, question_id):
        """
        Get the step number of the question id
        """
        for question in self.get_children_objects():
            if hasattr(question, 'step_number') and (question.name == question_id):
                return question.step_number
        raise ValueError("Question ID in answer set not a step of this Mentoring Block!")

    def answer_mapper(self, answer_status):
        """
        Create a JSON-dumpable object with readable key names from a list of student answers.
        """
        answer_map = []
        for answer in self.student_results:
            if answer[1]['status'] == answer_status:
                try:
                    answer_map.append({
                        'number': self.get_question_number(answer[0]),
                        'id': answer[0],
                        'details': answer[1],
                    })
                except ValueError:
                    pass
        return answer_map

    @property
    def score(self):
        """Compute the student score taking into account the light child weight."""
        total_child_weight = sum(float(step.weight) for step in self.steps)
        if total_child_weight == 0:
            return Score(0, 0, [], [], [])
        steps_map = {q.name: q for q in self.steps}
        points_earned = 0
        for q_name, q_details in self.student_results:
            question = steps_map.get(q_name)
            if question:
                points_earned += q_details['score'] * question.weight
        score = points_earned / total_child_weight
        correct = self.answer_mapper(CORRECT)
        incorrect = self.answer_mapper(INCORRECT)
        partially_correct = self.answer_mapper(PARTIAL)

        return Score(score, int(round(score * 100)), correct, incorrect, partially_correct)

    @property
    def assessment_message(self):
        if not self.max_attempts_reached:
            return self.get_message_html('on-assessment-review')
        else:
            return None

    def show_extended_feedback(self):
        return self.extended_feedback and self.max_attempts_reached

    def feedback_dispatch(self, target_data, stringify):
        if self.show_extended_feedback():
            if stringify:
                return json.dumps(target_data)
            else:
                return target_data

    def correct_json(self, stringify=True):
        return self.feedback_dispatch(self.score.correct, stringify)

    def incorrect_json(self, stringify=True):
        return self.feedback_dispatch(self.score.incorrect, stringify)

    def partial_json(self, stringify=True):
        return self.feedback_dispatch(self.score.partially_correct, stringify)

    def student_view(self, context):
        # Migrate stored data if necessary
        self.migrate_fields()

        # Validate self.step:
        num_steps = len([child for child in self.get_children_objects() if not isinstance(child, self.FLOATING_BLOCKS)])
        if self.step > num_steps:
            self.step = num_steps

        fragment, named_children = self.get_children_fragment(
            context, view_name='mentoring_view',
            not_instance_of=self.FLOATING_BLOCKS,
        )

        fragment.add_content(loader.render_template('templates/html/mentoring.html', {
            'self': self,
            'named_children': named_children,
            'missing_dependency_url': self.has_missing_dependency and self.next_step_url,
        }))
        fragment.add_css_url(self.runtime.local_resource_url(self, 'public/css/mentoring.css'))
        fragment.add_javascript_url(
            self.runtime.local_resource_url(self, 'public/js/vendor/underscore-min.js'))

        js_view = 'mentoring_assessment_view.js' if self.is_assessment else 'mentoring_standard_view.js'
        fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/'+js_view))

        fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/mentoring.js'))
        fragment.add_resource(loader.load_unicode('templates/html/mentoring_attempts.html'), "text/html")
        fragment.add_resource(loader.load_unicode('templates/html/mentoring_grade.html'), "text/html")
        fragment.add_resource(loader.load_unicode('templates/html/mentoring_review_questions.html'), "text/html")

        fragment.initialize_js('MentoringBlock')

        if not self.display_submit:
            self.runtime.publish(self, 'progress', {})

        return fragment

    def migrate_fields(self):
        """
        Migrate data stored in the fields, when a format change breaks backward-compatibility with
        previous data formats
        """
        # Partial answers replaced the `completed` with `status` in `self.student_results`
        if self.student_results and 'completed' in self.student_results[0][1]:
            # Rename the field and use the new value format (text instead of boolean)
            for result in self.student_results:
                result[1]['status'] = CORRECT if result[1]['completed'] else INCORRECT
                del result[1]['completed']

    @property
    def additional_publish_event_data(self):
        return {
            'user_id': self.scope_ids.user_id,
            'component_id': self.url_name,
        }

    @property
    def title(self):
        """
        Returns the title child.
        """
        for child in self.get_children_objects():
            if isinstance(child, TitleBlock):
                return child
        return None

    @property
    def header(self):
        """
        Return the header child.
        """
        for child in self.get_children_objects():
            if isinstance(child, SharedHeaderBlock):
                return child
        return None

    @property
    def has_missing_dependency(self):
        """
        Returns True if the student needs to complete another step before being able to complete
        the current one, and False otherwise
        """
        return self.enforce_dependency and (not self.completed) and (self.next_step != self.url_name)

    @property
    def next_step_url(self):
        """
        Returns the URL of the next step's page
        """
        return '/jump_to_id/{}'.format(self.next_step)

    @XBlock.json_handler
    def get_results(self, queries, suffix=''):
        """
        Gets detailed results in the case of extended feedback.

        It may be a good idea to eventually have this function get results
        in the general case instead of loading them in the template in the future,
        and only using it for extended feedback situations.

        Right now there are two ways to get results-- through the template upon loading up
        the mentoring block, or after submission of an AJAX request like in
        submit or get_results here.
        """
        results = []
        if not self.show_extended_feedback():
            return {
                'results': [],
                'error': 'Extended feedback results cannot be obtained.'
            }
        completed = True
        choices = dict(self.student_results)
        step = self.step
        # Only one child should ever be of concern with this method.
        for child in self.get_children_objects():
            if child.name and child.name in queries:
                results = [child.name, child.get_results(choices[child.name])]
                # Children may have their own definition of 'completed' which can vary from the general case
                # of the whole mentoring block being completed. This is because in standard mode, all children
                # must be correct to complete the block. In assessment mode with extended feedback, completion
                # happens when you're out of attempts, no matter how you did.
                completed = choices[child.name]['status']
                break

        # The 'completed' message should always be shown in this case, since no more attempts are available.
        message = self.get_message(True)

        return {
            'results': results,
            'completed': completed,
            'attempted': self.attempted,
            'message': message,
            'step': step,
            'max_attempts': self.max_attempts,
            'num_attempts': self.num_attempts,
        }

    def get_message(self, completed):
        if self.max_attempts_reached:
            return self.get_message_html('max_attempts_reached')
        elif completed:
            return self.get_message_html('completed')
        else:
            return self.get_message_html('incomplete')

    @XBlock.json_handler
    def submit(self, submissions, suffix=''):
        log.info(u'Received submissions: {}'.format(submissions))
        self.attempted = True

        if self.is_assessment:
            return self.handleAssessmentSubmit(submissions, suffix)

        submit_results = []
        completed = True
        for child in self.get_children_objects():
            if child.name and child.name in submissions:
                submission = submissions[child.name]
                child_result = child.submit(submission)
                submit_results.append([child.name, child_result])
                child.save()
                completed = completed and (child_result['status'] == CORRECT)

        message = self.get_message(completed)

        # Once it has been completed once, keep completion even if user changes values
        if self.completed:
            completed = True

        # server-side check to not set completion if the max_attempts is reached
        if self.max_attempts_reached:
            completed = False

        if self.has_missing_dependency:
            completed = False
            message = 'You need to complete all previous steps before being able to complete the current one.'
        elif completed and self.next_step == self.url_name:
            self.next_step = self.followed_by

        # Once it was completed, lock score
        if not self.completed:
            # save user score and results
            while self.student_results:
                self.student_results.pop()
            for result in submit_results:
                self.student_results.append(result)

            self.runtime.publish(self, 'grade', {
                'value': self.score.raw,
                'max_value': 1,
            })

        if not self.completed and self.max_attempts > 0:
            self.num_attempts += 1

        self.completed = completed is True

        raw_score = self.score.raw

        self.publish_event_from_dict('xblock.mentoring.submitted', {
            'num_attempts': self.num_attempts,
            'submitted_answer': submissions,
            'grade': raw_score,
        })

        return {
            'results': submit_results,
            'completed': self.completed,
            'attempted': self.attempted,
            'message': message,
            'max_attempts': self.max_attempts,
            'num_attempts': self.num_attempts
        }

    def handleAssessmentSubmit(self, submissions, suffix):
        completed = False
        current_child = None
        children = [child for child in self.get_children_objects()
                    if not isinstance(child, self.FLOATING_BLOCKS)]

        assessment_message = None

        for child in children:
            if child.name and child.name in submissions:
                submission = submissions[child.name]

                # Assessment mode doesn't allow to modify answers
                # This will get the student back at the step he should be
                current_child = child
                step = children.index(child)
                if self.step > step or self.max_attempts_reached:
                    step = self.step
                    completed = False
                    break

                self.step = step + 1

                child_result = child.submit(submission)
                if 'tips' in child_result:
                    del child_result['tips']
                self.student_results.append([child.name, child_result])
                child.save()
                completed = child_result['status']

        event_data = {}

        score = self.score

        if current_child == self.steps[-1]:
            log.info(u'Last assessment step submitted: {}'.format(submissions))
            if not self.max_attempts_reached:
                self.runtime.publish(self, 'grade', {
                    'value': score.raw,
                    'max_value': 1,
                    'score_type': 'proficiency',
                })
                event_data['final_grade'] = score.raw
                assessment_message = self.assessment_message

            self.num_attempts += 1
            self.completed = True

        event_data['exercise_id'] = current_child.name
        event_data['num_attempts'] = self.num_attempts
        event_data['submitted_answer'] = submissions

        self.publish_event_from_dict('xblock.mentoring.assessment.submitted', event_data)

        return {
            'completed': completed,
            'attempted': self.attempted,
            'max_attempts': self.max_attempts,
            'num_attempts': self.num_attempts,
            'step': self.step,
            'score': score.percentage,
            'correct_answer': len(score.correct),
            'incorrect_answer': len(score.incorrect),
            'partially_correct_answer': len(score.partially_correct),
            'extended_feedback': self.show_extended_feedback() or '',
            'correct': self.correct_json(stringify=False),
            'incorrect': self.incorrect_json(stringify=False),
            'partial': self.partial_json(stringify=False),
            'assessment_message': assessment_message,
        }

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

        if self.max_attempts_reached:
            return {
                'result': 'error',
                'message': 'max attempts reached'
            }

        # reset
        self.step = 0
        self.completed = False

        while self.student_results:
            self.student_results.pop()

        return {
            'result': 'success'
        }

    @property
    def max_attempts_reached(self):
        return self.max_attempts > 0 and self.num_attempts >= self.max_attempts

    def get_message_fragment(self, message_type):
        for child in self.get_children_objects():
            if isinstance(child, MentoringMessageBlock) and child.type == message_type:
                frag = self.render_child(child, 'mentoring_view', {})
                return self.fragment_text_rewriting(frag)

    def get_message_html(self, message_type):
        fragment = self.get_message_fragment(message_type)
        if fragment:
            return fragment.body_html()
        else:
            return ''

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

        fragment.initialize_js('MentoringEditBlock')

        return fragment

    @XBlock.json_handler
    def studio_submit(self, submissions, suffix=''):
        log.info(u'Received studio submissions: {}'.format(submissions))

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

        else:
            success = True
            root = content.getroot()
            if 'mode' in root.attrib:
                if root.attrib['mode'] not in self.MENTORING_MODES:
                    response = {
                        'result': 'error',
                        'message': "Invalid mentoring mode: should be 'standard' or 'assessment'"
                    }
                    success = False
                elif root.attrib['mode'] == 'assessment' and 'max_attempts' not in root.attrib:
                    # assessment has a default of 2 max_attempts
                    root.attrib['max_attempts'] = '2'

            if success:
                response = {
                    'result': 'success',
                }
                self.xml_content = etree.tostring(content, pretty_print=True)

        log.debug(u'Response from Studio: {}'.format(response))
        return response

    @property
    def url_name_with_default(self):
        """
        Ensure the `url_name` is set to a unique, non-empty value.
        This should ideally be handled by Studio, but we need to declare the attribute
        to be able to use it from the workbench, and when this happen Studio doesn't set
        a unique default value - this property gives either the set value, or if none is set
        a randomized default value
        """
        if self.url_name == 'mentoring-default':
            return 'mentoring-{}'.format(uuid.uuid4())
        else:
            return self.url_name

    @staticmethod
    def workbench_scenarios():
        """
        Scenarios displayed by the workbench. Load them from external (private) repository
        """
        return loader.load_scenarios_from_path('templates/xml')
Beispiel #29
0
class XBlockDataMixin:
    """
    Mixin XBlock field data
    """
    # pylint: disable=too-few-public-methods

    display_name = String(
        display_name=_('Display Name'),
        help=_('The display name for this component.'),
        default=_('SQL Problem'),
        scope=Scope.content,
    )
    dataset = String(
        display_name=_('Dataset'),
        help=_('Which initial dataset/database to be used for queries'),
        default='rating',
        scope=Scope.content,
        values=list(DATABASES),
    )
    answer_query = String(
        display_name=_('Answer Query'),
        help=_('A correct response SQL query'),
        default='',
        scope=Scope.content,
        multiline_editor=True,
    )
    verify_query = String(
        display_name=_('Verify Query'),
        help=_(
            'A secondary verification SQL query, to be used if the '
            'answer_query modifies the database (UPDATE, INSERT, DELETE, etc.)'
        ),
        default='',
        scope=Scope.content,
        multiline_editor=True,
    )
    is_ordered = Boolean(
        display_name=_('Is Ordered?'),
        help=_('Should results be in order?'),
        default=False,
        scope=Scope.content,
    )
    editable_fields = [
        'answer_query',
        'dataset',
        'display_name',
        'verify_query',
        'is_ordered',
        'prompt',
        'weight',
    ]
    prompt = String(
        display_name=_('Prompt'),
        help=_('Explanatory text to accompany the problem'),
        default='',
        scope=Scope.content,
    )
    raw_response = String(
        display_name=_('Submission Query'),
        help=_('A Submission Query'),
        default='',
        scope=Scope.user_state,
    )

    def provide_context(self, context):  # pragma: no cover
        """
        Build a context dictionary to render the student view
        """
        context = context or {}
        context = dict(context)
        error_class = ''
        if not bool(self.score) and bool(self.raw_response):
            error_class = 'error'
        context.update({
            'display_name': self.display_name,
            'prompt': self.prompt,
            'answer': self.raw_response,
            'score': self.score,
            'score_weighted': int(self.score * self.weight),
            'max_score': int(self.max_score()),
            'error_class': error_class,
            'raw_response': self.raw_response,
            'verify_query': self.verify_query,
        })
        return context
Beispiel #30
0
class DragAndDropBlock(ScorableXBlockMixin, XBlock, XBlockWithSettingsMixin,
                       ThemableXBlockMixin):
    """
    XBlock that implements a friendly Drag-and-Drop problem
    """

    CATEGORY = "drag-and-drop-v2"

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    block_settings_key = 'drag-and-drop-v2'

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

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

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

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

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

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

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

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

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

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

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

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

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

        self.include_theme_files(fragment)

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

        return fragment

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

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

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

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

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

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

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

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

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

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

        return fragment

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

        return {
            'result': 'success',
        }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        return self._get_correct_state()

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

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

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

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

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

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

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

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

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

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

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

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

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

        feedback_msgs = []

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

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

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

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

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

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

        feedback_msgs.append(
            FeedbackMessage(problem_feedback_message, problem_feedback_class))

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

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

        return feedback_msgs, misplaced_ids

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

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

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

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

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

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

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

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

        return {}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        return {'items': state}

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

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

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

        return state

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

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

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

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

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

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

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

        return correct_count, total_count

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

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

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

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

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

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

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

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

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

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

    @staticmethod
    def workbench_scenarios():
        """
        A canned scenario for display in the workbench.
        """
        return [
            ("Drag-and-drop-v2 standard",
             "<vertical_demo><drag-and-drop-v2/></vertical_demo>"),
            ("Drag-and-drop-v2 assessment",
             "<vertical_demo><drag-and-drop-v2 mode='assessment' max_attempts='3'/></vertical_demo>"
             ),
        ]