Exemplo n.º 1
0
    def test_none(self):
        # test for NoneType for to_string and from_string methods
        test_field = String(enforce_type=True)

        result_to_string = test_field.to_string(None)
        self.assertEquals(result_to_string, '')

        result_from_string = test_field.from_string(None)
        self.assertEquals(result_from_string, '')
Exemplo n.º 2
0
class MarkdownXBlock(StudioEditableXBlockMixin, XBlockWithSettingsMixin,
                     XBlock):
    """
    This XBlock provides content editing in Markdown and displays it in HTML.
    """

    display_name = String(display_name=_('Display Name'),
                          help=_('The display name for this component.'),
                          scope=Scope.settings,
                          default=_('Markdown'))
    data = String(help=_('The Markdown content for this module'),
                  default=u'',
                  scope=Scope.content)
    editor = 'markdown'
    editable_fields = ('display_name', )

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

    @XBlock.supports('multi_device')
    def student_view(self, context=None):  # pylint: disable=unused-argument
        """
        Return a fragment that contains the html for the student view.
        """
        frag = Fragment()
        frag.content = xblock_loader.render_django_template(
            'static/html/lms.html', {'self': self})

        frag.add_css(
            self.resource_string('public/plugins/codesample/css/prism.css'))
        frag.add_javascript(
            self.resource_string('public/plugins/codesample/js/prism.js'))

        frag.add_css(self.resource_string('static/css/pygments.css'))

        return frag

    def studio_view(self, context=None):  # pylint: disable=unused-argument
        """
        Return a fragment that contains the html for the Studio view.
        """
        frag = Fragment()
        settings_fields = self.get_editable_fields()
        settings_page = loader.render_django_template(
            'templates/studio_edit.html', {'fields': settings_fields})
        context = {
            'self': self,
            'settings_page': settings_page,
        }

        frag.content = xblock_loader.render_django_template(
            'static/html/studio.html', context)

        self.add_stylesheets(frag)
        self.add_scripts(frag)

        js_data = {
            'editor': self.editor,
            'skin_url': self.runtime.local_resource_url(self, 'public/skin'),
            'external_plugins': self.get_editor_plugins()
        }
        frag.initialize_js('MarkdownXBlock', js_data)

        return frag

    @XBlock.json_handler
    def update_content(self, data, suffix=''):  # pylint: disable=unused-argument
        """
        Update the saved HTML data with the new HTML passed in the JSON 'content' field.
        """
        self.data = data['content']

        return {'content': self.data}

    @staticmethod
    def workbench_scenarios():
        """A canned scenario for display in the workbench."""
        return [
            ('MarkdownXBlock', """<vertical_demo>
                    <markdown data="
                        # This is h1
                        ## This is h2
                        ```
                        This is a code block
                        ```
                        * This is
                        * an unordered
                        * list
                        This is a regular paragraph
                        1. This is
                        1. an ordered
                        1. list
                        *This is italic*
                        **This is bold**
                    "/>
                </vertical_demo>
             """),
        ]

    def add_stylesheets(self, frag):
        """
        A helper method to add all necessary styles to the fragment.
        :param frag: The fragment that will hold the scripts.
        """
        frag.add_css(self.resource_string('static/css/html.css'))

        frag.add_css(
            self.resource_string(
                'public/plugins/codemirror/codemirror-4.8/lib/codemirror.css'))

    def add_scripts(self, frag):
        """
        A helper method to add all necessary scripts to the fragment.
        :param frag: The fragment that will hold the scripts.
        """
        frag.add_javascript(
            self.resource_string('static/js/tinymce/tinymce.min.js'))
        frag.add_javascript(
            self.resource_string(
                'static/js/tinymce/themes/modern/theme.min.js'))
        frag.add_javascript(self.resource_string('static/js/html.js'))
        frag.add_javascript(loader.load_unicode('public/studio_edit.js'))

        code_mirror_dir = 'public/plugins/codemirror/codemirror-4.8/'

        frag.add_javascript(
            self.resource_string(code_mirror_dir + 'lib/codemirror.js'))
        frag.add_javascript(
            self.resource_string(code_mirror_dir +
                                 'mode/markdown/markdown.js'))

    def get_editor_plugins(self):
        """
        This method will generate a list of external plugins urls to be used in TinyMCE editor.
        These plugins should live in `public` directory for us to generate URLs for.

        const PLUGINS_DIR = "/resource/html5/public/plugins/";
        const EXTERNAL_PLUGINS = PLUGINS.map(function(p) { return PLUGINS_DIR + p + "/plugin.min.js" });

        :return: A list of URLs
        """
        plugin_path = 'public/plugins/{plugin}/plugin.min.js'
        plugins = [
            'codesample', 'image', 'link', 'lists', 'textcolor', 'codemirror'
        ]

        return {
            plugin:
            self.runtime.local_resource_url(self,
                                            plugin_path.format(plugin=plugin))
            for plugin in plugins
        }

    def substitute_keywords(self, html):
        """
        Replaces all %%-encoded words using KEYWORD_FUNCTION_MAP mapping functions.

        Iterates through all keywords that must be substituted and replaces them by calling the corresponding functions
        stored in `keywords`. If the function throws a specified exception, the substitution is not performed.

        Functions stored in `keywords` must either:
            - return a replacement string
            - throw `KeyError` or `AttributeError`, `TypeError`.
        """
        data = html
        system = getattr(self, 'system', None)
        if not system:  # This shouldn't happen, but if `system` is missing, then skip substituting keywords.
            return data

        keywords = {
            '%%USER_ID%%': lambda: getattr(system, 'anonymous_student_id'),
            '%%COURSE_ID%%': lambda: getattr(system, 'course_id').html_id(),
        }

        for key, substitutor in keywords.items():
            if key in data:
                try:
                    data = data.replace(key, substitutor())
                except (KeyError, AttributeError, TypeError):
                    # Do not replace the keyword when substitutor is not present.
                    pass

        return data

    @property
    def html(self):
        """
        A property that returns the markdown content data as html.
        """
        settings = get_xblock_settings()
        extras = settings.get("extras", DEFAULT_EXTRAS)
        safe_mode = settings.get("safe_mode", True)

        html = markdown2.markdown(self.data,
                                  extras=extras,
                                  safe_mode=safe_mode)

        html = self.substitute_keywords(html)

        return html

    def get_editable_fields(self):
        """
        This method extracts the editable fields from this XBlock and returns them after validating them.

        Part of this method's copied from StudioEditableXBlockMixin#submit_studio_edits
        with some modifications..
        :return: A list of the editable fields with the information that
                the template needs to render a form field for them.

        """
        fields = []

        # Build a list of all the fields that can be edited:
        for field_name in self.editable_fields:
            field = self.fields[field_name]  # pylint: disable=unsubscriptable-object
            assert field.scope in (Scope.content, Scope.settings), (
                'Only Scope.content or Scope.settings fields can be used with '
                'StudioEditableXBlockMixin. Other scopes are for user-specific data and are '
                'not generally created/configured by content authors in Studio.'
            )
            field_info = self._make_field_info(field_name, field)
            if field_info is not None:
                fields.append(field_info)

        return fields
Exemplo n.º 3
0
class RecapXBlock(StudioEditableXBlockMixin, XBlock, XBlockWithSettingsMixin):
    """
    RecapXblock allows users to download a PDF copy of their answers to supported
    Xblock types. Supported types include freetextresponse and "problem".
    """

    display_name = String(
        display_name="Display Name",
        help="This is the name of the component",
        scope=Scope.settings,
        default="Recap"
    )

    xblock_list = List(
        display_name="Problems",
        help="Component ID's of the XBlocks you wish to include in the summary.",
        allow_reset=False,
        scope=Scope.settings
    )

    string_html = String(
        display_name="Layout",
        help="Include HTML formatting (introductory paragraphs or headings)that"
        " you would like to accompany the summary of questions and answers.",
        multiline_editor='html',
        default="<p>[[CONTENT]]</p>",
        scope=Scope.settings
    )

    allow_download = Boolean(
        display_name="Allow Download",
        help="Allow the user to download a pdf summary",
        default=True,
        scope=Scope.settings,
    )

    download_text = String(
        display_name="Download Button Text",
        help="Text to display on the download button",
        default="Download",
        scope=Scope.settings,
    )

    html_file = String(
        display_name="HTML File.",
        help="""
            Custom html template to wrap the content generated by the Xblock.
            If you have uploaded a file, the layout section will be deactivated.
        """,
        default=None,
        scope=Scope.settings,
    )

    css_file = String(
        display_name="CSS File.",
        help="CSS file that allow to modify the style content for the student view.",
        default=None,
        scope=Scope.settings,
    )

    editable_fields = (
        'display_name',
        'xblock_list',
        'html_file',
        'css_file',
        'string_html',
        'allow_download',
        'download_text',
    )
    show_in_read_only_mode = True

    def get_recap_course_blocks(self, course_key):
        """
        Retrieve all XBlocks in the course for a particular category.

        Returns only XBlocks that are published and haven't been deleted.
        """
        # Note: we need to check if found components have been orphaned
        # due to a bug in split modulestore (PLAT-799).  Once that bug
        # is resolved, we can skip the `_is_in_course_tree()` check entirely.
        return [
            block for block in modulestore().get_items(
                course_key,
                qualifiers={"category": "recap"},
            )
        ]

    def validate_field_data(self, validation, data):
        """
        Validate this block's field data. We are validating that the chosen
        freetextresponse xblocks ID's exist in the course
        """
        for x_id, x_type in data.xblock_list:
            try:
                usage_key =\
                    self.scope_ids.usage_id.course_key.make_usage_key(
                        x_type,
                        x_id
                    )
                self.runtime.get_block(usage_key)
            except Exception as e:
                logger.warn(e)
                validation.add(
                    ValidationMessage(
                        ValidationMessage.ERROR,
                        u"Component freetextresponse ID: {} does not exist.".format(x_id)
                    )
                )

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

    def get_blocks(self, xblock_list):
        for x_id, x_type in xblock_list:
            try:
                usage_key = \
                    self.scope_ids.usage_id.course_key.make_usage_key(
                        x_type,
                        x_id
                    )
                yield usage_key, x_type
            except InvalidKeyError:
                pass

    def get_blocks_filtering_list(self, xblock_list):
        filter_list = []
        for x_id, x_type in xblock_list:
            try:
                usage_key = \
                    self.scope_ids.usage_id.course_key.make_usage_key(
                        x_type,
                        x_id
                    )
                filter_list.append(usage_key)
            except InvalidKeyError:
                pass
        return filter_list

    def get_submission_key(self, usage_key, user=None):
        """
        Returns submission key needed for submissions api
        """
        try:
            logger.info('Attempting to retrieve student item dictionary.')
            if not user:
                user = self.runtime.get_real_user(self.runtime.anonymous_student_id)

            student_item_dictionary = dict(
                student_id=user.id,
                course_id=str(usage_key.course_key),
                item_id=str(usage_key),
                item_type=usage_key.block_type,
            )
        except AttributeError:
            student_item_dictionary = ''
            logger.error('Studio cannot access self.runtime.get_real_user')
        return student_item_dictionary

    def get_submission(self, usage_key, user=None):
        """
        Returns submission from submissions api
        """
        try:
            submission_key = self.get_submission_key(usage_key, user)
            submission = api.get_submissions(submission_key, limit=1)
            if submission is not None:
                logger.info(
                    'Attempting to retreive submission from submissions api.'
                )
            value = submission[0]["answer"]
        except IndexError:
            logger.warn(
                'IndexError: no submssion matched given student item dict.'
            )
            value = _("Nothing to recap.")
        return value

    def get_display_answer(self, answer):
        """
        Returns formatted answer or placeholder string
        """
        answer_str = _("Nothing to recap.")
        if answer:
            answer_str = re.sub(r'\n+', '<div></div>', str(answer))
        return answer_str

    @XBlock.supports("multi_device")
    def student_view(self, context=None):
        """
        The primary view of the RecapXBlock seen in LMS
        """
        blocks = []
        for usage_key, xblock_type in self.get_blocks(self.xblock_list):
            block = self.runtime.get_block(usage_key)

            if hasattr(block, 'custom_report_format'):
                question = str(block.display_name)

                try:
                    user = self.runtime.get_real_user(self.runtime.anonymous_student_id)
                except TypeError:
                    user = None

                answer = block.custom_report_format(
                    user=user,
                    block=block,
                )

                blocks.append((question, answer))
            elif xblock_type == 'problem':
                answer = u""
                question = u""
                try:
                    question = str(block.display_name)
                    answer = self.get_submission(usage_key)
                    if answer is None:
                        answer = block.lcp.get_question_answer_text()
                    blocks.append((question, answer))
                except Exception as e:
                    logger.warn(str(e))
                    answer = block.lcp.get_question_answer_text()
                    blocks.append((question, answer))

        layout = self.get_user_layout(blocks)

        idArray = self.scope_ids.usage_id._to_string().split('@')
        xblockId = idArray[len(idArray) - 1]
        context = {
            'recap_answers_id': 'recap_answers_' + xblockId,
            'recap_editor_id': 'recap_editor_' + xblockId,
            'recap_cmd_id': 'recap_cmd_' + xblockId,
            'blocks': blocks,
            'layout': layout,
            'allow_download': self.allow_download,
            'download_text': self.download_text,
        }

        frag = Fragment(
            loader.render_django_template(
                "static/html/recap.html",
                context).format(self=self)
        )
        if (self.css_file):
            frag.add_css(self.css_file)

        frag.add_css(self.resource_string("static/css/recap.css"))
        frag.add_javascript_url(
            self.runtime.local_resource_url(
                self,
                'public/FileSaver.js/FileSaver.min.js'
            )
        )
        frag.add_javascript_url(
            self.runtime.local_resource_url(
                self,
                'public/jsPDF-1.3.2/jspdf.min.js'
            )
        )

        frag.add_javascript_url(
            self.runtime.local_resource_url(
                self,
                'public/jsPDF-1.3.2/html2canvas.min.js'
            )
        )
        frag.add_javascript_url(
            self.runtime.local_resource_url(
                self,
                'public/jsPDF-1.3.2/html2pdf.js'
            )
        )

        frag.add_javascript(self.resource_string("static/js/src/recap.js"))
        frag.initialize_js('RecapXBlock', {
            'recap_answers_id': 'recap_answers_' + xblockId,
            'recap_editor_id': 'recap_editor_' + xblockId,
            'recap_cmd_id': 'recap_cmd_' + xblockId,
        })

        return frag

    def get_blocks_list(self, user, block_list):
        blocks = []
        for usage_key, xblock_type in self.get_blocks(block_list):
            try:
                block = self.runtime.get_block(usage_key)

                if hasattr(block, 'custom_report_format'):
                    question = str(block.display_name)
                    answer = block.custom_report_format(
                        user=user,
                        block=block,
                    )
                    blocks.append((question, answer))
                elif xblock_type == 'problem':
                    answer = ""
                    question = ""
                    try:
                        question = str(block.display_name)
                        answer = self.get_submission(usage_key, user)
                        blocks.append((question, answer))
                    except Exception as e:
                        blocks.append((str(usage_key), str(e)))
            except Exception as e:
                logger.warn(str(e))
        return blocks

    def get_user_layout(self, blocks, user=None):
        '''
        For the Recap Instructor dashboard, get HTML layout of user's answers
        '''

        def update_layout(layout, pattern, layout_type):
            '''
            '''
            block_sets = []
            current = 0
            for m in re.finditer(pattern, layout):
                try:
                    title = blocks[int(m.group(1)) - 1][0]
                    answer = blocks[int(m.group(1)) - 1][1]

                    if layout_type == LayoutType.BLOCKS:
                        subblocks = []
                        for x in range(current, current + int(m.group(1))):
                            if len(blocks) > x:
                                subblocks.append((blocks[x][0], blocks[x][1]))
                                current += 1
                        answers = [block_layout.format(q, self.get_display_answer(a)) for q, a in subblocks]
                        qa_str = str(''.join(answers))
                    elif layout_type == LayoutType.SINGLE_BLOCK:
                        qa_str = str(block_layout).format(title, self.get_display_answer(answer))
                    elif layout_type == LayoutType.TITLE:
                        qa_str = title
                    elif layout_type == LayoutType.ANSWER:
                        qa_str = str(self.get_display_answer(answer))

                    block_sets.append((m.start(0), m.end(0), qa_str))
                except IndexError as error:
                    logger.info('Update layout error: %s', error)
                    pass

            for start, end, string in reversed(block_sets):
                layout = layout[0:start] + string + layout[end:]

            return layout

        template = self.html_file if self.html_file else self.string_html

        block_layout = (
            '<p class="recap_question"><strong>{}</strong></p>'
            '<div class="recap_answer" '
            'style="page-break-before:always">{}</div>'
        )
        qa_str = str(
            ''.join(
                str(block_layout).format(
                    q,
                    self.get_display_answer(a)
                ) for q, a in blocks
            )
        )
        layout = template.replace('[[CONTENT]]', qa_str)

        pattern = re.compile(r'\[\[SINGLEBLOCK\(([0-9]+)\)\.(TITLE)\]\]')
        layout = update_layout(layout, pattern, LayoutType.TITLE)
        pattern = re.compile(r'\[\[SINGLEBLOCK\(([0-9]+)\)\.(ANSWER)\]\]')
        layout = update_layout(layout, pattern, LayoutType.ANSWER)

        pattern = re.compile(r'\[\[SINGLEBLOCK\(([0-9]+)\)\]\]')
        layout = update_layout(layout, pattern, LayoutType.SINGLE_BLOCK)

        pattern = re.compile(r'\[\[BLOCKS\(([0-9]+)\)\]\]')
        return update_layout(layout, pattern, LayoutType.BLOCKS)

    def studio_view(self, context):
        """
        Render a form for editing this XBlock
        """
        frag = Fragment()
        context = {
            'fields': [],
            'xblock_list': self.xblock_list,
        }
        # Build a list of all the fields that can be edited:
        for field_name in self.editable_fields:
            field = self.fields[field_name]
            assert field.scope in (Scope.content, Scope.settings), (
                "Only Scope.content or Scope.settings fields can be used with "
                "StudioEditableXBlockMixin. Other scopes are for user-specific data and are "
                "not generally created/configured by content authors in Studio."
            )
            field_info = self._make_field_info(field_name, field)
            if field_info is not None:
                context["fields"].append(field_info)
        frag.content = loader.render_django_template("static/html/recap_edit.html", context)
        frag.add_javascript(loader.load_unicode("static/js/src/recap_edit.js"))
        frag.initialize_js('StudioEditableXBlockMixin')
        return frag

    def recap_blocks_listing_view(self, context=None):
        """This view is used in the Racap tab in the LMS Instructor Dashboard
        to display all available course Recap xblocks.

        Args:
            context: contains two items:
                "recap_items" - all course items with names and parents, example:
                    [{"parent_name": "Vertical name",
                      "name": "Recap Display Name",
                     }, ...]
        Returns:
            (Fragment): The HTML Fragment for this XBlock.
        """
        course_id = self.location.course_key
        recap_blocks = self.get_recap_course_blocks(course_id)
        recap_name_list = []

        for block in recap_blocks:
            recap_name_list.append((block.display_name, block.xblock_list))

        make_pdf_json = reverse('xblock_handler', args=[course_id, block.location, 'make_pdf_json'])
        refresh_table = reverse('xblock_handler', args=[course_id, block.location, 'refresh_table'])

        user = self.runtime.get_real_user(self.runtime.anonymous_student_id)
        lang_prefs = get_user_preference(user, LANGUAGE_KEY)

        context_dict = {
            "make_pdf_json": make_pdf_json,
            "refresh_table": refresh_table,
            "recap_name_list": recap_name_list,
            "lang_prefs": lang_prefs
        }

        instructor_dashboard_fragment = Fragment()
        instructor_dashboard_fragment.content = loader.render_django_template(
            'static/html/recap_dashboard.html',
            context_dict
        )
        instructor_dashboard_fragment.add_css(
            self.resource_string("static/css/recap.css")
        )
        instructor_dashboard_fragment.add_css(
            self.resource_string("public/DataTables/css/jquery.dataTables.css")
        )
        instructor_dashboard_fragment.add_javascript_url(
            self.runtime.local_resource_url(
                self,
                'public/FileSaver.js/FileSaver.min.js'
            )
        )
        instructor_dashboard_fragment.add_javascript_url(
            self.runtime.local_resource_url(
                self,
                'public/jsPDF-1.3.2/jspdf.min.js'
            )
        )
        instructor_dashboard_fragment.add_javascript_url(
            self.runtime.local_resource_url(
                self,
                'public/jsPDF-1.3.2/html2canvas.min.js'
            )
        )
        instructor_dashboard_fragment.add_javascript_url(
            self.runtime.local_resource_url(
                self,
                'public/jsPDF-1.3.2/html2pdf.js'
            )
        )
        instructor_dashboard_fragment.add_javascript_url(
            self.runtime.local_resource_url(
                self,
                'public/DataTables/js/jquery.dataTables.js'
            )
        )

        instructor_dashboard_fragment.add_javascript_url(
            self.runtime.local_resource_url(
                self,
                "public/recap_dashboard.js"
            )
        )
        instructor_dashboard_fragment.initialize_js('RecapDashboard')

        return instructor_dashboard_fragment

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

        '''
        This is a XBlock json handler for the async pdf download
        '''
        user = User.objects.get(id=data['user_id'])
        which_blocks = ast.literal_eval(data['these_blocks'])
        blocks = self.get_blocks_list(user, which_blocks)
        html = self.get_user_layout(blocks, user)

        if self.css_file:
            html = '<style>{}</style>{}'.format(self.css_file, html)

        if "<h3>" in html:
            html = re.sub("<h3>(.*?)<\/h3>","<h3>{}</h3>".format(data['document_heading']), html)
        else:
            html = u"<h3>{}</h3> \n {}".format(str(data['document_heading']), str(html))

        return {'html': html, 'user_name': user.username}

    @XBlock.json_handler
    def refresh_table(self, data, suffix=''):
        """ Complete HTML view of the XBlock, for refresh by client """
        course_id = self.location.course_key
        recap_blocks = self.get_recap_course_blocks(course_id)
        selected_recap_index = data["recap_id"]

        block_ids = []
        for block in recap_blocks:
            block_ids.append(self.get_blocks_filtering_list(block.xblock_list))

        query_list = []
        for block_id in block_ids[selected_recap_index]:
            query_list.append(Submission.objects.filter(
                student_item__item_id=block_id
            ).values_list('student_item__student_id', flat=True))

        student_ids_intersection = list(set.intersection(*map(set, query_list)))

        downloadable_users = User.objects.filter(
            courseenrollment__course_id=course_id,
            courseenrollment__is_active=1,
            id__in=map(int, student_ids_intersection)
        ).values('username', 'email', 'id')

        return {'data': list(downloadable_users)}

    @staticmethod
    def workbench_scenarios():
        """A canned scenario for display in the workbench."""
        return [
            ("RecapXBlock",
             """<recap/>
             """),
            ("Multiple RecapXBlock",
             """<vertical_demo>
                <recap/>
                <recap/>
                <recap/>
                </vertical_demo>
             """),
        ]
