Exemplo n.º 1
0
def test_field_name_defaults():
    # Tests field display name default values
    attempts = Integer()
    attempts.__name__ = "max_problem_attempts"
    assert_equals('max_problem_attempts', attempts.display_name)

    class TestBlock(XBlock):
        """
        Block for testing
        """
        field_x = List()

    assert_equals("field_x", TestBlock.field_x.display_name)
Exemplo n.º 2
0
def test_field_display_name():
    attempts = Integer(display_name='Maximum Problem Attempts')
    attempts._name = "max_problem_attempts"
    assert_equals("Maximum Problem Attempts", attempts.display_name)

    boolean_field = Boolean(display_name="boolean field")
    assert_equals("boolean field", boolean_field.display_name)

    class TestBlock(XBlock):
        """
        Block for testing
        """
        field_x = List(display_name="Field Known as X")

    assert_equals("Field Known as X", TestBlock.field_x.display_name)
Exemplo n.º 3
0
class BaseMentoringBlock(
    XBlock, XBlockWithTranslationServiceMixin, XBlockWithSettingsMixin,
    StudioEditableXBlockMixin, MessageParentMixin, StudentViewUserStateMixin,
    ExpandStaticURLMixin
):
    """
    An XBlock that defines functionality shared by mentoring blocks.
    """
    # Content
    show_title = Boolean(
        display_name=_("Show title"),
        help=_("Display the title?"),
        default=True,
        scope=Scope.content
    )
    max_attempts = Integer(
        display_name=_("Max. attempts allowed"),
        help=_("Maximum number of times students are allowed to attempt the questions belonging to this block"),
        default=0,
        scope=Scope.content,
        enforce_type=True
    )
    weight = Float(
        display_name=_("Weight"),
        help=_("Defines the maximum total grade of the block."),
        default=1,
        scope=Scope.settings,
        enforce_type=True
    )

    # User state
    num_attempts = Integer(
        # Number of attempts a user has answered for this questions
        default=0,
        scope=Scope.user_state,
        enforce_type=True
    )

    has_children = True
    has_score = True  # The Problem/Step Builder XBlocks produce scores. (Their children do not send scores to the LMS.)

    icon_class = 'problem'
    block_settings_key = 'mentoring'
    options_key = 'options'

    @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 usage_id.
        """
        try:
            return super(BaseMentoringBlock, self).url_name
        except AttributeError:
            return six.text_type(self.scope_ids.usage_id)

    @property
    def review_tips_json(self):
        return json.dumps(self.review_tips)

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

    def get_content_titles(self):
        """
        By default, each Sequential block in a course ("Subsection" in Studio parlance) will
        display the display_name of each descendant in a tooltip above the content. We don't
        want that - we only want to display one title for this mentoring block as a whole.
        Otherwise things like "Choice (yes) (Correct)" will appear in the tooltip.

        If this block has no title set, don't display any title. Then, if this is the only block
        in the unit, the unit's title will be used. (Why isn't it always just used?)
        """
        has_explicitly_set_title = self.fields['display_name'].is_set_on(self)
        if has_explicitly_set_title:
            return [self.display_name]
        return []

    def get_options(self):
        """
        Get options settings for this block from settings service.

        Fall back on default options if xblock settings have not been customized at all
        or no customizations for options available.
        """
        xblock_settings = self.get_xblock_settings(default={})
        if xblock_settings and self.options_key in xblock_settings:
            return xblock_settings[self.options_key]
        return _default_options_config

    def get_option(self, option):
        """
        Get value of a specific instance-wide `option`.
        """
        return self.get_options().get(option)

    @XBlock.json_handler
    def view(self, data, suffix=''):
        """
        Current HTML view of the XBlock, for refresh by client
        """
        frag = self.student_view({})
        return {'html': frag.content}

    @XBlock.json_handler
    def publish_event(self, data, suffix=''):
        """
        Publish data for analytics purposes
        """
        event_type = data.pop('event_type')
        if (event_type == 'grade'):
            # This handler can be called from the browser. Don't allow the browser to submit arbitrary grades ;-)
            raise JsonHandlerError(403, "Posting grade events from the browser is forbidden.")

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

    def author_preview_view(self, context):
        """
        Child blocks can override this to add a custom preview shown to
        authors in Studio when not editing this block's children.
        """
        fragment = self.student_view(context)
        fragment.add_content(loader.render_django_template('templates/html/mentoring_url_name.html', {
            "url_name": self.url_name
        }))
        fragment.add_css_url(self.runtime.local_resource_url(self, 'public/css/problem-builder-edit.css'))
        return fragment

    def max_score(self):
        """ Maximum score. We scale all scores to a maximum of 1.0 so this is always 1.0 """
        return 1.0
Exemplo n.º 4
0
class MentoringBlock(
    StudentViewUserStateResultsTransformerMixin, I18NService,
    BaseMentoringBlock, StudioContainerWithNestedXBlocksMixin, StepParentMixin, TranslationContentMixin
):
    """
    An XBlock providing mentoring capabilities

    Composed of text, answers input fields, and a set of MRQ/MCQ with advices.
    A set of conditions on the provided answers and MCQ/MRQ choices will determine if the
    student is a) provided mentoring advices and asked to alter his answer, or b) is given the
    ok to continue.
    """
    # Content
    USER_STATE_FIELDS = ['completed', 'num_attempts', 'student_results']
    followed_by = String(
        display_name=_("Followed by"),
        help=_("url_name of the step after the current mentoring block in workflow."),
        default=None,
        scope=Scope.content
    )
    enforce_dependency = Boolean(
        display_name=_("Enforce Dependency"),
        help=_("Should the next step be the current block to complete?"),
        default=False,
        scope=Scope.content,
        enforce_type=True
    )
    display_submit = Boolean(
        display_name=_("Show Submit Button"),
        help=_("Allow submission of the current block?"),
        default=True,
        scope=Scope.content,
        enforce_type=True
    )
    xml_content = String(
        display_name=_("XML content"),
        help=_("Not used for version 2. This field is here only to preserve the data needed to upgrade from v1 to v2."),
        default='',
        scope=Scope.content,
        multiline_editor=True
    )

    # Settings
    display_name = String(
        display_name=_("Title (Display name)"),
        help=_("Title to display"),
        default=_("Problem Builder"),
        scope=Scope.settings
    )
    feedback_label = String(
        display_name=_("Feedback Header"),
        help=_("Header for feedback messages"),
        default=_("Feedback"),
        scope=Scope.content
    )

    # User state
    attempted = Boolean(
        # Has the student attempted this mentoring step?
        default=False,
        scope=Scope.user_state
        # TODO: Does anything use this 'attempted' field? May want to delete it.
    )
    completed = Boolean(
        # Has the student completed this mentoring step?
        default=False,
        scope=Scope.user_state
    )
    step = Integer(
        # Keep track of the student assessment progress.
        default=0,
        scope=Scope.user_state,
        enforce_type=True
    )
    student_results = List(
        # Store results of student choices.
        default=[],
        scope=Scope.user_state
    )
    extended_feedback = Boolean(
        help=_("Show extended feedback details when all attempts are used up."),
        default=False,
        Scope=Scope.content
    )

    # Global user state
    next_step = String(
        # url_name of the next step the student must complete (global to all blocks)
        default='mentoring_first',
        scope=Scope.preferences
    )

    editable_fields = (
        'display_name', 'followed_by', 'max_attempts', 'enforce_dependency',
        'display_submit', 'feedback_label', 'weight', 'extended_feedback'
    )

    @property
    def allowed_nested_blocks(self):
        """
        Returns a list of allowed nested XBlocks. Each item can be either
        * An XBlock class
        * A NestedXBlockSpec

        If XBlock class is used it is assumed that this XBlock is enabled and allows multiple instances.
        NestedXBlockSpec allows explicitly setting disabled/enabled state, disabled reason (if any) and single/multiple
        instances
        """
        additional_blocks = []
        try:
            from xmodule.video_module.video_module import VideoBlock
            additional_blocks.append(NestedXBlockSpec(
                VideoBlock, category='video', label=_(u"Video")
            ))
        except ImportError:
            pass
        try:
            from imagemodal import ImageModal
            additional_blocks.append(NestedXBlockSpec(
                ImageModal, category='imagemodal', label=_(u"Image Modal")
            ))
        except ImportError:
            pass

        from .platform_dependencies import XBlockConfiguration
        if XBlockConfiguration:
            opt = XBlockConfiguration.objects.filter(name="pb-swipe")
            if opt.count() and opt.first().enabled:
                additional_blocks.append(SwipeBlock)

        try:
            from ooyala_player.ooyala_player import OoyalaPlayerBlock
            additional_blocks.append(NestedXBlockSpec(
                OoyalaPlayerBlock, category='ooyala-player', label=_(u"Ooyala Player")
            ))
        except ImportError:
            pass

        message_block_shims = [
            NestedXBlockSpec(
                MentoringMessageBlock,
                category='pb-message',
                boilerplate=message_type,
                label=get_message_label(message_type),
            )
            for message_type in (
                'completed',
                'incomplete',
                'max_attempts_reached',
            )
        ]

        return [
            NestedXBlockSpec(AnswerBlock, boilerplate='studio_default'),
            MCQBlock, RatingBlock, MRQBlock, CompletionBlock,
            NestedXBlockSpec(None, category="html", label=self._("HTML")),
            AnswerRecapBlock, MentoringTableBlock, PlotBlock, SliderBlock
        ] + additional_blocks + message_block_shims

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

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

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

        return Score(
            float(score),
            int(Decimal(score * 100).quantize(Decimal('1.'), rounding=ROUND_HALF_UP)),
            correct,
            incorrect,
            partially_correct
        )

    @XBlock.supports("multi_device")  # Mark as mobile-friendly
    def student_view(self, context):
        from .questionnaire import QuestionnaireAbstractBlock  # Import here to avoid circular dependency

        # Migrate stored data if necessary
        self.migrate_fields()

        # Validate self.step:
        num_steps = len(self.steps)
        if self.step > num_steps:
            self.step = num_steps

        fragment = Fragment()
        child_content = u""

        mcq_hide_previous_answer = self.get_option('pb_mcq_hide_previous_answer')

        for child_id in self.children:
            child = self.runtime.get_block(child_id)
            if child is None:  # child should not be None but it can happen due to bugs or permission issues
                child_content += u"<p>[{}]</p>".format(self._(u"Error: Unable to load child component."))
            elif not isinstance(child, MentoringMessageBlock):
                try:
                    if mcq_hide_previous_answer and isinstance(child, QuestionnaireAbstractBlock):
                        context['hide_prev_answer'] = True
                    else:
                        context['hide_prev_answer'] = False
                    child_fragment = child.render('mentoring_view', context)
                except NoSuchViewError:
                    if child.scope_ids.block_type == 'html' and getattr(self.runtime, 'is_author_mode', False):
                        # html block doesn't support mentoring_view, and if we use student_view Studio will wrap
                        # it in HTML that we don't want in the preview. So just render its HTML directly:
                        child_fragment = Fragment(child.data)
                    else:
                        child_fragment = child.render('student_view', context)
                fragment.add_frag_resources(child_fragment)
                child_content += child_fragment.content

        fragment.add_content(loader.render_django_template('templates/html/mentoring.html', {
            'self': self,
            'title': self.display_name,
            'show_title': self.show_title,
            'child_content': child_content,
            'missing_dependency_url': self.has_missing_dependency and self.next_step_url,
        }, i18n_service=self.i18n_service))
        fragment.add_javascript(self.get_translation_content())
        fragment.add_css_url(self.runtime.local_resource_url(self, 'public/css/problem-builder.css'))
        fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/vendor/underscore-min.js'))
        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_standard_view.js'))
        fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/mentoring.js'))

        # Workbench doesn't have font awesome, so add it:
        if WorkbenchRuntime and isinstance(self.runtime, WorkbenchRuntime):
            fragment.add_css_url('//maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css')

        fragment.initialize_js('MentoringBlock')

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

        return fragment

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

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

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

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

    @property
    def hide_feedback(self):
        return self.get_option("pb_hide_feedback_if_attempts_remain") and not self.max_attempts_reached

    def get_message(self, completed):
        """
        Get the message to display to a student following a submission.
        """
        if completed:
            # Student has achieved a perfect score
            return self.get_message_content('completed')
        elif self.max_attempts_reached:
            # Student has not achieved a perfect score and cannot try again
            return self.get_message_content('max_attempts_reached')
        else:
            # Student did not achieve a perfect score but can try again:
            return self.get_message_content('incomplete')

    @property
    def review_tips(self):
        review_tips = []
        return review_tips

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

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

        Right now there are two ways to get results-- through the template upon loading up
        the mentoring block, or after submission of an AJAX request like in
        submit or get_results here.
        """
        results, completed, show_message = self._get_standard_results()
        mentoring_completed = completed

        result = {
            'results': results,
            'completed': completed,
            'step': self.step,
            'max_attempts': self.max_attempts,
            'num_attempts': self.num_attempts,
        }

        if show_message:
            result['message'] = self.get_message(mentoring_completed)

        return result

    def _get_standard_results(self):
        """
        Gets previous submissions results as if submit was called with exactly the same values as last time.
        """
        results = []
        completed = True
        show_message = (not self.hide_feedback) and bool(self.student_results)

        # All children are visible simultaneously, so need to collect results for all of them
        for child in self.steps:
            child_result = child.get_last_result()
            results.append([child.name, child_result])
            completed = completed and (child_result.get('status', None) == 'correct')

        return results, completed, show_message

    @XBlock.json_handler
    def submit(self, submissions, suffix=''):
        log.info(u'Received submissions: {}'.format(submissions))
        # server-side check that the user is allowed to submit:
        if self.max_attempts_reached:
            raise JsonHandlerError(403, "Maximum number of attempts already reached.")
        if self.has_missing_dependency:
            raise JsonHandlerError(
                403,
                "You need to complete all previous steps before being able to complete the current one."
            )

        # This has now been attempted:
        self.attempted = True

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

        if completed and self.next_step == self.url_name:
            self.next_step = self.followed_by

        # Update the score and attempts, unless the user had already achieved a perfect score ("completed"):
        if not previously_completed:
            # Update the results
            while self.student_results:
                self.student_results.pop()
            for result in submit_results:
                self.student_results.append(result)

            # Save the user's latest score
            self.runtime.publish(self, 'grade', {
                'value': self.score.raw,
                'max_value': self.max_score(),
            })

            # Mark this as having used an attempt:
            if self.max_attempts > 0:
                self.num_attempts += 1

        # Save the completion status.
        # Once it has been completed once, keep completion even if user changes values
        self.completed = bool(completed) or previously_completed

        message = self.get_message(completed)
        raw_score = self.score.raw

        self.runtime.publish(self, 'xblock.problem_builder.submitted', {
            'num_attempts': self.num_attempts,
            'submitted_answer': submissions,
            'grade': raw_score,
        })

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

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

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

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

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

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

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

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

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

        return {
            'result': 'success'
        }

    def validate(self):
        """
        Validates the state of this XBlock except for individual field values.
        """
        validation = super(MentoringBlock, self).validate()
        a_child_has_issues = False
        message_types_present = set()
        for child_id in self.children:
            child = self.runtime.get_block(child_id)
            # Check if the child has any errors:
            if not child.validate().empty:
                a_child_has_issues = True
            # Ensure there is only one "message" block of each type:
            if isinstance(child, MentoringMessageBlock):
                msg_type = child.type
                if msg_type in message_types_present:
                    validation.add(ValidationMessage(
                        ValidationMessage.ERROR,
                        self._(u"There should only be one '{msg_type}' message component.").format(msg_type=msg_type)
                    ))
                message_types_present.add(msg_type)
        if a_child_has_issues:
            validation.add(ValidationMessage(
                ValidationMessage.ERROR,
                self._(u"A component inside this mentoring block has issues.")
            ))
        return validation

    def author_edit_view(self, context):
        """
        Add some HTML to the author view that allows authors to add child blocks.
        """
        local_context = context.copy()
        local_context['author_edit_view'] = True
        fragment = super(MentoringBlock, self).author_edit_view(local_context)
        fragment.add_content(loader.render_django_template('templates/html/mentoring_url_name.html', {
            'url_name': self.url_name
        }))
        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/problem-builder-edit.css'))
        fragment.add_css_url(self.runtime.local_resource_url(self, 'public/css/problem-builder-tinymce-content.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/container_edit.js'))
        fragment.initialize_js('ProblemBuilderContainerEdit')

        return fragment

    @staticmethod
    def workbench_scenarios():
        """
        Scenarios displayed by the workbench. Load them from external (private) repository
        """
        return loader.load_scenarios_from_path('templates/xml')

    def student_view_data(self, context=None):
        """
        Returns a JSON representation of the student_view of this XBlock,
        retrievable from the Course Block API.
        """
        components = []
        for child_id in self.children:
            block = self.runtime.get_block(child_id)
            if hasattr(block, 'student_view_data'):
                components.append(block.student_view_data())

        return {
            'block_id': six.text_type(self.scope_ids.usage_id),
            'display_name': self.display_name,
            'max_attempts': self.max_attempts,
            'extended_feedback': self.extended_feedback,
            'feedback_label': self.feedback_label,
            'components': components,
            'messages': {
                message_type: self.expand_static_url(self.get_message_content(message_type))
                for message_type in (
                        'completed',
                        'incomplete',
                        'max_attempts_reached',
                )
            }
        }
Exemplo n.º 5
0
class RandomizeFields(object):
    choice = Integer(help="Which random child was chosen",
                     scope=Scope.user_state)
Exemplo n.º 6
0
class StaffGradedAssignmentXBlock(StudioEditableXBlockMixin,
                                  ShowAnswerXBlockMixin,
                                  XBlockWithSettingsMixin, 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 = ('team_view', 'display_name', 'points', 'weight',
                       'showanswer', 'solution', 'activity_description')

    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."))

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

    team_view = Boolean(
        display_name=_("Team View"),
        scope=Scope.settings,
        default=False,
        help=_("This option allows to select the standard view or team view."))

    activity_description = String(
        display_name=_("Activity Description"),
        scope=Scope.settings,
        default="Set your team activity description",
        help=_(
            "This contains the description of the activity for every team."),
        multiline_editor=True,
        resettable_editor=False)

    @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 team_download(self, request, suffix=''):
        # pylint: disable=unused-argument
        """
        Return an assignment file requested by a team member.
        """
        require(self.is_team_member(request.params['student_id']))
        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'],
        )

    @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.
        """
        require(self.is_course_staff())
        user = self.get_real_user()
        require(user)
        zip_file_ready = False
        location = unicode(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",
                    last_assignment_date, zip_file_time, 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

        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.
        """
        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.encode('utf-8'))
        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)})

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

    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
        }
        if self.show_staff_grading_interface():
            context['is_course_staff'] = True
            self.update_staff_debug_context(context)
        elif self.team_view:
            return self.teams_view(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 teams_view(self, context=None):
        # pylint: disable=no-member
        """
        The team view of the modify StaffGradedAssignmentXBlock, shown to students
        when the team option is activated.
        """
        context["data_team_activity"] = {
            "description": self.activity_description,
            "name": self.display_name
        }
        fragment = Fragment()
        fragment.add_content(
            render_template(
                'templates/staff_graded_assignment/teams_view.html', context))
        fragment.add_css(_resource("static/css/sga_team_view.css"))
        fragment.add_javascript(_resource("static/js/src/edx_sga.js"))
        fragment.add_javascript(
            _resource("static/js/src/jquery.tablesorter.min.js"))
        fragment.add_javascript(_resource("static/js/src/sga_team_view.js"))
        fragment.initialize_js('SgaTeamView')
        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 unicode(self.scope_ids.usage_id)

    @reify
    def block_course_id(self):
        """
        Return the course_id of the block.
        """
        return unicode(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
        """
        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 = ''

        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:
            return Response(
                app_iter=file_contents_iter(path),
                content_type=mime_type,
                content_disposition="attachment; filename*=UTF-8''" +
                urllib.quote(filename.encode('utf-8')))
        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
        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.
        """
        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 get_real_user(self):
        """returns session user"""
        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()

    def team_grading_data(self):
        """
        Return team member assignment information for display on the
        grading screen.
        """
        members = self.get_teams_members()
        if not members:
            return {"assignments": list()}

        course_data = self.staff_grading_data()

        assignments = course_data["assignments"]

        assignments = self.filter_assigments_by_team_members(
            assignments, members)

        course_data["assignments"] = list(assignments)

        return course_data

    def get_teams_members(self):
        runtime = self.xmodule_runtime
        user = runtime.service(self, 'user').get_current_user()
        course_id = runtime.course_id
        username = user.opt_attrs['edx-platform.username']

        xblock_settings = self.get_xblock_settings()

        try:
            user = xblock_settings["username"]
            password = xblock_settings["password"]
            client_id = xblock_settings["client_id"]
            client_secret = xblock_settings["client_secret"]
        except KeyError:
            raise

        server_url = settings.LMS_ROOT_URL

        api = ApiTeams(user, password, client_id, client_secret, server_url)
        team = api.get_user_team(course_id, username)
        if team:
            team = team[0]
            team_id = team["id"]
            members = api.get_members(team_id)
            if members:
                return members

    def filter_assigments_by_team_members(self, assignments, members):
        """This method compares the team's users with an assigments' list"""
        for member in members:
            user = get_user_by_username_or_email(member["user"]["username"])
            for assignment in assignments:
                if user == user_by_anonymous_id(assignment["student_id"]):
                    assignment["profile_image_url"] = self._user_image_url(
                        user)
                    yield assignment

    def is_team_member(self, student_id):
        """This methods verifies if the user is a team member """
        team_data = self.team_grading_data()
        assignments = team_data["assignments"]
        for assignment in assignments:
            if assignment["student_id"] == student_id:
                return True
        return False

    def _user_image_url(self, user):
        """Returns an image url for the current user"""
        from openedx.core.djangoapps.user_api.accounts.image_helpers import get_profile_image_urls_for_user  # pylint: disable=relative-import
        profile_image_url = get_profile_image_urls_for_user(user)["full"]

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

        base_url = settings.LMS_ROOT_URL
        image_url = "{}{}".format(base_url, profile_image_url)
        return image_url
Exemplo n.º 7
0
class PeerInstructionXBlock(XBlock, MissingDataFetcherMixin,
                            PublishEventMixin):
    """
    Peer Instruction XBlock

    Notes:
    storing index vs option text: when storing index, it is immune to the
    option text changes. But when the order changes, the results will be
    skewed. When storing option text, it is impossible (at least for now) to
    update the existing students responses when option text changed.

    a warning may be shown to the instructor that they may only do the minor
    changes to the question options and may not change the order of the options,
    add or delete options
    """

    event_namespace = 'ubc.peer_instruction'

    # the display name that used on the interface
    display_name = String(default=_("Peer Instruction Question"))

    question_text = Dict(
        default={
            'text':
            _('<p>Where does most of the mass in a fully grown tree originate?</p>'
              ),
            'image_url':
            '',
            'image_position':
            'below',
            'image_show_fields':
            0,
            'image_alt':
            ''
        },
        scope=Scope.content,
        help=
        _("The question the students see. This question appears above the possible answers which you set below. "
          "You can use text, an image or a combination of both. If you wish to add an image to your question, press "
          "the 'Add Image' button."))

    options = List(
        default=[{
            'text': _('Air'),
            'image_url': '',
            'image_position': 'below',
            'image_show_fields': 0,
            'image_alt': ''
        }, {
            'text': _('Soil'),
            'image_url': '',
            'image_position': 'below',
            'image_show_fields': 0,
            'image_alt': ''
        }, {
            'text': _('Water'),
            'image_url': '',
            'image_position': 'below',
            'image_show_fields': 0,
            'image_alt': ''
        }],
        scope=Scope.content,
        help=_("The possible options from which the student may select"),
    )

    rationale_size = Dict(
        default={
            'min': 1,
            'max': MAX_RATIONALE_SIZE
        },
        scope=Scope.content,
        help=
        _("The minimum and maximum number of characters a student is allowed for their rationale."
          ),
    )

    correct_answer = Integer(
        default=0,
        scope=Scope.content,
        help=_("The correct option for the question"),
    )

    correct_rationale = Dict(
        default={'text': _("Photosynthesis")},
        scope=Scope.content,
        help=_("The feedback for student for the correct answer"),
    )

    stats = Dict(
        default={
            'original': {},
            'revised': {}
        },
        scope=Scope.user_state_summary,
        help=_("Overall stats for the instructor"),
    )
    seeds = List(
        default=[{
            'answer': 0,
            'rationale': _('Tree gets carbon from air.')
        }, {
            'answer': 1,
            'rationale': _('Tree gets minerals from soil.')
        }, {
            'answer': 2,
            'rationale': _('Tree drinks water.')
        }],
        scope=Scope.content,
        help=
        _("Instructor configured examples to give to students during the revise stage."
          ),
    )

    # sys_selected_answers dict format:
    # {
    #     option1_index: {
    #         'student_id1': { can store algorithm specific info here },
    #         'student_id2': { can store algorithm specific info here },
    #         ...
    #     }
    #     option2_index: ...
    # }
    sys_selected_answers = Dict(
        default={},
        scope=Scope.user_state_summary,
        help=
        _("System selected answers to give to students during the revise stage."
          ),
    )

    other_answers_shown = Dict(
        default={},
        scope=Scope.user_state,
        help=
        _("Stores the specific answers of other students shown, for a given student."
          ),
    )

    algo = Dict(
        default={
            'name': 'simple',
            'num_responses': '#'
        },
        scope=Scope.content,
        help=
        _("The algorithm for selecting which answers to be presented to students"
          ),
    )

    # Declare that we are not part of the grading System. Disabled for now as for the concern about the loading
    # speed of the progress page.
    has_score = True

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

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

    # required field for LMS progress page
    weight = Float(
        default=1,
        display_name=_("Problem Weight"),
        help=_(("Defines the number of points each problem is worth. "
                "If the value is not set, the problem is worth the sum of the "
                "option point values.")),
        values={
            "min": 0,
            "step": .1
        },
        scope=Scope.settings)

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

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

    def studio_view(self, context=None):
        """
        view function for studio edit
        """
        html = self.resource_string("static/html/ubcpi_edit.html")
        frag = Fragment(html)
        frag.add_javascript(
            self.resource_string("static/js/src/ubcpi_edit.js"))

        frag.initialize_js(
            'PIEdit', {
                'display_name': self.ugettext(self.display_name),
                'weight': self.weight,
                'correct_answer': self.correct_answer,
                'correct_rationale': self.correct_rationale,
                'rationale_size': self.rationale_size,
                'question_text': self.question_text,
                'options': self.options,
                'algo': self.algo,
                'algos': {
                    'simple':
                    self.ugettext(
                        'System will select one of each option to present to the students.'
                    ),
                    'random':
                    self.ugettext(
                        'Completely random selection from the response pool.')
                },
                'image_position_locations': {
                    'above': self.ugettext('Appears above'),
                    'below': self.ugettext('Appears below')
                },
                'seeds': self.seeds,
                'lang': translation.get_language(),
            })

        return frag

    @XBlock.json_handler
    def studio_submit(self, data, suffix=''):
        """
        Submit handler for studio edit

        Args:
            data (dict): data submitted from the form
            suffix (str): not sure

        Returns:
            dict: result of the submission
        """
        self.display_name = data['display_name']
        self.weight = data['weight']
        self.question_text = data['question_text']
        self.rationale_size = data['rationale_size']
        self.options = data['options']
        self.correct_answer = data['correct_answer']
        self.correct_rationale = data['correct_rationale']
        self.algo = data['algo']
        self.seeds = data['seeds']

        return {'success': 'true'}

    @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.handler
    def get_asset(self, request, suffix=''):
        """
        Get static partial assets from this XBlock

        As there is no way to directly access the static assets within the XBlock, we use
        a handler to expose assets by name. Only html is needed for now.

        Args:
            request (Request): HTTP request
            suffix (str): not sure

        Returns:
            Response: HTTP response with the content of the asset
        """
        filename = request.params.get('f')
        return Response(self.resource_string('static/js/partials/' + filename),
                        content_type='text/html')

    @classmethod
    def get_base_url_path_for_course_assets(cls, course_key):
        """
        Slightly modified version of StaticContent.get_base_url_path_for_course_assets.

        Code is copied as we don't want to introduce the dependency of edx-platform so that we can
        develop in workbench

        Args:
            course_key (str): CourseKey

        Returns:
            str: course asset base URL string
        """
        if course_key is None:
            return None

        # assert isinstance(course_key, CourseKey)
        placeholder_id = uuid.uuid4().hex
        # create a dummy asset location with a fake but unique name. strip off the name, and return it
        url_path = cls.serialize_asset_key_with_slash(
            course_key.make_asset_key('asset',
                                      placeholder_id).for_branch(None))
        return url_path.replace(placeholder_id, '')

    @staticmethod
    def serialize_asset_key_with_slash(asset_key):
        """
        Legacy code expects the serialized asset key to start w/ a slash; so, do that in one place

        Args:
            asset_key (str): Asset key to generate URL
        """
        url = unicode(asset_key)
        if not url.startswith('/'):
            url = '/' + url
        return url

    def get_asset_url(self, static_url):
        """
        Returns the asset url for imported files (eg. images) that are uploaded in Files & Uploads

        Args:
            static_url(str): The static url for the file

        Returns:
            str: The URL for the file
        """
        # if static_url is not a "asset url", we will use it as it is
        if not static_url.startswith('/static/'):
            return static_url

        if hasattr(self, "xmodule_runtime"):
            file_name = os.path.split(static_url)[-1]
            return self.get_base_url_path_for_course_assets(
                self.course_id) + file_name
        else:
            return static_url

    def student_view(self, context=None):
        """
        The primary view of the PeerInstructionXBlock, shown to students when viewing courses.
        """
        # convert key into integers as json.dump and json.load convert integer dictionary key into string
        self.sys_selected_answers = {
            int(k): v
            for k, v in self.sys_selected_answers.items()
        }

        # generate a random seed for student
        student_item = self.get_student_item_dict()
        random.seed(student_item['student_id'])

        answers = self.get_answers_for_student()
        html = ""
        html += self.resource_string("static/html/ubcpi.html")

        frag = Fragment(html)
        frag.add_css(self.resource_string("static/css/ubcpi.css"))
        frag.add_javascript_url(
            "//ajax.googleapis.com/ajax/libs/angularjs/1.3.13/angular.js")
        frag.add_javascript_url(
            "//ajax.googleapis.com/ajax/libs/angularjs/1.3.13/angular-messages.js"
        )
        frag.add_javascript_url(
            "//ajax.googleapis.com/ajax/libs/angularjs/1.3.13/angular-sanitize.js"
        )
        frag.add_javascript_url(
            "//ajax.googleapis.com/ajax/libs/angularjs/1.3.13/angular-cookies.js"
        )
        frag.add_javascript_url(
            "//cdnjs.cloudflare.com/ajax/libs/angular-gettext/2.3.8/angular-gettext.min.js"
        )
        frag.add_javascript_url(
            "//cdnjs.cloudflare.com/ajax/libs/d3/3.3.13/d3.min.js")
        frag.add_javascript(self.resource_string("static/js/src/d3-pibar.js"))
        frag.add_javascript(self.resource_string("static/js/src/ubcpi.js"))
        frag.add_javascript(
            self.resource_string(
                "static/js/src/ubcpi-answer-result-directive.js"))
        frag.add_javascript(
            self.resource_string("static/js/src/ubcpi-barchart-directive.js"))
        frag.add_javascript(
            self.resource_string("static/js/src/translations.js"))

        # convert image URLs
        question = deepcopy(self.question_text)
        question.update(
            {'image_url': self.get_asset_url(question.get('image_url'))})

        options = deepcopy(self.options)
        for option in options:
            if option.get('image_url'):
                option.update(
                    {'image_url': self.get_asset_url(option.get('image_url'))})

        js_vals = {
            'answer_original': answers.get_vote(0),
            'rationale_original': answers.get_rationale(0),
            'answer_revised': answers.get_vote(1),
            'rationale_revised': answers.get_rationale(1),
            'display_name': self.ugettext(self.display_name),
            'question_text': question,
            'weight': self.weight,
            'options': options,
            'rationale_size': self.rationale_size,
            'user_role': self.get_user_role(),
            'all_status': {
                'NEW': STATUS_NEW,
                'ANSWERED': STATUS_ANSWERED,
                'REVISED': STATUS_REVISED
            },
            'lang': translation.get_language(),
        }
        if answers.has_revision(0) and not answers.has_revision(1):
            js_vals['other_answers'] = self.other_answers_shown

        # reveal the correct answer in the end
        if answers.has_revision(1):
            js_vals['correct_answer'] = self.correct_answer
            js_vals['correct_rationale'] = self.correct_rationale

        # Pass the answer to out Javascript
        frag.initialize_js('PeerInstructionXBlock', js_vals)

        self.publish_event_from_dict(self.event_namespace + '.accessed', {})

        return frag

    def record_response(self, answer, rationale, status):
        """
        Store response from student to the backend

        Args:
            answer (int): the option index that student responded
            rationale (str): the rationale text
            status (int): the progress status for this student. Possible values are:
                STATUS_NEW, STATUS_ANSWERED, STATUS_REVISED

        Raises:
            PermissionDenied: if we got an invalid status
        """
        answers = self.get_answers_for_student()
        stats = self.get_current_stats()
        truncated_rationale, was_truncated = truncate_rationale(rationale)
        corr_ans_text = ''
        if self.correct_answer == len(
                self.options):  # handle scenario with no correct answer
            corr_ans_text = 'n/a'
        else:
            corr_ans_text = self.options[self.correct_answer].get('text'),
        event_dict = {
            'answer': answer,
            'answer_text': self.options[answer].get('text'),
            'rationale': truncated_rationale,
            'correct_answer': self.correct_answer,
            'correct_answer_text': corr_ans_text,
            'correct_rationale': self.correct_rationale,
            'truncated': was_truncated
        }
        if not answers.has_revision(0) and status == STATUS_NEW:
            student_item = self.get_student_item_dict()
            sas_api.add_answer_for_student(student_item, answer, rationale)
            num_resp = stats['original'].setdefault(answer, 0)
            stats['original'][answer] = num_resp + 1
            offer_answer(self.sys_selected_answers, answer, rationale,
                         student_item['student_id'], self.algo, self.options)
            self.other_answers_shown = get_other_answers(
                self.sys_selected_answers, self.seeds,
                self.get_student_item_dict, self.algo, self.options)
            event_dict['other_student_responses'] = self.other_answers_shown
            self.publish_event_from_dict(
                self.event_namespace + '.original_submitted', event_dict)
            return event_dict['other_student_responses']
        elif answers.has_revision(0) and not answers.has_revision(
                1) and status == STATUS_ANSWERED:
            sas_api.add_answer_for_student(self.get_student_item_dict(),
                                           answer, rationale)
            num_resp = stats['revised'].setdefault(answer, 0)
            stats['revised'][answer] = num_resp + 1

            # Fetch the grade
            grade = self.get_grade()

            # Send the grade
            self.runtime.publish(self, 'grade', {
                'value': grade,
                'max_value': 1
            })

            self.publish_event_from_dict(
                self.event_namespace + '.revised_submitted', event_dict)
        else:
            raise PermissionDenied

    def get_grade(self):
        """
        Return the grade

        Only returns 1 for now as a completion grade.
        """
        return 1

    def get_current_stats(self):
        """
        Get the progress status for current user. This function also converts option index into integers
        """
        # convert key into integers as json.dump and json.load convert integer dictionary key into string
        self.stats = {
            'original':
            {int(k): v
             for k, v in self.stats['original'].iteritems()},
            'revised':
            {int(k): v
             for k, v in self.stats['revised'].iteritems()}
        }
        return self.stats

    @XBlock.json_handler
    def get_stats(self, data, suffix=''):
        """
        Get the progress status for current user

        Args:
            data (dict): no input required
            suffix (str): not sure

        Return:
            dict: current progress status
        """
        return self.get_current_stats()

    @XBlock.json_handler
    def submit_answer(self, data, suffix=''):
        """
        Answer submission handler to process the student answers
        """
        # convert key into integers as json.dump and json.load convert integer dictionary key into string
        self.sys_selected_answers = {
            int(k): v
            for k, v in self.sys_selected_answers.items()
        }

        return self.get_persisted_data(
            self.record_response(data['q'], data['rationale'], data['status']))

    def get_persisted_data(self, other_answers):
        """
        Formats a usable dict based on what data the user has persisted
        Adds the other answers and correct answer/rationale when needed
        """
        answers = self.get_answers_for_student()
        ret = {
            "answer_original": answers.get_vote(0),
            "rationale_original": answers.get_rationale(0),
            "answer_revised": answers.get_vote(1),
            "rationale_revised": answers.get_rationale(1),
        }
        if answers.has_revision(0) and not answers.has_revision(1):
            ret['other_answers'] = other_answers

        # reveal the correct answer in the end
        if answers.has_revision(1):
            ret['correct_answer'] = self.correct_answer
            ret['correct_rationale'] = self.correct_rationale

        return ret

    @XBlock.json_handler
    def get_data(self, data, suffix=''):
        """
        Retrieve persisted date from backend for current user
        """
        return self.get_persisted_data(self.other_answers_shown)

    def get_answers_for_student(self):
        """
        Retrieve answers from backend for current user
        """
        return sas_api.get_answers_for_student(self.get_student_item_dict())

    @XBlock.json_handler
    def validate_form(self, data, suffix=''):
        """
        Validate edit form from studio.

        This will check if all the parameters set up for peer instruction question satisfy all the constrains defined
        by the algorithm. E.g. we need at least one seed for each option for simple algorithm.

        Args:
            data (dict): form data
            suffix (str): not sure

        Returns:
            dict: {success: true} if there is no problem

        Raises:
            JsonHandlerError: with 400 error code, if there is any problem. This is necessary for angular async form
                validation to be able to tell if the async validation success or failed
        """
        msg = validate_seeded_answers(data['seeds'], data['options'],
                                      data['algo'])
        options_msg = validate_options(data)
        if msg is None and options_msg is None:
            return {'success': 'true'}
        else:
            msg = msg if msg else {}
            options_msg = options_msg if options_msg else {}
            msg.update(options_msg)
            raise JsonHandlerError(400, msg)

    @classmethod
    def workbench_scenarios(cls):  # pragma: no cover
        """A canned scenario for display in the workbench."""
        return [
            ("UBC Peer Instruction: Basic",
             cls.resource_string('static/xml/basic_scenario.xml')),
        ]

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

        Inherited from XBlock core.
        """
        config = parse_from_xml(node)
        block = runtime.construct_xblock_from_class(cls, keys)

        # TODO: more validation

        for key, value in config.iteritems():
            setattr(block, key, value)

        return block

    def add_xml_to_node(self, node):
        """
        Serialize the XBlock to XML for exporting.
        """
        serialize_to_xml(node, self)
Exemplo n.º 8
0
class SecondMixin(XBlockMixin):
    """Test class for mixin ordering."""
    number = 2
    field = Integer(default=2)
Exemplo n.º 9
0
class TestIntegerXblock(XBlock):
    counter = Integer(scope=Scope.content)
Exemplo n.º 10
0
class OpenAssessmentBlock(
        MessageMixin,
        SubmissionMixin,
        PeerAssessmentMixin,
        SelfAssessmentMixin,
        StudioMixin,
        GradeMixin,
        LeaderboardMixin,
        StaffAreaMixin,
        WorkflowMixin,
        StudentTrainingMixin,
        LmsCompatibilityMixin,
        XBlock,
):
    """Displays a prompt and provides an area where students can compose a response."""

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    @property
    def course_id(self):
        return self._serialize_opaque_key(self.xmodule_runtime.course_id)  # pylint:disable=E1101

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

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

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

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

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

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

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

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

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

        See also: submissions.api for details.

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

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

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

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

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

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

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

        Args:
            context: Not used for this view.

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

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

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

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

        if settings.DEBUG:
            fragment.add_css_url(self.runtime.local_resource_url(
                self, css_url))
            self.add_javascript_files(fragment, "static/js/src/oa_shared.js")
            self.add_javascript_files(fragment, "static/js/src/oa_server.js")
            self.add_javascript_files(fragment, "static/js/src/lms")
        else:
            # TODO: load CSS and JavaScript as URLs once they can be served by the CDN
            fragment.add_css(load(css_url))
            fragment.add_javascript(
                load("static/js/openassessment-lms.min.js"))
        js_context_dict = {
            "ALLOWED_IMAGE_MIME_TYPES": self.ALLOWED_IMAGE_MIME_TYPES,
            "ALLOWED_FILE_MIME_TYPES": self.ALLOWED_FILE_MIME_TYPES,
            "FILE_EXT_BLACK_LIST": self.FILE_EXT_BLACK_LIST,
            "FILE_TYPE_WHITE_LIST": self.white_listed_file_types,
        }
        fragment.initialize_js('OpenAssessmentBlock', js_context_dict)
        return fragment

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

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

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

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

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

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

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

        Returns:
            bool

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

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

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

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

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

        return ui_models

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

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

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

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

        Inherited by XBlock core.

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

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

        block.rubric_criteria = config['rubric_criteria']
        block.rubric_feedback_prompt = config['rubric_feedback_prompt']
        block.rubric_feedback_default_text = config[
            'rubric_feedback_default_text']
        block.rubric_assessments = config['rubric_assessments']
        block.submission_start = config['submission_start']
        block.submission_due = config['submission_due']
        block.title = config['title']
        block.prompts = config['prompts']
        block.allow_file_upload = config['allow_file_upload']
        block.file_upload_type = config['file_upload_type']
        block.white_listed_file_types_string = config[
            'white_listed_file_types']
        block.allow_latex = config['allow_latex']
        block.leaderboard_show = config['leaderboard_show']

        return block

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

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

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

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

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

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

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

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

        Returns:
            list

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

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

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

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

        Returns:
            list of criteria dictionaries

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        Returns:
            dict

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

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

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

        Returns:
            None

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

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

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

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

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

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

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

        Returns:
            unicode

        """
        if hasattr(key, 'to_deprecated_string'):
            return key.to_deprecated_string()
        else:
            return unicode(key)

    def get_username(self, anonymous_user_id):
        if hasattr(self, "xmodule_runtime"):
            return self.xmodule_runtime.get_real_user(
                anonymous_user_id).username

    def _adjust_start_date_for_beta_testers(self, start):
        if hasattr(self, "xmodule_runtime"):
            days_early_for_beta = getattr(self.xmodule_runtime,
                                          'days_early_for_beta', 0)
            if days_early_for_beta is not None:
                delta = dt.timedelta(days_early_for_beta)
                effective = start - delta
                return effective

        return start
Exemplo n.º 11
0
class PollBase(XBlock, ResourceMixin, PublishEventMixin):
    """
    Base class for Poll-like XBlocks.
    """
    event_namespace = 'xblock.pollbase'
    private_results = Boolean(
        default=False, help="Whether or not to display results to the user.")
    max_submissions = Integer(
        default=1,
        help="The maximum number of times a user may send a submission.")
    submissions_count = Integer(
        default=0,
        help="Number of times the user has sent a submission.",
        scope=Scope.user_state)
    feedback = String(default='', help="Text to display after the user votes.")

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

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

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

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

        # Make sure all components are present and clean them.
        for item in source_items:
            if not isinstance(item, dict):
                result['success'] = False
                result['errors'].append(
                    "{0} {1} not a javascript object!".format(noun, item))
                continue
            key = item.get('key', '').strip()
            if not key:
                result['success'] = False
                result['errors'].append("{0} {1} contains no key.".format(
                    noun, item))
            image_link = item.get('img', '').strip()
            label = item.get('label', '').strip()
            if not label:
                if image and not image_link:
                    result['success'] = False
                    result['errors'].append(
                        "{0} has no text or img. Please make sure all {1}s "
                        "have one or the other, or both.".format(
                            noun, noun.lower()))
                elif not image:
                    result['success'] = False
                    # If there's a bug in the code or the user just forgot to relabel a question,
                    # votes could be accidentally lost if we assume the omission was an
                    # intended deletion.
                    result['errors'].append(
                        "{0} was added with no label. "
                        "All {1}s must have labels. Please check the form. "
                        "Check the form and explicitly delete {1}s "
                        "if not needed.".format(noun, noun.lower()))
            if image:
                # Labels might have prefixed space for markdown, though it's unlikely.
                items.append((key, {
                    'label': label,
                    'img': image_link.strip()
                }))
            else:
                items.append([key, label])

        if not items:
            result['errors'].append(
                "You must include at least one {0}.".format(noun.lower()))
            result['success'] = False

        return items

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

    def can_view_private_results(self):
        """
        Checks to see if the user has permissions to view private results.
        This only works inside the LMS.
        """
        if HAS_EDX_ACCESS and hasattr(self.runtime, 'user') and hasattr(
                self.runtime, 'course_id'):
            # Course staff users have permission to view results.
            if has_access(self.runtime.user, 'staff', self,
                          self.runtime.course_id):
                return True
            else:
                # Check if user is member of a group that is explicitly granted
                # permission to view the results through django configuration.
                group_names = getattr(settings,
                                      'XBLOCK_POLL_EXTRA_VIEW_GROUPS', [])
                if group_names:
                    group_ids = self.runtime.user.groups.values_list('id',
                                                                     flat=True)
                    return GroupProfile.objects.filter(
                        group_id__in=group_ids, name__in=group_names).exists()
        else:
            return False

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

        # Better to send an error than to confuse the user by thinking this would work.
        if (max_submissions != 1) and not private_results:
            result['success'] = False
            result['errors'].append(
                "Private results may not be False when Maximum Submissions is not 1."
            )
        return max_submissions
Exemplo n.º 12
0
class ThumbsBlockBase(object):
    """
    An XBlock with thumbs-up/thumbs-down voting.

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

    This demonstrates multiple data scopes and ajax handlers.

    """
    upvotes = Integer(help="Number of up votes",
                      default=0,
                      scope=Scope.user_state_summary)
    downvotes = Integer(help="Number of down votes",
                        default=0,
                        scope=Scope.user_state_summary)
    voted = Boolean(help="Has this student voted?",
                    default=False,
                    scope=Scope.user_state)

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

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

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

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

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

        frag.initialize_js('ThumbsBlock')
        return frag

    problem_view = student_view

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

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

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

        self.voted = True

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

    @staticmethod
    def workbench_scenarios():
        """A canned scenario for display in the workbench."""
        return [("three thumbs at once", """\
                <vertical_demo>
                    <thumbs/>
                    <thumbs/>
                    <thumbs/>
                </vertical_demo>
             """)]
Exemplo n.º 13
0
class FlowCheckPointXblock(StudioEditableXBlockMixin, XBlock):
    """ FlowCheckPointXblock allows to take different
    learning paths based on a certain condition status """

    display_name = String(display_name="Display Name",
                          scope=Scope.settings,
                          default="Flow Control")

    action = String(display_name="Action",
                    help="Select the action to be performed "
                    "when the condition is met",
                    scope=Scope.content,
                    default="display_message",
                    values_provider=_actions_generator)

    condition = String(display_name="Flow control condition",
                       help="Select a conditon to evaluate",
                       scope=Scope.content,
                       default='single_problem',
                       values_provider=_conditions_generator)

    operator = String(display_name="Comparison type",
                      help="Select an operator for the condition",
                      scope=Scope.content,
                      default='eq',
                      values_provider=_operators_generator)

    ref_value = Integer(help="Enter the value to be used in "
                        "the comparison. (From 0 to 100)",
                        default=0,
                        scope=Scope.content,
                        display_name="Score percentage")

    tab_to = Integer(help="Number of unit tab to redirect to. (1, 2, 3...)",
                     default=1,
                     scope=Scope.content,
                     display_name="Tab to redirect to")

    target_url = String(help="URL to redirect to, supports relative "
                        "or absolute urls",
                        scope=Scope.content,
                        display_name="URL to redirect to")

    target_id = String(help="Unit identifier to redirect to (Location id)",
                       scope=Scope.content,
                       display_name="Unit identifier to redirect to")

    message = String(help="Message for the learners to view "
                     "when the condition is met",
                     scope=Scope.content,
                     default='',
                     display_name="Message",
                     multiline_editor='html')

    problem_id = String(help="Problem id to use for the condition.  (Not the "
                        "complete problem locator. Only the 32 characters "
                        "alfanumeric id. "
                        "Example: 618c5933b8b544e4a4cc103d3e508378)",
                        scope=Scope.content,
                        display_name="Problem id")

    list_of_problems = String(help="List of problems ids separated by commas "
                              "or line breaks. (Not the complete problem "
                              "locators. Only the 32 characters alfanumeric "
                              "ids. Example: 618c5933b8b544e4a4cc103d3e508378"
                              ", 905333bd98384911bcec2a94bc30155f). "
                              "The simple average score for all problems will "
                              "be used.",
                              scope=Scope.content,
                              display_name="List of problems",
                              multiline_editor=True,
                              resettable_editor=False)

    editable_fields = ('condition', 'problem_id', 'list_of_problems',
                       'operator', 'ref_value', 'action', 'tab_to',
                       'target_url', 'target_id', 'message')

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

        if data.tab_to <= 0:
            validation.add(
                ValidationMessage(
                    ValidationMessage.ERROR,
                    u"Tab to redirect to must be greater than zero"))

        if data.ref_value < 0 or data.ref_value > 100:
            validation.add(
                ValidationMessage(
                    ValidationMessage.ERROR, u"Score percentage field must "
                    u"be an integer number between 0 and 100"))

    def get_location_string(self, locator, is_draft=False):
        """  Returns the location string for one problem, given its id  """
        # pylint: disable=no-member
        course_prefix = 'course'
        resource = 'problem'
        course_url = self.course_id.to_deprecated_string()

        if is_draft:
            course_url = course_url.split(self.course_id.run)[0]
            prefix = 'i4x://'
            location_string = '{prefix}{couse_str}{type_id}/{locator}'.format(
                prefix=prefix,
                couse_str=course_url,
                type_id=resource,
                locator=locator)
        else:
            course_url = course_url.replace(course_prefix, '', 1)

            location_string = '{prefix}{couse_str}+{type}@{type_id}+{prefix}@{locator}'.format(
                prefix=self.course_id.BLOCK_PREFIX,
                couse_str=course_url,
                type=self.course_id.BLOCK_TYPE_PREFIX,
                type_id=resource,
                locator=locator)

        return location_string

    def get_condition_status(self):
        """  Returns the current condition status  """
        condition_reached = False
        problems = []

        if self.problem_id and self.condition == 'single_problem':
            # now split problem id by spaces or commas
            problems = re.split('\s*,*|\s*,\s*', self.problem_id)
            problems = filter(None, problems)
            problems = problems[:1]

        if self.list_of_problems and self.condition == 'average_problems':
            # now split list of problems id by spaces or commas
            problems = re.split('\s*,*|\s*,\s*', self.list_of_problems)
            problems = filter(None, problems)

        if problems:
            condition_reached = self.condition_on_problem_list(problems)

        return condition_reached

    def student_view(self, context=None):  # pylint: disable=unused-argument
        """  Returns a fragment for student view  """
        fragment = Fragment(u"<!-- This is the FlowCheckPointXblock -->")
        fragment.add_javascript(load("static/js/injection.js"))

        # helper variables
        # pylint: disable=no-member
        in_studio_runtime = hasattr(self.xmodule_runtime, 'is_author_mode')
        index_base = 1
        default_tab = 'tab_{}'.format(self.tab_to - index_base)

        fragment.initialize_js('FlowControlGoto',
                               json_args={
                                   "display_name": self.display_name,
                                   "default": default_tab,
                                   "default_tab_id": self.tab_to,
                                   "action": self.action,
                                   "target_url": self.target_url,
                                   "target_id": self.target_id,
                                   "message": self.message,
                                   "in_studio_runtime": in_studio_runtime
                               })

        return fragment

    @XBlock.json_handler
    def condition_status_handler(self, data, suffix=''):  # pylint: disable=unused-argument
        """  Returns the actual condition state  """

        return {'success': True, 'status': self.get_condition_status()}

    def author_view(self, context=None):  # pylint: disable=unused-argument, no-self-use
        """  Returns author view fragment on Studio """
        # creating xblock fragment
        # TO-DO display for studio with setting resume
        fragment = Fragment(u"<!-- This is the studio -->")
        fragment.add_javascript(load("static/js/injection.js"))
        fragment.initialize_js('StudioFlowControl')

        return fragment

    def studio_view(self, context=None):
        """  Returns studio view fragment """
        fragment = super(FlowCheckPointXblock,
                         self).studio_view(context=context)

        # We could also move this function to a different file
        fragment.add_javascript(load("static/js/injection.js"))
        fragment.initialize_js('EditFlowControl')

        return fragment

    def compare_scores(self, correct, total):
        """  Returns the result of comparison using custom operator """
        result = False
        if total:
            # getting percentage score for that section
            percentage = (correct / total) * 100

            if self.operator == 'eq':
                result = percentage == self.ref_value
            if self.operator == 'noeq':
                result = percentage != self.ref_value
            if self.operator == 'lte':
                result = percentage <= self.ref_value
            if self.operator == 'gte':
                result = percentage >= self.ref_value
            if self.operator == 'lt':
                result = percentage < self.ref_value
            if self.operator == 'gt':
                result = percentage > self.ref_value

        return result

    def are_all_not_null(self, problems_to_answer):
        """  Returns true when all problems have been answered """
        result = False
        all_problems_were_answered = n_all(problems_to_answer)
        if problems_to_answer and all_problems_were_answered:
            result = True
        return result

    def has_null(self, problems_to_answer):
        """  Returns true when at least one problem have not been answered """
        result = False
        all_problems_were_answered = n_all(problems_to_answer)
        if not problems_to_answer or not all_problems_were_answered:
            result = True
        return result

    def are_all_null(self, problems_to_answer):
        """  Returns true when all problems have not been answered """
        for element in problems_to_answer:
            if element is not None:
                return False
        return True

    SPECIAL_COMPARISON_DISPATCHER = {
        'all_not_null': are_all_not_null,
        'all_null': are_all_null,
        'has_null': has_null
    }

    def condition_on_problem_list(self, problems):
        """ Returns the score for a list of problems """
        # pylint: disable=no-member
        user_id = self.xmodule_runtime.user_id
        scores_client = ScoresClient(self.course_id, user_id)
        correct_neutral = {'correct': 0.0}
        total_neutral = {'total': 0.0}
        total = 0
        correct = 0

        def _get_usage_key(problem):

            loc = self.get_location_string(problem)
            try:
                uk = UsageKey.from_string(loc)
            except InvalidKeyError:
                uk = _get_draft_usage_key(problem)
            return uk

        def _get_draft_usage_key(problem):

            loc = self.get_location_string(problem, True)
            try:
                uk = UsageKey.from_string(loc)
                uk = uk.map_into_course(self.course_id)
            except InvalidKeyError:
                uk = None

            return uk

        def _to_reducible(score):
            correct_default = 0.0
            total_default = 1.0
            if not score.total:
                return {'correct': correct_default, 'total': total_default}
            else:
                return {'correct': score.correct, 'total': score.total}

        def _calculate_correct(first_score, second_score):
            correct = first_score['correct'] + second_score['correct']
            return {'correct': correct}

        def _calculate_total(first_score, second_score):
            total = first_score['total'] + second_score['total']
            return {'total': total}

        usages_keys = map(_get_usage_key, problems)
        scores_client.fetch_scores(usages_keys)
        scores = map(scores_client.get, usages_keys)
        scores = filter(None, scores)

        problems_to_answer = [score.total for score in scores]
        if self.operator in self.SPECIAL_COMPARISON_DISPATCHER.keys():
            evaluation = self.SPECIAL_COMPARISON_DISPATCHER[self.operator](
                self, problems_to_answer)

            return evaluation

        reducible_scores = map(_to_reducible, scores)
        correct = reduce(_calculate_correct, reducible_scores, correct_neutral)
        total = reduce(_calculate_total, reducible_scores, total_neutral)

        return self.compare_scores(correct['correct'], total['total'])
Exemplo n.º 14
0
class ProblemBlock(XBlock):
    """A generalized container of InputBlocks and Checkers.

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

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

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

        return block

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

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

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

        context = self.calc_context(context)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

                    <problem_demo>
                        <html_demo><p>What is $a+$b?</p></html_demo>
                        <textinput_demo name="sum_input" input_type="int" />
                        <equality_demo name="sum_checker" left="./sum_input/@student_input" right="$c" />
                        <script>
                            import random
                            a = random.randint(3, 5)
                            b = random.randint(2, 6)
                            c = a + b
                        </script>
                    </problem_demo>
                </vertical_demo>
             """),
        ]