Exemplo n.º 4
0
class StaffGradedAssignmentXBlock(StudioEditableXBlockMixin,
                                  ShowAnswerXBlockMixin, XBlock):
    """
    This block defines a Staff Graded Assignment.  Students are shown a rubric
    and invited to upload a file which is then graded by staff.
    """
    has_score = True
    icon_class = 'problem'
    STUDENT_FILEUPLOAD_MAX_SIZE = 4 * 1000 * 1000  # 4 MB
    editable_fields = ('display_name', 'points', 'weight', 'file_types',
                       'showanswer', 'solution')

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

    file_types = String(
        display_name=_("Accepted filetypes"),
        default=_(''),
        scope=Scope.settings,
        help=
        _("Accepted filetypes, like: ^image\/(gif|jpe?g|png)$, ^application\/(pdf|document)$"
          ))

    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)

    points = Integer(
        display_name=_("Maximum score"),
        help=_("Maximum grade score given to assignment by staff."),
        default=100,
        scope=Scope.settings)

    staff_score = Integer(
        display_name=_("Score assigned by non-instructor staff"),
        help=_("Score will need to be approved by instructor before being "
               "published."),
        default=None,
        scope=Scope.settings)

    comment = String(display_name=_("Instructor comment"),
                     default='',
                     scope=Scope.user_state,
                     help=_("Feedback given to student by instructor."))

    annotated_sha1 = String(
        display_name=_("Annotated SHA1"),
        scope=Scope.user_state,
        default=None,
        help=_("sha1 of the annotated file uploaded by the instructor for "
               "this assignment."))

    annotated_filename = String(
        display_name=_("Annotated file name"),
        scope=Scope.user_state,
        default=None,
        help=_("The name of the annotated file uploaded for this assignment."))

    annotated_mimetype = String(
        display_name=_("Mime type of annotated file"),
        scope=Scope.user_state,
        default=None,
        help=_(
            "The mimetype of the annotated file uploaded for this assignment.")
    )

    annotated_timestamp = DateTime(
        display_name=_("Timestamp"),
        scope=Scope.user_state,
        default=None,
        help=_("When the annotated file was uploaded"))

    @classmethod
    def student_upload_max_size(cls):
        """
        returns max file size limit in system
        """
        return getattr(settings, "STUDENT_FILEUPLOAD_MAX_SIZE",
                       cls.STUDENT_FILEUPLOAD_MAX_SIZE)

    @classmethod
    def file_size_over_limit(cls, file_obj):
        """
        checks if file size is under limit.
        """
        file_obj.seek(0, os.SEEK_END)
        return file_obj.tell() > cls.student_upload_max_size()

    @classmethod
    def parse_xml(cls, node, runtime, keys, id_generator):
        """
        Override default serialization to handle <solution /> elements
        """
        block = runtime.construct_xblock_from_class(cls, keys)

        for child in node:
            if child.tag == "solution":
                # convert child elements of <solution> into HTML for display
                block.solution = ''.join(
                    etree.tostring(subchild) for subchild in child)

        # Attributes become fields.
        # Note that a solution attribute here will override any solution XML element
        for name, value in node.items():  # lxml has no iteritems
            cls._set_field_if_present(block, name, value, {})

        return block

    def add_xml_to_node(self, node):
        """
        Override default serialization to output solution field as a separate child element.
        """
        super(StaffGradedAssignmentXBlock, self).add_xml_to_node(node)

        if 'solution' in node.attrib:
            # Try outputting it as an XML element if we can
            solution = node.attrib['solution']
            wrapped = "<solution>{}</solution>".format(solution)
            try:
                child = etree.fromstring(wrapped)
            except:  # pylint: disable=bare-except
                # Parsing exception, leave the solution as an attribute
                pass
            else:
                node.append(child)
                del node.attrib['solution']

    @XBlock.json_handler
    def save_sga(self, data, suffix=''):
        # pylint: disable=unused-argument
        """
        Persist block data when updating settings in studio.
        """
        self.display_name = data.get('display_name', self.display_name)

        # Validate points before saving
        points = data.get('points', self.points)
        # Check that we are an int
        try:
            points = int(points)
        except ValueError:
            raise JsonHandlerError(400, 'Points must be an integer')
        # Check that we are positive
        if points < 0:
            raise JsonHandlerError(400, 'Points must be a positive integer')
        self.points = points

        # Validate weight before saving
        weight = data.get('weight', self.weight)
        # Check that weight is a float.
        if weight:
            try:
                weight = float(weight)
            except ValueError:
                raise JsonHandlerError(400, 'Weight must be a decimal number')
            # Check that we are positive
            if weight < 0:
                raise JsonHandlerError(
                    400, 'Weight must be a positive decimal number')
        self.weight = weight

    @XBlock.handler
    def upload_assignment(self, request, suffix=''):
        # pylint: disable=unused-argument, protected-access
        """
        Save a students submission file.
        """
        require(self.upload_allowed())
        user = self.get_real_user()
        require(user)
        upload = request.params['assignment']
        sha1 = get_sha1(upload.file)
        if self.file_size_over_limit(upload.file):
            raise JsonHandlerError(
                413, 'Unable to upload file. Max size limit is {size}'.format(
                    size=self.student_upload_max_size()))
        # Uploading an assignment represents a change of state with this user in this block,
        # so we need to ensure that the user has a StudentModule record, which represents that state.
        self.get_or_create_student_module(user)
        answer = {
            "sha1": sha1,
            "filename": upload.file.name,
            "mimetype": mimetypes.guess_type(upload.file.name)[0],
            "finalized": False
        }
        student_item_dict = self.get_student_item_dict()
        submissions_api.create_submission(student_item_dict, answer)
        path = self.file_storage_path(sha1, upload.file.name)
        log.info("Saving file: %s at path: %s for user: %s", upload.file.name,
                 path, user.username)
        if default_storage.exists(path):
            # save latest submission
            default_storage.delete(path)
        default_storage.save(path, File(upload.file))
        return Response(json_body=self.student_state())

    @XBlock.handler
    def finalize_uploaded_assignment(self, request, suffix=''):
        # pylint: disable=unused-argument
        """
        Finalize a student's uploaded submission. This prevents further uploads for the
        given block, and makes the submission available to instructors for grading
        """
        submission_data = self.get_submission()
        require(self.upload_allowed(submission_data=submission_data))
        # Editing the Submission record directly since the API doesn't support it
        submission = Submission.objects.get(uuid=submission_data['uuid'])
        if not submission.answer.get('finalized'):
            submission.answer['finalized'] = True
            submission.submitted_at = django_now()
            submission.save()
        return Response(json_body=self.student_state())

    @XBlock.handler
    def staff_upload_annotated(self, request, suffix=''):
        # pylint: disable=unused-argument
        """
        Save annotated assignment from staff.
        """
        require(self.is_course_staff())
        upload = request.params['annotated']
        sha1 = get_sha1(upload.file)
        if self.file_size_over_limit(upload.file):
            raise JsonHandlerError(
                413, 'Unable to upload file. Max size limit is {size}'.format(
                    size=self.student_upload_max_size()))
        module = self.get_student_module(request.params['module_id'])
        state = json.loads(module.state)
        state['annotated_sha1'] = sha1
        state['annotated_filename'] = filename = upload.file.name
        state['annotated_mimetype'] = mimetypes.guess_type(upload.file.name)[0]
        state['annotated_timestamp'] = utcnow().strftime(
            DateTime.DATETIME_FORMAT)
        path = self.file_storage_path(sha1, filename)
        if not default_storage.exists(path):
            default_storage.save(path, File(upload.file))
        module.state = json.dumps(state)
        module.save()
        log.info("staff_upload_annotated for course:%s module:%s student:%s ",
                 module.course_id, module.module_state_key,
                 module.student.username)
        return Response(json_body=self.staff_grading_data())

    @XBlock.handler
    def download_assignment(self, request, suffix=''):
        # pylint: disable=unused-argument
        """
        Fetch student assignment from storage and return it.
        """
        answer = self.get_submission()['answer']
        path = self.file_storage_path(answer['sha1'], answer['filename'])
        return self.download(path, answer['mimetype'], answer['filename'])

    @XBlock.handler
    def download_annotated(self, request, suffix=''):
        # pylint: disable=unused-argument
        """
        Fetch assignment with staff annotations from storage and return it.
        """
        path = self.file_storage_path(
            self.annotated_sha1,
            self.annotated_filename,
        )
        return self.download(path, self.annotated_mimetype,
                             self.annotated_filename)

    @XBlock.handler
    def staff_download(self, request, suffix=''):
        # pylint: disable=unused-argument
        """
        Return an assignment file requested by staff.
        """
        require(self.is_course_staff())
        submission = self.get_submission(request.params['student_id'])
        answer = submission['answer']
        path = self.file_storage_path(answer['sha1'], answer['filename'])
        return self.download(path,
                             answer['mimetype'],
                             answer['filename'],
                             require_staff=True)

    @XBlock.handler
    def staff_download_annotated(self, request, suffix=''):
        # pylint: disable=unused-argument
        """
        Return annotated assignment file requested by staff.
        """
        require(self.is_course_staff())
        module = self.get_student_module(request.params['module_id'])
        state = json.loads(module.state)
        path = self.file_storage_path(state['annotated_sha1'],
                                      state['annotated_filename'])
        return self.download(path,
                             state['annotated_mimetype'],
                             state['annotated_filename'],
                             require_staff=True)

    @XBlock.handler
    def get_staff_grading_data(self, request, suffix=''):
        # pylint: disable=unused-argument
        """
        Return the html for the staff grading view
        """
        require(self.is_course_staff())
        return Response(json_body=self.staff_grading_data())

    @XBlock.handler
    def enter_grade(self, request, suffix=''):
        # pylint: disable=unused-argument
        """
        Persist a score for a student given by staff.
        """
        require(self.is_course_staff())
        score = request.params.get('grade', None)
        module = self.get_student_module(request.params['module_id'])
        if not score:
            return Response(json_body=self.validate_score_message(
                module.course_id, module.student.username))

        state = json.loads(module.state)
        try:
            score = int(score)
        except ValueError:
            return Response(json_body=self.validate_score_message(
                module.course_id, module.student.username))

        if self.is_instructor():
            uuid = request.params['submission_id']
            submissions_api.set_score(uuid, score, self.max_score())
        else:
            state['staff_score'] = score
        state['comment'] = request.params.get('comment', '')
        module.state = json.dumps(state)
        module.save()
        log.info("enter_grade for course:%s module:%s student:%s",
                 module.course_id, module.module_state_key,
                 module.student.username)

        return Response(json_body=self.staff_grading_data())

    @XBlock.handler
    def remove_grade(self, request, suffix=''):
        # pylint: disable=unused-argument
        """
        Reset a students score request by staff.
        """
        require(self.is_course_staff())
        student_id = request.params['student_id']
        submissions_api.reset_score(student_id, self.block_course_id,
                                    self.block_id)
        module = self.get_student_module(request.params['module_id'])
        state = json.loads(module.state)
        state['staff_score'] = None
        state['comment'] = ''
        state['annotated_sha1'] = None
        state['annotated_filename'] = None
        state['annotated_mimetype'] = None
        state['annotated_timestamp'] = None
        module.state = json.dumps(state)
        module.save()
        log.info("remove_grade for course:%s module:%s student:%s",
                 module.course_id, module.module_state_key,
                 module.student.username)
        return Response(json_body=self.staff_grading_data())

    @XBlock.handler
    def prepare_download_submissions(self, request, suffix=''):  # pylint: disable=unused-argument
        """
        Runs a async task that collects submissions in background and zip them.
        """
        # pylint: disable=no-member
        require(self.is_course_staff())
        user = self.get_real_user()
        require(user)
        zip_file_ready = False
        location = six.text_type(self.location)

        if self.is_zip_file_available(user):
            log.info(
                "Zip file already available for block: %s for instructor: %s",
                location, user.username)
            assignments = self.get_sorted_submissions()
            if assignments:
                last_assignment_date = assignments[0]['timestamp'].astimezone(
                    pytz.utc)
                zip_file_path = get_zip_file_path(user.username,
                                                  self.block_course_id,
                                                  self.block_id, self.location)
                zip_file_time = get_file_modified_time_utc(zip_file_path)
                log.info(
                    "Zip file modified time: %s, last zip file time: %s for block: %s for instructor: %s",
                    zip_file_time, last_assignment_date, location,
                    user.username)
                # if last zip file is older the last submission then recreate task
                if zip_file_time >= last_assignment_date:
                    zip_file_ready = True

                # check if some one reset submission. If yes the recreate zip file
                assignment_count = len(assignments)
                zip_count = self.count_archive_files(user)
                log.info("Zip file content count: %s, assignment count: %s",
                         zip_count, assignment_count)
                if zip_count != assignment_count:
                    zip_file_ready = False

                log.info("Zip checking result: %s", zip_file_ready)

        if not zip_file_ready:
            log.info("Creating new zip file for block: %s for instructor: %s",
                     location, user.username)
            zip_student_submissions.delay(self.block_course_id, self.block_id,
                                          location, user.username)

        return Response(json_body={"downloadable": zip_file_ready})

    @XBlock.handler
    def download_submissions(self, request, suffix=''):  # pylint: disable=unused-argument
        """
        Api for downloading zip file which consist of all students submissions.
        """
        # pylint: disable=no-member
        require(self.is_course_staff())
        user = self.get_real_user()
        require(user)
        try:
            zip_file_path = get_zip_file_path(user.username,
                                              self.block_course_id,
                                              self.block_id, self.location)
            zip_file_name = get_zip_file_name(user.username,
                                              self.block_course_id,
                                              self.block_id)
            return Response(app_iter=file_contents_iter(zip_file_path),
                            content_type='application/zip',
                            content_disposition="attachment; filename=" +
                            zip_file_name)
        except IOError:
            return Response(
                "Sorry, submissions cannot be found. Press Collect ALL Submissions button or"
                " contact {} if you issue is consistent".format(
                    settings.TECH_SUPPORT_EMAIL),
                status_code=404)

    @XBlock.handler
    def download_submissions_status(self, request, suffix=''):  # pylint: disable=unused-argument
        """
        returns True if zip file is available for download
        """
        require(self.is_course_staff())
        user = self.get_real_user()
        require(user)
        return Response(
            json_body={"zip_available": self.is_zip_file_available(user)})

    def student_view(self, context=None):
        # pylint: disable=no-member
        """
        The primary view of the StaffGradedAssignmentXBlock, shown to students
        when viewing courses.
        """
        context = {
            "student_state": json.dumps(self.student_state()),
            "id": self.location.name.replace('.', '_'),
            "max_file_size": self.student_upload_max_size(),
            "support_email": settings.TECH_SUPPORT_EMAIL,
            "file_types": self.file_types
        }
        if self.show_staff_grading_interface():
            context['is_course_staff'] = True
            self.update_staff_debug_context(context)

        fragment = Fragment()
        fragment.add_content(
            render_template('templates/staff_graded_assignment/show.html',
                            context))
        fragment.add_css(_resource("static/css/edx_sga.css"))
        fragment.add_javascript(_resource("static/js/src/edx_sga.js"))
        fragment.add_javascript(
            _resource("static/js/src/jquery.tablesorter.min.js"))
        fragment.initialize_js('StaffGradedAssignmentXBlock')
        return fragment

    def studio_view(self, context=None):  # pylint: disable=useless-super-delegation
        """
        Render a form for editing this XBlock
        """
        # this method only exists to provide context=None for backwards compat
        return super(StaffGradedAssignmentXBlock, self).studio_view(context)

    def clear_student_state(self, *args, **kwargs):
        # pylint: disable=unused-argument
        """
        For a given user, clears submissions and uploaded files for this XBlock.

        Staff users are able to delete a learner's state for a block in LMS. When that capability is
        used, the block's "clear_student_state" function is called if it exists.
        """
        student_id = kwargs['user_id']
        for submission in submissions_api.get_submissions(
                self.get_student_item_dict(student_id)):
            submission_file_sha1 = submission['answer'].get('sha1')
            submission_filename = submission['answer'].get('filename')
            submission_file_path = self.file_storage_path(
                submission_file_sha1, submission_filename)
            if default_storage.exists(submission_file_path):
                default_storage.delete(submission_file_path)
            submissions_api.reset_score(student_id,
                                        self.block_course_id,
                                        self.block_id,
                                        clear_state=True)

    def max_score(self):
        """
        Return the maximum score possible.
        """
        return self.points

    @reify
    def block_id(self):
        """
        Return the usage_id of the block.
        """
        return six.text_type(self.scope_ids.usage_id)

    @reify
    def block_course_id(self):
        """
        Return the course_id of the block.
        """
        return six.text_type(self.course_id)

    def get_student_item_dict(self, student_id=None):
        # pylint: disable=no-member
        """
        Returns dict required by the submissions app for creating and
        retrieving submissions for a particular student.
        """
        if student_id is None:
            student_id = self.xmodule_runtime.anonymous_student_id
            assert student_id != ('MOCK',
                                  "Forgot to call 'personalize' in test.")
        return {
            "student_id": student_id,
            "course_id": self.block_course_id,
            "item_id": self.block_id,
            "item_type": ITEM_TYPE,
        }

    def get_submission(self, student_id=None):
        """
        Get student's most recent submission.
        """
        submissions = submissions_api.get_submissions(
            self.get_student_item_dict(student_id))
        if submissions:
            # If I understand docs correctly, most recent submission should
            # be first
            return submissions[0]

    def get_score(self, student_id=None):
        """
        Return student's current score.
        """
        score = submissions_api.get_score(
            self.get_student_item_dict(student_id))
        if score:
            return score['points_earned']

    @reify
    def score(self):
        """
        Return score from submissions.
        """
        return self.get_score()

    def update_staff_debug_context(self, context):
        # pylint: disable=no-member
        """
        Add context info for the Staff Debug interface.
        """
        published = self.start
        context['is_released'] = published and published < utcnow()
        context['location'] = self.location
        context['category'] = type(self).__name__
        context['fields'] = [(name, field.read_from(self))
                             for name, field in self.fields.items()]

    def get_student_module(self, module_id):
        """
        Returns a StudentModule that matches the given id

        Args:
            module_id (int): The module id

        Returns:
            StudentModule: A StudentModule object
        """
        return StudentModule.objects.get(pk=module_id)

    def get_or_create_student_module(self, user):
        """
        Gets or creates a StudentModule for the given user for this block

        Returns:
            StudentModule: A StudentModule object
        """
        # pylint: disable=no-member
        student_module, created = StudentModule.objects.get_or_create(
            course_id=self.course_id,
            module_state_key=self.location,
            student=user,
            defaults={
                'state': '{}',
                'module_type': self.category,
            })
        if created:
            log.info("Created student module %s [course: %s] [student: %s]",
                     student_module.module_state_key, student_module.course_id,
                     student_module.student.username)
        return student_module

    def student_state(self):
        """
        Returns a JSON serializable representation of student's state for
        rendering in client view.
        """
        submission = self.get_submission()
        if submission:
            uploaded = {"filename": submission['answer']['filename']}
        else:
            uploaded = None

        if self.annotated_sha1:
            annotated = {"filename": force_text(self.annotated_filename)}
        else:
            annotated = None

        score = self.score
        if score is not None:
            graded = {'score': score, 'comment': force_text(self.comment)}
        else:
            graded = None

        if self.answer_available():
            solution = self.runtime.replace_urls(force_text(self.solution))
        else:
            solution = ''
        # pylint: disable=no-member
        return {
            "display_name":
            force_text(self.display_name),
            "uploaded":
            uploaded,
            "annotated":
            annotated,
            "graded":
            graded,
            "max_score":
            self.max_score(),
            "upload_allowed":
            self.upload_allowed(submission_data=submission),
            "solution":
            solution,
            "base_asset_url":
            StaticContent.get_base_url_path_for_course_assets(
                self.location.course_key),
        }

    def staff_grading_data(self):
        """
        Return student assignment information for display on the
        grading screen.
        """
        def get_student_data():
            # pylint: disable=no-member
            """
            Returns a dict of student assignment information along with
            annotated file name, student id and module id, this
            information will be used on grading screen
            """
            # Submissions doesn't have API for this, just use model directly.
            students = SubmissionsStudent.objects.filter(
                course_id=self.course_id, item_id=self.block_id)
            for student in students:
                submission = self.get_submission(student.student_id)
                if not submission:
                    continue
                user = user_by_anonymous_id(student.student_id)
                student_module = self.get_or_create_student_module(user)
                state = json.loads(student_module.state)
                score = self.get_score(student.student_id)
                approved = score is not None
                if score is None:
                    score = state.get('staff_score')
                    needs_approval = score is not None
                else:
                    needs_approval = False
                instructor = self.is_instructor()
                yield {
                    'module_id':
                    student_module.id,
                    'student_id':
                    student.student_id,
                    'submission_id':
                    submission['uuid'],
                    'username':
                    student_module.student.username,
                    'fullname':
                    student_module.student.profile.name,
                    'filename':
                    submission['answer']["filename"],
                    'timestamp':
                    submission['created_at'].strftime(
                        DateTime.DATETIME_FORMAT),
                    'score':
                    score,
                    'approved':
                    approved,
                    'needs_approval':
                    instructor and needs_approval,
                    'may_grade':
                    instructor or not approved,
                    'annotated':
                    force_text(state.get("annotated_filename", '')),
                    'comment':
                    force_text(state.get("comment", '')),
                    'finalized':
                    is_finalized_submission(submission_data=submission)
                }

        return {
            'assignments': list(get_student_data()),
            'max_score': self.max_score(),
            'display_name': force_text(self.display_name)
        }

    def get_sorted_submissions(self):
        """returns student recent assignments sorted on date"""
        assignments = []
        submissions = submissions_api.get_all_submissions(
            self.course_id, self.block_id, ITEM_TYPE)

        for submission in submissions:
            if is_finalized_submission(submission_data=submission):
                assignments.append({
                    'submission_id':
                    submission['uuid'],
                    'filename':
                    submission['answer']["filename"],
                    'timestamp':
                    submission['submitted_at'] or submission['created_at']
                })

        assignments.sort(key=lambda assignment: assignment['timestamp'],
                         reverse=True)
        return assignments

    def download(self, path, mime_type, filename, require_staff=False):
        """
        Return a file from storage and return in a Response.
        """
        try:
            content_disposition = "attachment; filename*=UTF-8''"
            content_disposition += six.moves.urllib.parse.quote(
                filename.encode('utf-8'))
            output = Response(app_iter=file_contents_iter(path),
                              content_type=mime_type,
                              content_disposition=content_disposition)
            return output
        except IOError:
            if require_staff:
                return Response("Sorry, assignment {} cannot be found at"
                                " {}. Please contact {}".format(
                                    filename.encode('utf-8'), path,
                                    settings.TECH_SUPPORT_EMAIL),
                                status_code=404)
            return Response("Sorry, the file you uploaded, {}, cannot be"
                            " found. Please try uploading it again or contact"
                            " course staff".format(filename.encode('utf-8')),
                            status_code=404)

    def validate_score_message(self, course_id, username):  # lint-amnesty, pylint: disable=missing-docstring
        # pylint: disable=no-member
        log.error(
            "enter_grade: invalid grade submitted for course:%s module:%s student:%s",
            course_id, self.location, username)
        return {"error": "Please enter valid grade"}

    def is_course_staff(self):
        # pylint: disable=no-member
        """
         Check if user is course staff.
        """
        return getattr(self.xmodule_runtime, 'user_is_staff', False)

    def is_instructor(self):
        # pylint: disable=no-member
        """
        Check if user role is instructor.
        """
        return self.xmodule_runtime.get_user_role() == 'instructor'

    def show_staff_grading_interface(self):
        """
        Return if current user is staff and not in studio.
        """
        in_studio_preview = self.scope_ids.user_id is None
        return self.is_course_staff() and not in_studio_preview

    def past_due(self):
        """
        Return whether due date has passed.
        """
        due = get_extended_due_date(self)
        try:
            graceperiod = self.graceperiod
        except AttributeError:
            # graceperiod and due are defined in InheritanceMixin
            # It's used automatically in edX but the unit tests will need to mock it out
            graceperiod = None

        if graceperiod is not None and due:
            close_date = due + graceperiod
        else:
            close_date = due

        if close_date is not None:
            return utcnow() > close_date
        return False

    def upload_allowed(self, submission_data=None):
        """
        Return whether student is allowed to upload an assignment.
        """
        submission_data = submission_data if submission_data is not None else self.get_submission(
        )
        return (not self.past_due() and self.score is None
                and not is_finalized_submission(submission_data))

    def file_storage_path(self, file_hash, original_filename):
        # pylint: disable=no-member
        """
        Helper method to get the path of an uploaded file
        """
        return get_file_storage_path(self.location, file_hash,
                                     original_filename)

    def is_zip_file_available(self, user):
        """
        returns True if zip file exists.
        """
        # pylint: disable=no-member
        zip_file_path = get_zip_file_path(user.username, self.block_course_id,
                                          self.block_id, self.location)
        return True if default_storage.exists(zip_file_path) else False

    def count_archive_files(self, user):
        """
        returns number of files archive in zip.
        """
        # pylint: disable=no-member
        zip_file_path = get_zip_file_path(user.username, self.block_course_id,
                                          self.block_id, self.location)
        with default_storage.open(zip_file_path, 'rb') as zip_file:
            with closing(ZipFile(zip_file)) as archive:
                return len(archive.infolist())

    def get_real_user(self):
        """returns session user"""
        # pylint: disable=no-member
        return self.runtime.get_real_user(
            self.xmodule_runtime.anonymous_student_id)

    def correctness_available(self):
        """
        For SGA is_correct just means the user submitted the problem, which we always know one way or the other
        """
        return True

    def is_past_due(self):
        """
        Is it now past this problem's due date?
        """
        return self.past_due()

    def is_correct(self):
        """
        For SGA we show the answer as soon as we know the user has given us their submission
        """
        return self.has_attempted()

    def has_attempted(self):
        """
        True if the student has already attempted this problem
        """
        submission = self.get_submission()
        if not submission:
            return False
        return submission['answer']['finalized']

    def can_attempt(self):
        """
        True if the student can attempt the problem
        """
        return not self.has_attempted()

    def runtime_user_is_staff(self):
        """
        Is the logged in user a staff user?
        """
        return self.is_course_staff()