class 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.º 16
0
class Goss92XBlock(ScorableXBlockMixin, XBlock):
    """
    XBlock checks if a certain URL returns what is expected
    """

    # Fields are defined on the class.  You can access them in your code as
    # self.<fieldname>.
    #package = __package__
    always_recalculate_grades = True

    score2 = Integer(
        default=0,
        scope=Scope.user_state,
        help="An indicator of success",
    )

    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 has_submitted_answer(self):
        """
        Returns True if the user has made a submission.
        """
        return self.fields['score2'].is_set_on(self)

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

    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()
        #score.raw_earned = 1/2
        self.score2 = 1  #score.raw_earned

    def get_score(self):
        """
        Return the problem's current score as raw values.
        """

        return Score(1, self.max_score())

    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(1, self.max_score())

    # TO-DO: change this view to display your data your own way.
    def student_view(self, context=None):
        """
        The primary view, shown to students
        when viewing courses.
        """
        user_service = self.runtime.service(self, 'user')
        xb_user = user_service.get_current_user()
        CURRENT = xb_user.opt_attrs.get('edx-platform.username')

        html = self.resource_string("static/html/gossxblock.html")
        frag = Fragment(html.format(self=self))

        res0 = textwrap.dedent("""
            <p id='goss_hidden'><span id="gosscurrent">{}</span></p>
        """).format(CURRENT)
        frag.add_content(SafeText(res0))

        HTMLURL = 'https://node-server.online/r/assets/x92.html'
        if sys.version_info.major >= 3:
            response = urlopen(HTMLURL)
            encoding = response.info().get_content_charset('utf-8')
            html_data = response.read().decode(encoding)
        else:
            html_data = urlopen(HTMLURL).read()

        res = textwrap.dedent(html_data)
        frag.add_content(SafeText(res))

        frag.add_css(self.resource_string("static/css/gossxblock.css"))
        frag.add_javascript(
            self.resource_string("static/js/src/goss92xblock.js"))
        frag.initialize_js('Goss92XBlock')
        return frag

    @XBlock.json_handler
    def set_score2(self, data, suffix=''):
        """
        An example handler, which increments the data.
        """
        # indicator is now 100...
        if data['key'] == 'hundred':
            self.score2 = 1
        else:
            self.score2 = 0

        self._publish_grade(Score(self.score2, self.max_score()))

        return {"score": self.score2}

    # TO-DO: change this to create the scenarios you'd like to see in the
    # workbench while developing your XBlock.
    @staticmethod
    def workbench_scenarios():
        """A canned scenario for display in the workbench."""
        return [
            ("goss92XBlock", """<problem/>
             """),
            ("Multiple goss92XBlock", """<vertical_demo>
                <goss92xblock/>
                <goss92xblock/>
                <goss92xblock/>
                </vertical_demo>
             """),
        ]