class ImageExplorerBlock(XBlock):  # pylint: disable=no-init
    """
    XBlock that renders an image with tooltips
    """
    display_name = String(
        display_name=_("Display Name"),
        help=
        _("This name appears in the horizontal navigation at the top of the page."
          ),
        scope=Scope.settings,
        default=_("Image Explorer"))

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

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

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

    @property
    def hotspot_coordinates_centered(self):
        if self._hotspot_coordinates_centered:
            return True

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

        return schema_version > 1

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

        xmltree = etree.fromstring(self.data)

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

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

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

            if hotspot.feedback.ooyala:
                has_ooyala = True

        context = {
            'title': self.display_name,
            'hotspot_coordinates_centered': self.hotspot_coordinates_centered,
            'description_html': description,
            'hotspots': hotspots,
            'background': background,
        }

        fragment = Fragment()
        fragment.add_content(
            loader.render_django_template(
                '/templates/html/image_explorer.html',
                context=context,
                i18n_service=self.runtime.service(self, 'i18n')))
        fragment.add_css_url(
            self.runtime.local_resource_url(self,
                                            'public/css/image_explorer.css'))
        fragment.add_javascript_url(
            self.runtime.local_resource_url(self,
                                            'public/js/image_explorer.js'))
        if has_youtube:
            fragment.add_javascript_url('https://www.youtube.com/iframe_api')

        if has_ooyala:
            fragment.add_javascript_url(
                'https://player.ooyala.com/v3/635104fd644c4170ae227af2de27deab?platform=html5-priority'
            )
            fragment.add_javascript_url(
                self.runtime.local_resource_url(self,
                                                'public/js/ooyala_player.js'))

        fragment.initialize_js('ImageExplorerBlock')

        return fragment

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

        description = self._get_description(xmltree)
        background = self._get_background(xmltree)
        background['src'] = self._replace_static_from_url(background['src'])
        hotspots = self._get_hotspots(xmltree)

        return {
            'description': description,
            'background': background,
            'hotspots': hotspots,
        }

    @XBlock.json_handler
    def publish_event(self, data, suffix=''):
        try:
            event_type = data.pop('event_type')
        except KeyError:
            return {
                'result': 'error',
                'message': self.ugettext('Missing event_type in JSON data')
            }

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

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

        return {'result': 'success'}

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

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

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

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

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

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

        fragment.initialize_js('ImageExplorerEditBlock')

        return fragment

    @XBlock.json_handler
    def studio_submit(self, submissions, suffix=''):
        self.display_name = submissions['display_name']
        if submissions.get('hotspot_coordinates_centered', False):
            self._hotspot_coordinates_centered = True

        xml_content = submissions['data']

        try:
            etree.parse(StringIO(xml_content))
            self.data = xml_content
        except etree.XMLSyntaxError as e:
            return {'result': 'error', 'message': e.message}

        return {
            'result': 'success',
        }

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

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

        url = '"{}"'.format(url)
        lms_relative_url = replace_static_urls(url, course_id=self.course_id)
        lms_relative_url = lms_relative_url.strip('"')
        return self._make_url_absolute(lms_relative_url)

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

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

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

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

            feedback = AttrDict()
            feedback.width = feedback_element.get('width')
            feedback.height = feedback_element.get('height')
            feedback.max_height = feedback_element.get('max-height')
            feedback.header = self._inner_content(
                feedback_element.find('header'))

            feedback.side = hotspot_element.get('side', 'auto')

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

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

            feedback.ooyala = None
            ooyala_element = feedback_element.find('ooyala')
            if ooyala_element is not None:
                feedback.type = 'ooyala'
                feedback.ooyala = AttrDict()
                feedback.ooyala.video_id = ooyala_element.get('video_id')
                feedback.ooyala.width = ooyala_element.get('width')
                feedback.ooyala.height = ooyala_element.get('height')

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

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

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

            hotspots.append(hotspot)

        return hotspots

    @staticmethod
    def workbench_scenarios():
        """A canned scenario for display in the workbench."""
        return [("Image explorer scenario",
                 "<vertical_demo><image-explorer/></vertical_demo>")]
Exemplo n.º 6
0
class ConditionalBlock(
        SequenceMixin,
        MakoTemplateBlockBase,
        XmlMixin,
        XModuleDescriptorToXBlockMixin,
        XModuleToXBlockMixin,
        HTMLSnippet,
        ResourceTemplates,
        XModuleMixin,
        StudioEditableBlock,
):
    """
    Blocks child blocks from showing unless certain conditions are met.

    Example:

        <conditional sources="i4x://.../problem_1; i4x://.../problem_2" completed="True">
            <show sources="i4x://.../test_6; i4x://.../Avi_resources"/>
            <video url_name="secret_video" />
        </conditional>

        <conditional> tag attributes:
            sources - location id of required modules, separated by ';'

            submitted - map to `is_submitted` module method.
            (pressing RESET button makes this function to return False.)

            attempted - map to `is_attempted` module method
            correct - map to `is_correct` module method
            poll_answer - map to `poll_answer` module attribute
            voted - map to `voted` module attribute

        <show> tag attributes:
            sources - location id of required modules, separated by ';'

        You can add you own rules for <conditional> tag, like
        "completed", "attempted" etc. To do that yo must extend
        `ConditionalBlock.conditions_map` variable and add pair:
            my_attr: my_property/my_method

        After that you can use it:
            <conditional my_attr="some value" ...>
                ...
            </conditional>

        And my_property/my_method will be called for required modules.

    """

    display_name = String(display_name=_("Display Name"),
                          help=_("The display name for this component."),
                          scope=Scope.settings,
                          default=_('Conditional'))

    show_tag_list = ReferenceList(help=_(
        "List of urls of children that are references to external modules"),
                                  scope=Scope.content)

    sources_list = ReferenceList(
        display_name=_("Source Components"),
        help=
        _("The component location IDs of all source components that are used to determine whether a learner is "
          "shown the content of this conditional module. Copy the component location ID of a component from its "
          "Settings dialog in Studio."),
        scope=Scope.content)

    conditional_attr = String(
        display_name=_("Conditional Attribute"),
        help=_(
            "The attribute of the source components that determines whether a learner is shown the content of this "
            "conditional module."),
        scope=Scope.content,
        default='correct',
        values=lambda: [{
            'display_name': xml_attr,
            'value': xml_attr
        } for xml_attr in ConditionalBlock.conditions_map])

    conditional_value = String(
        display_name=_("Conditional Value"),
        help=_(
            "The value that the conditional attribute of the source components must match before a learner is shown "
            "the content of this conditional module."),
        scope=Scope.content,
        default='True')

    conditional_message = String(
        display_name=_("Blocked Content Message"),
        help=
        _("The message that is shown to learners when not all conditions are met to show the content of this "
          "conditional module. Include {link} in the text of your message to give learners a direct link to "
          "required units. For example, 'You must complete {link} before you can access this unit'."
          ),
        scope=Scope.content,
        default=_('You must complete {link} before you can access this unit.'))

    has_children = True

    _tag_name = 'conditional'

    resources_dir = None

    filename_extension = "xml"

    has_score = False

    show_in_read_only_mode = True

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

    mako_template = 'widgets/metadata-edit.html'
    studio_js_module_name = 'SequenceDescriptor'
    studio_view_js = {
        'js': [resource_string(__name__, 'js/src/sequence/edit.js')],
        'xmodule_js': resource_string(__name__, 'js/src/xmodule.js'),
    }
    studio_view_css = {
        'scss': [],
    }

    # Map
    # key: <tag attribute in xml>
    # value: <name of module attribute>
    conditions_map = {
        'poll_answer': 'poll_answer',  # poll_question attr

        # problem was submitted (it can be wrong)
        # if student will press reset button after that,
        # state will be reverted
        'submitted': 'is_submitted',  # capa_problem attr

        # if student attempted problem
        'attempted': 'is_attempted',  # capa_problem attr

        # if problem is full points
        'correct': 'is_correct',
        'voted': 'voted'  # poll_question attr
    }

    def __init__(self, *args, **kwargs):
        """
        Create an instance of the Conditional XBlock.
        """
        super().__init__(*args, **kwargs)
        # Convert sources xml_attribute to a ReferenceList field type so Location/Locator
        # substitution can be done.
        if not self.sources_list:
            if 'sources' in self.xml_attributes and isinstance(
                    self.xml_attributes['sources'], str):
                self.sources_list = [
                    # TODO: it is not clear why we are replacing the run here (which actually is a no-op
                    # for old-style course locators. However, this is the implementation of
                    # CourseLocator.make_usage_key_from_deprecated_string, which was previously
                    # being called in this location.
                    BlockUsageLocator.from_string(item).replace(
                        run=self.location.course_key.run) for item in
                    ConditionalBlock.parse_sources(self.xml_attributes)
                ]

    def is_condition_satisfied(self):  # lint-amnesty, pylint: disable=missing-function-docstring
        attr_name = self.conditions_map[self.conditional_attr]

        if self.conditional_value and self.get_required_blocks:
            for module in self.get_required_blocks:
                if not hasattr(module, attr_name):
                    # We don't throw an exception here because it is possible for
                    # the descriptor of a required module to have a property but
                    # for the resulting module to be a (flavor of) ErrorBlock.
                    # So just log and return false.
                    if module is not None:
                        # We do not want to log when module is None, and it is when requester
                        # does not have access to the requested required module.
                        log.warning('Error in conditional module: \
                            required module {module} has no {module_attr}'.
                                    format(module=module,
                                           module_attr=attr_name))
                    return False

                attr = getattr(module, attr_name)
                if callable(attr):
                    attr = attr()

                if self.conditional_value != str(attr):
                    break
            else:
                return True
        return False

    def student_view(self, _context):
        """
        Renders the student view.
        """
        fragment = Fragment()
        fragment.add_content(self.get_html())
        add_webpack_to_fragment(fragment, 'ConditionalBlockPreview')
        shim_xmodule_js(fragment, 'Conditional')
        return fragment

    def get_html(self):
        required_html_ids = [
            descriptor.location.html_id()
            for descriptor in self.get_required_blocks
        ]
        return self.runtime.service(self, 'mako').render_template(
            'conditional_ajax.html', {
                'element_id': self.location.html_id(),
                'ajax_url': self.ajax_url,
                'depends': ';'.join(required_html_ids)
            })

    def author_view(self, context):
        """
        Renders the Studio preview by rendering each child so that they can all be seen and edited.
        """
        fragment = Fragment()
        root_xblock = context.get('root_xblock')
        is_root = root_xblock and root_xblock.location == self.location
        if is_root:
            # User has clicked the "View" link. Show a preview of all possible children:
            self.render_children(context,
                                 fragment,
                                 can_reorder=True,
                                 can_add=True)
        # else: When shown on a unit page, don't show any sort of preview -
        # just the status of this block in the validation area.

        return fragment

    def studio_view(self, _context):
        """
        Return the studio view.
        """
        fragment = Fragment(
            self.runtime.service(self, 'mako').render_template(
                self.mako_template, self.get_context()))
        add_webpack_to_fragment(fragment, 'ConditionalBlockStudio')
        shim_xmodule_js(fragment, self.studio_js_module_name)
        return fragment

    def handle_ajax(self, _dispatch, _data):
        """This is called by courseware.moduleodule_render, to handle
        an AJAX call.
        """
        if not self.is_condition_satisfied():
            context = {'module': self, 'message': self.conditional_message}
            html = self.runtime.service(self, 'mako').render_template(
                'conditional_module.html', context)
            return json.dumps({
                'fragments': [{
                    'content': html
                }],
                'message': bool(self.conditional_message)
            })

        fragments = [
            child.render(STUDENT_VIEW).to_dict()
            for child in self.get_display_items()
        ]

        return json.dumps({'fragments': fragments})

    def get_icon_class(self):
        new_class = 'other'
        # HACK: This shouldn't be hard-coded to two types
        # OBSOLETE: This obsoletes 'type'
        class_priority = ['video', 'problem']

        child_classes = [
            child_descriptor.get_icon_class()
            for child_descriptor in self.get_children()
        ]
        for c in class_priority:
            if c in child_classes:
                new_class = c
        return new_class

    @staticmethod
    def parse_sources(xml_element):
        """ Parse xml_element 'sources' attr and return a list of location strings. """
        sources = xml_element.get('sources')
        if sources:
            return [location.strip() for location in sources.split(';')]

    @lazy
    def get_required_blocks(self):
        """
        Returns a list of bound XBlocks instances upon which XBlock depends.
        """
        return [
            self.system.get_module(descriptor)
            for descriptor in self.get_required_module_descriptors()
        ]

    def get_required_module_descriptors(self):
        """
        Returns a list of unbound XBlocks instances upon which this XBlock depends.
        """
        descriptors = []
        for location in self.sources_list:
            try:
                descriptor = self.system.load_item(location)
                descriptors.append(descriptor)
            except ItemNotFoundError:
                msg = "Invalid module by location."
                log.exception(msg)
                self.system.error_tracker(msg)

        return descriptors

    @classmethod
    def definition_from_xml(cls, xml_object, system):
        children = []
        show_tag_list = []
        definition = {}
        for conditional_attr in cls.conditions_map:
            conditional_value = xml_object.get(conditional_attr)
            if conditional_value is not None:
                definition.update({
                    'conditional_attr': conditional_attr,
                    'conditional_value': str(conditional_value),
                })
        for child in xml_object:
            if child.tag == 'show':
                locations = cls.parse_sources(child)
                for location in locations:
                    children.append(location)
                    show_tag_list.append(location)
            else:
                try:
                    descriptor = system.process_xml(
                        etree.tostring(child, encoding='unicode'))
                    children.append(descriptor.scope_ids.usage_id)
                except:  # lint-amnesty, pylint: disable=bare-except
                    msg = "Unable to load child when parsing Conditional."
                    log.exception(msg)
                    system.error_tracker(msg)
        definition.update({
            'show_tag_list': show_tag_list,
            'conditional_message': xml_object.get('message', '')
        })
        return definition, children

    def definition_to_xml(self, resource_fs):
        xml_object = etree.Element(self._tag_name)
        for child in self.get_children():
            if child.location not in self.show_tag_list:
                self.runtime.add_block_as_child_node(child, xml_object)

        if self.show_tag_list:
            show_str = HTML('<show sources="{sources}" />').format(
                sources=Text(';'.join(
                    str(location) for location in self.show_tag_list)))
            xml_object.append(etree.fromstring(show_str))

        # Overwrite the original sources attribute with the value from sources_list, as
        # Locations may have been changed to Locators.
        stringified_sources_list = [str(loc) for loc in self.sources_list]
        self.xml_attributes['sources'] = ';'.join(stringified_sources_list)
        self.xml_attributes[self.conditional_attr] = self.conditional_value
        self.xml_attributes['message'] = self.conditional_message
        return xml_object

    def validate(self):
        validation = super().validate()
        if not self.sources_list:
            conditional_validation = StudioValidation(self.location)
            conditional_validation.add(
                StudioValidationMessage(
                    StudioValidationMessage.NOT_CONFIGURED,
                    _("This component has no source components configured yet."
                      ),
                    action_class='edit-button',
                    action_label=_("Configure list of sources")))
            validation = StudioValidation.copy(validation)
            validation.summary = conditional_validation.messages[0]
        return validation

    @property
    def non_editable_metadata_fields(self):
        non_editable_fields = super().non_editable_metadata_fields
        non_editable_fields.extend([
            ConditionalBlock.due,
            ConditionalBlock.show_tag_list,
        ])
        return non_editable_fields
Exemplo n.º 7
0
class OoyalaPlayerBlock(OoyalaPlayerMixin, XBlock):
    """
    XBlock providing a video player for videos hosted on Ooyala
    """

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

    content_id = String(display_name=_("Content Id"),
                        help=_("Identifier for the Content Id."),
                        scope=Scope.content,
                        default='VtZWc4ODE61SNu7RdCJTlDhHLJ23vl5d')

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

    cc_language_preference = String(
        display_name=_("Closed Captions Language"),
        help=_("User's preference for closed captions language"),
        scope=Scope.user_info,
        default='en')

    disable_cc_and_translations = Boolean(
        display_name=_("Turn Off Closed Captions and Translated transcripts"),
        help=
        _("Hides the CC button and transcript languages selection for this video"
          ),
        scope=Scope.settings,
        default=False)

    autoplay = Boolean(
        display_name=_("Enable Player Autoplay"),
        help=_("Set to True if you the player to automatically play."),
        scope=Scope.content,
        default=True)

    enable_player_token = Boolean(
        display_name=_("Enable Player Token"),
        help=
        _("Set to True if a player token is required, e.g. if streaming videos to the mobile app."
          ),
        scope=Scope.content,
        default=False)

    partner_code = String(
        display_name=_("Partner Code"),
        help=_(
            "Required for V4 Player. Also needed to generate a player token."),
        scope=Scope.content,
        default='')

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

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

    api_key_3play = String(display_name=_("3play Api Key"),
                           help=_("3play Api Key for transcript."),
                           scope=Scope.content,
                           default='')

    width = String(display_name=_("Player Width"),
                   help=_("The width of the player in pixels."),
                   scope=Scope.content,
                   default="100%")

    height = String(display_name=_("Player Height"),
                    help=_('The height of the player in pixels.'),
                    scope=Scope.content,
                    default="100%")

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

    fire_progress_event_on_student_view = Boolean(
        display_name=_("Fire Progress Event on Student View"),
        help=
        _('Set to True if you would like to get a progress event in the event stream when the user views this xBlock.'
          ),
        scope=Scope.content,
        default=True)

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

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

    @XBlock.json_handler
    def publish_event(self, data, suffix=''):
        try:
            event_type = data.pop('event_type')
        except KeyError as e:
            return {
                'result': 'error',
                'message': self.ugettext('Missing event_type in JSON data')
            }

        data['content_id'] = self.content_id
        data['user_id'] = self.scope_ids.user_id

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

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

        fragment.initialize_js('OoyalaPlayerEditBlock')

        return fragment

    @XBlock.json_handler
    def store_language_preference(self, data, suffix=''):
        """
        Store user's cc language selection
        """
        lang = data.get('lang')
        if lang:
            self.cc_language_preference = lang

        return {'result': 'success'}

    @XBlock.json_handler
    def load_transcript(self, data, suffix=''):
        """
        Store user's cc language selection
        """
        threeplay_id = data.get('threeplay_id')
        transcript_id = data.get('transcript_id')
        content = ''

        if threeplay_id:
            content = Transcript.get_transcript_by_threeplay_id(
                api_key=self.get_attribute_or_default('api_key_3play'),
                threeplay_id=threeplay_id,
                transcript_id=transcript_id,
            )

        return {'content': content}

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

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

            self.xml_config = xml_config
            self.display_name = submissions['display_name']
            self.content_id = submissions['content_id'].strip()
            self.transcript_file_id = submissions['transcript_file_id'].strip()
            self.enable_player_token = submissions['enable_player_token']
            self.partner_code = submissions['partner_code']
            self.api_key = submissions['api_key']
            self.api_secret_key = submissions['api_secret_key']
            self.api_key_3play = submissions['api_key_3play']
            self.expiration_time = submissions['expiration_time']
            self.width = submissions['width']
            self.height = submissions['height']
            self.disable_cc_and_translations = submissions['cc_disable']

        return response

    @staticmethod
    def workbench_scenarios():
        """A canned scenario for display in the workbench."""
        return [("Ooyala scenario",
                 "<vertical_demo><ooyala-player/></vertical_demo>")]