Exemplo n.º 17
0
class FirstMixin(XBlockMixin):
    """Test class for mixin ordering."""
    number = 1
    field = Integer(default=1)
Exemplo n.º 18
0
class LtiConsumerXBlock(StudioEditableXBlockMixin, XBlock):
    """
    This XBlock provides an LTI consumer interface for integrating
    third-party tools using the LTI specification.

    Except usual Xmodule structure it proceeds with OAuth signing.
    How it works::

    1. Get credentials from course settings.

    2.  There is minimal set of parameters need to be signed (presented for Vitalsource)::

            user_id
            oauth_callback
            lis_outcome_service_url
            lis_result_sourcedid
            launch_presentation_return_url
            lti_message_type
            lti_version
            roles
            *+ all custom parameters*

        These parameters should be encoded and signed by *OAuth1* together with
        `launch_url` and *POST* request type.

    3. Signing proceeds with client key/secret pair obtained from course settings.
        That pair should be obtained from LTI provider and set into course settings by course author.
        After that signature and other OAuth data are generated.

        OAuth data which is generated after signing is usual::

            oauth_callback
            oauth_nonce
            oauth_consumer_key
            oauth_signature_method
            oauth_timestamp
            oauth_version


    4. All that data is passed to form and sent to LTI provider server by browser via
        autosubmit via JavaScript.

        Form example::

            <form
                action="${launch_url}"
                name="ltiLaunchForm-${element_id}"
                class="ltiLaunchForm"
                method="post"
                target="ltiLaunchFrame-${element_id}"
                encType="application/x-www-form-urlencoded"
            >
                <input name="launch_presentation_return_url" value="" />
                <input name="lis_outcome_service_url" value="" />
                <input name="lis_result_sourcedid" value="" />
                <input name="lti_message_type" value="basic-lti-launch-request" />
                <input name="lti_version" value="LTI-1p0" />
                <input name="oauth_callback" value="about:blank" />
                <input name="oauth_consumer_key" value="${oauth_consumer_key}" />
                <input name="oauth_nonce" value="${oauth_nonce}" />
                <input name="oauth_signature_method" value="HMAC-SHA1" />
                <input name="oauth_timestamp" value="${oauth_timestamp}" />
                <input name="oauth_version" value="1.0" />
                <input name="user_id" value="${user_id}" />
                <input name="role" value="student" />
                <input name="oauth_signature" value="${oauth_signature}" />
                <input name="tool_consumer_info_product_family_code" value="openedx" />
                <input name="tool_consumer_instance_guid" value="${tool_consumer_instance_guid}" />

                <input name="custom_1" value="${custom_param_1_value}" />
                <input name="custom_2" value="${custom_param_2_value}" />
                <input name="custom_..." value="${custom_param_..._value}" />

                <input type="submit" value="Press to Launch" />
            </form>

    5. LTI provider has same secret key and it signs data string via *OAuth1* and compares signatures.

        If signatures are correct, LTI provider redirects iframe source to LTI tool web page,
        and LTI tool is rendered to iframe inside course.

        Otherwise error message from LTI provider is generated.
    """

    display_name = String(
        display_name=_("Display Name"),
        help=_(
            "Enter the name that students see for this component. "
            "Analytics reports may also use the display name to identify this component."
        ),
        scope=Scope.settings,
        default=_("LTI Consumer"),
    )
    description = String(
        display_name=_("LTI Application Information"),
        help=_(
            "Enter a description of the third party application. "
            "If requesting username and/or email, use this text box to inform users "
            "why their username and/or email will be forwarded to a third party application."
        ),
        default="",
        scope=Scope.settings
    )
    lti_id = String(
        display_name=_("LTI ID"),
        help=_(
            "Enter the LTI ID for the external LTI provider. "
            "This value must be the same LTI ID that you entered in the "
            "LTI Passports setting on the Advanced Settings page."
            "<br />See the {docs_anchor_open}edX LTI documentation{anchor_close} for more details on this setting."
        ).format(
            docs_anchor_open=DOCS_ANCHOR_TAG_OPEN,
            anchor_close="</a>"
        ),
        default='',
        scope=Scope.settings
    )
    launch_url = String(
        display_name=_("LTI URL"),
        help=_(
            "Enter the URL of the external tool that this component launches. "
            "This setting is only used when Hide External Tool is set to False."
            "<br />See the {docs_anchor_open}edX LTI documentation{anchor_close} for more details on this setting."
        ).format(
            docs_anchor_open=DOCS_ANCHOR_TAG_OPEN,
            anchor_close="</a>"
        ),
        default='',
        scope=Scope.settings
    )
    custom_parameters = List(
        display_name=_("Custom Parameters"),
        help=_(
            "Add the key/value pair for any custom parameters, such as the page your e-book should open to or "
            "the background color for this component. Ex. [\"page=1\", \"color=white\"]"
            "<br />See the {docs_anchor_open}edX LTI documentation{anchor_close} for more details on this setting."
        ).format(
            docs_anchor_open=DOCS_ANCHOR_TAG_OPEN,
            anchor_close="</a>"
        ),
        scope=Scope.settings
    )
    launch_target = String(
        display_name=_("LTI Launch Target"),
        help=_(
            "Select Inline if you want the LTI content to open in an IFrame in the current page. "
            "Select Modal if you want the LTI content to open in a modal window in the current page. "
            "Select New Window if you want the LTI content to open in a new browser window. "
            "This setting is only used when Hide External Tool is set to False."
        ),
        default=LaunchTarget.IFRAME.value,
        scope=Scope.settings,
        values=[
            {"display_name": LaunchTarget.IFRAME.display_name, "value": LaunchTarget.IFRAME.value},
            {"display_name": LaunchTarget.MODAL.display_name, "value": LaunchTarget.MODAL.value},
            {"display_name": LaunchTarget.NEW_WINDOW.display_name, "value": LaunchTarget.NEW_WINDOW.value},
        ],
    )
    button_text = String(
        display_name=_("Button Text"),
        help=_(
            "Enter the text on the button used to launch the third party application. "
            "This setting is only used when Hide External Tool is set to False and "
            "LTI Launch Target is set to Modal or New Window."
        ),
        default="",
        scope=Scope.settings
    )
    inline_height = Integer(
        display_name=_("Inline Height"),
        help=_(
            "Enter the desired pixel height of the iframe which will contain the LTI tool. "
            "This setting is only used when Hide External Tool is set to False and "
            "LTI Launch Target is set to Inline."
        ),
        default=800,
        scope=Scope.settings
    )
    modal_height = Integer(
        display_name=_("Modal Height"),
        help=_(
            "Enter the desired viewport percentage height of the modal overlay which will contain the LTI tool. "
            "This setting is only used when Hide External Tool is set to False and "
            "LTI Launch Target is set to Modal."
        ),
        default=80,
        scope=Scope.settings
    )
    modal_width = Integer(
        display_name=_("Modal Width"),
        help=_(
            "Enter the desired viewport percentage width of the modal overlay which will contain the LTI tool. "
            "This setting is only used when Hide External Tool is set to False and "
            "LTI Launch Target is set to Modal."
        ),
        default=80,
        scope=Scope.settings
    )
    has_score = Boolean(
        display_name=_("Scored"),
        help=_("Select True if this component will receive a numerical score from the external LTI system."),
        default=False,
        scope=Scope.settings
    )
    weight = Float(
        display_name="Weight",
        help=_(
            "Enter the number of points possible for this component.  "
            "The default value is 1.0.  "
            "This setting is only used when Scored is set to True."
        ),
        default=1.0,
        scope=Scope.settings,
        values={"min": 0},
    )
    module_score = Float(
        help=_("The score kept in the xblock KVS -- duplicate of the published score in django DB"),
        default=None,
        scope=Scope.user_state
    )
    score_comment = String(
        help=_("Comment as returned from grader, LTI2.0 spec"),
        default="",
        scope=Scope.user_state
    )
    hide_launch = Boolean(
        display_name=_("Hide External Tool"),
        help=_(
            "Select True if you want to use this component as a placeholder for syncing with an external grading  "
            "system rather than launch an external tool.  "
            "This setting hides the Launch button and any IFrames for this component."
        ),
        default=False,
        scope=Scope.settings
    )
    accept_grades_past_due = Boolean(
        display_name=_("Accept grades past deadline"),
        help=_("Select True to allow third party systems to post grades past the deadline."),
        default=True,
        scope=Scope.settings
    )
    # Users will be presented with a message indicating that their e-mail/username would be sent to a third
    # party application. When "Open in New Page" is not selected, the tool automatically appears without any
    # user action.
    ask_to_send_username = Boolean(
        display_name=_("Request user's username"),
        # Translators: This is used to request the user's username for a third party service.
        help=_("Select True to request the user's username."),
        default=False,
        scope=Scope.settings
    )
    ask_to_send_email = Boolean(
        display_name=_("Request user's email"),
        # Translators: This is used to request the user's email for a third party service.
        help=_("Select True to request the user's email address."),
        default=False,
        scope=Scope.settings
    )

    # StudioEditableXBlockMixin configuration of fields editable in Studio
    editable_fields = (
        'display_name', 'description', 'lti_id', 'launch_url', 'custom_parameters', 'launch_target', 'button_text',
        'inline_height', 'modal_height', 'modal_width', 'has_score', 'weight', 'hide_launch', 'accept_grades_past_due',
        'ask_to_send_username', 'ask_to_send_email'
    )

    def validate_field_data(self, validation, data):
        if not isinstance(data.custom_parameters, list):
            _ = self.runtime.service(self, "i18n").ugettext
            validation.add(ValidationMessage(ValidationMessage.ERROR, unicode(_("Custom Parameters must be a list"))))

    @property
    def descriptor(self):
        """
        Returns this XBlock object.

        This is for backwards compatibility with the XModule API.
        Some LMS code still assumes a descriptor attribute on the XBlock object.
        See courseware.module_render.rebind_noauth_module_to_user.
        """
        return self

    @property
    def context_id(self):
        """
        Return context_id.

        context_id is an opaque identifier that uniquely identifies the context (e.g., a course)
        that contains the link being launched.
        """
        return unicode(self.course_id)  # pylint: disable=no-member

    @property
    def role(self):
        """
        Get system user role and convert it to LTI role.
        """
        return ROLE_MAP.get(self.runtime.get_user_role(), u'Student')

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

    @property
    def lti_provider_key_secret(self):
        """
        Obtains client_key and client_secret credentials from current course.
        """
        for lti_passport in self.course.lti_passports:
            try:
                lti_id, key, secret = [i.strip() for i in lti_passport.split(':')]
            except ValueError:
                msg = self.ugettext('Could not parse LTI passport: {lti_passport}. Should be "id:key:secret" string.').\
                    format(lti_passport='{0!r}'.format(lti_passport))
                raise LtiError(msg)

            if lti_id == self.lti_id.strip():
                return key, secret

        return '', ''

    @property
    def user_id(self):
        """
        Returns the opaque anonymous_student_id for the current user.
        """
        user_id = self.runtime.anonymous_student_id
        if user_id is None:
            raise LtiError(self.ugettext("Could not get user id for current request"))
        return unicode(urllib.quote(user_id))

    @property
    def resource_link_id(self):
        """
        This is an opaque unique identifier that the LTI Tool Consumer guarantees will be unique
        within the Tool Consumer for every placement of the link.

        If the tool / activity is placed multiple times in the same context,
        each of those placements will be distinct.

        This value will also change if the item is exported from one system or
        context and imported into another system or context.

        resource_link_id is a required LTI launch parameter.

        Example:  u'edx.org-i4x-2-3-lti-31de800015cf4afb973356dbe81496df'

        Hostname, edx.org,
        makes resource_link_id change on import to another system.

        Last part of location, location.name - 31de800015cf4afb973356dbe81496df,
        is random hash, updated by course_id,
        this makes resource_link_id unique inside single course.

        First part of location is tag-org-course-category, i4x-2-3-lti.

        Location.name itself does not change on import to another course,
        but org and course_id change.

        So together with org and course_id in a form of
        i4x-2-3-lti-31de800015cf4afb973356dbe81496df this part of resource_link_id:
        makes resource_link_id to be unique among courses inside same system.
        """
        return unicode(urllib.quote(
            "{}-{}".format(self.runtime.hostname, self.location.html_id())  # pylint: disable=no-member
        ))

    @property
    def lis_result_sourcedid(self):
        """
        This field contains an identifier that indicates the LIS Result Identifier (if any)
        associated with this launch.  This field identifies a unique row and column within the
        TC gradebook.  This field is unique for every combination of context_id / resource_link_id / user_id.
        This value may change for a particular resource_link_id / user_id  from one launch to the next.
        The TP should only retain the most recent value for this field for a particular resource_link_id / user_id.
        This field is generally optional, but is required for grading.
        """
        return "{context}:{resource_link}:{user_id}".format(
            context=urllib.quote(self.context_id),
            resource_link=self.resource_link_id,
            user_id=self.user_id
        )

    @property
    def outcome_service_url(self):
        """
        Return URL for storing grades.

        To test LTI on sandbox we must use http scheme.

        While testing locally and on Jenkins, mock_lti_server use http.referer
        to obtain scheme, so it is ok to have http(s) anyway.

        The scheme logic is handled in lms/lib/xblock/runtime.py
        """
        return self.runtime.handler_url(self, "outcome_service_handler", thirdparty=True).rstrip('/?')

    @property
    def tool_consumer_info_product_family_code(self):
        """
        Return a code that identifies openedx as a tool consumer.

        In order to better assist tools in using extensions and also making their user interface 
        fit into the TC's user interface that they are being called from, each TC is encouraged 
        to include the this parameter.
        """
        return "openedx"

    @property
    def tool_consumer_instance_guid(self):
        """
        Return a code that identifies the instance openedx as a tool consumer.

        This is a unique identifier for the TC.  A common practice is to use the DNS of the organization 
        or the DNS of the TC instance.  If the organization has multiple TC instances, then the best practice 
        is to prefix the domain name with a locally unique identifier for the TC instance.

        In the single-tenancy case, the tool consumer data can be often be derived from the oauth_consumer_key.

        In a multi-tenancy case this can be used to differentiate between the multiple tenants within a 
        single installation of a Tool Consumer.

        This parameter is strongly recommended in systems capable of multi-tenancy.
        """
        return "{context}".format(
            context=urllib.quote(self.context_id)
        )

    @property
    def prefixed_custom_parameters(self):
        """
        Apply prefix to configured custom LTI parameters

        LTI provides a list of default parameters that might be passed as
        part of the POST data. These parameters should not be prefixed.
        Likewise, The creator of an LTI link can add custom key/value parameters
        to a launch which are to be included with the launch of the LTI link.
        In this case, we will automatically add `custom_` prefix before this parameters.
        See http://www.imsglobal.org/LTI/v1p1p1/ltiIMGv1p1p1.html#_Toc316828520
        """

        # parsing custom parameters to dict
        custom_parameters = {}
        if isinstance(self.custom_parameters, list):
            for custom_parameter in self.custom_parameters:
                try:
                    param_name, param_value = [p.strip() for p in custom_parameter.split('=', 1)]
                except ValueError:
                    _ = self.runtime.service(self, "i18n").ugettext
                    # pylint: disable=line-too-long
                    msg = self.ugettext('Could not parse custom parameter: {custom_parameter}. Should be "x=y" string.').\
                        format(custom_parameter="{0!r}".format(custom_parameter))
                    raise LtiError(msg)

                # LTI specs: 'custom_' should be prepended before each custom parameter, as pointed in link above.
                if param_name not in LTI_PARAMETERS:
                    param_name = 'custom_' + param_name

                custom_parameters[unicode(param_name)] = unicode(param_value)
        return custom_parameters

    @property
    def is_past_due(self):
        """
        Is it now past this problem's due date, including grace period?
        """
        due_date = self.due  # pylint: disable=no-member
        if self.graceperiod is not None and due_date:  # pylint: disable=no-member
            close_date = due_date + self.graceperiod  # pylint: disable=no-member
        else:
            close_date = due_date
        return close_date is not None and timezone.now() > close_date

    def student_view(self, context):
        """
        XBlock student view of this component.

        Makes a request to `lti_launch_handler` either
        in an iframe or in a new window depending on the
        configuration of the instance of this XBlock

        Arguments:
            context (dict): XBlock context

        Returns:
            xblock.fragment.Fragment: XBlock HTML fragment
        """
        fragment = Fragment()
        loader = ResourceLoader(__name__)
        context.update(self._get_context_for_template())
        fragment.add_content(loader.render_mako_template('/templates/html/student.html', context))
        fragment.add_css(loader.load_unicode('static/css/student.css'))
        fragment.add_javascript(loader.load_unicode('static/js/xblock_lti_consumer.js'))
        fragment.initialize_js('LtiConsumerXBlock')
        return fragment

    @XBlock.handler
    def lti_launch_handler(self, request, suffix=''):  # pylint: disable=unused-argument
        """
        XBlock handler for launching the LTI provider.

        Displays a form which is submitted via Javascript
        to send the LTI launch POST request to the LTI
        provider.

        Arguments:
            request (xblock.django.request.DjangoWebobRequest): Request object for current HTTP request
            suffix (unicode): Request path after "lti_launch_handler/"

        Returns:
            webob.response: HTML LTI launch form
        """
        lti_consumer = LtiConsumer(self)
        lti_parameters = lti_consumer.get_signed_lti_parameters()
        loader = ResourceLoader(__name__)
        context = self._get_context_for_template()
        context.update({'lti_parameters': lti_parameters})
        template = loader.render_mako_template('/templates/html/lti_launch.html', context)
        return Response(template, content_type='text/html')

    @XBlock.handler
    def outcome_service_handler(self, request, suffix=''):  # pylint: disable=unused-argument
        """
        XBlock handler for LTI Outcome Service requests.

        Instantiates an `OutcomeService` instance to handle
        requests made by LTI providers to update a user's grade
        for this component.

        For details about the LTI Outcome Service see:
        https://www.imsglobal.org/specs/ltiomv1p0

        Arguments:
            request (xblock.django.request.DjangoWebobRequest): Request object for current HTTP request
            suffix (unicode): Request path after "outcome_service_handler/"

        Returns:
            webob.response: XML Outcome Service response
        """
        outcome_service = OutcomeService(self)
        return Response(outcome_service.handle_request(request), content_type="application/xml")

    @XBlock.handler
    def result_service_handler(self, request, suffix=''):
        """
        Handler function for LTI 2.0 JSON/REST result service.

        See http://www.imsglobal.org/lti/ltiv2p0/uml/purl.imsglobal.org/vocab/lis/v2/outcomes/Result/service.html
        An example JSON object:
        {
         "@context" : "http://purl.imsglobal.org/ctx/lis/v2/Result",
         "@type" : "Result",
         "resultScore" : 0.83,
         "comment" : "This is exceptional work."
        }
        For PUTs, the content type must be "application/vnd.ims.lis.v2.result+json".
        We use the "suffix" parameter to parse out the user from the end of the URL.  An example endpoint url is
        http://localhost:8000/courses/org/num/run/xblock/i4x:;_;_org;_num;_lti;_GUID/handler_noauth/lti_2_0_result_rest_handler/user/<anon_id>
        so suffix is of the form "user/<anon_id>"
        Failures result in 401, 404, or 500s without any body.  Successes result in 200.  Again see
        http://www.imsglobal.org/lti/ltiv2p0/uml/purl.imsglobal.org/vocab/lis/v2/outcomes/Result/service.html
        (Note: this prevents good debug messages for the client, so we might want to change this, or the spec)

        Arguments:
            request (xblock.django.request.DjangoWebobRequest):  Request object for current HTTP request
            suffix (unicode):  request path after "lti_2_0_result_rest_handler/".  expected to be "user/<anon_id>"

        Returns:
            webob.response:  response to this request.  See above for details.
        """
        lti_consumer = LtiConsumer(self)

        if self.runtime.debug:
            lti_provider_key, lti_provider_secret = self.lti_provider_key_secret
            log_authorization_header(request, lti_provider_key, lti_provider_secret)

        if not self.accept_grades_past_due and self.is_past_due:
            return Response(status=404)  # have to do 404 due to spec, but 400 is better, with error msg in body

        try:
            anon_id = parse_handler_suffix(suffix)
        except LtiError:
            return Response(status=404)  # 404 because a part of the URL (denoting the anon user id) is invalid
        try:
            lti_consumer.verify_result_headers(request, verify_content_type=True)
        except LtiError:
            return Response(status=401)  # Unauthorized in this case.  401 is right

        user = self.runtime.get_real_user(anon_id)
        if not user:  # that means we can't save to database, as we do not have real user id.
            msg = _("[LTI]: Real user not found against anon_id: {}").format(anon_id)
            log.info(msg)
            return Response(status=404)  # have to do 404 due to spec, but 400 is better, with error msg in body

        try:
            # Call the appropriate LtiConsumer method
            args = []
            if request.method == 'PUT':
                # Request body should be passed as an argument
                # to result handler method on PUT
                args.append(request.body)
            response_body = getattr(lti_consumer, "{}_result".format(request.method.lower()))(user, *args)
        except (AttributeError, LtiError):
            return Response(status=404)

        return Response(
            json.dumps(response_body),
            content_type=LtiConsumer.CONTENT_TYPE_RESULT_JSON,
        )

    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 if self.has_score else None

    def clear_user_module_score(self, user):
        """
        Clears the module user state, including grades and comments, and also scoring in db's courseware_studentmodule

        Arguments:
            user (django.contrib.auth.models.User):  Actual user whose module state is to be cleared

        Returns:
            nothing
        """
        self.set_user_module_score(user, None, None)

    def set_user_module_score(self, user, score, max_score, comment=u''):
        """
        Sets the module user state, including grades and comments, and also scoring in db's courseware_studentmodule

        Arguments:
            user (django.contrib.auth.models.User):  Actual user whose module state is to be set
            score (float):  user's numeric score to set.  Must be in the range [0.0, 1.0]
            max_score (float):  max score that could have been achieved on this module
            comment (unicode):  comments provided by the grader as feedback to the student

        Returns:
            nothing
        """
        if score is not None and max_score is not None:
            scaled_score = score * max_score
        else:
            scaled_score = None

        self.runtime.rebind_noauth_module_to_user(self, user)

        # have to publish for the progress page...
        self.runtime.publish(
            self,
            'grade',
            {
                'value': scaled_score,
                'max_value': max_score,
                'user_id': user.id,
            },
        )
        self.module_score = scaled_score
        self.score_comment = comment

    def _get_context_for_template(self):
        """
        Returns the context dict for LTI templates.

        Arguments:
            None

        Returns:
            dict: Context variables for templates
        """

        # use bleach defaults. see https://github.com/jsocol/bleach/blob/master/bleach/__init__.py
        # ALLOWED_TAGS are
        # ['a', 'abbr', 'acronym', 'b', 'blockquote', 'code', 'em', 'i', 'li', 'ol',  'strong', 'ul']
        #
        # ALLOWED_ATTRIBUTES are
        #     'a': ['href', 'title'],
        #     'abbr': ['title'],
        #     'acronym': ['title'],
        #
        # This lets all plaintext through.
        sanitized_comment = bleach.clean(self.score_comment)

        return {
            'launch_url': self.launch_url.strip(),
            'element_id': self.location.html_id(),  # pylint: disable=no-member
            'element_class': self.category,  # pylint: disable=no-member
            'launch_target': self.launch_target,
            'display_name': self.ugettext(self.display_name),
            'form_url': self.runtime.handler_url(self, 'lti_launch_handler').rstrip('/?'),
            'hide_launch': self.hide_launch,
            'has_score': self.has_score,
            'weight': self.weight,
            'module_score': self.module_score,
            'comment': self.ugettext(sanitized_comment),
            'description': self.description,
            'ask_to_send_username': self.ask_to_send_username,
            'ask_to_send_email': self.ask_to_send_email,
            'button_text': self.ugettext(self.button_text),
            'inline_height': self.inline_height,
            'modal_vertical_offset': self._get_modal_position_offset(self.modal_height),
            'modal_horizontal_offset': self._get_modal_position_offset(self.modal_width),
            'modal_width': self.modal_width,
            'accept_grades_past_due': self.accept_grades_past_due,
        }

    def _get_modal_position_offset(self, viewport_percentage):
        """
        Returns the css position offset to apply to the modal window
        element when launch_target is modal. This enables us to position
        the modal window as a percentage of the viewport dimensions.

        Arguments:
            viewport_percentage (int): The percentage of the viewport that the modal should occupy

        Returns:
            float: The css position offset to apply to the modal window
        """
        return (100 - viewport_percentage) / 2