Exemplo n.º 8
0
class QuestionnaireAbstractBlock(StudioEditableXBlockMixin,
                                 StudioContainerXBlockMixin, QuestionMixin,
                                 XBlock, XBlockWithPreviewMixin,
                                 XBlockWithTranslationServiceMixin):
    """
    An abstract class used for MCQ/MRQ blocks

    Must be a child of a MentoringBlock. Allow to display a tip/advice depending on the
    values entered by the student, and supports multiple types of multiple-choice
    set, with preset choices and author-defined values.
    """
    question = String(
        display_name=_("Question"),
        help=_("Question to ask the student"),
        scope=Scope.content,
        default="",
        multiline_editor=True,
    )

    editable_fields = ('question', 'weight', 'display_name', 'show_title')
    has_children = True
    answerable = True

    @lazy
    def html_id(self):
        """
        A short, simple ID string used to uniquely identify this question.

        This is only used by templates for matching <input> and <label> elements.
        """
        return uuid.uuid4().hex[:20]

    def student_view(self, context=None):
        name = getattr(self, "unmixed_class", self.__class__).__name__

        template_path = 'templates/html/{}.html'.format(name.lower())

        context = context.copy() if context else {}
        context['self'] = self
        context['custom_choices'] = self.custom_choices
        context['hide_header'] = context.get('hide_header',
                                             False) or not self.show_title

        fragment = Fragment(loader.render_template(template_path, context))
        # If we use local_resource_url(self, ...) the runtime may insert many identical copies
        # of questionnaire.[css/js] into the DOM. So we use the mentoring block here if possible.
        block_with_resources = self.get_parent()
        from .mentoring import MentoringBlock
        # We use an inline import here to avoid a circular dependency with the .mentoring module.
        if not isinstance(block_with_resources, MentoringBlock):
            block_with_resources = self
        fragment.add_css_url(
            self.runtime.local_resource_url(block_with_resources,
                                            'public/css/questionnaire.css'))
        fragment.add_javascript_url(
            self.runtime.local_resource_url(block_with_resources,
                                            'public/js/questionnaire.js'))
        fragment.initialize_js(name)
        return fragment

    def mentoring_view(self, context=None):
        return self.student_view(context)

    @property
    def custom_choices(self):
        custom_choices = []
        for child_id in self.children:
            if child_isinstance(self, child_id, ChoiceBlock):
                custom_choices.append(self.runtime.get_block(child_id))
        return custom_choices

    @property
    def all_choice_values(self):
        return [c.value for c in self.custom_choices]

    @property
    def human_readable_choices(self):
        return [{
            "display_name": mark_safe(c.content),
            "value": c.value
        } for c in self.custom_choices]

    @staticmethod
    def choice_values_provider(question):
        """
        Get a list a {"display_name": "Choice Description", "value": value}
        objects for use with studio_view editor.
        """
        return question.human_readable_choices

    def get_tips(self):
        """
        Returns the tips contained in this block
        """
        tips = []
        for child_id in self.children:
            if child_isinstance(self, child_id, TipBlock):
                tips.append(self.runtime.get_block(child_id))
        return tips

    def get_submission_display(self, submission):
        """
        Get the human-readable version of a submission value
        """
        for choice in self.custom_choices:
            if choice.value == submission:
                return choice.content
        return submission

    def get_author_edit_view_fragment(self, context):
        fragment = super(QuestionnaireAbstractBlock,
                         self).author_edit_view(context)
        return fragment

    def author_edit_view(self, context):
        """
        Add some HTML to the author view that allows authors to add choices and tips.
        """
        from .mentoring import MentoringWithExplicitStepsBlock

        fragment = self.get_author_edit_view_fragment(context)

        # * Step Builder units can show review components in the Review Step.
        fragment.add_content(
            loader.render_template(
                'templates/html/questionnaire_add_buttons.html', {
                    'show_review':
                    isinstance(self.get_parent(),
                               MentoringWithExplicitStepsBlock)
                }))
        fragment.add_css_url(
            self.runtime.local_resource_url(self,
                                            'public/css/problem-builder.css'))
        fragment.add_css_url(
            self.runtime.local_resource_url(
                self, 'public/css/questionnaire-edit.css'))
        fragment.add_javascript_url(
            self.runtime.local_resource_url(self, 'public/js/util.js'))
        fragment.add_javascript_url(
            self.runtime.local_resource_url(self,
                                            'public/js/mentoring_edit.js'))
        fragment.initialize_js('MentoringEditComponents')
        return fragment

    def validate_field_data(self, validation, data):
        """
        Validate this block's field data. Instead of checking fields like self.name, check the
        fields set on data, e.g. data.name. This allows the same validation method to be re-used
        for the studio editor. Any errors found should be added to "validation".

        This method should not return any value or raise any exceptions.
        All of this XBlock's fields should be found in "data", even if they aren't being changed
        or aren't even set (i.e. are defaults).
        """
        super(QuestionnaireAbstractBlock,
              self).validate_field_data(validation, data)

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

        if not data.name:
            add_error(self._(u"A unique Question ID is required."))
        elif ' ' in data.name:
            add_error(self._(u"Question ID should not contain spaces."))

    def validate(self):
        """
        Validates the state of this XBlock.
        """
        validation = super(QuestionnaireAbstractBlock, self).validate()

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

        # Validate the choice values:
        all_choice_values = self.all_choice_values
        all_choice_values_set = set(all_choice_values)
        if len(all_choice_values) != len(all_choice_values_set):
            add_error(self._(u"Some choice values are not unique."))
        # Validate the tips:
        values_with_tips = set()
        for tip in self.get_tips():
            values = set(tip.values)
            if values & values_with_tips:
                add_error(
                    self._(u"Multiple tips configured for the same choice."))
                break
            values_with_tips.update(values)
        return validation

    def get_review_tip(self):
        """ Get the text to show on the assessment review when the student gets this question wrong """
        for child_id in self.children:
            if child_isinstance(self, child_id, MentoringMessageBlock):
                child = self.runtime.get_block(child_id)
                if child.type == "on-assessment-review-question":
                    return child.content

    @property
    def message_formatted(self):
        """ Get the feedback message HTML, if any, formatted by the runtime """
        if self.message:
            # For any HTML that we aren't 'rendering' through an XBlock view such as
            # student_view the runtime may need to rewrite URLs
            # e.g. converting '/static/x.png' to '/c4x/.../x.png'
            format_html = getattr(self.runtime, 'replace_urls',
                                  lambda html: html)
            return format_html(self.message)
        return ""
Exemplo n.º 9
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
Exemplo n.º 10
0
class StaffGradedAssignmentXBlock(XBlock):
    """
    This block defines a Staff Graded Assignment.  Students are shown a rubric
    and invited to upload a file which is then graded by staff.
    """
    has_score = True
    icon_class = 'problem'

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

    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)

    points = Float(display_name="Maximum score",
                   help=("Maximum grade score given to assignment by staff."),
                   values={
                       "min": 0,
                       "step": .1
                   },
                   default=100,
                   scope=Scope.settings)

    staff_score = Integer(
        display_name="Score assigned by non-instructor staff",
        help=("Score will need to be approved by instructor before being "
              "published."),
        default=None,
        scope=Scope.settings)

    comment = String(display_name="Instructor comment",
                     default='',
                     scope=Scope.user_state,
                     help="Feedback given to student by instructor.")

    annotated_sha1 = String(
        display_name="Annotated SHA1",
        scope=Scope.user_state,
        default=None,
        help=("sha1 of the annotated file uploaded by the instructor for "
              "this assignment."))

    annotated_filename = String(
        display_name="Annotated file name",
        scope=Scope.user_state,
        default=None,
        help="The name of the annotated file uploaded for this assignment.")

    annotated_mimetype = String(
        display_name="Mime type of annotated file",
        scope=Scope.user_state,
        default=None,
        help="The mimetype of the annotated file uploaded for this assignment."
    )

    annotated_timestamp = DateTime(display_name="Timestamp",
                                   scope=Scope.user_state,
                                   default=None,
                                   help="When the annotated file was uploaded")

    def max_score(self):
        return self.points

    @reify
    def block_id(self):
        # cargo culted gibberish
        return self.scope_ids.usage_id

    def student_submission_id(self, id=None):
        """
        Returns dict required by the submissions app for creating and
        retrieving submissions for a particular student.
        """
        if id is None:
            id = self.xmodule_runtime.anonymous_student_id
            assert id != 'MOCK', "Forgot to call 'personalize' in test."
        return {
            "student_id": id,
            "course_id": self.course_id,
            "item_id": self.block_id,
            "item_type": 'sga',  # ???
        }

    def get_submission(self, id=None):
        """
        Get student's most recent submission.
        """
        submissions = submissions_api.get_submissions(
            self.student_submission_id(id))
        if submissions:
            # If I understand docs correctly, most recent submission should
            # be first
            return submissions[0]

    def get_score(self, id=None):
        """
        Get student's current score.
        """
        score = submissions_api.get_score(self.student_submission_id(id))
        if score:
            return score['points_earned']

    @reify
    def score(self):
        return self.get_score()

    def student_view(self, context=None):
        """
        The primary view of the StaffGradedAssignmentXBlock, shown to students
        when viewing courses.
        """
        context = {
            "student_state": json.dumps(self.student_state()),
            "id": self.location.name.replace('.', '_')
        }
        if self.show_staff_grading_interface():
            context['is_course_staff'] = True
            self.update_staff_debug_context(context)

        fragment = Fragment()
        fragment.add_content(
            render_template('templates/staff_graded_assignment/show.html',
                            context))
        fragment.add_css(_resource("static/css/edx_sga.css"))
        fragment.add_javascript(_resource("static/js/src/edx_sga.js"))
        fragment.initialize_js('StaffGradedAssignmentXBlock')
        return fragment

    def update_staff_debug_context(self, context):
        published = self.start
        context['is_released'] = published and published < _now()
        context['location'] = self.location
        context['category'] = type(self).__name__
        context['fields'] = [(name, field.read_from(self))
                             for name, field in self.fields.items()]

    def student_state(self):
        """
        Returns a JSON serializable representation of student's state for
        rendering in client view.
        """
        submission = self.get_submission()
        if submission:
            uploaded = {"filename": submission['answer']['filename']}
        else:
            uploaded = None

        if self.annotated_sha1:
            annotated = {"filename": self.annotated_filename}
        else:
            annotated = None

        score = self.score
        if score is not None:
            graded = {'score': score, 'comment': self.comment}
        else:
            graded = None

        return {
            "uploaded": uploaded,
            "annotated": annotated,
            "graded": graded,
            "max_score": self.max_score(),
            "upload_allowed": self.upload_allowed(),
        }

    def staff_grading_data(self):
        def get_student_data():
            # Submissions doesn't have API for this, just use model directly
            students = SubmissionsStudent.objects.filter(
                course_id=self.course_id, item_id=self.block_id)
            for student in students:
                submission = self.get_submission(student.student_id)
                if not submission:
                    continue
                user = user_by_anonymous_id(student.student_id)
                module, _ = StudentModule.objects.get_or_create(
                    course_id=self.course_id,
                    module_state_key=self.location,
                    student=user,
                    defaults={
                        'state': '{}',
                        'module_type': self.category,
                    })
                state = json.loads(module.state)
                score = self.get_score(student.student_id)
                approved = score is not None
                if score is None:
                    score = state.get('staff_score')
                    needs_approval = score is not None
                else:
                    needs_approval = False
                instructor = self.is_instructor()
                yield {
                    'module_id':
                    module.id,
                    'student_id':
                    student.student_id,
                    'submission_id':
                    submission['uuid'],
                    'username':
                    module.student.username,
                    'fullname':
                    module.student.profile.name,
                    'filename':
                    submission['answer']["filename"],
                    'timestamp':
                    submission['created_at'].strftime(
                        DateTime.DATETIME_FORMAT),
                    'score':
                    score,
                    'approved':
                    approved,
                    'needs_approval':
                    instructor and needs_approval,
                    'may_grade':
                    instructor or not approved,
                    'annotated':
                    state.get("annotated_filename"),
                    'comment':
                    state.get("comment", ''),
                }

        return {
            'assignments': list(get_student_data()),
            'max_score': self.max_score(),
        }

    def studio_view(self, context=None):
        try:
            cls = type(self)

            def none_to_empty(x):
                return x if x is not None else ''

            edit_fields = ((field, none_to_empty(getattr(self, field.name)),
                            validator)
                           for field, validator in ((cls.display_name,
                                                     'string'), (cls.points,
                                                                 'number'),
                                                    (cls.weight, 'number')))

            context = {'fields': edit_fields}
            fragment = Fragment()
            fragment.add_content(
                render_template('templates/staff_graded_assignment/edit.html',
                                context))
            fragment.add_javascript(_resource("static/js/src/studio.js"))
            fragment.initialize_js('StaffGradedAssignmentXBlock')
            return fragment
        except:  # pragma: NO COVER
            log.error("Don't swallow my exceptions", exc_info=True)
            raise

    @XBlock.json_handler
    def save_sga(self, data, suffix=''):
        for name in ('display_name', 'points', 'weight'):
            setattr(self, name, data.get(name, getattr(self, name)))

    @XBlock.handler
    def upload_assignment(self, request, suffix=''):
        require(self.upload_allowed())
        upload = request.params['assignment']
        sha1 = _get_sha1(upload.file)
        answer = {
            "sha1": sha1,
            "filename": upload.file.name,
            "mimetype": mimetypes.guess_type(upload.file.name)[0],
        }
        student_id = self.student_submission_id()
        submissions_api.create_submission(student_id, answer)
        path = _file_storage_path(str(self.location), sha1, upload.file.name)
        if not default_storage.exists(path):
            default_storage.save(path, File(upload.file))
        return Response(json_body=self.student_state())

    @XBlock.handler
    def staff_upload_annotated(self, request, suffix=''):
        require(self.is_course_staff())
        upload = request.params['annotated']
        module = StudentModule.objects.get(pk=request.params['module_id'])
        state = json.loads(module.state)
        state['annotated_sha1'] = sha1 = _get_sha1(upload.file)
        state['annotated_filename'] = filename = upload.file.name
        state['annotated_mimetype'] = mimetypes.guess_type(upload.file.name)[0]
        state['annotated_timestamp'] = _now().strftime(
            DateTime.DATETIME_FORMAT)
        path = _file_storage_path(str(self.location), sha1, filename)
        if not default_storage.exists(path):
            default_storage.save(path, File(upload.file))
        module.state = json.dumps(state)
        module.save()
        return Response(json_body=self.staff_grading_data())

    @XBlock.handler
    def download_assignment(self, request, suffix=''):
        answer = self.get_submission()['answer']
        path = _file_storage_path(str(self.location), answer['sha1'],
                                  answer['filename'])
        return self.download(path, answer['mimetype'], answer['filename'])

    @XBlock.handler
    def download_annotated(self, request, suffix=''):
        path = _file_storage_path(str(self.location), self.annotated_sha1,
                                  self.annotated_filename)
        return self.download(path, self.annotated_mimetype,
                             self.annotated_filename)

    @XBlock.handler
    def staff_download(self, request, suffix=''):
        require(self.is_course_staff())
        submission = self.get_submission(request.params['student_id'])
        answer = submission['answer']
        path = _file_storage_path(str(self.location), answer['sha1'],
                                  answer['filename'])
        return self.download(path, answer['mimetype'], answer['filename'])

    @XBlock.handler
    def staff_download_annotated(self, request, suffix=''):
        require(self.is_course_staff())
        module = StudentModule.objects.get(pk=request.params['module_id'])
        state = json.loads(module.state)
        path = _file_storage_path(str(self.location), state['annotated_sha1'],
                                  state['annotated_filename'])
        return self.download(path, state['annotated_mimetype'],
                             state['annotated_filename'])

    def download(self, path, mimetype, filename):
        BLOCK_SIZE = (1 << 10) * 8  # 8kb
        file = default_storage.open(path)
        app_iter = iter(partial(file.read, BLOCK_SIZE), '')
        return Response(app_iter=app_iter,
                        content_type=mimetype,
                        content_disposition="attachment; filename=" + filename)

    @XBlock.handler
    def get_staff_grading_data(self, request, suffix=''):
        require(self.is_course_staff())
        return Response(json_body=self.staff_grading_data())

    @XBlock.handler
    def enter_grade(self, request, suffix=''):
        require(self.is_course_staff())
        module = StudentModule.objects.get(pk=request.params['module_id'])
        state = json.loads(module.state)
        score = int(request.params['grade'])
        if self.is_instructor():
            uuid = request.params['submission_id']
            submissions_api.set_score(uuid, score, self.max_score())
        else:
            state['staff_score'] = score
        state['comment'] = request.params.get('comment', '')
        module.state = json.dumps(state)
        module.save()

        return Response(json_body=self.staff_grading_data())

    @XBlock.handler
    def remove_grade(self, request, suffix=''):
        require(self.is_course_staff())
        student_id = request.params['student_id']
        submissions_api.reset_score(student_id, self.course_id, self.block_id)
        module = StudentModule.objects.get(pk=request.params['module_id'])
        state = json.loads(module.state)
        state['staff_score'] = None
        state['comment'] = ''
        state['annotated_sha1'] = None
        state['annotated_filename'] = None
        state['annotated_mimetype'] = None
        state['annotated_timestamp'] = None
        module.state = json.dumps(state)
        module.save()
        return Response(json_body=self.staff_grading_data())

    def is_course_staff(self):
        return getattr(self.xmodule_runtime, 'user_is_staff', False)

    def is_instructor(self):
        return self.xmodule_runtime.get_user_role() == 'instructor'

    def show_staff_grading_interface(self):
        in_studio_preview = self.scope_ids.user_id is None
        return self.is_course_staff() and not in_studio_preview

    def past_due(self):
        due = get_extended_due_date(self)
        if due is not None:
            return _now() > due
        return False

    def upload_allowed(self):
        return not self.past_due() and self.score is None
Exemplo n.º 11
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.
    """
    resources_dir = None

    display_name = String(help=_("The display name for this component."),
                          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

    advanced_modules = [
        "agnosticcontentxblock", "google-document", "google-calendar",
        "drag-and-drop-v2", "problem-builder", "word_cloud", "survey", "done",
        "annotatable", "bibblio", "inline-dropdown", "freetextresponse",
        "recap", "badger", "edx_sga", "library_content", "poll"
    ]

    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)
        context['can_move'] = False

        for child_key in children_to_show:
            # 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}
Exemplo n.º 12
0
class MCQBlock(SubmittingXBlockMixin, QuestionnaireAbstractBlock):
    """
    An XBlock used to ask multiple-choice questions
    """
    CATEGORY = 'pb-mcq'
    STUDIO_LABEL = _(u"Multiple Choice Question")

    message = String(
        display_name=_("Message"),
        help=_(
            "General feedback provided when submitting. "
            "(This is not shown if there is a more specific feedback tip for the choice selected by the learner.)"
        ),
        scope=Scope.content,
        default="")

    student_choice = String(
        # {Last input submitted by the student
        default="",
        scope=Scope.user_state,
    )

    correct_choices = List(
        display_name=_("Correct Choice[s]"),
        help=
        _("Specify the value[s] that students may select for this question to be considered correct."
          ),
        scope=Scope.content,
        list_values_provider=QuestionnaireAbstractBlock.choice_values_provider,
        list_style='set',  # Underered, unique items. Affects the UI editor.
    )
    editable_fields = QuestionnaireAbstractBlock.editable_fields + (
        'message',
        'correct_choices',
    )

    def describe_choice_correctness(self, choice_value):
        if choice_value in self.correct_choices:
            if len(self.correct_choices) == 1:
                # Translators: This is an adjective, describing a choice as correct
                return self._(u"Correct")
            return self._(u"Acceptable")
        else:
            if len(self.correct_choices) == 1:
                return self._(u"Wrong")
            return self._(u"Not Acceptable")

    def calculate_results(self, submission):
        correct = submission in self.correct_choices
        tips_html = []
        for tip in self.get_tips():
            if submission in tip.values:
                tips_html.append(tip.render('mentoring_view').content)

        formatted_tips = None

        if tips_html:
            formatted_tips = loader.render_template(
                'templates/html/tip_choice_group.html', {
                    'tips_html': tips_html,
                })

        self.student_choice = submission

        if sub_api:
            # Also send to the submissions API:
            sub_api.create_submission(self.student_item_key, submission)

        return {
            'submission': submission,
            'message': self.message_formatted,
            'status': 'correct' if correct else 'incorrect',
            'tips': formatted_tips,
            'weight': self.weight,
            'score': 1 if correct else 0,
        }

    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 MCQ submission: "%s"', submission)
        result = self.calculate_results(submission)
        self.student_choice = submission
        log.debug(u'MCQ submission result: %s', result)
        return result

    def get_author_edit_view_fragment(self, context):
        """
        The options for the 1-5 values of the Likert scale aren't child blocks but we want to
        show them in the author edit view, for clarity.
        """
        fragment = Fragment(u"<p>{}</p>".format(self.question))
        self.render_children(context,
                             fragment,
                             can_reorder=True,
                             can_add=False)
        return fragment

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

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

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

        all_values = set(self.all_choice_values)
        correct = set(data.correct_choices)

        if all_values and not correct:
            add_error(
                self.
                _(u"You must indicate the correct answer[s], or the student will always get this question wrong."
                  ))
        if len(correct) < len(data.correct_choices):
            add_error(self._(u"Duplicate correct choices set"))
        for val in (correct - all_values):
            add_error(
                self.
                _(u"A choice value listed as correct does not exist: {choice}"
                  ).format(choice=choice_name(val)))
Exemplo n.º 13
0
class RatingBlock(MCQBlock):
    """
    An XBlock used to rate something on a five-point scale, e.g. Likert Scale
    """
    CATEGORY = 'pb-rating'
    STUDIO_LABEL = _(u"Rating Question")

    low = String(
        display_name=_("Low"),
        help=_("Label for low ratings"),
        scope=Scope.content,
        default=_("Less"),
    )
    high = String(
        display_name=_("High"),
        help=_("Label for high ratings"),
        scope=Scope.content,
        default=_("More"),
    )
    FIXED_VALUES = ["1", "2", "3", "4", "5"]
    correct_choices = List(
        display_name=_("Accepted Choice[s]"),
        help=
        _("Specify the rating value[s] that students may select for this question to be considered correct."
          ),
        scope=Scope.content,
        default=FIXED_VALUES,
        list_values_provider=QuestionnaireAbstractBlock.choice_values_provider,
        list_style='set',  # Underered, unique items. Affects the UI editor.
    )
    editable_fields = MCQBlock.editable_fields + ('low', 'high')

    @property
    def all_choice_values(self):
        return self.FIXED_VALUES + [c.value for c in self.custom_choices]

    @property
    def human_readable_choices(self):
        display_names = [
            "1 - {}".format(self.low), "2", "3", "4",
            "5 - {}".format(self.high)
        ]
        return [{
            "display_name": dn,
            "value": val
        } for val, dn in zip(self.FIXED_VALUES, display_names)] + super(
            RatingBlock, self).human_readable_choices

    def get_author_edit_view_fragment(self, context):
        """
        The options for the 1-5 values of the Likert scale aren't child blocks but we want to
        show them in the author edit view, for clarity.
        """
        fragment = Fragment()
        fragment.add_content(
            loader.render_template(
                'templates/html/ratingblock_edit_preview.html', {
                    'question':
                    self.question,
                    'low':
                    self.low,
                    'high':
                    self.high,
                    'accepted_statuses': [None] +
                    [self.describe_choice_correctness(c) for c in "12345"],
                }))
        self.render_children(context,
                             fragment,
                             can_reorder=True,
                             can_add=False)
        return fragment

    @property
    def url_name(self):
        """
        Get the url_name for this block. In Studio/LMS it is provided by a mixin, so we just
        defer to super(). In the workbench or any other platform, we use the name.
        """
        try:
            return super(RatingBlock, self).url_name
        except AttributeError:
            return self.name

    def student_view(self, context):
        fragment = super(RatingBlock, self).student_view(context)
        rendering_for_studio = None
        if context:  # Workbench does not provide context
            rendering_for_studio = context.get('author_edit_view')
        if rendering_for_studio:
            fragment.add_content(
                loader.render_template(
                    'templates/html/rating_edit_footer.html',
                    {"url_name": self.url_name}))
        return fragment
Exemplo n.º 14
0
class CourseFields(object):
    lti_passports = List(help="LTI tools passports as id:client_key:client_secret", scope=Scope.settings)
    textbooks = TextbookList(help="List of pairs of (title, url) for textbooks used in this course",
                             default=[], scope=Scope.content)

    # This is should be scoped to content, but since it's defined in the policy
    # file, it is currently scoped to settings.
    user_partitions = UserPartitionList(
        help="List of user partitions of this course into groups, used e.g. for experiments",
        default=[],
        scope=Scope.settings
    )

    wiki_slug = String(help="Slug that points to the wiki for this course", scope=Scope.content)
    enrollment_start = Date(help="Date that enrollment for this class is opened", scope=Scope.settings)
    enrollment_end = Date(help="Date that enrollment for this class is closed", scope=Scope.settings)
    start = Date(help="Start time when this module is visible",
                 default=datetime(2030, 1, 1, tzinfo=UTC()),
                 scope=Scope.settings)
    end = Date(help="Date that this class ends", scope=Scope.settings)
    advertised_start = String(help="Date that this course is advertised to start", scope=Scope.settings)
    grading_policy = Dict(help="Grading policy definition for this class",
                          default={"GRADER": [
                              {
                                  "type": "Homework",
                                  "min_count": 12,
                                  "drop_count": 2,
                                  "short_label": "HW",
                                  "weight": 0.15
                              },
                              {
                                  "type": "Lab",
                                  "min_count": 12,
                                  "drop_count": 2,
                                  "weight": 0.15
                              },
                              {
                                  "type": "Midterm Exam",
                                  "short_label": "Midterm",
                                  "min_count": 1,
                                  "drop_count": 0,
                                  "weight": 0.3
                              },
                              {
                                  "type": "Final Exam",
                                  "short_label": "Final",
                                  "min_count": 1,
                                  "drop_count": 0,
                                  "weight": 0.4
                              }
                          ],
                              "GRADE_CUTOFFS": {
                                  "Pass": 0.5
                              }},
                          scope=Scope.content)
    show_calculator = Boolean(help="Whether to show the calculator in this course", default=False, scope=Scope.settings)
    display_name = String(help="Display name for this module", default="Empty", display_name=_("Display Name"), scope=Scope.settings)
    course_edit_method = String(help="Method with which this course is edited.", default="Studio", scope=Scope.settings)
    show_chat = Boolean(help="Whether to show the chat widget in this 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(help="Url for the end-of-course survey", scope=Scope.settings)
    discussion_blackouts = List(help="List of pairs of start/end dates for discussion blackouts", scope=Scope.settings)
    discussion_topics = Dict(help="Map of topics names to ids", scope=Scope.settings)
    discussion_sort_alpha = Boolean(scope=Scope.settings, default=False, help="Sort forum categories and subcategories alphabetically.")
    announcement = Date(help="Date this course is announced", scope=Scope.settings)
    cohort_config = Dict(help="Dictionary defining cohort configuration", scope=Scope.settings)
    is_new = Boolean(help="Whether this course should be flagged as new", scope=Scope.settings)
    no_grade = Boolean(help="True if this course isn't graded", default=False, scope=Scope.settings)
    disable_progress_graph = Boolean(help="True if this course shouldn't display the progress graph", default=False, scope=Scope.settings)
    pdf_textbooks = List(help="List of dictionaries containing pdf_textbook configuration", scope=Scope.settings)
    html_textbooks = List(help="List of dictionaries containing html_textbook configuration", scope=Scope.settings)
    remote_gradebook = Dict(scope=Scope.settings)
    allow_anonymous = Boolean(scope=Scope.settings, default=True)
    allow_anonymous_to_peers = Boolean(scope=Scope.settings, default=False)
    advanced_modules = List(help="Beta modules used in your course", scope=Scope.settings)
    has_children = True
    checklists = List(scope=Scope.settings,
                      default=[
                          {"short_description": _("Getting Started With Studio"),
                           "items": [{"short_description": _("Add Course Team Members"),
                                      "long_description": _("Grant your collaborators permission to edit your course so you can work together."),
                                      "is_checked": False,
                                      "action_url": "ManageUsers",
                                      "action_text": _("Edit Course Team"),
                                      "action_external": False},
                                     {"short_description": _("Set Important Dates for Your Course"),
                                      "long_description": _("Establish your course's student enrollment and launch dates on the Schedule and Details page."),
                                      "is_checked": False,
                                      "action_url": "SettingsDetails",
                                      "action_text": _("Edit Course Details &amp; Schedule"),
                                      "action_external": False},
                                     {"short_description": _("Draft Your Course's Grading Policy"),
                                      "long_description": _("Set up your assignment types and grading policy even if you haven't created all your assignments."),
                                      "is_checked": False,
                                      "action_url": "SettingsGrading",
                                      "action_text": _("Edit Grading Settings"),
                                      "action_external": False},
                                     {"short_description": _("Explore the Other Studio Checklists"),
                                      "long_description": _("Discover other available course authoring tools, and find help when you need it."),
                                      "is_checked": False,
                                      "action_url": "",
                                      "action_text": "",
                                      "action_external": False}]},
                          {"short_description": _("Draft a Rough Course Outline"),
                           "items": [{"short_description": _("Create Your First Section and Subsection"),
                                      "long_description": _("Use your course outline to build your first Section and Subsection."),
                                      "is_checked": False,
                                      "action_url": "CourseOutline",
                                      "action_text": _("Edit Course Outline"),
                                      "action_external": False},
                                     {"short_description": _("Set Section Release Dates"),
                                      "long_description": _("Specify the release dates for each Section in your course. Sections become visible to students on their release dates."),
                                      "is_checked": False,
                                      "action_url": "CourseOutline",
                                      "action_text": _("Edit Course Outline"),
                                      "action_external": False},
                                     {"short_description": _("Designate a Subsection as Graded"),
                                      "long_description": _("Set a Subsection to be graded as a specific assignment type. Assignments within graded Subsections count toward a student's final grade."),
                                      "is_checked": False,
                                      "action_url": "CourseOutline",
                                      "action_text": _("Edit Course Outline"),
                                      "action_external": False},
                                     {"short_description": _("Reordering Course Content"),
                                      "long_description": _("Use drag and drop to reorder the content in your course."),
                                      "is_checked": False,
                                      "action_url": "CourseOutline",
                                      "action_text": _("Edit Course Outline"),
                                      "action_external": False},
                                     {"short_description": _("Renaming Sections"),
                                      "long_description": _("Rename Sections by clicking the Section name from the Course Outline."),
                                      "is_checked": False,
                                      "action_url": "CourseOutline",
                                      "action_text": _("Edit Course Outline"),
                                      "action_external": False},
                                     {"short_description": _("Deleting Course Content"),
                                      "long_description": _("Delete Sections, Subsections, or Units you don't need anymore. Be careful, as there is no Undo function."),
                                      "is_checked": False,
                                      "action_url": "CourseOutline",
                                      "action_text": _("Edit Course Outline"),
                                      "action_external": False},
                                     {"short_description": _("Add an Instructor-Only Section to Your Outline"),
                                      "long_description": _("Some course authors find using a section for unsorted, in-progress work useful. To do this, create a section and set the release date to the distant future."),
                                      "is_checked": False,
                                      "action_url": "CourseOutline",
                                      "action_text": _("Edit Course Outline"),
                                      "action_external": False}]},
                          {"short_description": _("Explore edX's Support Tools"),
                           "items": [{"short_description": _("Explore the Studio Help Forum"),
                                      "long_description": _("Access the Studio Help forum from the menu that appears when you click your user name in the top right corner of Studio."),
                                      "is_checked": False,
                                      "action_url": "http://help.edge.edx.org/",
                                      "action_text": _("Visit Studio Help"),
                                      "action_external": True},
                                     {"short_description": _("Enroll in edX 101"),
                                      "long_description": _("Register for edX 101, edX's primer for course creation."),
                                      "is_checked": False,
                                      "action_url": "https://edge.edx.org/courses/edX/edX101/How_to_Create_an_edX_Course/about",
                                      "action_text": _("Register for edX 101"),
                                      "action_external": True},
                                     {"short_description": _("Download the Studio Documentation"),
                                      "long_description": _("Download the searchable Studio reference documentation in PDF form."),
                                      "is_checked": False,
                                      "action_url": "http://files.edx.org/Getting_Started_with_Studio.pdf",
                                      "action_text": _("Download Documentation"),
                                      "action_external": True}]},
                          {"short_description": _("Draft Your Course About Page"),
                           "items": [{"short_description": _("Draft a Course Description"),
                                      "long_description": _("Courses on edX have an About page that includes a course video, description, and more. Draft the text students will read before deciding to enroll in your course."),
                                      "is_checked": False,
                                      "action_url": "SettingsDetails",
                                      "action_text": _("Edit Course Schedule &amp; Details"),
                                      "action_external": False},
                                     {"short_description": _("Add Staff Bios"),
                                      "long_description": _("Showing prospective students who their instructor will be is helpful. Include staff bios on the course About page."),
                                      "is_checked": False,
                                      "action_url": "SettingsDetails",
                                      "action_text": _("Edit Course Schedule &amp; Details"),
                                      "action_external": False},
                                     {"short_description": _("Add Course FAQs"),
                                      "long_description": _("Include a short list of frequently asked questions about your course."),
                                      "is_checked": False,
                                      "action_url": "SettingsDetails",
                                      "action_text": _("Edit Course Schedule &amp; Details"),
                                      "action_external": False},
                                     {"short_description": _("Add Course Prerequisites"),
                                      "long_description": _("Let students know what knowledge and/or skills they should have before they enroll in your course."),
                                      "is_checked": False,
                                      "action_url": "SettingsDetails",
                                      "action_text": _("Edit Course Schedule &amp; Details"),
                                      "action_external": False}]}
        ])
    info_sidebar_name = String(scope=Scope.settings, default='Course Handouts')
    show_timezone = Boolean(
        help="True if timezones should be shown on dates in the courseware. Deprecated in favor of due_date_display_format.",
        scope=Scope.settings, default=True
    )
    due_date_display_format = String(
        help="Format supported by strftime for displaying due dates. Takes precedence over show_timezone.",
        scope=Scope.settings, default=None
    )
    enrollment_domain = String(help="External login method associated with user accounts allowed to register in course",
                               scope=Scope.settings)
    certificates_show_before_end = Boolean(help="True if students may download certificates before course end",
                                           scope=Scope.settings,
                                           default=False)
    course_image = String(
        help="Filename of the course image",
        scope=Scope.settings,
        # Ensure that courses imported from XML keep their image
        default="images_course_image.jpg"
    )

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

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

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

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

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

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

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

    allow_public_wiki_access = Boolean(help="Whether to allow an unenrolled user to view the Wiki",
                                       default=False,
                                       scope=Scope.settings)
Exemplo n.º 15
0
class OpenClassroomXBlock(XBlock):
    """
    An XBlock providing an embedded Open Classroom lesson.
    """
    loader = ResourceLoader(__name__)

    _EVENT_NAME_EXPLORATION_LOADED = 'openclassroom.exploration.loaded'
    _EVENT_NAME_EXPLORATION_COMPLETED = 'openclassroom.exploration.completed'
    _EVENT_NAME_STATE_TRANSITION = 'openclassroom.exploration.state.changed'

    display_name = String(help=_("Display name of the component"),
                          default=_("Open Classroom lesson"),
                          scope=Scope.content)
    openclassroomid = String(
        help=_("ID of the Open Classroom lesson to embed"),
        default="2DB88aOgiXgD",
        scope=Scope.content)
    src = String(help=_("Source URL of the site"),
                 default="https://lessons.openclassroom.edu.vn",
                 scope=Scope.content)

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

    def render_template(self, path, context):
        return self.loader.render_django_template(
            os.path.join('templates', path),
            context=Context(context),
        )

    def get_translation_content(self):
        try:
            return self.resource_string(
                'static/js/translations/{lang}/textjs.js'.format(
                    lang=utils.translation.get_language(), ))
        except IOError:
            return self.resource_string('static/js/translations/en/textjs.js')

    def student_view(self, context=None):
        """
        The primary view of the OpenClassroomXBlock, shown to students
        when viewing courses.
        """
        frag = Fragment(
            self.render_template("openclassroom.html", {
                'src': self.src,
                'openclassroomid': self.openclassroomid,
            }))
        frag.add_javascript(self.get_translation_content())
        frag.add_javascript(
            self.resource_string('static/js/openclassroom_player.js'))
        frag.add_javascript(self.resource_string("static/js/openclassroom.js"))
        frag.initialize_js('OpenClassroomXBlock')
        return frag

    def author_view(self, context=None):
        """
        A view of the XBlock to show within the Studio preview. For some
        reason, the student_view() does not display, so we show a placeholder
        instead.
        """
        frag = Fragment(
            self.render_template("openclassroom_preview.html", {
                'src': self.src,
                'openclassroomid': self.openclassroomid,
            }))
        frag.add_javascript(self.get_translation_content())
        return frag

    def _log(self, event_name, payload):
        """
        Logger for load, state transition and completion events.
        """
        self.runtime.publish(self, event_name, payload)

    @XBlock.json_handler
    def on_exploration_loaded(self, data, suffix=''):
        """Called when an exploration has loaded."""
        self._log(
            self._EVENT_NAME_EXPLORATION_LOADED, {
                'exploration_id': self.openclassroomid,
                'exploration_version': data['explorationVersion'],
            })

    @XBlock.json_handler
    def on_state_transition(self, data, suffix=''):
        """Called when a state transition in the exploration has occurred."""
        self._log(
            self._EVENT_NAME_STATE_TRANSITION, {
                'exploration_id': self.openclassroomid,
                'old_state_name': data['oldStateName'],
                'new_state_name': data['newStateName'],
                'exploration_version': data['explorationVersion'],
            })

    @XBlock.json_handler
    def on_exploration_completed(self, data, suffix=''):
        """Called when the exploration has been completed."""
        self._log(
            self._EVENT_NAME_EXPLORATION_COMPLETED, {
                'exploration_id': self.openclassroomid,
                'exploration_version': data['explorationVersion'],
            })

    def studio_view(self, context):
        """
        Create a fragment used to display the edit view in the Studio.
        """
        frag = Fragment(
            self.render_template(
                "openclassroom_edit.html", {
                    'src': self.src,
                    'openclassroomid': self.openclassroomid or '',
                    'display_name': self.display_name,
                }))

        frag.add_javascript(self.get_translation_content())
        js_str = pkg_resources.resource_string(
            __name__, "static/js/openclassroom_edit.js")
        frag.add_javascript(unicode(js_str))
        frag.initialize_js('OpenClassroomXBlockEditor')

        return frag

    @XBlock.json_handler
    def studio_submit(self, data, suffix=''):
        """
        Called when submitting the form in Studio.
        """
        self.openclassroomid = data.get('openclassroomid')
        self.src = data.get('src')
        self.display_name = data.get('display_name')

        return {'result': 'success'}

    @staticmethod
    def workbench_scenarios():
        """A canned scenario for display in the workbench."""
        return [
            ("Open Classroom Embedding", """<vertical_demo>
                <openclassroom openclassroomid="0" src="https://lessons.openclassroom.edu.vn"/>
                </vertical_demo>
             """),
        ]
Exemplo n.º 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 changes a self assessed problem to "
        "self and peer assessed."),
                           scope=Scope.user_state)
    task_states = List(
        help="List of state dictionaries of each task within this module.",
        scope=Scope.user_state)
    state = String(
        help="Which step within the current task that the student is on.",
        default="initial",
        scope=Scope.user_state)
    graded = Boolean(
        display_name="Graded",
        help=
        'Defines whether the student gets credit for grading 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)
    track_changes = Boolean(
        display_name="Peer Track Changes",
        help=
        ("EXPERIMENTAL FEATURE FOR PEER GRADING ONLY:  "
         "If set to 'True', peer graders will be able to make changes to the student "
         "submission and those changes will be tracked and shown along with the graded feedback."
         ),
        default=False,
        scope=Scope.settings)
    due = Date(help="Date that this problem is due by", scope=Scope.settings)
    extended_due = Date(
        help="Date that this problem is due by for a particular student. This "
        "can be set by an instructor, and will override the global due "
        "date if it is set to a date that is later than the global due "
        "date.",
        default=None,
        scope=Scope.user_state,
    )
    graceperiod = Timedelta(
        help=
        "Amount of time after the due date that submissions will be accepted",
        scope=Scope.settings)
    version = VersionInteger(help="Current version number",
                             default=DEFAULT_VERSION,
                             scope=Scope.settings)
    data = String(help="XML data for the problem",
                  scope=Scope.content,
                  default=DEFAULT_DATA)
    weight = Float(
        display_name="Problem Weight",
        help=
        "Defines the number of points each problem is worth. If the value is not set, each problem is worth one point.",
        scope=Scope.settings,
        values={
            "min": 0,
            "step": ".1"
        },
        default=1)
    min_to_calibrate = Integer(
        display_name="Minimum Peer Grading Calibrations",
        help=
        "The minimum number of calibration essays each student will need to complete for peer grading.",
        default=3,
        scope=Scope.settings,
        values={
            "min": 1,
            "max": 20,
            "step": "1"
        })
    max_to_calibrate = Integer(
        display_name="Maximum Peer Grading Calibrations",
        help=
        "The maximum number of calibration essays each student will need to complete for peer grading.",
        default=6,
        scope=Scope.settings,
        values={
            "min": 1,
            "max": 20,
            "step": "1"
        })
    peer_grader_count = Integer(
        display_name="Peer Graders per Response",
        help="The number of peers who will grade each submission.",
        default=3,
        scope=Scope.settings,
        values={
            "min": 1,
            "step": "1",
            "max": 5
        })
    required_peer_grading = Integer(
        display_name="Required Peer Grading",
        help=
        "The number of other students each student making a submission will have to grade.",
        default=3,
        scope=Scope.settings,
        values={
            "min": 1,
            "step": "1",
            "max": 5
        })
    peer_grade_finished_submissions_when_none_pending = Boolean(
        display_name='Allow "overgrading" of peer submissions',
        help=
        ("EXPERIMENTAL FEATURE.  Allow students to peer grade submissions that already have the requisite number of graders, "
         "but ONLY WHEN all submissions they are eligible to grade already have enough graders.  "
         "This is intended for use when settings for `Required Peer Grading` > `Peer Graders per Response`"
         ),
        default=False,
        scope=Scope.settings,
    )
    markdown = String(help="Markdown source of this module",
                      default=textwrap.dedent("""\
                    [prompt]
                        <h3>Censorship in the Libraries</h3>

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

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

        """),
                      scope=Scope.settings)
Exemplo n.º 17
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_fragment_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>
             """),
        ]
Exemplo n.º 18
0
class AnnotatableBlock(
    RawMixin,
    XmlMixin,
    EditingMixin,
    XModuleDescriptorToXBlockMixin,
    XModuleToXBlockMixin,
    HTMLSnippet,
    ResourceTemplates,
    XModuleMixin,
):
    """
    Annotatable XBlock.
    """

    data = String(
        help=_("XML data for the annotation"),
        scope=Scope.content,
        default=textwrap.dedent(HTML(u"""
        <annotatable>
            <instructions>
                <p>Enter your (optional) instructions for the exercise in HTML format.</p>
                <p>Annotations are specified by an <code>{}annotation{}</code> tag which may may have the following attributes:</p>
                <ul class="instructions-template">
                    <li><code>title</code> (optional). Title of the annotation. Defaults to <i>Commentary</i> if omitted.</li>
                    <li><code>body</code> (<b>required</b>). Text of the annotation.</li>
                    <li><code>problem</code> (optional). Numeric index of the problem associated with this annotation. This is a zero-based index, so the first problem on the page would have <code>problem="0"</code>.</li>
                    <li><code>highlight</code> (optional). Possible values: yellow, red, orange, green, blue, or purple. Defaults to yellow if this attribute is omitted.</li>
                </ul>
            </instructions>
            <p>Add your HTML with annotation spans here.</p>
            <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. <annotation title="My title" body="My comment" highlight="yellow" problem="0">Ut sodales laoreet est, egestas gravida felis egestas nec.</annotation> Aenean at volutpat erat. Cras commodo viverra nibh in aliquam.</p>
            <p>Nulla facilisi. <annotation body="Basic annotation example." problem="1">Pellentesque id vestibulum libero.</annotation> Suspendisse potenti. Morbi scelerisque nisi vitae felis dictum mattis. Nam sit amet magna elit. Nullam volutpat cursus est, sit amet sagittis odio vulputate et. Curabitur euismod, orci in vulputate imperdiet, augue lorem tempor purus, id aliquet augue turpis a est. Aenean a sagittis libero. Praesent fringilla pretium magna, non condimentum risus elementum nec. Pellentesque faucibus elementum pharetra. Pellentesque vitae metus eros.</p>
        </annotatable>
        """).format(Text('<'), Text('>')))
    )
    display_name = String(
        display_name=_("Display Name"),
        help=_("The display name for this component."),
        scope=Scope.settings,
        default=_('Annotation'),
    )

    uses_xmodule_styles_setup = True

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

    studio_view_js = {
        'js': [
            resource_string(__name__, 'js/src/raw/edit/xml.js'),
        ],
        'xmodule_js': resource_string(__name__, 'js/src/xmodule.js'),
    }
    studio_view_css = {
        'scss': [
            resource_string(__name__, 'css/codemirror/codemirror.scss'),
        ],
    }
    studio_js_module_name = "XMLEditingDescriptor"
    mako_template = "widgets/raw-edit.html"

    icon_class = 'annotatable'
    resources_dir = None

    HIGHLIGHT_COLORS = ['yellow', 'orange', 'purple', 'blue', 'green']

    def _get_annotation_class_attr(self, index, el):  # lint-amnesty, pylint: disable=unused-argument
        """ Returns a dict with the CSS class attribute to set on the annotation
            and an XML key to delete from the element.
         """

        attr = {}
        cls = ['annotatable-span', 'highlight']
        highlight_key = 'highlight'
        color = el.get(highlight_key)

        if color is not None:
            if color in self.HIGHLIGHT_COLORS:
                cls.append('highlight-' + color)
            attr['_delete'] = highlight_key
        attr['value'] = ' '.join(cls)

        return {'class': attr}

    def _get_annotation_data_attr(self, index, el):  # lint-amnesty, pylint: disable=unused-argument
        """ Returns a dict in which the keys are the HTML data attributes
            to set on the annotation element. Each data attribute has a
            corresponding 'value' and (optional) '_delete' key to specify
            an XML attribute to delete.
        """

        data_attrs = {}
        attrs_map = {
            'body': 'data-comment-body',
            'title': 'data-comment-title',
            'problem': 'data-problem-id'
        }

        for xml_key in attrs_map.keys():  # lint-amnesty, pylint: disable=consider-iterating-dictionary
            if xml_key in el.attrib:
                value = el.get(xml_key, '')
                html_key = attrs_map[xml_key]
                data_attrs[html_key] = {'value': value, '_delete': xml_key}

        return data_attrs

    def _render_annotation(self, index, el):
        """ Renders an annotation element for HTML output.  """
        attr = {}
        attr.update(self._get_annotation_class_attr(index, el))
        attr.update(self._get_annotation_data_attr(index, el))

        el.tag = 'span'

        for key in attr.keys():  # lint-amnesty, pylint: disable=consider-iterating-dictionary
            el.set(key, attr[key]['value'])
            if '_delete' in attr[key] and attr[key]['_delete'] is not None:
                delete_key = attr[key]['_delete']
                del el.attrib[delete_key]

    def _render_content(self):
        """ Renders annotatable content with annotation spans and returns HTML. """

        xmltree = etree.fromstring(self.data)
        content = etree.tostring(xmltree, encoding='unicode')

        xmltree = etree.fromstring(content)
        xmltree.tag = 'div'
        if 'display_name' in xmltree.attrib:
            del xmltree.attrib['display_name']

        index = 0
        for el in xmltree.findall('.//annotation'):
            self._render_annotation(index, el)
            index += 1

        return etree.tostring(xmltree, encoding='unicode')

    def _extract_instructions(self, xmltree):
        """ Removes <instructions> from the xmltree and returns them as a string, otherwise None. """
        instructions = xmltree.find('instructions')
        if instructions is not None:
            instructions.tag = 'div'
            xmltree.remove(instructions)
            return etree.tostring(instructions, encoding='unicode')
        return None

    def get_html(self):
        """ Renders parameters to template. """

        xmltree = etree.fromstring(self.data)
        instructions = self._extract_instructions(xmltree)

        context = {
            'display_name': self.display_name_with_default,
            'element_id': self.location.html_id(),
            'instructions_html': instructions,
            'content_html': self._render_content()
        }

        return self.system.render_template('annotatable.html', context)

    def student_view(self, context):  # lint-amnesty, pylint: disable=unused-argument
        """
        Renders the output that a student will see.
        """
        fragment = Fragment()
        fragment.add_content(self.get_html())
        add_webpack_to_fragment(fragment, 'AnnotatableBlockPreview')
        shim_xmodule_js(fragment, 'Annotatable')

        return fragment

    def studio_view(self, _context):
        """
        Return the studio view.
        """
        fragment = Fragment(
            self.system.render_template(self.mako_template, self.get_context())
        )
        add_webpack_to_fragment(fragment, 'AnnotatableBlockStudio')
        shim_xmodule_js(fragment, self.studio_js_module_name)
        return fragment
Exemplo n.º 19
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": unicode(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': urllib.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 unicode(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),
                              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()
        if current_raw_earned is None or current_raw_earned > saved_raw_earned:
            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 self.item_state.iteritems():
            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>"
             ),
        ]