Exemplo n.º 19
0
class ThirdMixin(XBlockMixin):
    """Test class for mixin ordering."""
    field = Integer(default=3)
Exemplo n.º 20
0
class TakeOneXBlock(XBlock):
    """
    TO-DO: document what your XBlock does.
    """

    # Fields are defined on the class.  You can access them in your code as
    # self.<fieldname>.

    # TO-DO: delete count, and define your own fields.
    display_name = String(default="Take ONE",
                          scope=Scope.content,
                          help="edX display name for this block")
    count = Integer(
        default=0,
        scope=Scope.user_state,
        help="A simple counter, to show something happening",
    )

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

    # TO-DO: change this view to display your data your own way.
    def student_view(self, context=None):
        """
        The primary view of the TakeOneXBlock, shown to students
        when viewing courses.
        """
        html = self.resource_string("static/html/takeone.html")
        frag = Fragment(html.format(self=self))
        frag.add_css(self.resource_string("static/css/takeone.css"))
        frag.add_javascript(self.resource_string("static/js/src/takeone.js"))
        frag.initialize_js('TakeOneXBlock')
        return frag

    # TO-DO: change this handler to perform your own actions.  You may need more
    # than one handler, or you may not need any handlers at all.
    @XBlock.json_handler
    def increment_count(self, data, suffix=''):
        """
        An example handler, which increments the data.
        """
        # Just to show data coming in...
        assert data['hello'] == 'world'

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

    # TO-DO: change this to create the scenarios you'd like to see in the
    # workbench while developing your XBlock.
    @staticmethod
    def workbench_scenarios():
        """A canned scenario for display in the workbench."""
        return [
            ("TakeOneXBlock", """<takeone/>
             """),
            ("TakeOne & TakeTwo", """<vertical_demo>
                <takeone/>
                <taketwo/>
                </vertical_demo>
             """),
        ]