Exemplo n.º 20
0
class TrueOrFalseXBlock(XBlock):
    """
        XBlock Settings
    """
    display_name = String(display_name="Nombre del Componente",
                          help="Nombre del componente",
                          scope=Scope.settings,
                          default="Preguntas Verdadero o Falso")
    questions = Dict(default={
        '1': {
            'question': 'Enunciado de ejemplo (Verdadero)',
            'answer': True
        },
        '2': {
            'question': 'Enunciado de ejemplo (Falso)',
            'answer': False
        }
    },
                     scope=Scope.settings,
                     help="Lista de preguntas")
    weight = Float(
        display_name='Puntaje Máximo',
        help='Ingrese el puntaje máximo del ejercicio',
        default=1,
        values={
            'min': 0,
            'step': 1
        },
        scope=Scope.settings,
    )
    max_attempts = Integer(
        display_name='Intentos Permitidos',
        help=
        'Ingrese la cantidad de intentos máximos permitidos para el ejercicio',
        default=2,
        values={
            'min': 1,
            'step': 1
        },
        scope=Scope.settings,
    )
    show_answer = String(
        display_name="Mostrar Respuestas",
        help=
        "Ingrese cuándo se habilita el botón para mostrar respuestas correctas",
        default="Finalizado",
        values=["Finalizado", "Ocultar"],
        scope=Scope.settings)
    has_score = True
    icon_class = "problem"
    """
        Student state
    """
    is_answered = Boolean(default=False, scope=Scope.user_state)
    student_answers = Dict(default={'1': '', '2': ''}, scope=Scope.user_state)
    score = Float(
        default=0.0,
        scope=Scope.user_state,
    )
    attempts = Integer(
        default=0,
        scope=Scope.user_state,
    )

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

    def build_fragment(
        self,
        rendered_template,
        initialize_js_func,
        additional_css=[],
        additional_js=[],
    ):
        #  pylint: disable=dangerous-default-value, too-many-arguments
        """
            Creates a fragment for display.
        """
        fragment = Fragment(rendered_template)
        for item in additional_css:
            url = self.runtime.local_resource_url(self, item)
            fragment.add_css_url(url)
        for item in additional_js:
            url = self.runtime.local_resource_url(self, item)
            fragment.add_javascript_url(url)
        settings = {
            'image_path':
            self.runtime.local_resource_url(self, 'static/images/'),
            'is_past_due': self.get_is_past_due()
        }
        fragment.initialize_js(initialize_js_func, json_args=settings)
        return fragment

    def student_view(self, context={}):
        """
            Create a fragment used to display the student view in the LMS.
        """
        # sort questions list
        questions_list = [[k, v] for k, v in list(self.questions.items())]
        questions_list = sorted(questions_list, key=lambda x: int(x[0]))

        # student status
        indicator_class = self.get_indicator_class()

        context.update({
            'xblock':
            self,
            'no_more_attempts':
            self.max_attempts and self.max_attempts > 0
            and self.attempts >= self.max_attempts,
            'questions_list':
            questions_list,
            'problem_progress':
            self.get_problem_progress(),
            'indicator_class':
            indicator_class,
            'image_path':
            self.runtime.local_resource_url(self, 'static/images/'),
            'location':
            str(self.location).split('@')[-1],
            'show_correctness':
            self.get_show_correctness(),
            'is_past_due':
            self.get_is_past_due
        })
        template = loader.render_django_template(
            'static/html/trueorfalse.html',
            context=Context(context),
            i18n_service=self.runtime.service(self, 'i18n'),
        )
        frag = self.build_fragment(
            template,
            initialize_js_func='TrueOrFalseXBlock',
            additional_css=[
                'static/css/trueorfalse.css',
            ],
            additional_js=[
                'static/js/src/trueorfalse.js',
            ],
        )
        return frag

    def studio_view(self, context):
        """
            Create a fragment used to display the edit view in the Studio.
        """
        # sort questions list
        questions_list = [[k, v] for k, v in list(self.questions.items())]
        questions_list = sorted(questions_list, key=lambda x: int(x[0]))
        context.update({
            'field_display_name': self.fields['display_name'],
            'field_questions': self.fields['questions'],
            'field_weight': self.fields['weight'],
            'field_show_answer': self.fields['show_answer'],
            'field_max_attempts': self.fields['max_attempts'],
            'xblock': self,
            'questions_list': questions_list,
            'location': self.location
        })
        template = loader.render_django_template(
            'static/html/studio.html',
            context=Context(context),
            i18n_service=self.runtime.service(self, 'i18n'),
        )
        frag = self.build_fragment(
            template,
            initialize_js_func='TrueOrFalseEditBlock',
            additional_css=[
                'static/css/trueorfalse.css',
            ],
            additional_js=[
                'static/js/src/studio.js',
            ],
        )
        return frag

    @XBlock.json_handler
    def studio_submit(self, data, suffix=''):
        """
            Called when submitting the form in Studio.
        """
        new_questions = {}
        questions = data.get('questions_list')
        for q in questions:
            answer = True
            if q['answer'] == 'false':
                answer = False
            new_questions[q['id_question']] = {
                'question': q['question'],
                'answer': answer
            }

        self.display_name = data.get('display_name')
        self.show_answer = data.get('show_answer')
        if data.get('weight') and int(data.get('weight')) >= 0:
            self.weight = int(data.get('weight'))
        if data.get('max_attempts') and int(data.get('max_attempts')) > 0:
            self.max_attempts = int(data.get('max_attempts'))
        self.questions = new_questions
        return {'result': 'success'}

    def get_indicator_class(self):
        indicator_class = 'unanswered'
        if self.is_answered and self.attempts:
            if self.score >= 1:
                indicator_class = 'correct'
            else:
                indicator_class = 'incorrect'
        return indicator_class

    def get_show_correctness(self):
        if hasattr(self, 'show_correctness'):
            if self.show_correctness == 'past_due':
                if self.is_past_due():
                    return "always"
                else:
                    return "never"
            else:
                return self.show_correctness
        else:
            return "always"

    def get_is_past_due(self):
        if hasattr(self, 'show_correctness'):
            return self.is_past_due()
        else:
            return False

    def is_past_due(self):
        """
            Determine if component is past-due
        """
        # These values are pulled from platform.
        # They are defaulted to None for tests.
        due = getattr(self, 'due', None)
        graceperiod = getattr(self, 'graceperiod', None)
        # Calculate the current DateTime so we can compare the due date to it.
        # datetime.utcnow() returns timezone naive date object.
        now = datetime.datetime.utcnow()
        if due is not None:
            # Remove timezone information from platform provided due date.
            # Dates are stored as UTC timezone aware objects on platform.
            due = due.replace(tzinfo=None)
            if graceperiod is not None:
                # Compare the datetime objects (both have to be timezone naive)
                due = due + graceperiod
            return now > due
        return False

    def get_problem_progress(self):
        """
            Returns a statement of progress for the XBlock, which depends
            on the user's current score
        """
        calif = ' (no calificable)'
        if hasattr(self, 'graded') and self.graded:
            calif = ' (calificable)'
        if self.weight == 0:
            result = '0 puntos posibles' + calif
        elif self.attempts <= 0:
            if self.weight == 1:
                result = "1 punto posible" + calif
            else:
                result = str(self.weight) + " puntos posibles" + calif
        else:
            scaled_score = self.score * self.weight
            # No trailing zero and no scientific notation
            score_string = ('%.15f' % scaled_score).rstrip('0').rstrip('.')
            if self.weight == 1:
                result = str(score_string) + "/" + str(
                    self.weight) + " punto" + calif
            else:
                result = str(score_string) + "/" + str(
                    self.weight) + " puntos" + calif
        return result

    def max_score(self):
        """
            Returns the configured number of possible points for this component.
            Arguments:
                None
            Returns:
                float: The number of possible points for this component
        """
        return self.weight

    # handler para votar sí o no
    @XBlock.json_handler
    def responder(self, data, suffix=''):  # pylint: disable=unused-argument
        """
        Answer true or false
        """
        # Avoid two answer at the same time
        if ((self.attempts + 1) <=
                self.max_attempts) or self.max_attempts <= 0:
            nuevas_resps = {}
            texto = "¡Respuesta Correcta!"
            buenas = 0.0
            malas = 0.0
            total = len(self.questions)
            for e in data['answers']:
                idpreg = e['name']
                miresp = ''
                if e['value'] == 'verdadero':
                    miresp = True
                    nuevas_resps[idpreg] = 'verdadero'
                elif e['value'] == 'falso':
                    miresp = False
                    nuevas_resps[idpreg] = 'falso'
                if miresp != self.questions[idpreg]['answer']:
                    texto = "Respuesta Incorrecta"
                    malas += 1
                else:
                    buenas += 1

            malas = (total - buenas)
            if malas > 0:
                texto = "Respuesta Incorrecta"

            if nuevas_resps:
                self.student_answers = nuevas_resps

            self.score = float(buenas / (malas + buenas))

            if self.score > 0 and self.score < 1:
                texto = "Respuesta parcialmente correcta"

            ptje = float(self.weight) * self.score
            try:
                self.runtime.publish(self, 'grade', {
                    'value': ptje,
                    'max_value': self.weight
                })
                self.attempts += 1
            except IntegrityError:
                pass

            self.is_answered = True

            indicator_class = self.get_indicator_class()

            return {
                'texto': texto,
                'score': self.score,
                'nro_de_intentos': self.max_attempts,
                'intentos': self.attempts,
                'indicator_class': indicator_class,
                'show_correctness': self.get_show_correctness(),
                'show_answers': self.show_answer,
                'problem_progress': self.get_problem_progress()
            }
        else:
            return {
                'texto':
                str('Error: El estado de este problema fue modificado, por favor recargue la página.'
                    ),
                'score':
                self.score,
                'nro_de_intentos':
                self.max_attempts,
                'intentos':
                self.attempts,
                'indicator_class':
                self.get_indicator_class(),
                'show_correctness':
                self.get_show_correctness(),
                'show_answers':
                self.show_answer,
                'problem_progress':
                self.get_problem_progress()
            }

    @XBlock.json_handler
    def mostrar_respuesta(self, data, suffix=''):
        """
            Show correct/incorrect answers
        """
        if (self.attempts >= self.max_attempts and self.show_answer
                == 'Finalizado') or self.show_answer == 'Mostrar':
            return {'preguntas': self.questions}
        else:
            return {}

    @staticmethod
    def workbench_scenarios():
        """A canned scenario for display in the workbench."""
        return [
            ("TrueOrFalseXBlock", """<trueorfalse/>
             """),
            ("Multiple TrueOrFalseXBlock", """<vertical_demo>
                <trueorfalse/>
                <trueorfalse/>
                <trueorfalse/>
                </vertical_demo>
             """),
        ]
Exemplo n.º 21
0
class GenericXBlock(XBlock):
    """XBlock for testing pure xblock xml import"""
    has_children = True
    field1 = String(default="something", scope=Scope.user_state)
    field2 = Integer(scope=Scope.user_state)
Exemplo n.º 22
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():
                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(
            "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)
            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
        else:
            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

        for user_partition_id, group_ids in self.group_access.iteritems():
            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))

        if has_invalid_groups:
            validation.add(
                ValidationMessage(ValidationMessage.ERROR,
                                  INVALID_USER_PARTITION_GROUP_VALIDATION))

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

        return validation
Exemplo n.º 23
0
class LibraryContentFields(object):
    """
    Fields for the LibraryContentModule.

    Separated out for now because they need to be added to the module and the
    descriptor.
    """
    # Please note the display_name of each field below is used in
    # common/test/acceptance/pages/studio/library.py:StudioLibraryContentXBlockEditModal
    # to locate input elements - keep synchronized
    display_name = String(
        display_name=_("Display Name"),
        help=_("The display name for this component."),
        default="Randomized Content Block",
        scope=Scope.settings,
    )
    source_library_id = String(
        display_name=_("Library"),
        help=_("Select the library from which you want to draw content."),
        scope=Scope.settings,
        values_provider=lambda instance: instance.source_library_values(),
    )
    source_library_version = String(
        # This is a hidden field that stores the version of source_library when we last pulled content from it
        display_name=_("Library Version"),
        scope=Scope.settings,
    )
    mode = String(
        display_name=_("Mode"),
        help=_("Determines how content is drawn from the library"),
        default="random",
        values=
        [{
            "display_name": _("Choose n at random"),
            "value": "random"
        }
         # Future addition: Choose a new random set of n every time the student refreshes the block, for self tests
         # Future addition: manually selected blocks
         ],
        scope=Scope.settings,
    )
    max_count = Integer(
        display_name=_("Count"),
        help=_("Enter the number of components to display to each student."),
        default=1,
        scope=Scope.settings,
    )
    capa_type = String(
        display_name=_("Problem Type"),
        help=
        _('Choose a problem type to fetch from the library. If "Any Type" is selected no filtering is applied.'
          ),
        default=ANY_CAPA_TYPE_VALUE,
        values=_get_capa_types(),
        scope=Scope.settings,
    )
    selected = List(
        # This is a list of (block_type, block_id) tuples used to record
        # which random/first set of matching blocks was selected per user
        default=[],
        scope=Scope.user_state,
    )
    has_children = True

    @property
    def source_library_key(self):
        """
        Convenience method to get the library ID as a LibraryLocator and not just a string
        """
        return LibraryLocator.from_string(self.source_library_id)
Exemplo n.º 24
0
 class TestableInheritingXBlock(XmlDescriptor):  # lint-amnesty, pylint: disable=abstract-method
     """
     An XBlock we can use in these tests.
     """
     inherited = String(scope=Scope.settings, default="the default")
     not_inherited = String(scope=Scope.settings, default="nothing")
Exemplo n.º 25
0
class RecommenderXBlock(HelperXBlock):
    """
    This XBlock will show a set of recommended resources which may be helpful
    to students solving a given problem. The resources are provided and edited
    by students; they can also vote for useful resources and flag problematic
    ones.
    """
    seen = Boolean(
        help=
        "Has the student interacted with the XBlock before? Used to show optional tutorial.",
        default=False,
        scope=Scope.user_info)

    version = String(
        help=
        "The version of this RecommenderXBlock. Used to simplify migrations.",
        default="recommender.v1.0",
        scope=Scope.content)

    intro_enabled = Boolean(
        help=
        "Should we show the users a short usage tutorial the first time they see the XBlock?",
        default=True,
        scope=Scope.content)

    # A dict of default recommendations supplied by the instructors to
    # seed the list with before students add new recommendations.

    # Also, useful for testing.
    # Usage: default_recommendations[index] = {
    #    "id": (String) A unique ID. The ID is currently derived from
    #          the URL, but this has changed and may change again
    #    "title": (String) a 1-3 sentence summary description of a resource
    #    "upvotes" : (Integer) number of upvotes,
    #    "downvotes" : (Integer) number of downvotes,
    #    "url" : (String) link to resource,
    #    "description" : (String) the url of a resource's screenshot.
    #                    'screenshot' would be a better name, but would
    #                    require a cumbersome data migration.
    #    "descriptionText" : (String) a potentially longer overview of the resource }
    #    we use url as key (index) of resource
    default_recommendations = JSONField(
        help=
        "Dict of instructor-supplied help resources to seed the resource list with.",
        default={},
        scope=Scope.content)

    # A dict of recommendations provided by students.
    # Usage: the same as default_recommendations
    recommendations = JSONField(help="Current set of recommended resources",
                                default={},
                                scope=Scope.user_state_summary)

    # A list of recommendations removed by course staff. This is used to filter out
    # cheats, give-aways, spam, etc.
    # Usage: the same as default_recommendations plus
    #    removed_recommendations[index]['reason'] = (String) the reason why
    #            course staff remove this resource
    removed_recommendations = Dict(help="Dict of removed resources",
                                   default={},
                                   scope=Scope.user_state_summary)

    # A list of endorsed recommendations' ids -- the recommendations the course
    # staff marked as particularly helpful.
    # Usage: endorsed_recommendation_ids[index] = (String) id of a
    #    endorsed resource
    endorsed_recommendation_ids = List(help="List of endorsed resources' ID",
                                       default=[],
                                       scope=Scope.user_state_summary)

    # A list of reasons why the resources were endorsed.
    # Usage: endorsed_recommendation_reasons[index] = (String) the reason
    #    why the resource (id = endorsed_recommendation_ids[index]) is endorsed
    endorsed_recommendation_reasons = List(
        help="List of reasons why the resources are endorsed",
        default=[],
        scope=Scope.user_state_summary)

    # A dict of problematic recommendations which are flagged by users for review
    # by instructors. Used to remove spam, etc.
    # Usage: flagged_accum_resources[userId] = {
    #    "problematic resource id": (String) reason why the resource is
    #            flagged as problematic by that user }
    flagged_accum_resources = Dict(
        help=
        "Dict of potentially problematic resources which were flagged by users",
        default={},
        scope=Scope.user_state_summary)

    # A list of recommendations' ids which a particular user upvoted, so users
    # cannot vote twice
    # Usage: upvoted_ids[index] = (String) id of a resource which was
    #    upvoted by the current user
    upvoted_ids = List(help="List of resources' ids which user upvoted",
                       default=[],
                       scope=Scope.user_state)

    # A list of recommendations' ids which user downvoted, so users cannot vote twice.
    # Usage: downvoted_ids[index] = (String) id of a resource which was
    #    downvoted by the current user
    downvoted_ids = List(help="List of resources' ids which user downvoted",
                         default=[],
                         scope=Scope.user_state)

    # A list of problematic recommendations' ids which user flagged.
    # Usage: flagged_ids[index] = (String) id of a problematic resource which
    #    was flagged by the current user
    flagged_ids = List(
        help="List of problematic resources' ids which the user flagged",
        default=[],
        scope=Scope.user_state)

    # A list of reasons why the resources corresponding to those in flagged_ids were flagged
    # Usage: flagged_reasons[index] = (String) reason why the resource
    #   'flagged_ids[index]' was flagged by the current user as problematic
    flagged_reasons = List(
        help="List of reasons why the corresponding resources were flagged",
        default=[],
        scope=Scope.user_state)

    # The file system we used to store uploaded screenshots
    fs = Filesystem(help="File system for screenshots",
                    scope=Scope.user_state_summary)

    client_configuration = Dict(help="Dict of customizable settings",
                                default={
                                    'disable_dev_ux': True,
                                    'entries_per_page': 5,
                                    'page_span': 2
                                },
                                scope=Scope.content)

    # the dictionary keys for storing the content of a recommendation
    resource_content_fields = [
        'url', 'title', 'description', 'descriptionText'
    ]

    def _get_onetime_url(self, filename):
        """
        Return one time url for uploaded screenshot

        We benchmarked this as less than 8ms on a sandbox machine.
        """
        if filename.startswith('fs://'):
            return str(
                self.fs.get_url(filename.replace('fs://', ''),
                                1000 * 60 * 60 * 10))
        else:
            return filename

    def _error_handler(self, error_msg, event, resource_id=None):
        """
        Generate an error dictionary if something unexpected happens, such as
        a user upvoting a resource which no longer exists. We both log to this
        to the event logs, and return to the browser.
        """
        result = {'error': error_msg}
        if resource_id is not None:
            result['id'] = resource_id
        tracker.emit(event, result)
        raise JsonHandlerError(400, result['error'])

    def _check_redundant_resource(self, resource_id, event_name, result):
        """
        Check whether the submitted resource is redundant. If true, raise an
        exception and return a HTTP status code for the error.
        """
        # check url for redundancy
        if resource_id in self.recommendations:
            result['error'] = self.ugettext(
                'The resource you are attempting to provide already exists')
            for field in self.resource_content_fields:
                result['dup_' +
                       field] = self.recommendations[resource_id][field]
            result['dup_id'] = self.recommendations[resource_id]['id']
            tracker.emit(event_name, result)
            raise JsonHandlerError(409, result['error'])

    def _check_removed_resource(self, resource_id, event_name, result):
        """
        Check whether the submitted resource is removed. If true, raise an
        exception and return a HTTP status code for the error.
        """
        if resource_id in self.removed_recommendations:
            result['error'] = self.ugettext(
                'The resource you are attempting to '
                'provide has been disallowed by the staff. '
                'Reason: ' +
                self.removed_recommendations[resource_id]['reason'])
            for field in self.resource_content_fields:
                result[
                    'dup_' +
                    field] = self.removed_recommendations[resource_id][field]
            result['dup_id'] = self.removed_recommendations[resource_id]['id']
            tracker.emit(event_name, result)
            raise JsonHandlerError(405, result['error'])

    def _validate_resource(self, data_id, event):
        """
        Validate whether the resource exists in the database. If not,
        generate the error message, and return to the browser for a given
        event, otherwise, return the stemmed id.
        """
        resource_id = stem_url(data_id)
        if resource_id not in self.recommendations:
            msg = self.ugettext('The selected resource does not exist')
            self._error_handler(msg, event, resource_id)
        return resource_id

    def _check_upload_file(self, request, file_types, file_type_error_msg,
                           event, file_size_threshold):
        """
        Check the type and size of uploaded file. If the file type is
        unexpected or the size exceeds the threshold, log the error and return
        to browser, otherwise, return None.
        """
        # Check invalid file types
        file_type_error = False
        file_type = [
            ft for ft in file_types if any(
                str(request.POST['file'].file).lower().endswith(ext)
                for ext in file_types[ft]['extension'])
        ]

        # Check extension
        if not file_type:
            file_type_error = True
        else:
            file_type = file_type[0]
            # Check mimetypes
            if request.POST['file'].file.content_type not in file_types[
                    file_type]['mimetypes']:
                file_type_error = True
            else:
                if 'magic' in file_types[file_type]:
                    # Check magic number
                    headers = file_types[file_type]['magic']
                    if request.POST['file'].file.read(
                            len(headers[0]) / 2).encode('hex') not in headers:
                        file_type_error = True
                    request.POST['file'].file.seek(0)

        if file_type_error:
            response = Response()
            tracker.emit(event, {'uploadedFileName': 'FILE_TYPE_ERROR'})
            response.status = 415
            response.body = json.dumps({'error': file_type_error_msg})
            response.headers['Content-Type'] = 'application/json'
            return response

        # Check whether file size exceeds threshold (30MB)
        if request.POST['file'].file.size > file_size_threshold:
            response = Response()
            tracker.emit(event, {'uploadedFileName': 'FILE_SIZE_ERROR'})
            response.status = 413
            response.body = json.dumps({
                'error':
                self.ugettext('Size of uploaded file exceeds threshold')
            })
            response.headers['Content-Type'] = 'application/json'
            return response

        return file_type

    def _raise_pyfs_error(self, event):
        """
        Log and return an error if the pyfs is not properly set.
        """
        response = Response()
        error = self.ugettext('The configuration of pyfs is not properly set')
        tracker.emit(event, {'uploadedFileName': 'IMPROPER_FS_SETUP'})
        response.status = 404
        response.body = json.dumps({'error': error})
        response.headers['Content-Type'] = 'application/json'
        return response

    def _init_template_lookup(self):
        """
        Initialize template_lookup by adding mappings between strings and urls.
        """
        global template_lookup
        template_lookup = TemplateLookup()
        template_lookup.put_string(
            "recommenderstudio.html",
            self.resource_string("static/html/recommenderstudio.html"))
        template_lookup.put_string(
            "recommender.html",
            self.resource_string("static/html/recommender.html"))
        template_lookup.put_string(
            "resourcebox.html",
            self.resource_string("static/html/resourcebox.html"))

    def get_client_configuration(self):
        """
        Return the parameters for client-side configuration settings.

        Returns:
                disable_dev_ux: feature flag for any new UX under development
                                which should not appear in prod
                entries_per_page: the number of resources in each page
                page_span: page range in pagination control
                intro: whether to take users through a short usage tutorial
                       the first time they see the RecommenderXBlock
                is_user_staff: whether the user is staff
        """
        result = self.client_configuration.copy()
        result['is_user_staff'] = self.get_user_is_staff()
        result['intro'] = not self.seen and self.intro_enabled
        if not self.seen:
            # Mark the user who interacted with the XBlock first time as seen,
            # in order not to show the usage tutorial in future.
            self.seen = True
        tracker.emit('get_client_configuration', result)
        return result

    @XBlock.json_handler
    def set_client_configuration(self, data, _suffix=''):  # pylint: disable=unused-argument
        """
        Set the parameters for student-view, client side configurations.

        Args:
                data: dict in JSON format. Keys in data:
                  disable_dev_ux: feature flag for any new UX under development
                                  which should not appear in prod
                  entries_per_page: the number of resources in each page
                  page_span: page range in pagination control
                  intro_enable: Should we show the users a short usage tutorial
                                the first time they see the XBlock?
        """
        self.intro_enabled = data['intro_enable']
        for key in ['disable_dev_ux', 'page_span', 'entries_per_page']:
            self.client_configuration[key] = data[key]

        tracker.emit('set_client_configuration', data)
        return {}

    @XBlock.json_handler
    def handle_vote(self, data, _suffix=''):  # pylint: disable=unused-argument
        """
        Add/Subtract a vote to a resource entry.

        Args:
                data: dict in JSON format
                data['id']: the ID of the resouce which was upvoted/downvoted
                data['event']: recommender_upvote or recommender_downvote
        Returns:
                result: dict in JSON format
                result['error']: error message generated if the process fails
                result['oldVotes']: original # of votes
                result['newVotes']: votes after this action
                result['toggle']: boolean indicator for whether the resource
                                  was switched from downvoted to upvoted
        """
        resource_id = self._validate_resource(data['id'], data['event'])

        result = {}
        result['id'] = resource_id
        is_event_upvote = (data['event'] == 'recommender_upvote')
        result['oldVotes'] = (self.recommendations[resource_id]['upvotes'] -
                              self.recommendations[resource_id]['downvotes'])

        upvoting_existing_upvote = is_event_upvote and resource_id in self.upvoted_ids
        downvoting_existing_downvote = not is_event_upvote and resource_id in self.downvoted_ids

        if upvoting_existing_upvote:
            # While the user is trying to upvote a resource which has been
            # upvoted, we restore the resource to unvoted
            self.upvoted_ids.remove(resource_id)
            self.recommendations[resource_id]['upvotes'] -= 1
        elif downvoting_existing_downvote:
            # While the user is trying to downvote a resource which has
            # been downvoted, we restore the resource to unvoted
            self.downvoted_ids.remove(resource_id)
            self.recommendations[resource_id]['downvotes'] -= 1
        elif is_event_upvote:  # New upvote
            if resource_id in self.downvoted_ids:
                self.downvoted_ids.remove(resource_id)
                self.recommendations[resource_id]['downvotes'] -= 1
                result['toggle'] = True
            self.upvoted_ids.append(resource_id)
            self.recommendations[resource_id]['upvotes'] += 1
        else:  # New downvote
            if resource_id in self.upvoted_ids:
                self.upvoted_ids.remove(resource_id)
                self.recommendations[resource_id]['upvotes'] -= 1
                result['toggle'] = True
            self.downvoted_ids.append(resource_id)
            self.recommendations[resource_id]['downvotes'] += 1

        result['newVotes'] = (self.recommendations[resource_id]['upvotes'] -
                              self.recommendations[resource_id]['downvotes'])
        tracker.emit(data['event'], result)
        return result

    @XBlock.handler
    def upload_screenshot(self, request, _suffix=''):  # pylint: disable=unused-argument
        """
        Upload a screenshot for an entry of resource as a preview (typically to S3 or filesystem).

        Args:
                request: HTTP POST request
                request.POST['file'].file: the file to be uploaded
        Returns:
                response: HTTP response
                response.body (response.responseText): name of the uploaded file

        We validate that this is a valid JPG, GIF, or PNG by checking magic number, mimetype,
        and extension all correspond. We also limit to 30MB. We save the file under its MD5
        hash to (1) avoid name conflicts, (2) avoid race conditions and (3) save space.
        """
        # Check invalid file types
        image_types = {
            'jpeg': {
                'extension': [".jpeg", ".jpg"],
                'mimetypes': ['image/jpeg', 'image/pjpeg'],
                'magic': ["ffd8"]
            },
            'png': {
                'extension': [".png"],
                'mimetypes': ['image/png'],
                'magic': ["89504e470d0a1a0a"]
            },
            'gif': {
                'extension': [".gif"],
                'mimetypes': ['image/gif'],
                'magic': ["474946383961", "474946383761"]
            }
        }
        file_type_error_msg = 'Please upload an image in GIF/JPG/PNG'
        result = self._check_upload_file(request, image_types,
                                         file_type_error_msg,
                                         'upload_screenshot', 31457280)
        if isinstance(result, Response):
            return result

        try:
            content = request.POST['file'].file.read()
            file_id = hashlib.md5(content).hexdigest()
            file_name = (file_id + '.' + result)

            fhwrite = self.fs.open(file_name, "wb")
            fhwrite.write(content)
            fhwrite.close()
        except IOError:
            return self._raise_pyfs_error('upload_screenshot')

        response = Response()
        response.body = json.dumps({'file_name': str("fs://" + file_name)})
        response.headers['Content-Type'] = 'application/json'
        tracker.emit('upload_screenshot', {'uploadedFileName': response.body})
        response.status = 200
        return response

    @XBlock.json_handler
    def add_resource(self, data, _suffix=''):  # pylint: disable=unused-argument
        """
        Add a new resource entry.

        Args:
                data: dict in JSON format
                data[resource_content_field]: the resource to be added. Dictionary of
                                              description, etc. as defined above
        Returns:
                result: dict in JSON format
                result['error']: error message generated if the addition fails
                result[resource_content_field]: the content of the added resource
        """
        # Construct new resource
        result = {}
        for field in self.resource_content_fields:
            result[field] = data[field]

        resource_id = stem_url(data['url'])
        self._check_redundant_resource(resource_id, 'add_resource', result)
        self._check_removed_resource(resource_id, 'add_resource', result)

        result['id'] = resource_id

        result['upvotes'] = 0
        result['downvotes'] = 0
        self.recommendations[resource_id] = dict(result)
        tracker.emit('add_resource', result)
        result["description"] = self._get_onetime_url(result["description"])
        return result

    @XBlock.json_handler
    def edit_resource(self, data, _suffix=''):  # pylint: disable=unused-argument
        """
        Edit an entry of existing resource.

        Args:
                data: dict in JSON format
                data['id']: the ID of the edited resouce
                data[resource_content_field]: the content of the resource to be edited
        Returns:
                result: dict in JSON format
                result['error']: the error message generated when the edit fails
                result[old_resource_content_field]: the content of the resource before edited
                result[resource_content_field]: the content of the resource after edited
        """
        resource_id = self._validate_resource(data['id'], 'edit_resource')

        result = {}
        result['id'] = resource_id
        result['old_id'] = resource_id

        for field in self.resource_content_fields:
            result['old_' + field] = self.recommendations[resource_id][field]
            # If the content in resource is unchanged (i.e., data[field] is
            # empty), return and log the content stored in the database
            # (self.recommendations), otherwise, return and log the edited
            # one (data[field])
            if data[field] == "":
                result[field] = self.recommendations[resource_id][field]
            else:
                result[field] = data[field]

        ## Handle resource ID changes
        edited_resource_id = stem_url(data['url'])
        if edited_resource_id != resource_id:
            self._check_redundant_resource(edited_resource_id, 'edit_resource',
                                           result)
            self._check_removed_resource(edited_resource_id, 'edit_resource',
                                         result)

            self.recommendations[edited_resource_id] = deepcopy(
                self.recommendations[resource_id])
            self.recommendations[edited_resource_id]['id'] = edited_resource_id
            result['id'] = edited_resource_id
            del self.recommendations[resource_id]

        # Handle all other changes
        for field in data:
            if field == 'id':
                continue
            if data[field] == "":
                continue
            self.recommendations[edited_resource_id][field] = data[field]

        tracker.emit('edit_resource', result)
        result["description"] = self._get_onetime_url(result["description"])
        return result

    @XBlock.json_handler
    def flag_resource(self, data, _suffix=''):  # pylint: disable=unused-argument
        """
        Flag (or unflag) an entry of problematic resource and give the reason. This shows in a
        list for staff to review.

        Args:
                data: dict in JSON format
                data['id']: the ID of the problematic resouce
                data['isProblematic']: the boolean indicator for whether the resource is being
                                       flagged or unflagged. Only flagging works.
                data['reason']: the reason why the user believes the resource is problematic
        Returns:
                result: dict in JSON format
                result['reason']: the new reason
                result['oldReason']: the old reason
                result['id']: the ID of the problematic resouce
                result['isProblematic']: the boolean indicator for whether the resource
                                         is now flagged
        """
        result = {}
        result['id'] = data['id']
        result['isProblematic'] = data['isProblematic']
        result['reason'] = data['reason']

        user_id = self.get_user_id()

        # If already flagged, update the reason for the flag
        if data['isProblematic']:
            # If already flagged, update the reason
            if data['id'] in self.flagged_ids:
                result['oldReason'] = self.flagged_reasons[
                    self.flagged_ids.index(data['id'])]
                self.flagged_reasons[self.flagged_ids.index(
                    data['id'])] = data['reason']
            # Otherwise, flag it.
            else:
                self.flagged_ids.append(data['id'])
                self.flagged_reasons.append(data['reason'])

                if user_id not in self.flagged_accum_resources:
                    self.flagged_accum_resources[user_id] = {}
            self.flagged_accum_resources[user_id][data['id']] = data['reason']
        # Unflag resource. Currently unsupported.
        else:
            if data['id'] in self.flagged_ids:
                result['oldReason'] = self.flagged_reasons[
                    self.flagged_ids.index(data['id'])]
                result['reason'] = ''
                idx = self.flagged_ids.index(data['id'])
                del self.flagged_ids[idx]
                del self.flagged_reasons[idx]

                del self.flagged_accum_resources[user_id][data['id']]
        tracker.emit('flag_resource', result)
        return result

    @XBlock.json_handler
    def endorse_resource(self, data, _suffix=''):  # pylint: disable=unused-argument
        """
        Endorse an entry of resource. This shows the students the
        resource has the staff seal of approval.

        Args:
                data: dict in JSON format
                data['id']: the ID of the resouce to be endorsed
        Returns:
                result: dict in JSON format
                result['error']: the error message generated when the endorsement fails
                result['id']: the ID of the resouce to be endorsed
                result['status']: endorse the resource or undo it
        """
        # Auth+auth
        if not self.get_user_is_staff():
            msg = self.ugettext('Endorse resource without permission')
            self._error_handler(msg, 'endorse_resource')

        resource_id = self._validate_resource(data['id'], 'endorse_resource')

        result = {}
        result['id'] = resource_id

        # Unendorse previously endorsed resource
        if resource_id in self.endorsed_recommendation_ids:
            result['status'] = 'undo endorsement'
            endorsed_index = self.endorsed_recommendation_ids.index(
                resource_id)
            del self.endorsed_recommendation_ids[endorsed_index]
            del self.endorsed_recommendation_reasons[endorsed_index]
        # Endorse new resource
        else:
            result['reason'] = data['reason']
            result['status'] = 'endorsement'
            self.endorsed_recommendation_ids.append(resource_id)
            self.endorsed_recommendation_reasons.append(data['reason'])

        tracker.emit('endorse_resource', result)
        return result

    @XBlock.json_handler
    def remove_resource(self, data, _suffix=''):
        """
        Remove an entry of resource. This removes it from the student
        view, and prevents students from being able to add it back.

        Args:
                data: dict in JSON format
                data['id']: the ID of the resouce to be removed
                data['reason']: the reason why the resouce was removed
        Returns:
                result: dict in JSON format
                result['error']: the error message generated when the removal fails
                result['recommendation']: (Dict) the removed resource
                result['recommendation']['reason']: the reason why the resouce was removed

        """
        # Auth+auth
        if not self.get_user_is_staff():
            msg = self.ugettext(
                "You don't have the permission to remove this resource")
            self._error_handler(msg, 'remove_resource')

        resource_id = self._validate_resource(data['id'], 'remove_resource')

        # Grab a copy of the resource for the removed list
        # (swli: I reorganized the code a bit. First copy, then delete. This is more fault-tolerant)
        result = {}
        result['id'] = resource_id
        removed_resource = deepcopy(self.recommendations[resource_id])
        removed_resource['reason'] = data['reason']

        # Add it to removed resources and remove it from main resource list.
        self.removed_recommendations[resource_id] = removed_resource
        del self.recommendations[resource_id]

        # And return
        result['recommendation'] = removed_resource
        tracker.emit('remove_resource', result)
        return result

    @XBlock.json_handler
    def export_resources(self, _data, _suffix):  # pylint: disable=unused-argument
        """
        Export all resources from the Recommender. This is intentionally not limited to staff
        members (community contributions do not belong to the course staff). Sensitive
        information is exported *is* limited (flagged resources, and in the future, PII if
        any).
        """
        result = {}
        result['export'] = {
            'recommendations':
            self.recommendations,
            'removed_recommendations':
            self.removed_recommendations,
            'endorsed_recommendation_ids':
            self.endorsed_recommendation_ids,
            'endorsed_recommendation_reasons':
            self.endorsed_recommendation_reasons,
        }
        if self.get_user_is_staff():
            result['export'][
                'flagged_accum_resources'] = self.flagged_accum_resources

        tracker.emit('export_resources', result)
        return result

    @XBlock.handler
    def import_resources(self, request, _suffix=''):
        """
        Import resources into the recommender.
        """
        response = Response()
        response.headers['Content-Type'] = 'application/json'
        if not self.get_user_is_staff():
            response.status = 403
            response.body = json.dumps(
                {'error': self.ugettext('Only staff can import resources')})
            tracker.emit('import_resources', {'Status': 'NOT_A_STAFF'})
            return response

        # Check invalid file types
        file_types = {
            'json': {
                'extension': [".json"],
                'mimetypes': ['application/json', 'text/json', 'text/x-json']
            }
        }
        file_type_error_msg = self.ugettext(
            'Please submit the JSON file obtained with the download resources button'
        )
        result = self._check_upload_file(request, file_types,
                                         file_type_error_msg,
                                         'import_resources', 31457280)
        if isinstance(result, Response):
            return result

        try:
            data = json.load(request.POST['file'].file)

            self.flagged_accum_resources = data['flagged_accum_resources']
            self.endorsed_recommendation_reasons = data[
                'endorsed_recommendation_reasons']
            self.endorsed_recommendation_ids = data[
                'endorsed_recommendation_ids']

            if 'removed_recommendations' in data:
                self.removed_recommendations = data_structure_upgrade(
                    data['removed_recommendations'])
                data['removed_recommendations'] = self.removed_recommendations
            self.recommendations = data_structure_upgrade(
                data['recommendations'])
            data['recommendations'] = self.recommendations

            tracker.emit('import_resources', {
                'Status': 'SUCCESS',
                'data': data
            })
            response.body = json.dumps(data, sort_keys=True)
            response.status = 200
            return response
        except (ValueError, KeyError):
            response.status = 415
            response.body = json.dumps({
                'error':
                self.ugettext(
                    'Please submit the JSON file obtained with the download resources button'
                )
            })
            tracker.emit('import_resources', {'Status': 'FILE_FORMAT_ERROR'})
            return response
        except IOError:
            return self._raise_pyfs_error('import_resources')

    @XBlock.json_handler
    def accum_flagged_resource(self, _data, _suffix=''):  # pylint: disable=unused-argument
        """
        Accumulate the flagged resource ids and reasons from all students
        """
        if not self.get_user_is_staff():
            msg = self.ugettext(
                'Tried to access flagged resources without staff permission')
            self._error_handler(msg, 'accum_flagged_resource')
        result = {'flagged_resources': {}}
        for _, flagged_accum_resource_map in self.flagged_accum_resources.iteritems(
        ):
            for resource_id in flagged_accum_resource_map:
                if resource_id in self.removed_recommendations:
                    continue
                if resource_id not in result['flagged_resources']:
                    result['flagged_resources'][resource_id] = []
                if flagged_accum_resource_map[resource_id] != '':
                    result['flagged_resources'][resource_id].append(
                        flagged_accum_resource_map[resource_id])

        tracker.emit('accum_flagged_resource', result)
        return result

    def student_view(self, _context=None):  # pylint: disable=unused-argument
        """
        The primary view of the RecommenderXBlock, shown to students
        when viewing courses.
        """
        self.recommendations = (data_structure_upgrade(self.recommendations)
                                or data_structure_upgrade(
                                    self.default_recommendations) or {})

        # Transition between two versions. In the previous version, there is
        # no endorsed_recommendation_reasons. Thus, we add empty reasons to
        # make the length of the two lists equal
        #
        # TODO: Go through old lists of resources in course, and remove this
        # code. The migration should be done.
        while len(self.endorsed_recommendation_ids) > len(
                self.endorsed_recommendation_reasons):
            self.endorsed_recommendation_reasons.append('')

        global template_lookup
        if not template_lookup:
            self._init_template_lookup()

        # Ideally, we'd estimate score based on votes, such that items with
        # 1 vote have a sensible ranking (rather than a perfect rating)

        # We pre-generate URLs for all resources. We benchmarked doing this
        # for 44 URLs, and the time per URL was about 8ms. The 44 URLs were
        # all of the images added by students over several problem sets. If
        # load continues to be as-is, pre-generation is not a performance
        # issue. If students make substantially more resources, we may want
        # to paginate, and generate in sets of 5-20 URLs per load.
        resources = [{
            'id': r['id'],
            'title': r['title'],
            "votes": r['upvotes'] - r['downvotes'],
            'url': r['url'],
            'description': self._get_onetime_url(r['description']),
            'descriptionText': r['descriptionText']
        } for r in self.recommendations.values()]
        resources = sorted(resources, key=lambda r: r['votes'], reverse=True)

        frag = Fragment(
            template_lookup.get_template("recommender.html").render(
                resources=resources,
                upvoted_ids=self.upvoted_ids,
                downvoted_ids=self.downvoted_ids,
                endorsed_recommendation_ids=self.endorsed_recommendation_ids,
                endorsed_recommendation_reasons=self.
                endorsed_recommendation_reasons,
                flagged_ids=self.flagged_ids,
                flagged_reasons=self.flagged_reasons))
        frag.add_css_url(
            "//ajax.googleapis.com/ajax/libs/jqueryui/1.10.4/themes/smoothness/jquery-ui.css"
        )
        frag.add_javascript_url(
            "//ajax.googleapis.com/ajax/libs/jqueryui/1.10.4/jquery-ui.min.js")
        frag.add_javascript_url(
            '//cdnjs.cloudflare.com/ajax/libs/mustache.js/0.8.1/mustache.min.js'
        )
        frag.add_javascript_url(
            '//cdnjs.cloudflare.com/ajax/libs/intro.js/0.5.0/intro.min.js')
        frag.add_css(self.resource_string("static/css/tooltipster.css"))
        frag.add_css(self.resource_string("static/css/recommender.css"))
        frag.add_css(self.resource_string("static/css/introjs.css"))
        frag.add_javascript(
            self.resource_string("static/js/src/jquery.tooltipster.min.js"))
        frag.add_javascript(self.resource_string("static/js/src/cats.js"))
        frag.add_javascript(
            self.resource_string("static/js/src/recommender.js"))
        frag.initialize_js('RecommenderXBlock',
                           self.get_client_configuration())
        return frag

    def studio_view(self, _context=None):  # pylint: disable=unused-argument
        """
        The primary view of the RecommenderXBlock in studio. This is shown to
        course staff when editing a course in studio.
        """
        global template_lookup
        if not template_lookup:
            self._init_template_lookup()

        frag = Fragment(
            template_lookup.get_template("recommenderstudio.html").render())
        frag.add_css(
            pkg_resources.resource_string(__name__,
                                          "static/css/recommenderstudio.css"))
        frag.add_javascript_url(
            "//ajax.googleapis.com/ajax/libs/jqueryui/1.10.4/jquery-ui.min.js")
        frag.add_javascript(
            pkg_resources.resource_string(
                __name__, "static/js/src/recommenderstudio.js"))
        frag.initialize_js('RecommenderXBlock')
        return frag

    def add_xml_to_node(self, node):
        """
        Serialize the XBlock to XML for exporting.
        """
        node.tag = 'recommender'

        node.set('intro_enabled', 'true' if (self.intro_enabled) else 'false')
        node.set(
            'disable_dev_ux', 'true' if
            (self.client_configuration['disable_dev_ux']) else 'false')
        node.set('entries_per_page',
                 str(self.client_configuration['entries_per_page']))
        node.set('page_span', str(self.client_configuration['page_span']))

        el = etree.SubElement(node, 'resources')
        ## Note: The line below does not work in edX platform.
        ## We should figure out if the appropriate scope is available during import/export
        ## TODO: Talk to Cale
        el.text = json.dumps(self.recommendations).encode("utf-8")

    @staticmethod
    def workbench_scenarios():
        """
        A test sample scenario for display in the workbench.
        """
        return [
            ("RecommenderXBlock", """
                <vertical_demo>
                    <html_demo><img class="question" src="http://people.csail.mit.edu/swli/edx/recommendation/img/pset.png"></img></html_demo>
                    <recommender intro_enabled="true" disable_dev_ux="true" entries_per_page="2" page_span="1">
                        <resources>
                            [
                                {"id": 1, "title": "Covalent bonding and periodic trends", "upvotes" : 15, "downvotes" : 5, "url" : "https://courses.edx.org/courses/MITx/3.091X/2013_Fall/courseware/SP13_Week_4/SP13_Periodic_Trends_and_Bonding/", "description" : "http://people.csail.mit.edu/swli/edx/recommendation/img/videopage1.png", "descriptionText" : "short description for Covalent bonding and periodic trends"},
                                {"id": 2, "title": "Polar covalent bonds and electronegativity", "upvotes" : 10, "downvotes" : 7, "url" : "https://courses.edx.org/courses/MITx/3.091X/2013_Fall/courseware/SP13_Week_4/SP13_Covalent_Bonding/", "description" : "http://people.csail.mit.edu/swli/edx/recommendation/img/videopage2.png", "descriptionText" : "short description for Polar covalent bonds and electronegativity"},
                                {"id": 3, "title": "Longest wavelength able to to break a C-C bond ...", "upvotes" : 1230, "downvotes" : 7, "url" : "https://answers.yahoo.com/question/index?qid=20081112142253AA1kQN1", "description" : "http://people.csail.mit.edu/swli/edx/recommendation/img/dispage1.png", "descriptionText" : "short description for Longest wavelength able to to break a C-C bond ..."},
                                {"id": 4, "title": "Calculate the maximum wavelength of light for ...", "upvotes" : 10, "downvotes" : 3457, "url" : "https://answers.yahoo.com/question/index?qid=20100110115715AA6toHw", "description" : "http://people.csail.mit.edu/swli/edx/recommendation/img/dispage2.png", "descriptionText" : "short description for Calculate the maximum wavelength of light for ..."},
                                {"id": 5, "title": "Covalent bond - wave mechanical concept", "upvotes" : 10, "downvotes" : 7, "url" : "http://people.csail.mit.edu/swli/edx/recommendation/img/textbookpage1.png", "description" : "http://people.csail.mit.edu/swli/edx/recommendation/img/textbookpage1.png", "descriptionText" : "short description for Covalent bond - wave mechanical concept"},
                                {"id": 6, "title": "Covalent bond - Energetics of covalent bond", "upvotes" : 10, "downvotes" : 7, "url" : "http://people.csail.mit.edu/swli/edx/recommendation/img/textbookpage2.png", "description" : "http://people.csail.mit.edu/swli/edx/recommendation/img/textbookpage2.png", "descriptionText" : "short description for Covalent bond - Energetics of covalent bond"}
                            ]
                        </resources>
                    </recommender>
                    <recommender />
                </vertical_demo>
                """),
        ]

    @classmethod
    def parse_xml(cls, node, runtime, keys, _id_generator):  # pylint: disable=unused-argument
        """
        Parse the XML for the XBlock. It is a list of dictionaries of default recommendations.

        """
        block = runtime.construct_xblock_from_class(cls, keys)
        if node.tag != 'recommender':
            raise UpdateFromXmlError(
                "XML content must contain an 'recommender' root element.")

        if node.get('intro_enabled'):
            block.intro_enabled = (node.get('intro_enabled').lower().strip()
                                   not in ['false', '0', ''])

        if node.get('disable_dev_ux'):
            block.client_configuration['disable_dev_ux'] = (
                node.get('disable_dev_ux').lower().strip()
                not in ['false', '0', ''])

        for tag in ['entries_per_page', 'page_span']:
            if node.get(tag):
                block.client_configuration[tag] = int(node.get(tag))

        for child in node:
            if child.tag == 'resources' and child.text:
                lines = json.loads(child.text)
                block.default_recommendations = data_structure_upgrade(lines)

        return block