from xblock.core import XBlock
from xblock.fields import Integer, Scope
from xblock.fragment import Fragment


class {{cookiecutter.class_name}}(XBlock):
    """
    TO-DO: document what your XBlock does.
    """

    # Fields are defined on the class.  You can access them in your code as
    # self.<fieldname>.

    # TO-DO: delete count, and define your own fields.
    count = Integer(
        default=0, scope=Scope.user_state,
        help="A simple counter, to show something happening",
    )

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

    # TO-DO: change this view to display your data your own way.
    def student_view(self, context=None):
        """
        The primary view of the {{cookiecutter.class_name}}, shown to students
        when viewing courses.
        """
        html = self.resource_string("static/html/{{cookiecutter.short_name|lower}}.html")
        frag = Fragment(html.format(self=self))
Exemplo n.º 22
0
class PureXBlock(XBlock):
    """Class for testing pure XBlocks."""

    has_children = True
    field1 = String(default="something", scope=Scope.user_state)
    field2 = Integer(scope=Scope.user_state)
Exemplo n.º 23
0
class IF_P_OR_Q_THEN_R_XBLOCKCLASS(XBlock):
    """
    A XBlock providing CTAT tutors.
    """

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

    ### Grading variables
    has_score = Boolean(default=True, scope=Scope.content)
    icon_class = String(default="problem", scope=Scope.content)
    score = Integer(help="Current count of correctly completed student steps",
                    scope=Scope.user_state,
                    default=0)
    max_problem_steps = Integer(help="Total number of steps",
                                scope=Scope.user_state,
                                default=1)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        return {'result': 'success'}

    # -------------------------------------------------------------------
    # TO-DO: change this to create the scenarios you'd like to see in the
    # workbench while developing your XBlock.
    # -------------------------------------------------------------------
    @staticmethod
    def workbench_scenarios():
        return [
            ("IF_P_OR_Q_THEN_R_XBLOCKCLASS", """<vertical_demo>
                <if_p_or_q_then_r_xblock width="" height=""/>
                </vertical_demo>
             """),
        ]