Exemplo n.º 26
0
class LmsCompatibilityMixin:
    """
    Extra fields and methods used by LMS/Studio.
    """
    # Studio the default value for this field to show this XBlock
    # in the list of "Advanced Components"
    display_name = String(default="Open Response Assessment",
                          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."
    )

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

    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)

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

    icon_class = "problem"

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

        The LMS wants to know this to see if it has to instantiate our module
        and query it to find the children, or whether it can just trust what's
        in the static (cheaper) children listing.
        """
        return False

    @property
    def has_score(self):
        """Are we a scored type (read: a problem). Yes.

        For LMS Progress page/grades download purposes, we're always going to
        have a score, even if it's just 0 at the start.
        """
        return True

    def max_score(self):
        """The maximum raw score of our problem.

        Called whenever the LMS knows that something is scorable, but finds no
        recorded raw score for it (i.e. the student hasn't done it). In that
        case, the LMS knows that the earned score is 0, but it doesn't know what
        to put in the denominator. So we supply it with the total number of
        points that it is possible for us to earn -- the sum of the highest
        pointed options from each criterion.

        Note that if we have already recorded a score in submissions, this
        method will never be called. So it's perfectly possible for us to have
        10/10 on the progress page and a 12 returning from this method if our
        10/10 score was earned in the past and the problem has changed since
        then.
        """
        return sum(
            max(option["points"] for option in criterion["options"]
                ) if criterion["options"] else 0
            for criterion in self.rubric_criteria)
Exemplo n.º 27
0
class LaunchContainerXBlock(XBlock):
    """
    Provide a Fragment with associated Javascript to display to
    Students a button that will launch a configurable external course
    Container via a call to Appsembler's container deploy API.
    """

    display_name = String(help="Display name of the component",
                          default="Container Launcher",
                          scope=Scope.settings)

    project = String(
        display_name='Project name',
        default=u'(EDIT THIS COMPONENT TO SET PROJECT NAME)',
        scope=Scope.content,
        help=(u"The name of the project as defined for the "
              "Appsembler Virtual Labs (AVL) API."),
    )

    project_friendly = String(
        display_name='Project Friendly name',
        default=u'',
        scope=Scope.content,
        help=(u"The name of the container's Project as displayed to the end "
              "user"),
    )

    project_token = String(
        display_name='Project Token',
        default=u'',
        scope=Scope.content,
        help=(u"This is a unique token that can be found in the AVL dashboard")
    )

    enable_container_resetting = Boolean(
        display_name='Enable container resetting',
        default=False,
        scope=Scope.content,
        help=(u"Enables students to reset/delete their container and start over")
    )

    @property
    def wharf_url(self, force=False):
        """Determine which site we're on, then get the Wharf URL that said
        site has configured."""

        # The complexities of Tahoe require that we check several places
        # for the site configuration, which itself contains the URL
        # of the AVL cluster associated with this site.
        #
        # If we are in Tahoe studio, the Site object associated with this request
        # will not be the one used within Tahoe. To get the proper domain
        # we rely on the "organization", which always equals `Site.name`.
        # If the organization value does not return a site object, we are probably on
        # the LMS side. In this case, we use `get_current_site()`, which _does_
        # return the incorrect site object. If all this fails, we fallback
        # to the DEFAULT_WHARF_URL.
        try:
            # The name of the Site object will always match self.course_id.org.
            # See: https://git.io/vpilS
            site = Site.objects.get(name=self.course_id.org)
        except (Site.DoesNotExist, AttributeError):  # Probably on the lms side.
            if get_current_site:
                site = get_current_site()  # From the request.
            else:
                site = Site.objects.all().order_by('domain').first()

        url = cache.get(make_cache_key(site.domain))
        if url:
            return url

        # Nothing in the cache. Go find the URL.
        site_wharf_url = None
        if hasattr(site, 'configuration'):
            site_wharf_url = site.configuration.get_value(WHARF_URL_KEY)
        elif siteconfig_helpers:
            # Rely on edX's helper, which will fall back to the microsites app.
            site_wharf_url = siteconfig_helpers.get_value(WHARF_URL_KEY)

        urls = (
            # A SiteConfig object: this is the preferred implementation.
            (
                'SiteConfiguration',
                site_wharf_url
            ),
            # A string: the currently supported implementation.
            (
                "ENV_TOKENS[{}]".format(WHARF_URL_KEY),
                settings.ENV_TOKENS.get(WHARF_URL_KEY)
            ),
            # A dict: the deprecated version.
            (
                "ENV_TOKENS['LAUNCHCONTAINER_API_CONF']",
                settings.ENV_TOKENS.get('LAUNCHCONTAINER_API_CONF', {}).get('default')
            ),
        )

        try:
            url = next((x[1] for x in urls if is_valid(x[1])))
        except StopIteration:
            raise ImproperlyConfigured("No Virtual Labs URL was found, "
                                       "please contact your site administrator.")

        if not url:
            raise AssertionError("You must set a valid url for the launchcontainer XBlock. "
                                 "URLs attempted: {}".format(urls)
                                 )

        cache.set(make_cache_key(site), url, CACHE_KEY_TIMEOUT)

        logger.debug("XBlock-launchcontainer urls attempted: {}".format(urls))

        return url

    @property
    def wharf_delete_url(self):
        api_root = get_api_root_url(self.wharf_url)
        return "{}/isc/dashboard/userprojectdeployments/delete_user_deployments/".format(api_root)

    # TODO: Cache this property?
    @property
    def user_email(self):

        user = get_current_user()
        if hasattr(user, 'email') and user.email:
            return user.email

        user_service = self.runtime.service(self, 'user')
        user = user_service.get_current_user()
        email = user.emails[0] if type(user.emails) == list else user.email

        return email

    def student_view(self, context=None):
        """
        The primary view of the LaunchContainerXBlock, shown to students
        when viewing courses.
        """

        context = {
            'enable_container_resetting': self.enable_container_resetting,
            'project': self.project,
            'project_friendly': self.project_friendly,
            'project_token': self.project_token,
            'user_email': self.user_email,
            'API_url': self.wharf_url,
            'API_delete_url': self.wharf_delete_url,
        }

        return _add_static(Fragment(), 'student', context)

    def studio_view(self, context=None):
        """
        Return fragment for editing block in studio.
        """
        try:
            cls = type(self)

            def none_to_empty(data):
                """
                Return empty string if data is None else return data.
                """
                return data if data is not None else ''

            edit_fields = (
               (field, none_to_empty(getattr(self, field.name)), validator)
               for field, validator in (
                   (cls.project, 'string'),
                   (cls.project_friendly, 'string'),
                   (cls.project_token, 'string'),
                   (cls.enable_container_resetting, 'boolean'),
               )
            )

            context = {'fields': edit_fields,
                       'API_url': self.wharf_url,
                       'API_delete_url': self.wharf_delete_url,
                       'user_email': self.user_email
                       }

            return _add_static(Fragment(), 'studio', context)

        except:  # noqa E722 # pragma: NO COVER
            # TODO: Handle all the errors and handle them well.
            logger.error("Don't swallow my exceptions", exc_info=True)
            raise

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

        # TODO: This could use some better validation.
        try:
            self.enable_container_resetting = data['enable_container_resetting']
            self.project = data['project'].strip()
            self.project_friendly = data['project_friendly'].strip()
            self.project_token = data['project_token'].strip()
            self.api_url = self.wharf_url
            self.api_delete_url = self.wharf_delete_url

            return {'result': 'success'}

        except Exception as e:
            return {'result': 'Error saving data:{0}'.format(str(e))}

    @staticmethod
    def workbench_scenarios():
        """A canned scenario for display in the workbench."""
        return [
            ("A single launchcontainer",
             """\
                <vertical_demo>
                    <launchcontainer/>
                </vertical_demo>
             """)
        ]
class GroupProjectBlock(XBlock):
    """
    XBlock providing a group activity project for a group of students to collaborate upon
    """
    display_name = String(
        display_name="Display Name",
        help=
        "This name appears in the horizontal navigation at the top of the page.",
        scope=Scope.settings,
        default="Group Project")

    weight = Float(
        display_name="Weight",
        help=
        "This is the maximum score that the user receives when he/she successfully completes the problem",
        scope=Scope.settings,
        default=1)

    item_state = Dict(help="JSON payload for assessment values",
                      scope=Scope.user_state)

    with open(resource_filename(__name__, 'res/default.xml'),
              "r") as default_xml_file:
        default_xml = default_xml_file.read()

    data = String(display_name="",
                  help="XML contents to display for this module",
                  scope=Scope.content,
                  default=textwrap.dedent(default_xml))

    has_score = True

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

        group_activity = GroupActivity.import_xml_string(self.data)
        # TODO: Replace with workgroup call to get real workgroup
        team_members = [{
            "name": "Andy Parsons",
            "id": 1,
            "img": "/image/empty_avatar.png"
        }, {
            "name": "Jennifer Gormley",
            "id": 2,
            "img": "/image/empty_avatar.png"
        }, {
            "name": "Vishal Ghandi",
            "id": 3,
            "img": "/image/empty_avatar.png"
        }]

        # TODO: Replace with workgroup call to get assigned workgroups
        assess_groups = [{
            "id": 101,
            "img": "/image/empty_avatar.png"
        }, {
            "id": 102,
            "img": "/image/empty_avatar.png"
        }, {
            "id": 103,
            "img": "/image/empty_avatar.png"
        }]

        context = {
            "group_activity": group_activity,
            "team_members": json.dumps(team_members),
            "assess_groups": json.dumps(assess_groups),
        }

        fragment = Fragment()
        fragment.add_content(
            render_template('/templates/html/group_project.html', context))
        fragment.add_css(load_resource('public/css/group_project.css'))
        fragment.add_javascript(load_resource('public/js/group_project.js'))

        fragment.initialize_js('GroupProjectBlock')

        return fragment

    def studio_view(self, context):
        """
        Editing view in Studio
        """
        fragment = Fragment()
        fragment.add_content(
            render_template('/templates/html/group_project_edit.html', {
                'self': self,
            }))
        fragment.add_javascript(
            load_resource('public/js/group_project_edit.js'))

        fragment.initialize_js('GroupProjectEditBlock')

        return fragment

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

        self.display_name = submissions['display_name']
        xml_content = submissions['data']
        max_score = submissions['max_score']

        if not max_score:
            # empty = default
            max_score = 1
        else:
            try:
                # not an integer, then default
                max_score = int(max_score)
            except:
                max_score = 1

        self.weight = max_score

        try:
            etree.parse(StringIO(xml_content))
            self.data = xml_content
        except etree.XMLSyntaxError as e:
            return {'result': 'error', 'message': e.message}

        return {
            'result': 'success',
        }

    @XBlock.json_handler
    def submit_peer_feedback(self, submissions, suffix=''):
        try:
            peer_id = submissions["peer_id"]
            del submissions["peer_id"]

            print "Peer Review for {}: {}".format(peer_id, submissions)

            # Then something like this needs to happen

            # user_id = get_user_id_for_this_session() # ???
            # project_id = get_xblock_id_for_this_session()
            # api_manager.save_data_for_peer(user_id, peer_id, submissions)

            # or

            # for k,v in iteritems(submissions):
            #     api_manager.save_data_for_peer(user_id, peer_id, k, v)

        except Exception as e:
            return {
                'result': 'error',
                'message': e.message,
            }

        return {
            'result': 'success',
            'msg': _('Thanks for your feedback'),
        }

    @XBlock.json_handler
    def submit_other_group_feedback(self, submissions, suffix=''):
        try:
            group_id = submissions["group_id"]
            del submissions["group_id"]

            print "Group Review for {}: {}".format(group_id, submissions)

            # Then something like this needs to happen

            # user_id = get_user_id_for_this_session() # ???
            # project_id = get_xblock_id_for_this_session()
            # api_manager.save_data_for_group(user_id, group_id, submissions)

            # or

            # for k,v in iteritems(submissions):
            #     api_manager.save_data_for_group(user_id, group_id, k, v)

        except Exception as e:
            return {
                'result': 'error',
                'msg': e.message,
            }

        return {
            'result': 'success',
            'msg': _('Thanks for your feedback'),
        }

    @XBlock.handler
    def load_peer_feedback(self, request, suffix=''):

        peer_id = request.GET["peer_id"]

        results = {
            'peer_score': '5',
            'peer_q1': 'A',
            'peer_q2': 'BB',
            'peer_q3': 'CCC',
        }

        return webob.response.Response(body=json.dumps(results))

    @XBlock.handler
    def load_other_group_feedback(self, request, suffix=''):

        group_id = request.GET["group_id"]

        results = {}
        # results = {
        #     'other_team_comments': 'They Rocked!',
        #     'other_team_q1': '90',
        #     'other_team_q2': '95',
        #     'other_team_q3': '80',
        # }

        return webob.response.Response(body=json.dumps(results))
Exemplo n.º 29
0
class CompletionBlock(SubmittingXBlockMixin, QuestionMixin,
                      StudioEditableXBlockMixin,
                      XBlockWithTranslationServiceMixin,
                      StudentViewUserStateMixin, XBlock):
    """
    An XBlock used by students to indicate that they completed a given task.
    The student's answer is always considered "correct".
    """
    CATEGORY = 'pb-completion'
    STUDIO_LABEL = _(u'Completion')
    USER_STATE_FIELDS = ['student_value']

    answerable = True

    question = String(
        display_name=_('Question'),
        help=
        _('Mentions a specific activity and asks the student whether they completed it.'
          ),
        scope=Scope.content,
        default=
        _('Please indicate whether you attended the In Person Workshop session by (un-)checking the option below.'
          ),
    )

    answer = String(
        display_name=_('Answer'),
        help=_(
            'Represents the answer that the student can (un-)check '
            'to indicate whether they completed the activity that the question mentions.'
        ),
        scope=Scope.content,
        default=_('Yes, I attended the session.'),
    )

    student_value = NullableBoolean(
        help=_("Records student's answer."),
        scope=Scope.user_state,
        default=None,
    )

    editable_fields = ('display_name', 'show_title', 'question', 'answer')

    def mentoring_view(self, context):
        """
        Main view of this block.
        """
        context = context.copy() if context else {}
        context['question'] = self.question
        context['answer'] = self.answer
        context[
            'checked'] = self.student_value if self.student_value is not None else False
        context['title'] = self.display_name_with_default
        context['hide_header'] = context.get('hide_header',
                                             False) or not self.show_title

        html = loader.render_template('templates/html/completion.html',
                                      context)

        fragment = Fragment(html)
        fragment.add_javascript_url(
            self.runtime.local_resource_url(self, 'public/js/completion.js'))
        fragment.initialize_js('CompletionBlock')
        return fragment

    student_view = mentoring_view
    preview_view = mentoring_view

    def student_view_data(self, context=None):
        """
        Returns a JSON representation of the student_view of this XBlock,
        retrievable from the Course XBlock 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,
            'question': self.question,
            'answer': self.answer,
            'title': self.display_name_with_default,
            'hide_header': not self.show_title,
        }

    def get_last_result(self):
        """ Return the current/last result in the required format """
        if self.student_value is None:
            return {}
        return {
            'submission': self.student_value,
            'status': 'correct',
            'tips': [],
            'weight': self.weight,
            'score': 1,
        }

    def get_results(self):
        """ Alias for get_last_result() """
        return self.get_last_result()

    def submit(self, value):
        """
        Persist answer submitted by student.
        """
        log.debug(u'Received Completion submission: "%s"', value)
        self.student_value = value
        if sub_api:
            # Also send to the submissions API:
            sub_api.create_submission(self.student_item_key, value)
        result = self.get_last_result()
        log.debug(u'Completion submission result: %s', result)
        return result
Exemplo n.º 30
0
class WorkspaceBlock(XBlock):
    """
    An XBlock providing a responsive multimedia carousel and workspace
    """

    display_name = String(
        help=
        "This name appears in horizontal navigation at the top of the page.",
        default="Workspace",
        scope=Scope.content)

    data = List(
        help=
        "This is the representation of the data items as a list of tuples ",
        default=[
            ('img', 'http://met-content.bu.edu/etr2/content/images/Slide5.JPG',
             '100%', '96'),
            ('img', 'http://met-content.bu.edu/etr2/content/images/Slide6.JPG',
             '100%', '96'),
            ('img', 'http://met-content.bu.edu/etr2/content/images/Slide7.JPG',
             '100%', '96')
        ],
        scope=Scope.content)

    href = String(help="workspace url", default=None, scope=Scope.content)

    def student_view(self, context):
        """
        Lab view, displayed to the student
        """
        fragment = Fragment()

        context = {'items': self.data, 'url': self.href}

        fragment.add_content(
            render_template('/templates/html/workspace.html', context))
        fragment.add_javascript(
            load_resource('public/js/jquery-ui-1.10.4.custom.js'))
        fragment.add_css(load_resource('public/css/responsive-carousel.css'))
        fragment.add_css(
            load_resource('public/css/responsive-carousel.slide.css'))
        fragment.add_javascript(
            load_resource('public/js/responsive-carousel.js'))
        fragment.add_css(load_resource("public/css/video-js.css"))
        fragment.add_javascript(load_resource("public/js/video.js"))
        fragment.add_javascript(load_resource('public/js/youtube.js'))
        fragment.add_javascript(
            "function WorkspaceBlock(runtime, element) { $('.carousel').carousel(); }"
        )
        fragment.initialize_js("WorkspaceBlock")

        return fragment

    def studio_view(self, context):
        """
        Studio edit view
        """
        xml_data = self._build_xml(self.data)

        fragment = Fragment()
        fragment.add_content(
            render_template('templates/html/workspace_edit.html', {
                'xml_data': xml_data,
                self: self
            }))
        fragment.add_javascript(
            load_resource('public/js/jquery-ui-1.10.4.custom.js'))
        fragment.add_javascript(load_resource('public/js/workspace_edit.js'))
        fragment.initialize_js('WorkspaceEditBlock')

        return fragment

    @XBlock.json_handler
    def studio_submit(self, submissions, suffix=''):
        self.display_name = submissions['display_name']
        self.href = submissions['workspace_url']
        xml_content = submissions['data']

        try:
            etree.parse(StringIO(xml_content))
            xmltree = etree.fromstring(xml_content)
            items_list = self._get_items(xmltree)
            self.data = items_list

        except etree.XMLSyntaxError as e:
            return {'result': 'error', 'message': e.message}

        return {
            'result': 'success',
        }

    def _get_items(self, xmltree):
        """
        Helper method
        """
        items_elements = xmltree.getchildren()
        items = []
        for item_element in items_elements:
            item_tag = item_element.tag
            item_src = item_element.get('src')
            item_width = item_element.get('width', '100%')
            item_height = item_element.get('height', '625')
            items.append((item_tag, item_src, item_width, item_height))

        return items

    def _build_xml(self, items_list):
        """
        Helper method
        """
        xml = etree.Element('workspace')
        for item in items_list:
            tag = etree.SubElement(xml,
                                   item[0],
                                   src=item[1],
                                   width=item[2],
                                   height=item[3])
        return etree.tostring(xml, pretty_print=True)

    @staticmethod
    def workbench_scenarios():
        return [("workspace demo", "<workspace />")]
Exemplo n.º 31
0
class GoogleCalendarBlock(XBlock, PublishEventMixin):
    """
    XBlock providing a google calendar view for a specific calendar
    """
    display_name = String(
        display_name=_("Display Name"),
        help=
        _("This name appears in the horizontal navigation at the top of the page."
          ),
        scope=Scope.settings,
        default="Google Calendar")
    calendar_id = String(
        display_name=_("Public Calendar ID"),
        help=
        _("Google provides an ID for publicly available calendars. In the Google Calendar, "
          "open Settings and copy the ID from the Calendar Address section into this field."
          ),
        scope=Scope.settings,
        default=DEFAULT_CALENDAR_ID)
    default_view = Integer(
        display_name=_("Default View"),
        help=
        _("The calendar view that students see by default. A student can change this view."
          ),
        scope=Scope.settings,
        default=1)
    views = [(0, 'Week'), (1, 'Month'), (2, 'Agenda')]

    # Context argument is specified for xblocks, but we are not using herein
    def student_view(self, context):  # pylint: disable=unused-argument
        """
        Player view, displayed to the student
        """
        fragment = Fragment()

        fragment.add_content(
            RESOURCE_LOADER.render_django_template(
                CALENDAR_TEMPLATE,
                context={
                    "mode": self.views[self.default_view][1],
                    "src": self.calendar_id,
                    "title": self.display_name,
                    "language": utils.translation.get_language(),
                },
                i18n_service=self.runtime.service(self, "i18n"),
            ))
        fragment.add_css(
            RESOURCE_LOADER.load_unicode('public/css/google_calendar.css'))
        fragment.add_javascript(
            RESOURCE_LOADER.load_unicode('public/js/google_calendar.js'))

        fragment.initialize_js('GoogleCalendarBlock')

        return fragment

    # Context argument is specified for xblocks, but we are not using herein
    def studio_view(self, context):  # pylint: disable=unused-argument
        """
        Editing view in Studio
        """
        fragment = Fragment()
        # Need to access protected members of fields to get their default value
        default_name = self.fields['display_name']._default  # pylint: disable=protected-access,unsubscriptable-object
        fragment.add_content(
            RESOURCE_LOADER.render_template(
                CALENDAR_EDIT_TEMPLATE,
                {
                    'self': self,
                    'defaultName': default_name,
                    'defaultID': self.fields['calendar_id']._default  # pylint: disable=protected-access,unsubscriptable-object
                }))
        fragment.add_javascript(
            RESOURCE_LOADER.load_unicode('public/js/google_calendar_edit.js'))
        fragment.add_css(
            RESOURCE_LOADER.load_unicode('public/css/google_edit.css'))

        fragment.initialize_js('GoogleCalendarEditBlock')

        return fragment

    # suffix argument is specified for xblocks, but we are not using herein
    @XBlock.json_handler
    def studio_submit(self, submissions, suffix=''):  # pylint: disable=unused-argument
        """
        Change the settings for this XBlock given by the Studio user
        """
        if not isinstance(submissions, dict):
            LOG.error("submissions object from Studio is not a dict - %r",
                      submissions)
            return {'result': 'error'}

        if 'display_name' in submissions:
            self.display_name = submissions['display_name']
        if 'calendar_id' in submissions:
            self.calendar_id = submissions['calendar_id']
        if 'default_view' in submissions:
            self.default_view = submissions['default_view']

        return {
            'result': 'success',
        }

    @staticmethod
    def workbench_scenarios():
        """
        A canned scenario for display in the workbench.
        """
        return [("Google Calendar scenario",
                 "<vertical_demo><google-calendar/></vertical_demo>")]