Exemplo n.º 24
0
class PollBase(XBlock, ResourceMixin, PublishEventMixin):
    """
    Base class for Poll-like XBlocks.
    """
    event_namespace = 'xblock.pollbase'
    private_results = Boolean(
        default=False,
        help=_("Whether or not to display results to the user."))
    max_submissions = Integer(
        default=1,
        help=_("The maximum number of times a user may send a submission."))
    submissions_count = Integer(
        default=0,
        help=_("Number of times the user has sent a submission."),
        scope=Scope.user_state)
    feedback = String(default='',
                      help=_("Text to display after the user votes."))

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

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

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

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

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

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

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

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

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

        return items

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

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

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

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

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

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

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

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

        We would prefer to explicitly call an API function on single image URLs, but such a function
        is not exposed by the LMS API, so we have to fall back to this slightly hacky implementation.
        """
        @cls.json_handler
        @functools.wraps(func)
        def wrapper(self, request_json, suffix=''):
            response = json.dumps(func(self, request_json, suffix))
            response = replace_static_urls(response,
                                           course_id=self.runtime.course_id)
            return Response(response, content_type='application/json')

        if HAS_STATIC_REPLACE:
            # Only use URL translation if it is available
            return wrapper
        # Otherwise fall back to a standard JSON handler
        return cls.json_handler(func)
Exemplo n.º 25
0
class CourseFields(object):
    lti_passports = List(
        display_name=_("LTI Passports"),
        help=
        _("Enter the passports for course LTI tools in the following format: \"id:client_key:client_secret\"."
          ),
        scope=Scope.settings)
    textbooks = TextbookList(
        help="List of pairs of (title, url) for textbooks used in this course",
        default=[],
        scope=Scope.content)

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

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

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

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

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

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

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

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

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

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

    invitation_only = Boolean(
        display_name=_("Invitation Only"),
        help=
        "Whether to restrict enrollment to invitation by the course staff.",
        default=False,
        scope=Scope.settings)
class 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_django_template(
            CALENDAR_EDIT_TEMPLATE,
            {
                'self': self,
                'defaultName': default_name,
                'defaultID': self.fields['calendar_id']._default  # pylint: disable=protected-access,unsubscriptable-object
            },
            i18n_service=self.runtime.service(self, "i18n"),
        ))
        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>")]
Exemplo n.º 27
0
class MentoringWithExplicitStepsBlock(BaseMentoringBlock, StudioContainerWithNestedXBlocksMixin,
                                      I18NService):
    """
    An XBlock providing mentoring capabilities with explicit steps
    """
    USER_STATE_FIELDS = ['num_attempts']

    # Content
    extended_feedback = Boolean(
        display_name=_("Extended feedback"),
        help=_("Show extended feedback when all attempts are used up?"),
        default=False,
        Scope=Scope.content
    )

    # Settings
    display_name = String(
        display_name=_("Title (display name)"),
        help=_("Title to display"),
        default=_("Step Builder"),
        scope=Scope.settings
    )

    # User state
    active_step = Integer(
        # Keep track of the student progress.
        default=0,
        scope=Scope.user_state,
        enforce_type=True
    )

    editable_fields = ('display_name', 'max_attempts', 'extended_feedback', 'weight')

    def build_user_state_data(self, context=None):
        user_state_data = super(MentoringWithExplicitStepsBlock, self).build_user_state_data()
        user_state_data['active_step'] = self.active_step_safe
        user_state_data['score_summary'] = self.get_score_summary()
        return user_state_data

    @lazy
    def question_ids(self):
        """
        Get the usage_ids of all of this XBlock's children that are "Questions".
        """
        return list(chain.from_iterable(self.runtime.get_block(step_id).step_ids for step_id in self.step_ids))

    @lazy
    def questions(self):
        """
        Get all questions associated with this block.
        """
        return [self.runtime.get_block(question_id) for question_id in self.question_ids]

    @property
    def active_step_safe(self):
        """
        Get self.active_step and double-check that it is a valid value.
        The stored value could be invalid if this block has been edited and new steps were
        added/deleted.
        """
        active_step = self.active_step
        if 0 <= active_step < len(self.step_ids):
            return active_step
        if active_step == -1 and self.has_review_step:
            return active_step  # -1 indicates the review step
        return 0

    def get_active_step(self):
        """ Get the active step as an instantiated XBlock """
        block = self.runtime.get_block(self.step_ids[self.active_step_safe])
        if block is None:
            log.error("Unable to load step builder step child %s", self.step_ids[self.active_step_safe])
        return block

    @lazy
    def step_ids(self):
        """
        Get the usage_ids of all of this XBlock's children that are steps.
        """
        from .step import MentoringStepBlock  # Import here to avoid circular dependency
        return [
            _normalize_id(child_id) for child_id in self.children if
            child_isinstance(self, child_id, MentoringStepBlock)
        ]

    @lazy
    def steps(self):
        """
        Get the step children of this block.
        """
        return [self.runtime.get_block(step_id) for step_id in self.step_ids]

    def get_question_number(self, question_name):
        question_names = [q.name for q in self.questions]
        return question_names.index(question_name) + 1

    def answer_mapper(self, answer_status):
        steps = self.steps
        answer_map = []
        for step in steps:
            for answer in step.student_results:
                if answer[1]['status'] == answer_status:
                    answer_map.append({
                        'id': answer[0],
                        'details': answer[1],
                        'step': step.step_number,
                        'number': self.get_question_number(answer[0]),
                    })
        return answer_map

    @property
    def has_review_step(self):
        return any(child_isinstance(self, child_id, ReviewStepBlock) for child_id in self.children)

    @property
    def review_step(self):
        """ Get the Review Step XBlock child, if any. Otherwise returns None """
        for step_id in self.children:
            if child_isinstance(self, step_id, ReviewStepBlock):
                return self.runtime.get_block(step_id)

    @property
    def score(self):
        questions = self.questions
        total_child_weight = sum(float(question.weight) for question in questions)
        if total_child_weight == 0:
            return Score(0, 0, [], [], [])
        steps = self.steps
        questions_map = {question.name: question for question in questions}
        points_earned = 0
        for step in steps:
            for question_name, question_results in step.student_results:
                question = questions_map.get(question_name)
                if question:  # Under what conditions would this evaluate to False?
                    points_earned += question_results['score'] * question.weight
        score = Decimal(points_earned) / Decimal(total_child_weight)
        correct = self.answer_mapper(CORRECT)
        incorrect = self.answer_mapper(INCORRECT)
        partially_correct = self.answer_mapper(PARTIAL)

        return Score(
            float(score),
            int(Decimal(score * 100).quantize(Decimal('1.'), rounding=ROUND_HALF_UP)),
            correct,
            incorrect,
            partially_correct
        )

    @property
    def complete(self):
        return not self.score.incorrect and not self.score.partially_correct

    @property
    def review_tips(self):
        """ Get review tips, shown for wrong answers. """
        if self.max_attempts > 0 and self.num_attempts >= self.max_attempts:
            # Review tips are only shown if the student is allowed to try again.
            return []
        review_tips = []
        status_cache = dict()
        steps = self.steps
        for step in steps:
            status_cache.update(dict(step.student_results))
        for question in self.questions:
            result = status_cache.get(question.name)
            if result and result.get('status') != 'correct':
                # The student got this wrong. Check if there is a review tip to show.
                tip_html = question.get_review_tip()
                if tip_html:
                    if getattr(self.runtime, 'replace_jump_to_id_urls', None) is not None:
                        tip_html = self.runtime.replace_jump_to_id_urls(tip_html)
                    review_tips.append(tip_html)
        return review_tips

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

    @XBlock.supports("multi_device")  # Mark as mobile-friendly
    def student_view(self, context):
        fragment = Fragment()
        children_contents = []

        context = context or {}
        context['hide_prev_answer'] = True  # For Step Builder, we don't show the users' old answers when they try again
        context['score_summary'] = self.get_score_summary()
        for child_id in self.children:
            child = self.runtime.get_block(child_id)
            if child is None:  # child should not be None but it can happen due to bugs or permission issues
                child_content = u"<p>[{}]</p>".format(self._(u"Error: Unable to load child component."))
            else:
                child_fragment = self._render_child_fragment(child, context, view='mentoring_view')
                fragment.add_frag_resources(child_fragment)
                child_content = child_fragment.content
            children_contents.append(child_content)

        fragment.add_content(loader.render_django_template('templates/html/mentoring_with_steps.html', {
            'self': self,
            'title': self.display_name,
            'show_title': self.show_title,
            'children_contents': children_contents,
        }, i18n_service=self.i18n_service))
        fragment.add_css_url(self.runtime.local_resource_url(self, 'public/css/problem-builder.css'))
        fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/vendor/underscore-min.js'))
        fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/step_util.js'))
        fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/mentoring_with_steps.js'))

        fragment.initialize_js('MentoringWithStepsBlock', {
            'show_extended_feedback': self.show_extended_feedback(),
        })

        return fragment

    @property
    def allowed_nested_blocks(self):
        """
        Returns a list of allowed nested XBlocks. Each item can be either
        * An XBlock class
        * A NestedXBlockSpec

        If XBlock class is used it is assumed that this XBlock is enabled and allows multiple instances.
        NestedXBlockSpec allows explicitly setting disabled/enabled state, disabled reason (if any) and single/multiple
        instances
        """
        # Import here to avoid circular dependency
        from .step import MentoringStepBlock
        return [
            MentoringStepBlock,
            NestedXBlockSpec(ReviewStepBlock, single_instance=True),
        ]

    @XBlock.json_handler
    def submit(self, data, suffix=None):
        """
        Called when the user has submitted the answer[s] for the current step.
        """
        # First verify that active_step is correct:
        if data.get("active_step") != self.active_step_safe:
            raise JsonHandlerError(400, "Invalid Step. Refresh the page and try again.")

        # The step child will process the data:
        step_block = self.get_active_step()
        if not step_block:
            raise JsonHandlerError(500, "Unable to load the current step block.")
        response_data = step_block.submit(data)

        # Update the active step:
        new_value = self.active_step_safe + 1
        if new_value < len(self.step_ids):
            self.active_step = new_value
        elif new_value == len(self.step_ids):
            # The user just completed the final step.
            # Update the number of attempts:
            self.num_attempts += 1
            # Do we need to render a review (summary of the user's score):
            if self.has_review_step:
                self.active_step = -1
                response_data['review_html'] = self.runtime.render(self.review_step, "mentoring_view", {
                    'score_summary': self.get_score_summary(),
                }).content
            response_data['num_attempts'] = self.num_attempts
            # And publish the score:
            score = self.score
            grade_data = {
                'value': score.raw,
                'max_value': self.max_score(),
            }
            self.runtime.publish(self, 'grade', grade_data)

        response_data['active_step'] = self.active_step
        return response_data

    def get_score_summary(self):
        if self.num_attempts == 0:
            return {}
        score = self.score
        return {
            'score': score.percentage,
            'correct_answers': len(score.correct),
            'incorrect_answers': len(score.incorrect),
            'partially_correct_answers': len(score.partially_correct),
            'correct': score.correct,
            'incorrect': score.incorrect,
            'partial': score.partially_correct,
            'complete': self.complete,
            'max_attempts_reached': self.max_attempts_reached,
            'show_extended_review': self.show_extended_feedback(),
            'review_tips': self.review_tips,
        }

    @XBlock.json_handler
    def get_num_attempts(self, data, suffix):
        return {
            'num_attempts': self.num_attempts
        }

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

        step_blocks = [self.runtime.get_block(child_id) for child_id in self.step_ids]

        for step in step_blocks:
            step.reset()

        return {
            'active_step': self.active_step
        }

    def author_preview_view(self, context):
        return self.student_view(context)

    def author_edit_view(self, context):
        """
        Add some HTML to the author view that allows authors to add child blocks.
        """
        fragment = super(MentoringWithExplicitStepsBlock, self).author_edit_view(context)
        fragment.add_content(loader.render_django_template('templates/html/mentoring_url_name.html', {
            "url_name": self.url_name
        }))
        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/problem-builder-edit.css'))
        fragment.add_css_url(self.runtime.local_resource_url(self, 'public/css/problem-builder-tinymce-content.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/container_edit.js'))
        fragment.initialize_js('ProblemBuilderContainerEdit')
        return fragment

    def student_view_data(self, context=None):
        components = []

        for child_id in self.children:
            child = self.runtime.get_block(child_id)
            if hasattr(child, 'student_view_data'):
                components.append(child.student_view_data(context))

        return {
            'title': self.display_name,
            'block_id': six.text_type(self.scope_ids.usage_id),
            'display_name': self.display_name,
            'show_title': self.show_title,
            'weight': self.weight,
            'extended_feedback': self.extended_feedback,
            'max_attempts': self.max_attempts,
            'components': components,
        }
Exemplo n.º 28
0
def test_values_dict():
    # Test that the format expected for integers is allowed
    test_field = Integer(values={"min": 1, "max": 100})
    assert_equals({"min": 1, "max": 100}, test_field.values)
Exemplo n.º 29
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.º 30
0
class TestIntegerXblock(XBlock):
    """
    XBlock with an integer field, for testing.
    """
    __test__ = False
    counter = Integer(scope=Scope.content)
Exemplo n.º 31
0
class FieldTester(XBlock):
    """Test XBlock for field access testing"""
    field_a = Integer(scope=Scope.settings)
    field_b = Integer(scope=Scope.content, default=10)
    field_c = Integer(scope=Scope.user_state, default=42)
Exemplo n.º 32
0
class ProctoringFields(object):
    """
    Fields that are specific to Proctored or Timed Exams
    """
    is_time_limited = Boolean(
        display_name=_("Is Time Limited"),
        help=_(
            "This setting indicates whether students have a limited time"
            " to view or interact with this courseware component."
        ),
        default=False,
        scope=Scope.settings,
    )

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

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

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

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

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

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

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

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

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

    @is_proctored_exam.setter
    def is_proctored_exam(self, value):
        """ Alias the is_proctored_enabled field to the more legible is_proctored_exam """
        self.is_proctored_enabled = value