コード例 #1
0
ファイル: lms_mixin.py プロジェクト: edxblocks/xblock-sqli
class LmsCompatibilityMixin(object):
    """
    Extra fields and methods used by LMS/Studio.
    """
    # Studio the default value for this field to show this XBlock
    # in the list of "Advanced Components"
    display_name = String(
        default="SQL Injection capture-the-flag", scope=Scope.settings,
        help="Display name"
    )

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

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

    weight = Float(
        display_name="Problem Weight",
        help="Defines the number of points this problem is worth.",
        values={"min": 0, "step": .1},
        default=1.0,
        scope=Scope.settings
    )

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

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

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

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

    def max_score(self):
        """The maximum raw score of our problem.
        """
        return self.weight
コード例 #2
0
class EditableXBlock(StudioEditableXBlockMixin, XBlock):
    """
    A basic Studio-editable XBlock (for use in tests)
    """
    CATEGORY = "editable"
    STUDIO_LABEL = "Editable Block"

    color = String(default="red")
    count = Integer(default=42)
    comment = String(default="")
    date = DateTime(default=datetime.datetime(2014, 5, 14, tzinfo=pytz.UTC))
    editable_fields = ('color', 'count', 'comment', 'date')

    def student_view(self, context):
        return Fragment()

    def validate_field_data(self, validation, data):
        """
        A validation method to check that 'count' is positive and prevent
        swearing in the 'comment' field.
        """
        if data.count < 0:
            validation.add(
                ValidationMessage(ValidationMessage.ERROR,
                                  u"Count cannot be negative"))
        if "damn" in data.comment.lower():
            validation.add(
                ValidationMessage(ValidationMessage.ERROR,
                                  u"No swearing allowed"))
コード例 #3
0
class UserStateFieldsMixin:
    answer_1 = String(name="state1", scope=Scope.user_state)
    answer_2 = Boolean(name="state2", scope=Scope.user_state)

    preference_1 = String(name="pref1", scope=Scope.preferences)
    preference_2 = Integer(name="pref2", scope=Scope.preferences)

    user_info_1 = String(name="info1", scope=Scope.user_info)
    user_info_2 = DateTime(name="info2", scope=Scope.user_info)
コード例 #4
0
class ShowAnswerXBlock(ShowAnswerXBlockMixin, XBlock):
    """
    A basic ShowAnswer XBlock implementation (for use in tests)
    """
    CATEGORY = 'showanswer'
    STUDIO_LABEL = 'Show Answers'

    color = String(default="red")
    count = Integer(default=42)
    comment = String(default="")
    date = DateTime(default=datetime.datetime(2014, 5, 14, tzinfo=pytz.UTC))
    editable_fields = ('color', 'count', 'comment', 'date')

    def student_view(self, context):
        return Fragment()
コード例 #5
0
class DoneXBlock(XBlock):
    """
    Show a toggle which lets students mark things as done.
    """

    # Fields are defined on the class.  You can access them in your code as
    # self.<fieldname>.
    done = Boolean(scope=Scope.user_state,
                   help="Is the student done?",
                   default=False)

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

    has_score = True

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

    @XBlock.json_handler
    def toggle_button(self, data, suffix=''):
        self.done = data['done']
        if data['done']:
            grade = 1
        else:
            grade = 0

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

    def student_view(self, context=None):
        """
        The primary view of the DoneXBlock, shown to students
        when viewing courses.
        """
        html = self.resource_string("static/html/done.html")
        frag = Fragment(html)  #.format(uid=self.scope_ids.usage_id))
        frag.add_css_url(
            "//ajax.googleapis.com/ajax/libs/jqueryui/1.10.4/themes/smoothness/jquery-ui.css"
        )
        #frag.add_javascript_url("//ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js")
        frag.add_javascript_url(
            "//ajax.googleapis.com/ajax/libs/jqueryui/1.10.4/jquery-ui.min.js")
        frag.add_css(self.resource_string("static/css/done.css"))
        grow_left = 1
        grow_right = 1
        if self.align.lower() == "left":
            grow_left = 0
        if self.align.lower() == "right":
            grow_right = 0
        frag.add_css(
            ".done_left_spacer {{ flex-grow:{l}; }} .done_right_spacer {{ flex-grow:{r}; }}"
            .format(r=grow_right, l=grow_left))
        frag.add_javascript(self.resource_string("static/js/src/done.js"))
        frag.initialize_js("DoneXBlock", {'state': self.done})
        return frag

    # TO-DO: change this to create the scenarios you'd like to see in the
    # workbench while developing your XBlock.
    @staticmethod
    def workbench_scenarios():
        """A canned scenario for display in the workbench."""
        return [
            ("DoneXBlock", """<vertical_demo>
                  <done align="left"> </done>
                  <done align="left"> </done>
                </vertical_demo>
             """),
        ]

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

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

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

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

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

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

    def max_score(self):
        """The maximum raw score of our problem.
        """
        return 1
コード例 #6
0
ファイル: test_mixins.py プロジェクト: m3rryqold/XBlock
    class TestXBlockWithDateTime(XBlock):
        """ XBlock for DateTime fields export """
        etree_node_tag = 'test_xblock_with_datetime'

        datetime = DateTime(default=None)
コード例 #7
0
class GroupActivityXBlock(CommonMixinCollection, DashboardXBlockMixin,
                          XBlockWithPreviewMixin, ActivityNotificationsMixin,
                          XBlock):
    """
    XBlock providing a group activity project for a group of students to collaborate upon
    """
    display_name = String(
        display_name=_(u"Display Name"),
        help=
        _(u"This name appears in the horizontal navigation at the top of the page."
          ),
        scope=Scope.settings,
        default=_(u"Group Project Activity"))

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

    group_reviews_required_count = Integer(
        display_name=_(u"Reviews Required Minimum"),
        help=_(
            u"The minimum number of group-reviews that should be applied to a set of submissions "
            u"(set to 0 to be 'TA Graded')"),
        scope=Scope.settings,
        default=3)

    user_review_count = Integer(
        display_name=_(u"User Reviews Required Minimum"),
        help=
        _(u"The minimum number of other-group reviews that an individual user should perform"
          ),
        scope=Scope.settings,
        default=1)

    due_date = DateTime(display_name=_(u"Due date"),
                        help=_(u"Activity due date"),
                        has_score=Scope.settings,
                        default=None)

    CATEGORY = "gp-v2-activity"
    STUDIO_LABEL = _(u"Group Project Activity")

    editable_fields = ("display_name", "weight",
                       "group_reviews_required_count", "user_review_count",
                       "due_date")
    has_score = True
    has_children = True

    template_location = 'activity'

    DASHBOARD_DETAILS_URL_KEY = 'dashboard_details_url'
    DEFAULT_DASHBOARD_DETAILS_URL_TPL = "/dashboard_details_view?activate_block_id={activity_id}"
    TA_REVIEW_URL_KEY = 'ta_review_url'
    DEFAULT_TA_REVIEW_URL_TPL = "ta_grading=true&activate_block_id={activate_block_id}&group_id={group_id}"

    @property
    def id(self):
        return self.scope_ids.usage_id

    def max_score(self):
        """
        Used for grading purposes:
            * As max grade for submitting grade event. See :method:`assign_grade_to_group`
            * As theoretical max score for grade calculation when grade is not yet available
        :rtype: Float
        """
        return self.weight

    @property
    def project(self):
        return self.get_parent()

    @property
    def content_id(self):
        return get_block_content_id(self)

    @property
    def is_ta_graded(self):
        return self.group_reviews_required_count == 0

    @property
    def allowed_nested_blocks(self):  # pylint: disable=no-self-use
        return [
            BasicStage, CompletionStage, SubmissionStage, TeamEvaluationStage,
            PeerReviewStage, EvaluationDisplayStage, GradeDisplayStage
        ]

    @property
    def stages(self):
        return self._children

    @property
    def available_stages(self):
        for stage in self.stages:
            if stage.available_to_current_user:
                yield stage

    @property
    def default_stage(self):
        def_stage = get_default_stage(self.available_stages)
        if def_stage:
            return def_stage
        else:
            return self.stages[0] if self.stages else None

    @property
    def questions(self):
        return list(self._chain_questions(self.stages, 'questions'))

    @property
    def grade_questions(self):
        return list(self._chain_questions(self.stages, 'grade_questions'))

    @property
    def team_evaluation_questions(self):
        stages = self.get_children_by_category(TeamEvaluationStage.CATEGORY)
        return list(self._chain_questions(stages, 'questions'))

    @property
    def peer_review_questions(self):
        stages = self.get_children_by_category(PeerReviewStage.CATEGORY)
        return list(self._chain_questions(stages, 'questions'))

    def dashboard_details_url(self):
        """
        Gets dashboard details view URL for current activity. If settings service is not available or does not provide
        URL template, default template is used.
        """
        template = self._get_setting(self.DASHBOARD_DETAILS_URL_KEY,
                                     self.DEFAULT_DASHBOARD_DETAILS_URL_TPL)

        return template.format(program_id=self.user_preferences.get(
            self.DASHBOARD_PROGRAM_ID_KEY, None),
                               course_id=self.course_id,
                               project_id=self.project.scope_ids.usage_id,
                               activity_id=self.id)

    def get_ta_review_link(self, group_id, target_block_id=None):
        target_block_id = target_block_id if target_block_id else self.id
        template = self._get_setting(self.TA_REVIEW_URL_KEY,
                                     self.DEFAULT_TA_REVIEW_URL_TPL)
        return template.format(course_id=self.course_id,
                               group_id=group_id,
                               activate_block_id=target_block_id)

    @staticmethod
    def _chain_questions(stages, question_type):
        return itertools.chain.from_iterable(
            getattr(stage, question_type, ()) for stage in stages)

    def get_stage_to_display(self, target_block_id):
        try:
            if target_block_id:
                stage = self.runtime.get_block(target_block_id)
                if self.get_child_category(
                        stage
                ) in STAGE_TYPES and stage.available_to_current_user:
                    return stage
        except (InvalidKeyError, KeyError, NoSuchUsage) as exc:
            log.exception(exc)

        return self.default_stage

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

        current_stage_id = context.get(
            Constants.CURRENT_STAGE_ID_PARAMETER_NAME, None)
        target_stage = self.get_stage_to_display(current_stage_id)

        if not target_stage:
            fragment.add_content(messages.NO_STAGES)
        else:
            stage_fragment = target_stage.render('student_view', context)
            fragment.add_frag_resources(stage_fragment)
            render_context = {
                'activity': self,
                'stage_content': stage_fragment.content,
            }
            render_context.update(context)
            fragment.add_content(
                self.render_template('student_view', render_context))

        return fragment

    @groupwork_protected_view
    def navigation_view(self, context):
        fragment = Fragment()

        children_context = {}
        children_context.update(context)

        stage_fragments = self._render_children('navigation_view',
                                                children_context,
                                                self.available_stages)
        stage_contents = [frag.content for frag in stage_fragments]
        fragment.add_frags_resources(stage_fragments)

        render_context = {'activity': self, 'stage_contents': stage_contents}
        fragment.add_content(
            self.render_template('navigation_view', render_context))

        return fragment

    @groupwork_protected_view
    def resources_view(self, context):
        fragment = Fragment()

        resources = [
            resource for stage in self.stages for resource in stage.resources
        ]
        has_resources = bool(resources)

        resource_fragments = self._render_children('resources_view', context,
                                                   resources)
        resource_contents = [frag.content for frag in resource_fragments]
        fragment.add_frags_resources(resource_fragments)

        render_context = {
            'activity': self,
            'resource_contents': resource_contents,
            'has_resources': has_resources
        }
        fragment.add_content(
            self.render_template('resources_view', render_context))

        return fragment

    @groupwork_protected_view
    def submissions_view(self, context):
        fragment = Fragment()

        submissions = [
            submission for stage in self.stages
            if isinstance(stage, SubmissionStage)
            for submission in stage.submissions
        ]
        has_submissions = bool(submissions)

        submission_fragments = self._render_children('submissions_view',
                                                     context, submissions)
        submission_contents = [frag.content for frag in submission_fragments]
        fragment.add_frags_resources(submission_fragments)

        render_context = {
            'activity': self,
            'submission_contents': submission_contents,
            'has_submissions': has_submissions
        }
        fragment.add_content(
            self.render_template('submissions_view', render_context))

        return fragment

    @groupwork_protected_view
    @AuthXBlockMixin.check_dashboard_access_for_current_user
    def dashboard_view(self, context):
        fragment = Fragment()

        children_context = context.copy()

        stage_fragments = self._render_children('dashboard_view',
                                                children_context, self.stages)
        stage_contents = [frag.content for frag in stage_fragments]
        fragment.add_frags_resources(stage_fragments)

        render_context = {'activity': self, 'stage_contents': stage_contents}
        fragment.add_content(
            self.render_template('dashboard_view', render_context))

        return fragment

    @groupwork_protected_view
    @AuthXBlockMixin.check_dashboard_access_for_current_user
    def dashboard_detail_view(self, context):
        fragment = Fragment()

        children_context = context.copy()

        target_workgroups = context.get(Constants.TARGET_WORKGROUPS)
        target_users = context.get(Constants.TARGET_STUDENTS)
        filtered_users = children_context[Constants.FILTERED_STUDENTS]

        stages = []
        stage_stats = {}
        for stage in self.stages:
            if not stage.is_graded_stage:
                continue
            stage_fragment = stage.render('dashboard_detail_view',
                                          children_context)
            stage_fragment.add_frag_resources(fragment)
            stages.append({"id": stage.id, 'content': stage_fragment.content})
            stage_stats[stage.id] = self._get_stage_completion_details(
                stage, target_workgroups, target_users)

        groups_data = self._build_groups_data(target_workgroups, stage_stats,
                                              filtered_users)
        visible_groups = [
            group for group in groups_data if group["group_visible"]
        ]

        render_context = {
            'activity':
            self,
            'StageState':
            StageState,
            'stages':
            stages,
            'stages_count':
            len(stages),
            'groups':
            visible_groups,
            'filtered_out_workgroups':
            len(groups_data) - len(visible_groups),
            'stage_cell_width_percent': (100 - 30) /
            float(len(stages)),  # 30% is reserved for first column
            'assigned_to_groups_label':
            messages.ASSIGNED_TO_GROUPS_LABEL.format(
                group_count=len(groups_data))
        }
        fragment.add_content(
            self.render_template('dashboard_detail_view', render_context))

        return fragment

    def _render_user(self, user, stage_stats, filtered_students):
        """
        :param group_project_v2.project_api.dtos.ReducedUserDetail user:
        :param dict[str, StageCompletionDetailsData] stage_stats: Stage completion statistics
        :param set[int] filtered_students:  users filtered out from view
        :return: dict
        """

        return {
            'id': user.id,
            'full_name': user.full_name,
            'email': user.email,
            'is_filtered_out': user.id in filtered_students,
            'stage_states': {
                stage_id: stage_data.user_stats.get(user.id,
                                                    StageState.UNKNOWN)
                for stage_id, stage_data in stage_stats.iteritems()
            },
            'groups_to_grade': {
                stage_id: [{
                    'id':
                    group.id,
                    'ta_grade_link':
                    self.get_ta_review_link(group.id, stage_id)
                } for group in stage_data.groups_to_grade.get(user.id, [])]
                for stage_id, stage_data in stage_stats.iteritems()
            }
        }

    def _render_workgroup(self, workgroup, stage_stats, filtered_students):
        """
        :param group_project_v2.project_api.dtos.WorkgroupDetails workgroup:
        :param dict[str, StageCompletionDetailsData] stage_stats: Stage completion statistics
        :param set[int] filtered_students:  users filtered out from view
        :return: dict
        """

        users = [
            self._render_user(user, stage_stats, filtered_students)
            for user in workgroup.users
        ]

        users.sort(key=itemgetter('is_filtered_out'))

        group_visible = any((not user['is_filtered_out'] for user in users))

        return {
            'id': workgroup.id,
            'ta_grade_link': self.get_ta_review_link(workgroup.id),
            'group_visible': group_visible,
            'stage_states': {
                stage_id: {
                    'internal_status':
                    stage_data.internal_group_status.get(
                        workgroup.id, StageState.UNKNOWN),
                    'external_status':
                    stage_data.external_group_status.get(
                        workgroup.id, StageState.NOT_AVAILABLE),
                    'external_status_label':
                    stage_data.external_group_status_label.get(
                        workgroup.id, ""),
                }
                for stage_id, stage_data in stage_stats.iteritems()
            },
            'users': users
        }

    def _build_groups_data(self, workgroups, stage_stats, filtered_users):
        """
        Converts WorkgroupDetails into dict expected by dashboard_detail_view template.

        :param collections.Iterable[group_project_v2.project_api.dtos.WorkgroupDetails] workgroups: Workgroups
        :param dict[str, StageCompletionDetailsData] stage_stats: Stage statistics - group-wise and user-wise completion
            data and groups_to_review.
        :param set[int] filtered_users: users filtered out from view - depending on actual view
            (dashboard or dashboard details) such students are either completely excluded, or included but diplayed
            differently
        :rtype: list[dict]
        :returns:
            List of dictionaries with the following format:
                * id - Group ID
                * stage_states - dictionary stage_id -> StateState
                * users - dictionary with the following format:
                    * id - User ID
                    * full_name - User full name
                    * email - user email
                    * stage_states - dictionary stage_id -> StageState
                    * groups_to_grade - dictionary stage_id -> list of groups to grade
        """
        return [
            self._render_workgroup(workgroup, stage_stats, filtered_users)
            for workgroup in workgroups
        ]

    @classmethod
    def _get_stage_completion_details(cls, stage, target_workgroups,
                                      target_students):
        """
        Gets stage completion stats from individual stage
        :param group_project_v2.stage.BaseGroupActivityStage stage: Get stage stats from this stage
        :param collections.Iterable[group_project_v2.project_api.dtos.WorkgroupDetails] target_workgroups:
        :param collections.Iterable[group_project_v2.project_api.dtos.ReducedUserDetails] target_students:
        :rtype: StageCompletionDetailsData
        :returns: Stage completion stats
        """
        completed_users, partially_completed_users = stage.get_users_completion(
            target_workgroups, target_students)
        user_stats = {}
        groups_to_grade = {}
        for user in target_students:
            state = StageState.NOT_STARTED
            if user.id in completed_users:
                state = StageState.COMPLETED
            elif user.id in partially_completed_users:
                state = StageState.INCOMPLETE
            user_stats[user.id] = state

            if isinstance(stage, PeerReviewStage):
                groups_to_grade[user.id] = stage.get_review_subjects(user.id)

        external_group_status, external_group_status_label, internal_group_status = cls._get_group_statuses(
            stage, target_workgroups, user_stats)

        return StageCompletionDetailsData(
            internal_group_status=internal_group_status,
            external_group_status=external_group_status,
            external_group_status_label=external_group_status_label,
            user_stats=user_stats,
            groups_to_grade=groups_to_grade)

    @classmethod
    def _get_group_statuses(cls, stage, target_workgroups, user_stats):
        internal_group_status, external_group_status, external_group_status_label = {}, {}, {}
        for group in target_workgroups:
            user_completions = [
                user_stats.get(user.id, StageState.UNKNOWN)
                for user in group.users
            ]
            student_review_state = StageState.NOT_STARTED
            if all(completion == StageState.COMPLETED
                   for completion in user_completions):
                student_review_state = StageState.COMPLETED
            elif any(completion != StageState.NOT_STARTED
                     for completion in user_completions):
                student_review_state = StageState.INCOMPLETE
            elif any(completion == StageState.UNKNOWN
                     for completion in user_completions):
                student_review_state = StageState.UNKNOWN
            internal_group_status[group.id] = student_review_state

            external_status = stage.get_external_group_status(group)
            external_group_status[group.id] = external_status
            external_group_status_label[
                group.id] = stage.get_external_status_label(external_status)
        return external_group_status, external_group_status_label, internal_group_status

    def mark_complete(self, user_id):
        self.runtime.publish(self, 'progress', {'user_id': user_id})

    def validate_field_data(self, validation, data):
        super(GroupActivityXBlock, self).validate_field_data(validation, data)
        should_be_ints = ('weight', 'group_reviews_required_count',
                          'user_review_count')
        for field_name in should_be_ints:
            try:
                int(getattr(data, field_name))
            except (TypeError, ValueError):
                message = _(
                    u"{field_name} must be integer, {field_value} given"
                ).format(field_name=field_name,
                         field_value=getattr(data, field_name))
                validation.add(
                    ValidationMessage(ValidationMessage.ERROR, message))

    def calculate_and_send_grade(self, group_id):
        grade_value = self.calculate_grade(group_id)
        if grade_value:
            self.assign_grade_to_group(group_id, grade_value)

            workgroup = self.project_api.get_workgroup_by_id(group_id)
            for user in workgroup.users:
                self.mark_complete(user.id)

    def assign_grade_to_group(self, group_id, grade_value):
        """
        Assigns grade to group, fires required events and notifications
        :param int group_id: Group ID
        :param float grade_value: Grade to assign
        :return:
        """
        self.project_api.set_group_grade(group_id, self.course_id,
                                         self.content_id, grade_value,
                                         self.max_score())
        # Emit analytics event...
        self.runtime.publish(
            self, "group_activity.final_grade", {
                "grade_value": grade_value,
                "group_id": group_id,
                "content_id": self.content_id,
            })
        notifications_service = self.runtime.service(self, 'notifications')
        if notifications_service:
            self.fire_grades_posted_notification(group_id,
                                                 notifications_service)

    def calculate_grade(self, group_id):  # pylint:disable=too-many-locals,too-many-branches
        review_item_data = self.project_api.get_workgroup_review_items_for_group(
            group_id, self.content_id)
        review_item_map = {
            make_key(review_item['question'],
                     self.real_user_id(review_item['reviewer'])):
            review_item['answer']
            for review_item in review_item_data
        }
        all_reviewer_ids = set([
            self.real_user_id(review_item['reviewer'])
            for review_item in review_item_data
        ])
        group_reviewer_ids = [
            user["id"] for user in self.project_api.get_workgroup_reviewers(
                group_id, self.content_id)
        ]
        admin_reviewer_ids = [
            reviewer_id for reviewer_id in all_reviewer_ids
            if reviewer_id not in group_reviewer_ids
        ]

        def get_user_grade_value_list(user_id):
            user_grades = []
            for question in self.grade_questions:
                user_value = review_item_map.get(
                    make_key(question.question_id, user_id), None)
                if user_value is None:
                    # if any are incomplete, we consider the whole set to be unusable
                    return None
                else:
                    user_grades.append(user_value)

            return user_grades

        admin_provided_grades = None
        if len(admin_reviewer_ids) > 0:
            admin_provided_grades = []
            # Only include complete admin gradesets
            admin_reviewer_grades = [
                arg for arg in [
                    get_user_grade_value_list(admin_id)
                    for admin_id in admin_reviewer_ids
                ] if arg
            ]
            admin_grader_count = len(admin_reviewer_grades)
            if admin_grader_count > 1:
                for idx in range(len(self.grade_questions)):
                    admin_provided_grades.append(
                        mean([adm[idx] for adm in admin_reviewer_grades]))
            elif admin_grader_count > 0:  # which actually means admin_grader_count == 1
                admin_provided_grades = admin_reviewer_grades[0]

        user_grades = {}
        if len(group_reviewer_ids) > 0:
            for reviewer_id in group_reviewer_ids:
                this_reviewers_grades = get_user_grade_value_list(reviewer_id)
                if this_reviewers_grades is None:
                    if admin_provided_grades:
                        this_reviewers_grades = admin_provided_grades
                    else:
                        return None
                user_grades[reviewer_id] = this_reviewers_grades
        elif admin_provided_grades:
            group_reviewer_ids = [self.user_id]
            user_grades[self.user_id] = admin_provided_grades
        else:
            return None

        # Okay, if we've got here we have a complete set of marks to calculate the grade
        reviewer_grades = [
            mean(user_grades[reviewer_id])
            for reviewer_id in group_reviewer_ids
            if len(user_grades[reviewer_id]) > 0
        ]
        group_grade = round(
            mean(reviewer_grades)) if len(reviewer_grades) > 0 else None

        return group_grade
コード例 #8
0
 def test_date_format_error(self):
     with self.assertRaises(ValueError):
         DateTime().from_json('invalid')
コード例 #9
0
ファイル: sga.py プロジェクト: mamigot/edx-sga
class StaffGradedAssignmentXBlock(XBlock):
    """
    This block defines a Staff Graded Assignment.  Students are shown a rubric
    and invited to upload a file which is then graded by staff.
    """
    has_score = True
    icon_class = 'problem'
    STUDENT_FILEUPLOAD_MAX_SIZE = 4 * 1000 * 1000  # 4 MB

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

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

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

    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 self.scope_ids.usage_id

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

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

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

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

    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": getattr(
                settings, "STUDENT_FILEUPLOAD_MAX_SIZE",
                self.STUDENT_FILEUPLOAD_MAX_SIZE
            )
        }
        if self.show_staff_grading_interface():
            context['is_course_staff'] = True
            self.update_staff_debug_context(context)

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

    def 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 < _now()
        context['location'] = self.location
        context['category'] = type(self).__name__
        context['fields'] = [
            (name, field.read_from(self))
            for name, field in self.fields.items()]

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

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

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

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

    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)
                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(
                        "Init for course:%s module:%s student:%s  ",
                        module.course_id,
                        module.module_state_key,
                        module.student.username
                    )

                state = json.loads(module.state)
                score = self.get_score(student.student_id)
                approved = score is not None
                if score is None:
                    score = state.get('staff_score')
                    needs_approval = score is not None
                else:
                    needs_approval = False
                instructor = self.is_instructor()
                yield {
                    'module_id': module.id,
                    'student_id': student.student_id,
                    'submission_id': submission['uuid'],
                    'username': module.student.username,
                    'fullname': module.student.profile.name,
                    'filename': submission['answer']["filename"],
                    'timestamp': submission['created_at'].strftime(
                        DateTime.DATETIME_FORMAT
                    ),
                    'score': score,
                    'approved': approved,
                    'needs_approval': instructor and needs_approval,
                    'may_grade': instructor or not approved,
                    'annotated': state.get("annotated_filename"),
                    'comment': state.get("comment", ''),
                }

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

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

            def none_to_empty(data):
                """
                Return empty string if data is None else return data.
                """
                return data if data is not None else ''
            edit_fields = (
                (field, none_to_empty(getattr(self, field.name)), validator)
                for field, validator in (
                    (cls.display_name, 'string'),
                    (cls.points, 'number'),
                    (cls.weight, 'number'))
            )

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

    @XBlock.json_handler
    def save_sga(self, data, suffix=''):
        # 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
        """
        Save a students submission file.
        """
        require(self.upload_allowed())
        upload = request.params['assignment']
        sha1 = _get_sha1(upload.file)
        answer = {
            "sha1": sha1,
            "filename": upload.file.name,
            "mimetype": mimetypes.guess_type(upload.file.name)[0],
        }
        student_id = self.student_submission_id()
        submissions_api.create_submission(student_id, answer)
        path = self._file_storage_path(sha1, upload.file.name)
        if not default_storage.exists(path):
            default_storage.save(path, File(upload.file))
        return Response(json_body=self.student_state())

    @XBlock.handler
    def staff_upload_annotated(self, request, suffix=''):
        # pylint: disable=unused-argument
        """
        Save annotated assignment from staff.
        """
        require(self.is_course_staff())
        upload = request.params['annotated']
        module = StudentModule.objects.get(pk=request.params['module_id'])
        state = json.loads(module.state)
        state['annotated_sha1'] = sha1 = _get_sha1(upload.file)
        state['annotated_filename'] = filename = upload.file.name
        state['annotated_mimetype'] = mimetypes.guess_type(upload.file.name)[0]
        state['annotated_timestamp'] = _now().strftime(
            DateTime.DATETIME_FORMAT
        )
        path = self._file_storage_path(sha1, filename)
        if not default_storage.exists(path):
            default_storage.save(path, File(upload.file))
        module.state = json.dumps(state)
        module.save()
        log.info(
            "staff_upload_annotated for course:%s module:%s student:%s ",
            module.course_id,
            module.module_state_key,
            module.student.username
        )
        return Response(json_body=self.staff_grading_data())

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

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

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

    @XBlock.handler
    def staff_download_annotated(self, request, suffix=''):
        # pylint: disable=unused-argument
        """
        Return annotated assignment file requested by staff.
        """
        require(self.is_course_staff())
        module = StudentModule.objects.get(pk=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
        )

    def download(self, path, mime_type, filename, require_staff=False):
        """
        Return a file from storage and return in a Response.
        """
        try:
            file_descriptor = default_storage.open(path)
            app_iter = iter(partial(file_descriptor.read, BLOCK_SIZE), '')
            return Response(
                app_iter=app_iter,
                content_type=mime_type,
                content_disposition="attachment; filename=" + 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
            )

    @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())

    def validate_score_message(self, course_id, username):
        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"
        }

    @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 = StudentModule.objects.get(pk=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, unicode(self.course_id), unicode(self.block_id))
        module = StudentModule.objects.get(pk=request.params['module_id'])
        state = json.loads(module.state)
        state['staff_score'] = None
        state['comment'] = ''
        state['annotated_sha1'] = None
        state['annotated_filename'] = None
        state['annotated_mimetype'] = None
        state['annotated_timestamp'] = None
        module.state = json.dumps(state)
        module.save()
        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())

    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)
        if due is not None:
            return _now() > due
        return False

    def upload_allowed(self):
        """
        Return whether student is allowed to submit an assignment.
        """
        return not self.past_due() and self.score is None

    def _file_storage_path(self, sha1, filename):
        # pylint: disable=no-member
        """
        Get file path of storage.
        """
        path = (
            '{loc.org}/{loc.course}/{loc.block_type}/{loc.block_id}'
            '/{sha1}{ext}'.format(
                loc=self.location,
                sha1=sha1,
                ext=os.path.splitext(filename)[1]
            )
        )
        return path
コード例 #10
0
class BaseGroupActivityStage(
        CommonMixinCollection,
        DashboardXBlockMixin,
        XBlockWithPreviewMixin,
        StageNotificationsMixin,
        XBlockWithUrlNameDisplayMixin,
        AdminAccessControlXBlockMixin,
        XBlock,
):
    open_date = DateTime(display_name=_(u"Open Date"),
                         help=_(u"Stage open date"),
                         scope=Scope.settings)

    close_date = DateTime(display_name=_(u"Close Date"),
                          help=_(u"Stage close date"),
                          scope=Scope.settings)

    hide_stage_label = Boolean(
        display_name=_(u"Hide stage type label"),
        help=_(u"If true, hides stage type label in Project Navigator"),
        scope=Scope.settings,
        default=True)

    editable_fields = ('display_name', 'open_date', 'close_date',
                       'hide_stage_label')
    has_children = True
    has_score = False  # TODO: Group project V1 are graded at activity level. Check if we need to follow that
    submissions_stage = False

    CATEGORY = None
    STAGE_WRAPPER_TEMPLATE = 'templates/html/stages/stage_wrapper.html'
    STAGE_CONTENT_TEMPLATE = 'templates/html/stages/default_view.html'

    NAVIGATION_LABEL = None
    STUDIO_LABEL = _(u"Stage")
    EXTERNAL_STATUSES_LABEL_MAPPING = {}
    DEFAULT_EXTERNAL_STATUS_LABEL = ""

    js_file = None
    js_init = None

    template_location = 'stages'

    @property
    def id(self):
        return self.scope_ids.usage_id

    @property
    def navigation_label(self):
        return self._(self.NAVIGATION_LABEL)

    @property
    def allowed_nested_blocks(self):  # pylint: disable=no-self-use
        """
        This property outputs an ordered dictionary of allowed nested XBlocks in form of block_category: block_caption.
        """
        blocks = [HtmlXBlockShim, GroupProjectResourceXBlock]
        if GroupProjectVideoResourceXBlock.is_available():
            blocks.append(GroupProjectVideoResourceXBlock)
        blocks.append(ProjectTeamXBlock)
        return blocks

    @lazy
    def activity(self):
        """
        :rtype: group_project_v2.group_project.GroupActivityXBlock
        """
        return self.get_parent()

    @property
    def allow_admin_grader_access(self):
        return False

    @property
    def content_id(self):
        return get_block_content_id(self)

    @property
    def activity_content_id(self):
        return self.activity.content_id

    @property
    def resources(self):
        return self.get_children_by_category(
            GroupProjectResourceXBlock.CATEGORY,
            GroupProjectVideoResourceXBlock.CATEGORY)

    @property
    def team_members(self):
        """
        Returns teammates to review. May throw `class`: OutsiderDisallowedError
        """
        if not self.is_group_member:
            return []

        try:
            result = []
            for team_member in self.workgroup.users:
                team_member_id = team_member.id
                if self.user_id == int(team_member_id):
                    continue
                result.append(self.project_api.get_member_data(team_member_id))
            return result
        except ApiError:
            return []

    @property
    def formatted_open_date(self):
        return format_date(self.open_date)

    @property
    def formatted_close_date(self):
        return format_date(self.close_date)

    @property
    def is_open(self):
        return (self.open_date is None) or (
            self.open_date <= datetime.utcnow().replace(tzinfo=pytz.UTC))

    @property
    def is_closed(self):
        # If this stage is being loaded for the purposes of a TA grading,
        # then we never close the stage - in this way a TA can impose any
        # action necessary even if it has been closed to the group members
        if not self.is_group_member:
            return False

        if self.close_date is None:
            return False

        # A stage is closed at the end of the closing day.

        return self.close_date + timedelta(
            days=1) <= datetime.utcnow().replace(tzinfo=pytz.UTC)

    @property
    def completed(self):
        return self.get_stage_state() == StageState.COMPLETED

    @property
    def available_now(self):
        return self.is_open and not self.is_closed

    @property
    def url_name_caption(self):
        return self._(messages.STAGE_URL_NAME_TEMPLATE).format(
            stage_name=self._(self.STUDIO_LABEL))

    @property
    def can_mark_complete(self):
        return self.available_now and self.is_group_member

    @property
    def is_graded_stage(self):  # pylint: disable=no-self-use
        """
        If a stage is graded it is shown as graded on the the main dashboard, this property also is used by default
        implementation of ``shown_on_detail_view``.

        :rtype: bool
        """
        return False

    @property
    def shown_on_detail_view(self):
        """
        If true details of this stage are shown on the dashboard detail view, by default it returns ``is_graded_stage``.

        :rtype: bool
        """
        return self.is_graded_stage

    @property
    def dashboard_details_view_url(self):
        return self.activity.dashboard_details_url()

    def is_current_stage(self, context):
        target_stage_id = context.get(
            Constants.CURRENT_STAGE_ID_PARAMETER_NAME, None)
        if not target_stage_id:
            return False
        return target_stage_id == self.id

    def _view_render(self, context, view='student_view'):
        stage_fragment = self.get_stage_content_fragment(context, view)

        fragment = Fragment()
        fragment.add_fragment_resources(stage_fragment)
        render_context = {
            'stage': self,
            'stage_content': stage_fragment.content,
            "ta_graded": self.activity.group_reviews_required_count
        }
        fragment.add_content(
            loader.render_django_template(
                self.STAGE_WRAPPER_TEMPLATE,
                render_context,
                i18n_service=self.i18n_service,
            ))
        if stage_fragment.js_init_fn:
            fragment.initialize_js(stage_fragment.js_init_fn)

        return fragment

    @groupwork_protected_view
    def student_view(self, context):
        return self._view_render(context)

    @groupwork_protected_view
    def author_preview_view(self, context):
        # if we use student_view or author_view Studio will wrap it in HTML that we don't want in the preview
        fragment = self._view_render(context, "preview_view")
        url_name_fragment = self.get_url_name_fragment(self.url_name_caption)
        fragment.add_content(url_name_fragment.content)
        fragment.add_fragment_resources(url_name_fragment)
        return fragment

    @groupwork_protected_view
    def author_edit_view(self, context):
        fragment = super(BaseGroupActivityStage,
                         self).author_edit_view(context)
        url_name_fragment = self.get_url_name_fragment(self.url_name_caption)
        fragment.add_content(url_name_fragment.content)
        fragment.add_fragment_resources(url_name_fragment)
        return fragment

    def render_children_fragments(self, context, view='student_view'):
        children_fragments = []
        for child in self._children:
            child_fragment = self._render_child_fragment(child, context, view)
            children_fragments.append(child_fragment)

        return children_fragments

    def get_stage_content_fragment(self, context, view='student_view'):
        fragment = Fragment()
        children_fragments = self.render_children_fragments(context, view=view)
        render_context = {
            'stage': self,
            'children_contents': [frag.content for frag in children_fragments]
        }

        for frag in children_fragments:
            fragment.add_fragment_resources(frag)

        render_context.update(context)
        fragment.add_content(
            loader.render_django_template(
                self.STAGE_CONTENT_TEMPLATE,
                render_context,
                i18n_service=self.i18n_service,
            ))

        if self.js_file:
            add_resource(self, 'javascript', self.js_file, fragment)

        if self.js_init:
            fragment.initialize_js(self.js_init)

        return fragment

    def mark_complete(self, user_id=None):
        user_id = user_id if user_id is not None else self.user_id
        self.runtime.publish(self, 'progress', {'user_id': user_id})

    def get_stage_state(self):
        raise NotImplementedError(MUST_BE_OVERRIDDEN)

    def get_dashboard_stage_state(self, target_workgroups, target_students):
        """
        :param collections.Iterable[group_project_v2.project_api.dtos.WorkgroupDetails] target_workgroups:
        :param collections.Iterable[group_project_v2.project_api.dtos.ReducedUserDetails] target_students:
        :return (str, dict[str, float]): Stage state and detailed stage stats as returned by `get_stage_stats`
        """
        state_stats = self.get_stage_stats(target_workgroups, target_students)
        if state_stats.get(StageState.COMPLETED, 0) == 1:
            stage_state = StageState.COMPLETED
        elif state_stats.get(StageState.INCOMPLETE, 0) > 0 or state_stats.get(
                StageState.COMPLETED, 0) > 0:
            stage_state = StageState.INCOMPLETE
        else:
            stage_state = StageState.NOT_STARTED

        return stage_state, state_stats

    def get_stage_stats(self, target_workgroups, target_students):  # pylint: disable=no-self-use
        """
        Calculates stage state stats for given workgroups and students
        :param collections.Iterable[group_project_v2.project_api.dtos.WorkgroupDetails] target_workgroups:
        :param collections.Iterable[group_project_v2.project_api.dtos.ReducedUserDetails] target_students:
        :return dict[str, float]:
            Percentage of students completed, partially completed and not started the stage as floats in range[0..1]
        """
        target_user_ids = set(user.id for user in target_students)
        if not target_user_ids:
            return {
                StageState.COMPLETED: None,
                StageState.INCOMPLETE: None,
                StageState.NOT_STARTED: None
            }

        target_user_count = float(len(target_user_ids))

        completed_users_ids, partially_completed_users_ids = self.get_users_completion(
            target_workgroups, target_students)
        log_format_data = dict(
            stage=self.display_name,
            target_users=target_user_ids,
            completed=completed_users_ids,
            partially_completed=partially_completed_users_ids)
        log.info(STAGE_STATS_LOG_TPL, log_format_data)

        completed_ratio = len(completed_users_ids
                              & target_user_ids) / target_user_count
        partially_completed_ratio = len(partially_completed_users_ids
                                        & target_user_ids) / target_user_count

        return {
            StageState.COMPLETED: completed_ratio,
            StageState.INCOMPLETE: partially_completed_ratio,
            StageState.NOT_STARTED:
            1 - completed_ratio - partially_completed_ratio
        }

    def get_users_completion(self, target_workgroups, target_users):
        """
        Returns sets of completed user ids and partially completed user ids
        :param collections.Iterable[group_project_v2.project_api.dtos.WorkgroupDetails] target_workgroups:
        :param collections.Iterable[group_project_v2.project_api.dtos.ReducedUserDetails] target_users:
        :rtype: (set[int], set[int])
        """
        raise NotImplementedError(MUST_BE_OVERRIDDEN)

    def get_external_group_status(self, group):  # pylint: disable=unused-argument, no-self-use
        """
        Calculates external group status for the Stage.
        Meaning of external status varies by Stage - see actual implementations docstrings.
        :param group_project_v2.project_api.dtos.WorkgroupDetails group: workgroup
        :rtype: StageState
        """
        return StageState.NOT_AVAILABLE

    def get_external_status_label(self, status):
        """
        Gets human-friendly label for external status.
        Label vary by stage, so consult with actual implementaiton for details
        :param StageState status: external stage status
        :rtype: str
        """
        return self.EXTERNAL_STATUSES_LABEL_MAPPING.get(
            status, self.DEFAULT_EXTERNAL_STATUS_LABEL)

    def navigation_view(self, context):
        """
        Renders stage content for navigation view
        :param dict context:
        :rtype: Fragment
        """
        fragment = Fragment()
        rendering_context = {
            'stage': self,
            'activity_id': self.activity.id,
            'stage_state': self.get_stage_state(),
            'block_link': get_link_to_block(self),
            'is_current_stage': self.is_current_stage(context)
        }
        rendering_context.update(context)
        fragment.add_content(
            loader.render_django_template(
                "templates/html/stages/navigation_view.html",
                rendering_context,
                i18n_service=self.i18n_service,
            ))
        return fragment

    @classmethod
    def make_human_stats(cls, stats):
        """
        Readies stats dictionary for presentation, by sorting it's contents, and converting
        ratios to percentages.
        """
        return OrderedDict([
            (StageState.get_human_name(stage),
             stats[stage] * 100 if stats[stage] is not None else None)
            for stage in (StageState.NOT_STARTED, StageState.INCOMPLETE,
                          StageState.COMPLETED)
        ])

    @AuthXBlockMixin.check_dashboard_access_for_current_user
    def dashboard_view(self, context):
        """
        Renders stage content for dashboard view.
        :param dict context:
        :rtype: Fragment
        """
        fragment = Fragment()

        target_workgroups = context.get(Constants.TARGET_WORKGROUPS)
        target_students = context.get(Constants.TARGET_STUDENTS)
        filtered_students = context.get(Constants.FILTERED_STUDENTS)

        students_to_display = [
            student for student in target_students
            if student.id not in filtered_students
        ]

        state, stats = self.get_dashboard_stage_state(target_workgroups,
                                                      students_to_display)
        human_stats = self.make_human_stats(stats)
        render_context = {
            'stage': self,
            'stats': human_stats,
            'stage_state': state,
            'ta_graded': self.activity.is_ta_graded
        }
        render_context.update(context)
        fragment.add_content(
            self.render_template('dashboard_view', render_context))
        return fragment

    @AuthXBlockMixin.check_dashboard_access_for_current_user
    def dashboard_detail_view(self, context):
        """
        Renders stage header for dashboard details view.
        :param dict context:
        :rtype: Fragment
        """
        fragment = Fragment()
        render_context = {
            'stage':
            self,
            'ta_graded':
            self.activity.is_ta_graded,
            'download_incomplete_emails_handler_url':
            self.get_incomplete_emails_handler_url()
        }
        fragment.add_content(
            self.render_template('dashboard_detail_view', render_context))
        return fragment

    def get_incomplete_emails_handler_url(self):
        base_url = self.runtime.handler_url(self.activity.project,
                                            'download_incomplete_list')
        query_params = {Constants.ACTIVATE_BLOCK_ID_PARAMETER_NAME: self.id}
        return base_url + '?' + urlencode(query_params)

    def get_new_stage_state_data(self):
        return {
            "activity_id": str(self.activity.id),
            "stage_id": str(self.id),
            "state": self.get_stage_state()
        }
コード例 #11
0
class TCXBlock(XBlock):
	
	log = logging.getLogger(__name__)
	"""
	TO-DO: document what your XBlock does.
	"""
	# Fields are defined on the class.  You can access them in your code as
	# self.<fieldname>.	
	
	points= Integer(
	default=0, 
	scope=Scope.user,
	help="Number of points",
	)
	
	TournamentID= Integer(
	default=0, 
	scope=Scope.user,
	help="Number of points",
	)
        
	nquestions = Integer(
	default=0, 
	scope=Scope.user_state,
	help="Number of questions",
	)
    
	correct_answers = Integer(
	default=5, 
	scope=Scope.user_state,
	help="Number of questions",
	)
	
	tournamentID = Integer(
	default=0, 
	scope=Scope.user_state,
	help="Number of questions",
	)

	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."
	)
	
	visible = String(
	default="No", 
	scope=Scope.settings,
	help="Indicates wether the tournament is visible or not",
	)
	
	questions = List(
	scope=Scope.settings,
	help="List of questions for the assesment",
	)
	
	active_tournaments = List(
	scope=Scope.settings,
	help="List of questions for the assesment",
	)
	
	temp = List(
	scope=Scope.settings,
	help="List of questions for the assesment",
	)
	
	finished_tournaments = List(
	scope=Scope.settings,
	help="List of questions for the assesment",
	)
	
	active_tournaments = List(
	scope=Scope.settings,
	help="List of questions for the assesment",
	)
	
   
		
	def days_between(d1, d2):
		a = dt.datetime(2013,12,30,23,59,59)
		b = dt.datetime(2013,12,31,23,59,59)
		return (b-a).total_seconds()
		
		
	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 new_tournament(self, context=None):
	
		return frag
		
		
	def studio_view(self, context):
		"""
		The primary view of the TCXBlock course builder, shown to course builders
		when instantiating the xblock.
		"""
		html = self.resource_string("public/admin/html/mainMenu.html")
		frag2 = Fragment(html.format(self=self))
		html2 = self.resource_string("public/html/New_Tournament.html")
		frag2.add_css(self.resource_string("public/admin/css/tournamentcreator.css"))
		img=self.runtime.local_resource_url(self, "public/admin/icons/listasd.png")
		
		frag2.add_content(html2)
		frag2.add_javascript(self.resource_string("public/admin/js/interface.js"))
		frag2.initialize_js('studio')
		return frag2
		
		# TO-DO: change this view to display your data your own way.
	def student_view(self, context=None):
		"""
		The primary view of the TCXBlock, shown to students
		when viewing the xblock in their course.
		"""
		html = self.resource_string("public/html/tournamentcreator.html")
		frag = Fragment(html.format(self=self))
		frag.add_css(self.resource_string("public/admin/css/tournamentcreator.css"))
		frag.add_css(self.resource_string("public/css/bootstrap.css"))
		frag.add_javascript(self.resource_string("public/js/src/tournamentcreator.js"))
		frag.initialize_js('TCXBlock')
		return frag

	"""Course Builder Handlers """
	@XBlock.json_handler
	def Load_Tournaments_List(self, data, suffix=''):
		"""
		Loads a list of all the tournaments available
		"""
		for x in xrange(len(self.active_tournaments)):
			if data["Name"]==self.active_tournaments[x]['TName']:
				for y in xrange(len(self.questions)):
					if self.active_tournaments[x]['TournamentID']==self.nquestions[y]['TournamentID']:
						temp.push(self.nquestions[y])
		return 1
	@XBlock.json_handler
	def Load_Inactive_Tournaments(self, data, suffix=''):
		"""
		Loads a list of all inactive tournaments.
		"""
		self.temp[:]=[]
		for x in xrange(len(self.active_tournaments)):
			self.temp.append(self.active_tournaments[x])

		return self.temp

	@XBlock.json_handler
	def Publish_Unpublish_Tournament(self, data, suffix=''):
		"""
		Given a tournament id adds the tournament to the published or unpublished tournament list
		"""
		for x in xrange(len(self.active_tournaments)):
			if data["Name"]==self.active_tournaments[x]['TName']:
				for y in xrange(len(self.questions)):
					if self.active_tournaments[x]['TournamentID']==self.nquestions[y]['TournamentID']:
						temp.push(self.nquestions[y])
		return 1

	@XBlock.json_handler
	def Load_CourseBuilderTournament_Statistics(self, data, suffix=''):
		"""
		Loads the statistics of a tournament given by its id for the course builder
		"""
		self.temp[:]=[]
		for x in xrange(len(self.active_tournaments)):
			self.temp.append(self.active_tournaments[x])

		return self.temp

	"""Student Handlers """
	@XBlock.json_handler
	def Load_Student_Statistics(self, data, suffix=''):
		"""
		Loads the statistics of a tournament given by its id for student
		"""
		self.temp[:]=[]
		for x in xrange(len(self.active_tournaments)):
			self.temp.append(self.active_tournaments[x])

		return self.temp

	@XBlock.json_handler
	def Load_AllStudentsStatistics(self, data, suffix=''):
		"""
		Loads the statistics of all tournaments for a student
		"""
		self.temp[:]=[]
		for x in xrange(len(self.active_tournaments)):
			self.temp.append(self.active_tournaments[x])

		return self.temp

	@XBlock.json_handler
	def Load_AllCourseBuildersStatistics(self, data, suffix=''):
		"""
		Loads the statistics of all tournaments for a course builder
		"""
		self.temp[:]=[]
		for x in xrange(len(self.active_tournaments)):
			self.temp.append(self.active_tournaments[x])

		return self.temp

	@XBlock.json_handler
	def Save_Answer(self, data, suffix=''):
		"""
		Saves the answer for a certain student for a certain question
		"""
		self.temp[:]=[]
		for x in xrange(len(self.active_tournaments)):
			self.temp.append(self.active_tournaments[x])

		return self.temp


	"""Shared Handlers """
	@XBlock.json_handler
	def Load_Active_Tournaments(self, data, suffix=''):
		"""
		Loads a list of all the published tournaments
		"""
		self.temp[:]=[]
		for x in xrange(len(self.active_tournaments)):
			self.temp.append(self.active_tournaments[x])

		return self.temp

	@XBlock.json_handler
	def Load_Tournament(self, data, suffix=''):
		"""
		Given an id, loads the questions for the tournament
		"""
		for x in xrange(len(self.active_tournaments)):
				if data["Name"]==self.active_tournaments[x]['TName']:
					for y in xrange(len(self.questions)):
						if self.active_tournaments[x]['TournamentID']==self.nquestions[y]['TournamentID']:
							temp.push(self.nquestions[y])
		return 1


	@XBlock.json_handler
	def Save_Tournament(self, data, suffix=''):
		"""
		Given an id, saves the data for a tournament
		"""
		self.nquestions=data['NQuestions']
		self.active_tournaments.append({
			'TournamentID': self.tournamentID,
			'TName': data['TName'],
			'NQuestions': data['NQuestions']
		})
		
		for x in xrange(self.nquestions):
			
			if data['question'][x]['Type']=="Multiple": 
				self.questions.append({
					'TournamentID': self.tournamentID,
					'QuestionN': data['question'][x]['QuestionN'],
					'Type' : data['question'][x]['Type'],
					'Question' : data['question'][x]['Question'],
					'Answer1' : data['question'][x]['Answer1'],
					'Answer2' : data['question'][x]['Answer2'],
					'Answer3' : data['question'][x]['Answer3'],
					'Answer4' : data['question'][x]['Answer4'],
					'CAnswer' : data['question'][x]['CAnswer'],
				})
			
			if data['question'][x]['Type']=="Image": 
				self.questions.append({
					'TournamentID': self.tournamentID,
					'QuestionN': data['question'][x]['QuestionN'],
					'Type' : data['question'][x]['Type'],
					'Question' : data['question'][x]['Question'],
					'Tip' : data['question'][x]['Tip'],
					'Answer' : data['question'][x]['Answer'],
					'Url' : data['question'][x]['Url'],
				})
					
			if data['question'][x]['Type']=="Video": 
				self.questions.append({
					'TournamentID': self.tournamentID,
					'QuestionN': data['question'][x]['QuestionN'],
					'Type' : data['question'][x]['Type'],
					'Question' : data['question'][x]['Question'],
					'Tip' : data['question'][x]['Tip'],
					'Answer' : data['question'][x]['Answer'],
					'Url' : data['question'][x]['Url'],
				})
				
		return 0
		#for data['QuestionN']{
		
		#}


	# 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 speed_mode(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 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 config_screen_four(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 [("TCXBlock",
		"""
		<tournamentcreator/>
		"""),
		]
コード例 #12
0
class AntXBlockFields(object):

    # Отображаемое имя
    display_name = String(
        display_name="Display name",
        default='AcademicNT Assignment',
        help="This name appears in the horizontal navigation at the top of "
        "the page.",
        scope=Scope.settings)

    # Отображаемый текстовый комментарий
    content = String(display_name="Content",
                     default='',
                     help="Text.",
                     scope=Scope.settings)

    # Максимальный результат за задание
    points = Float(
        default=0,
        scope=Scope.user_state,
    )

    # Вес задания в рамках всего Grading'а
    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
        },
        default=1,
        scope=Scope.settings)

    # Идентификатор курса в рамках СУО ANT
    ant_course_id = String(display_name="Course id",
                           default='',
                           help="Course id in ant.",
                           scope=Scope.settings)

    # Идентификатор юнита лабораторной работы с рамках СУО ANT
    ant_unit_id = String(display_name="Course id",
                         default='',
                         help="Unit id in ant.",
                         scope=Scope.settings)

    # Время, отведённое на выполнение работы в ANT
    ant_time_limit = Integer(display_name="Time limit",
                             default=0,
                             help="Time limit (in minutes)",
                             scope=Scope.settings)

    # Количество попыток, отведённое на выполнение работы в ANT
    ant_attempts_limit = Integer(
        default=0,
        help="Submission id",
        scope=Scope.settings,
    )

    # Статус выполнения задания, магическая строка: RUNNING, IDLE
    ant_status = String(scope=Scope.user_state, )

    # Время последнего обновления лимитов (время, попытки), переданных от СУО
    limit_renewal = DateTime(display_name="Limits renewal",
                             help="When limits have been renewed.",
                             default=None,
                             scope=Scope.settings)

    # Пользовательские баллы за лабораторную
    score = Float(display_name="Grade score",
                  default=0,
                  help="Total calculated score.",
                  scope=Scope.user_state)

    # Количество затраченных на выполнение попыток
    attempts = Integer(display_name="Attempts used",
                       default=0,
                       help="Total used attempts.",
                       scope=Scope.user_state)

    # Идентификатор текущего задания проверки, поставленного в очередь
    # (Возможно, больше не нужен)
    celery_task_id = String(display_name="Task id",
                            default="",
                            scope=Scope.user_state)

    # Результат последней проверки лабораторной, полученный от СУО
    ant_result = String(display_name="Latest ANT result",
                        default="",
                        help="Latest result retrieved by grade() method",
                        scope=Scope.user_state)

    # Адрес для вытягивания информации о выполнении
    attempts_url = DefaultedDescriptor(
        base_class=String,
        display_name="Attempts API url",
        default=CONFIG.get('ATTEMPTS_URL'),
        help="Url api to get latest user attempts.",
        # scope=Scope(UserScope.NONE, BlockScope.TYPE)
        scope=Scope.content,
    )

    # Адрес с лабораторной, открывается в попапе
    lab_url = DefaultedDescriptor(
        base_class=String,
        display_name="Lab API url",
        default=CONFIG.get('LAB_URL'),
        help="Lab url.",
        # scope=Scope(UserScope.NONE, BlockScope.TYPE)
        scope=Scope.content,
    )
コード例 #13
0
ファイル: done.py プロジェクト: OPI-PIB/DoneWithAnswerXBlock
class DoneWithAnswerXBlock(XBlock):
    """
    Show a toggle which lets students mark things as done.
    """

    description = String(scope=Scope.content,
                         help=_("Problem description."),
                         default=_("Default problem description"))

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

    feedback = String(scope=Scope.content,
                      help=_("Feedback for student."),
                      default=_("Default feedback"))

    button_name = String(scope=Scope.content,
                         help=_("Button name."),
                         default=_("Done"))

    has_score = True
    skip_flag = False

    display_name = String(
        default=_("Self-reflection question with feedback answer"),
        scope=Scope.settings,
        help=_("Display name"))

    def init_emulation(self):
        """
        Emulation of init function, for translation purpose.
        """
        if not self.skip_flag:
            i18n_ = self.runtime.service(self, "i18n").ugettext
            self.fields['display_name']._default = i18n_(
                self.fields['display_name']._default)
            self.skip_flag = True

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

        return {'state': self.done}

    def student_view(self, context=None):  # pylint: disable=unused-argument
        """
        The primary view of the DoneXBlock, shown to students
        when viewing courses.
        """
        html_resource = resource_string("static/html/done.html")
        html = html_resource.format(done=self.done,
                                    feedback=self.feedback,
                                    description=self.description,
                                    button_name=self.button_name,
                                    id=uuid.uuid1(0))

        frag = Fragment(html)
        frag.add_css(resource_string("static/css/done.css"))
        frag.add_javascript(resource_string("static/js/src/done.js"))
        frag.initialize_js("DoneWithAnswerXBlock", {
            'state': self.done,
            'button_name': self.button_name
        })
        return frag

    def studio_view(self, _context=None):  # pylint: disable=unused-argument
        '''
        Minimal view with no configuration options giving some help text.
        '''
        self.init_emulation()

        ctx = {
            'done': self.done,
            'feedback': self.feedback,
            'description': self.description,
            'button_name': self.button_name,
            'id': uuid.uuid1(0)
        }

        frag = Fragment()

        frag.add_content(
            loader.render_django_template(
                "static/html/studioview.html",
                context=ctx,
                i18n_service=self.runtime.service(self, "i18n"),
            ))

        frag.add_javascript(resource_string("static/js/src/studioview.js"))
        frag.initialize_js("DoneWithAnswerXBlockEdit")
        return frag

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

        return {'result': 'success'}

    @staticmethod
    def workbench_scenarios():
        """A canned scenario for display in the workbench."""
        return [
            ("DoneWithAnswerXBlock", """<vertical_demo>
                  <donewithanswer description="Click Mark as complete" button_name="Mark as complete" feedback="Good job!"> </donewithanswer>
                  <donewithanswer description="Think about Poland" button_name="Poland!" feedback="Well done!"> </donewithanswer>
                  <donewithanswer description="Pres Alt+F4" button_name="Alt+F4" feedback="Great!"> </donewithanswer>
                  <donewithanswer description="" feedback=""></donewithanswer>
                  <donewithanswer></donewithanswer>
                </vertical_demo>
             """),
        ]

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

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

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

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

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

    def max_score(self):
        """The maximum raw score of our problem.
        """
        return 1
コード例 #14
0
class ShortAnswerXBlock(XBlock):
    """
    This block defines a Short Answer. Students can submit a short answer and
    instructors can view, grade, add feedback and download a CSV of all submissions.
    """
    has_score = True
    icons_class = 'problem'

    answer = String(display_name=_('Student\'s answer'),
                    default='',
                    scope=Scope.user_state,
                    help=_('Text the student entered as answer.'))

    answered_at = DateTime(
        display_name=_('Answer submission time.'),
        default=None,
        scope=Scope.user_state,
        help=_('Time and date when the answer has been submitted.'))

    description = String(
        display_name=_('Description'),
        default=
        _('Submit your questions and observations in 1-2 short paragraphs below.'
          ),
        scope=Scope.settings,
        help=_('Description that appears above the text input area.'))

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

    feedback = String(
        display_name=_('Instructor feedback'),
        default=_('Your answer was submitted successfully.'),
        scope=Scope.settings,
        help=
        _('Message that will be shown to the student once the student submits an answer.'
          ))

    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
        },
        default=100,
        scope=Scope.settings)

    score = Integer(display_name=_('Student score'),
                    default=0,
                    scope=Scope.settings,
                    help=_('Score given for this assignment.'))

    grades_published = Boolean(
        display_name='Display grade to students',
        scope=Scope.user_state_summary,
        default=False,
        help='Indicates if the grades will be displayed to students.')

    width = Integer(display_name=_('Input Area Width'),
                    default=500,
                    scope=Scope.settings,
                    help='Defines the width of the input text area in pixels.')

    @property
    def module(self):
        """
        Retrieve the student module for current user.
        """
        module, _ = StudentModule.objects.get_or_create(
            course_id=self.course_id,
            module_state_key=self.location,
            student=self.user,
        )
        return module

    @property
    def passed_due(self):
        """
        Return true if the due date has passed.
        """
        now = datetime.datetime.utcnow().replace(tzinfo=pytz.utc)
        due = get_extended_due_date(self)
        if due is not None:
            return now > due
        return False

    @property
    def student_grade(self):
        """
        Retrieve the manually added grade for the current user.
        """
        return self.module.grade

    @property
    def user(self):
        """
        Retrieve the user object from the user_id in xmodule_runtime.
        """
        return User.objects.get(id=self.xmodule_runtime.user_id)

    def max_score(self):
        return self.weight or 100

    def studio_view(self, context=None):
        """
        View for the form when editing this block in Studio.
        """
        cls = type(self)
        context['fields'] = (
            (cls.display_name, getattr(self, 'display_name',
                                       ''), 'input', 'text'),
            (cls.description, getattr(self, 'description',
                                      ''), 'textarea', 'text'),
            (cls.feedback, getattr(self, 'feedback', ''), 'textarea', 'text'),
            (cls.weight, getattr(self, 'weight', ''), 'input', 'number'),
            (cls.width, getattr(self, 'width', ''), 'input', 'number'),
        )

        frag = Fragment()
        frag.add_content(
            render_template('static/html/short_answer_edit.html', context))
        frag.add_css(resource_string('static/css/short_answer_edit.css'))
        frag.add_javascript(
            resource_string('static/js/src/short_answer_edit.js'))
        frag.initialize_js('ShortAnswerStudioXBlock')
        return frag

    def student_view(self, context=None):
        """
        The primary view of the ShortAnswerXBlock, shown to students
        when viewing courses.
        """
        js_options = {
            'gradesPublished': self.grades_published,
            'weight': self.weight,
            'passedDue': self.passed_due,
            'score': self.student_grade,
        }
        context.update({
            'answer':
            self.answer,
            'description':
            self.description,
            'feedback':
            self.feedback,
            'grades_published':
            self.grades_published,
            'is_course_staff':
            getattr(self.xmodule_runtime, 'user_is_staff', False),
            'max_score':
            self.max_score(),
            'module_id':
            self.module.
            id,  # Use the module id to generate different pop-up modals
            'passed_due':
            self.passed_due,
            'score':
            self.student_grade,
            'width':
            self.width,
        })
        frag = Fragment()
        frag.add_content(
            render_template('static/html/short_answer.html', context))
        frag.add_css(resource_string('static/css/short_answer.css'))
        frag.add_javascript(resource_string('static/js/src/short_answer.js'))
        frag.initialize_js('ShortAnswerXBlock', js_options)
        return frag

    def update_weight(self):
        """Update all previous CSM's."""
        StudentModule.objects.filter(
            course_id=self.course_id,
            module_state_key=self.location).update(max_grade=self.weight)

    def error_response(self, msg):
        return Response(status_code=400, body=json.dumps({'error': msg}))

    @XBlock.json_handler
    def student_submission(self, data, _):
        """
        Handle the student's answer submission.
        """
        if self.passed_due:
            return self.error_response('Submission due date has passed.')
        elif self.grades_published:
            return self.error_response('Grades already published.')
        self.answer = data.get('submission')
        self.answered_at = datetime.datetime.utcnow().replace(tzinfo=pytz.utc)
        return Response(status_code=201)

    @XBlock.json_handler
    def submit_edit(self, data, _):
        """
        Handle the Studio edit form request.
        """
        for key in data:
            setattr(self, key, data[key])
        self.update_weight()
        return Response(status_code=201)

    @XBlock.json_handler
    def submit_grade(self, data, _):
        """
        Handle the grade submission request.
        """
        score = data.get('score')
        module_id = data.get('module_id')
        if not (score and module_id):
            error_msg_tpl = 'Missing {params} parameter.'
            if not (score or module_id):
                missing_params = 'score and module_id'
            else:
                missing_params = 'score' if not score else 'module_id'
            return self.error_response(
                error_msg_tpl.format(params=missing_params))

        if float(score) > self.weight:
            return self.error_response(
                'Submitted score larger than the maximum allowed.')

        module = StudentModule.objects.get(pk=module_id)
        module.grade = float(score)
        module.max_grade = self.weight
        module.save()
        return Response(status_code=200,
                        body=json.dumps({'new_score': module.grade}))

    # pylint: disable=no-self-use
    @XBlock.json_handler
    def remove_grade(self, data, _):
        """
        Handle the grade removal request.
        """
        module_id = data.get('module_id')
        if not module_id:
            return self.error_response('Missing module_id parameters.')

        module = StudentModule.objects.get(pk=module_id)
        module.grade = None
        module.save()
        return Response(status_code=200)

    @XBlock.handler
    def update_grades_published(self, request, suffix=''):
        self.grades_published = json.loads(
            request.params.get('grades_published'))
        return Response(status=200)

    def get_submissions_list(self):
        """
        Return a list of all enrolled students and their answer submission information.
        """
        enrollments = CourseEnrollment.objects.filter(
            course_id=self.course_id, is_active=True).exclude(
                Q(user__is_staff=True) | Q(user__is_superuser=True))
        submissions_list = []

        for enrollment in enrollments:
            student = enrollment.user
            module, _ = StudentModule.objects.get_or_create(
                course_id=self.course_id,
                module_state_key=self.location,
                student=student,
                defaults={
                    'max_grade': self.weight,
                    'module_type': self.category,
                    'state': '{}',
                    'grade': 0,
                })
            state = json.loads(module.state) if module.state else {}
            state_answered_at = state.get('answered_at', '')

            # For answers not defaulted to zero.
            if module.grade is None:
                module.grade = 0
                module.save()

            submissions_list.append({
                'answer': state.get('answer'),
                'answered_at': str(state_answered_at),
                'email': student.email,
                'fullname': student.profile.name,
                'max_score': self.max_score(),
                'module_id': module.id,
                'score': '{:.2f}'.format(module.grade),
            })
        return submissions_list

    @XBlock.handler
    def answer_submissions(self, *args, **kwargs):  # pylint: disable=unused-argument
        """
        Return the submission information of the enrolled students.
        """
        submissions_list = self.get_submissions_list()
        return Response(status_code=200, body=json.dumps(submissions_list))

    def create_csv(self, local_timezone_offset):
        """
        CSV file generator. Yields a CSV line in each iteration.
        """
        submission_list = self.get_submissions_list()
        yield create_csv_row(
            ['Name', 'Email', 'Answer', 'Answered at', 'Score'])

        for entry in submission_list:
            answered_at = entry.get('answered_at', '')
            if answered_at and local_timezone_offset:
                answered_at = localize_datetime(answered_at,
                                                local_timezone_offset)
            yield create_csv_row([
                entry.get('fullname'),
                entry.get('email'),
                entry.get('answer', ''), answered_at,
                entry.get('score')
            ])

    @XBlock.handler
    def csv_download(self, request, *args, **kwargs):  # pylint: disable=unused-argument
        """
        Handles the CSV download request.
        """
        local_datetime = request.params.get('local-datetime')
        local_timezone_offset = dateutil.parser.parse(local_datetime).tzinfo
        response = Response(content_type='text/csv')
        response.content_disposition = 'attachment; filename="short_answer_submissions.csv"'
        response.app_iter = self.create_csv(local_timezone_offset)

        return response
コード例 #15
0
class ScormXBlock(XBlock):
    display_name = String(
        display_name=_("Display Name"),
        help=_("Display name for this module"),
        default="Scorm",
        scope=Scope.settings,
    )
    scorm_file = String(
        display_name=_("Upload scorm file"),
        scope=Scope.settings,
    )
    scorm_modified = DateTime(
        scope=Scope.settings,
    )
    version_scorm = String(
        default="SCORM_12",
        scope=Scope.settings,
    )
    # save completion_status for SCORM_2004
    lesson_status = String(
        scope=Scope.user_state,
        default='not attempted'
    )
    success_status = String(
        scope=Scope.user_state,
        default='unknown'
    )
    lesson_location = String(
        scope=Scope.user_state,
        default=''
    )
    suspend_data = String(
        scope=Scope.user_state,
        default=''
    )
    data_scorm = Dict(
        scope=Scope.user_state,
        default={}
    )
    lesson_score = Float(
        scope=Scope.user_state,
        default=0
    )
    weight = Float(
        display_name=_('Weight'),
        default=1.0,
        values={"min": 0, "step": .1},
        help=_("Weight of this Scorm, by default keep 1"),
        scope=Scope.settings
    )
    has_score = Boolean(
        display_name=_("Scored"),
        help=_("Select true if this component will receive a numerical score from the Scorm"),
        default=False,
        scope=Scope.settings
    )
    icon_class = String(
        default="video",
        scope=Scope.settings,
    )

    cmi_modified = DateTime(
        scope=Scope.user_state
    )
    cmi_data = Dict(
        scope=Scope.user_state,
        default={},
    )

    has_author_view = True


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

    def get_fields_data(self, only_value=False, *fields):

        data = {}
        for k, v in self.fields.iteritems():
            if k in fields:
                if not only_value:
                    data[k] = v
                data["{}_value".format(k)] = getattr(self, k)

        if 'scorm_file' in data and self.scorm_file:
            request = get_current_request()
            scheme = 'https' if settings.HTTPS == 'on' else 'http'
            scorm_file_value = '{}://{}{}'.format(scheme, request.site.domain, self.scorm_file)
            data['scorm_file_value'] = scorm_file_value

        if 'scorm_modified_value' in data and data['scorm_modified_value']:
            data['scorm_modified_value'] = dt2str(data['scorm_modified_value'])
        if 'cmi_modified_value' in data and data['cmi_modified_value']:
            data['cmi_modified_value'] = dt2str(data['cmi_modified_value'])

        return data

    def studio_view(self, context=None):
        # context_html = self.get_context_studio()
        fields_data = self.get_fields_data(False, 'display_name', 'scorm_file', 'has_score', 'weight')
        template = self.render_template('static/html/studio.html', fields_data)
        frag = Fragment(template)
        frag.add_css(self.resource_string("static/css/scormxblock.css"))
        frag.add_javascript(self.resource_string("static/js/src/studio.js"))
        frag.initialize_js('ScormStudioXBlock')
        return frag

    def get_student_data(self):
        fields_data = self.get_fields_data(False, 'lesson_score', 'weight',
                                           'has_score', 'success_status', 'scorm_file')
        return fields_data

    def student_view(self, context=None):
        template = self.render_template('static/html/scormxblock.html', self.get_student_data())
        frag = Fragment(template)
        frag.add_css(self.resource_string("static/css/scormxblock.css"))
        frag.add_javascript(self.resource_string("static/js/src/scormxblock.js"))
        frag.initialize_js('ScormXBlock', json_args=self.get_fields_data(True, 'version_scorm', 'scorm_modified'))
        return frag

    @XBlock.handler
    def studio_submit(self, request, suffix=''):
        self.display_name = request.params['display_name']
        self.has_score = request.params['has_score']
        self.weight = request.params['weight']
        self.icon_class = 'problem' if self.has_score == 'True' else 'video'
        if hasattr(request.params['file'], 'file'):
            file = request.params['file'].file
            zip_file = zipfile.ZipFile(file, 'r')
            path_to_file = os.path.join(settings.PROFILE_IMAGE_BACKEND['options']['location'], self.location.block_id)
            path_to_file = str(path_to_file)
            if os.path.exists(path_to_file):
                shutil.rmtree(path_to_file, ignore_errors=True)
            zip_file.extractall(path_to_file)
            self.set_scorm(path_to_file)
        return Response(json.dumps({'result': 'success'}), content_type='application/json')

    def author_view(self, context):
        html = self.resource_string("static/html/author_view.html")
        frag = Fragment(html)
        return frag

    # @XBlock.json_handler
    # def scorm_get_value(self, data, suffix=''):
    #     name = data.get('name')
    #     if name in ['cmi.core.lesson_status', 'cmi.completion_status']:
    #         return {'value': self.lesson_status}
    #     elif name == 'cmi.success_status':
    #         return {'value': self.success_status}
    #     elif name == 'cmi.core.lesson_location':
    #         return {'value': self.lesson_location}
    #     elif name == 'cmi.suspend_data':
    #         return {'value': self.suspend_data}
    #     else:
    #         return {'value': self.data_scorm.get(name, '')}
    # @XBlock.json_handler
    # def commit(self, data, suffix=''):
    #     context = {'result': 'success'}
    #     for name, value in data.iteritems():
    #         if name in ['cmi.core.lesson_status', 'cmi.completion_status']:
    #             self.lesson_status = value
    #             if self.has_score and value in ['completed', 'failed', 'passed']:
    #                 context.update({"lesson_score": self.lesson_score})
    #
    #         elif name == 'cmi.success_status':
    #             self.success_status = value
    #             if self.has_score:
    #                 if self.success_status == 'unknown':
    #                     self.lesson_score = 0
    #                 context.update({"lesson_score": self.lesson_score})
    #
    #         elif name in ['cmi.core.score.raw', 'cmi.score.raw'] and self.has_score:
    #             score = float(data.get(name, 0))
    #             self.lesson_score = score / 100.0
    #             if self.lesson_score > self.weight:
    #                 logger.error("error score, user {}: {}".format(
    #                     self.get_user_id(),
    #                     self.data
    #                 ))
    #             context.update({"lesson_score": self.lesson_score})
    #
    #         elif name == 'cmi.core.lesson_location':
    #             self.lesson_location = str(value) or ''
    #
    #         elif name == 'cmi.suspend_data':
    #             self.suspend_data = value or ''
    #         else:
    #             self.data_scorm[name] = value or ''
    #
    #     self.publish_grade()
    #     context.update({"completion_status": self.get_completion_status()})
    #     return context

    def is_cmi_data_expired(self, package_date):
        expired = False
        need_update = True
        if self.scorm_modified:
            if package_date and str2dt(package_date) < self.scorm_modified:
                # when user still visit one scorm, but new package uploaded
                # so, no update, only update when user next time visit new uploaded scorm
                expired = True
                need_update = False
            elif self.cmi_modified and self.cmi_modified < self.scorm_modified:
                # normal case
                expired = True
                need_update = True
        return expired, need_update

    @XBlock.json_handler
    def scorm_get_value(self, data, suffix=''):
        name = data['name']
        package_version = data.pop('package_version', '')

        if package_version == 'SCORM_12':
            default = SCORM_12_RUNTIME_DEFAULT.get(name, '')
        else:
            default = SCORM_2004_RUNTIME_DEFAULT.get(name, '')

        package_date = data.pop('package_date', '')
        if self.is_cmi_data_expired(package_date)[0]:
            value = default
        else:
            value = self.cmi_data.get(name, default)

        return {"value": value}


    @XBlock.json_handler
    def commit(self, data, suffix=''):
        package_date = data.pop('package_date', '')
        package_version = data.pop('package_version', '')
        expired, need_update = self.is_cmi_data_expired(package_date)
        if expired:
            self.cmi_data = {}
        if need_update:
            self.cmi_data.update(data)

        self.cmi_modified = timezone.now()

        if self.set_lesson(data, package_version):
            self.publish_grade()

        return self.get_fields_data(True, 'success_status', 'lesson_score')

    def _set_lesson_12(self, data):
        score_updated = False
        if 'cmi.core.score.raw' in data:
            if 'cmi.core.score.min' in data and 'cmi.core.score.max' in data:
                score = (float(data['cmi.core.score.raw']) - float(data['cmi.core.score.min']))/(float(data['cmi.core.score.max']) - float(data['cmi.core.score.min']))
                self.lesson_score = score
                score_updated = True
            else:
                score = float(data['cmi.core.score.raw'])
                '''
                if score < 0:
                    score = float(0)
                if self.lesson_score != float(100):
                    self.lesson_score = score
                '''
                self.lesson_score = score
                score_updated = True

        if 'cmi.core.lesson_status' in data:
            self.cmi_data['cmi.core.lesson_status'] = data['cmi.core.lesson_status']
            self.success_status = data['cmi.core.lesson_status']

        return score_updated

    def _set_lesson_2004(self, data):
        score_updated = False
        if 'cmi.score.scaled' in data:
            self.lesson_score = float(data['cmi.score.scaled'])
            score_updated = True
        if 'cmi.success_status' in data:
            self.success_status = data['cmi.success_status']
        return score_updated

    def set_lesson(self, data, version):
        """
        all the score has been resize to [0, 1]
        """
        if version == 'SCORM_12':
            return self._set_lesson_12(data)
        else:
            return self._set_lesson_2004(data)

    def publish_grade(self):
        self.runtime.publish(
            self,
            'grade',
            {
                'value': self.lesson_score,
                'max_value': 1.0,
            })

    def get_user_id(self):
        return getattr(self.runtime, 'user_id', None)

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


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

    def set_scorm(self, path_to_file):
        path_index_page = 'index.html'
        try:
            tree = ET.parse('{}/imsmanifest.xml'.format(path_to_file))
        except IOError:
            pass
        else:
            namespace = ''
            for node in [node for _, node in
                         ET.iterparse('{}/imsmanifest.xml'.format(path_to_file), events=['start-ns'])]:
                if node[0] == '':
                    namespace = node[1]
                    break
            root = tree.getroot()

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

            if resource:
                path_index_page = resource.get('href')

            if (not schemaversion is None) and (re.match('^1.2$', schemaversion.text) is None):
                self.version_scorm = 'SCORM_2004'
            else:
                self.version_scorm = 'SCORM_12'

        self.scorm_modified = timezone.now()
        self.scorm_file = os.path.join(settings.PROFILE_IMAGE_BACKEND['options']['base_url'],
                                       '{}/{}'.format(self.location.block_id, path_index_page))

    def get_completion_status(self):
        return self.success_status

    @staticmethod
    def workbench_scenarios():
        """A canned scenario for display in the workbench."""
        return [
            ("ScormXBlock",
             """<vertical_demo>
                <scormxblock/>
                </vertical_demo>
             """),
        ]
コード例 #16
0
ファイル: sga.py プロジェクト: QtLearnCodeLab/edx-sga
class StaffGradedAssignmentXBlock(StudioEditableXBlockMixin,
                                  ShowAnswerXBlockMixin, XBlock):
    """
    This block defines a Staff Graded Assignment.  Students are shown a rubric
    and invited to upload a file which is then graded by staff.
    """
    has_score = True
    icon_class = 'problem'
    STUDENT_FILEUPLOAD_MAX_SIZE = 4 * 1000 * 1000  # 4 MB
    editable_fields = ('display_name', 'points', 'weight', 'showanswer',
                       'solution')

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        return block

    def add_xml_to_node(self, node):
        """
        Override default serialization to output solution field as a separate child element.
        """
        super().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,raise-missing-from
        """
        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
        """
        Save a students submission file.
        """
        require(self.upload_allowed())
        user = self.get_real_user()
        require(user)
        upload = request.params['assignment']
        sha1 = get_sha1(upload.file)
        if self.file_size_over_limit(upload.file):
            raise JsonHandlerError(
                413, 'Unable to upload file. Max size limit is {size}'.format(
                    size=self.student_upload_max_size()))
        # Uploading an assignment represents a change of state with this user in this block,
        # so we need to ensure that the user has a StudentModule record, which represents that state.
        self.get_or_create_student_module(user)
        answer = {
            "sha1": sha1,
            "filename": upload.file.name,
            "mimetype": mimetypes.guess_type(upload.file.name)[0],
            "finalized": False
        }
        student_item_dict = self.get_student_item_dict()
        submissions_api.create_submission(student_item_dict, answer)
        path = self.file_storage_path(sha1, upload.file.name)
        log.info("Saving file: %s at path: %s for user: %s", upload.file.name,
                 path, user.username)
        if default_storage.exists(path):
            # save latest submission
            default_storage.delete(path)
        default_storage.save(path, File(upload.file))
        return Response(json_body=self.student_state())

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

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

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

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

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

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

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

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

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

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

        return Response(json_body=self.staff_grading_data())

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

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

                # check if some one reset submission. If yes the recreate zip file
                assignment_count = len(assignments)
                if self.count_archive_files(user) != assignment_count:
                    zip_file_ready = False

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

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

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

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

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

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

    def studio_view(self, context=None):
        """
        Render a form for editing this XBlock
        """
        # this method only exists to provide context=None for backwards compat
        return super().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 str(self.scope_ids.usage_id)

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

        return None

    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']

        return None

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

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

    def get_student_module(self, module_id):
        """
        Returns a StudentModule that matches the given id
        Args:
            module_id (int): The module id
        Returns:
            StudentModule: A StudentModule object
        """
        return StudentModule.objects.get(pk=module_id)

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

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

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

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

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

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

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

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

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

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

    def download(self, path, mime_type, filename, require_staff=False):
        """
        Return a file from storage and return in a Response.
        """
        try:
            content_disposition = "attachment; filename*=UTF-8''"
            content_disposition += six.moves.urllib.parse.quote(
                filename.encode('utf-8'))
            output = Response(app_iter=file_contents_iter(path),
                              content_type=mime_type,
                              content_disposition=content_disposition)
            return output
        except OSError:
            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-function-docstring
        # pylint: disable=no-member
        log.error(
            "enter_grade: invalid grade submitted for course:%s module:%s student:%s",
            course_id, self.location, username)
        return {"error": "Please enter valid grade"}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    def runtime_user_is_staff(self):
        """
        Is the logged in user a staff user?
        """
        return self.is_course_staff()
コード例 #17
0
class ScormXBlock(XBlock):
    has_score = True
    has_author_view = True
    has_custom_completion = True

    display_name = String(display_name=_("Display Name"),
                          help=_("Display name for this module"),
                          default="SCORM",
                          scope=Scope.settings)
    description = String(
        display_name=_("Description"),
        help=
        _("Brief description of the SCORM modules will be displayed above the player. Can contain HTML."
          ),
        default="",
        scope=Scope.settings)
    scorm_file = String(display_name=_("Upload scorm file (.zip)"),
                        help=_('Upload a new SCORM package.'),
                        scope=Scope.settings)
    scorm_player = String(
        values=[{
            "value": key,
            "display_name": DEFINED_PLAYERS[key]['name']
        } for key in DEFINED_PLAYERS.keys()] + [
            SCORM_PKG_INTERNAL,
        ],
        display_name=_("SCORM player"),
        help=
        _("SCORM player configured in Django settings, or index.html file contained in SCORM package"
          ),
        scope=Scope.settings)
    # this stores latest raw SCORM API data in JSON string
    raw_scorm_status = String(scope=Scope.user_state, default='{}')
    scorm_initialized = Boolean(scope=Scope.user_state, default=False)
    lesson_status = String(scope=Scope.user_state, default='not attempted')
    lesson_score = Float(scope=Scope.user_state, default=0)
    scorm_progress = Float(scope=Scope.user_state, default=0)
    weight = Integer(
        default=1,
        help=
        _('SCORM block\'s problem weight in the course, in points.  If not graded, set to 0'
          ),
        scope=Scope.settings)
    auto_completion = Boolean(
        display_name=_("Enable completion upon viewing SCORM file"),
        default=False,
        scope=Scope.settings)
    is_next_module_locked = Boolean(
        display_name=_("Locking"),
        help=
        _('Enable requirement to complete SCORM content before moving to next module'
          ),
        default=False,
        scope=Scope.settings,
    )
    display_type = String(
        display_name=_("Display Type"),
        values=["iframe", "popup"],
        default="iframe",
        help=_(
            "Open in a new popup window, or an iframe.  This setting may be overridden by "
            "player-specific configuration."),
        scope=Scope.settings)
    popup_launch_type = String(
        display_name=_("Popup Launch Type"),
        values=["auto", "manual"],
        default="auto",
        help=_("Open in a new popup through button or automatically."),
        scope=Scope.settings)
    launch_button_text = String(display_name=_("Launch Button Text"),
                                help=_("Display text for Launch Button"),
                                default="Launch",
                                scope=Scope.settings)
    display_width = Integer(display_name=_("Display Width (px)"),
                            help=_('Width of iframe or popup window'),
                            default=820,
                            scope=Scope.settings)
    display_height = Integer(display_name=_("Display Height (px)"),
                             help=_('Height of iframe or popup window'),
                             default=450,
                             scope=Scope.settings)
    encoding = String(
        display_name=_("SCORM Package text encoding"),
        default='cp850',
        help=_(
            "Character set used in SCORM package.  Defaults to cp850 (or IBM850), "
            "for Latin-1: Western European languages)"),
        values=[{
            "value": AVAIL_ENCODINGS[key],
            "display_name": key
        } for key in sorted(AVAIL_ENCODINGS.keys())],
        scope=Scope.settings)
    player_configuration = String(
        display_name=_("Player Configuration"),
        default='',
        help=
        _("JSON object string with overrides to be passed to selected SCORM player.  "
          "These will be exposed as data attributes on the host iframe and sent in a window.postMessage "
          "to the iframe's content window. Attributes can be any.  "
          "'Internal player' will always check this field for an 'initial_html' attribute "
          "to override index.html as the initial page."),
        scope=Scope.settings)
    scorm_file_name = String(display_name=_("Scorm File Name"),
                             help=_("Scorm Package Uploaded File Name"),
                             default="",
                             scope=Scope.settings)
    file_uploaded_date = DateTime(default=None,
                                  scope=Scope.settings,
                                  help="Scorm File Last Uploaded Date")

    @property
    def student_id(self):
        if hasattr(self, "scope_ids"):
            return self.scope_ids.user_id
        else:
            return None

    @property
    def student_name(self):
        if hasattr(self, "xmodule_runtime"):
            user = self.xmodule_runtime._services['user'].get_current_user()
            try:
                return user.display_name
            except AttributeError:
                return user.full_name
        else:
            return None

    @property
    def course_id(self):
        if hasattr(self, "xmodule_runtime"):
            return self._serialize_opaque_key(self.xmodule_runtime.course_id)
        else:
            return None

    def _reverse_student_name(self, name):
        parts = name.split(' ', 1)
        parts.reverse()
        return ', '.join(parts)

    def _serialize_opaque_key(self, key):
        if hasattr(key, 'to_deprecated_string'):
            return key.to_deprecated_string()
        else:
            return six.text_type(key)

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

    def student_view(self, context=None, authoring=False):
        scheme = 'https' if settings.HTTPS == 'on' else 'http'
        lms_base = settings.ENV_TOKENS.get('LMS_BASE')
        if isinstance(context, QueryDict):
            context = context.dict()

        scorm_player_url = ""

        course_directory = self.scorm_file
        if self.scorm_player == 'SCORM_PKG_INTERNAL':
            # TODO: support initial filename other than index.html for internal players
            scorm_player_url = '{}://{}{}'.format(scheme, lms_base,
                                                  self.scorm_file)
        elif self.scorm_player:
            player_config = DEFINED_PLAYERS[self.scorm_player]
            player = player_config['location']
            if '://' in player:
                scorm_player_url = player
            else:
                scorm_player_url = '{}://{}{}'.format(scheme, lms_base, player)
            course_directory = '{}://{}{}'.format(
                scheme, lms_base,
                self.runtime.handler_url(self, "proxy_content"))

        html = self.resource_string("static/html/scormxblock.html")

        # don't call handlers if student_view is not called from within LMS
        # (not really a student)
        if not authoring:
            get_url = '{}://{}{}'.format(
                scheme, lms_base,
                self.runtime.handler_url(self, "get_raw_scorm_status"))
            set_url = '{}://{}{}'.format(
                scheme, lms_base,
                self.runtime.handler_url(self, "set_raw_scorm_status"))
            get_completion_url = '{}://{}{}'.format(
                scheme, lms_base,
                self.runtime.handler_url(self, "get_scorm_completion"))
        # PreviewModuleSystem (runtime Mixin from Studio) won't have a hostname
        else:
            # we don't want to get/set SCORM status from preview
            get_url = set_url = get_completion_url = '#'

        # if display type is popup, don't use the full window width for the host iframe
        iframe_width = self.display_type == 'popup' and DEFAULT_IFRAME_WIDTH or self.display_width
        iframe_height = self.display_type == 'popup' and DEFAULT_IFRAME_HEIGHT or self.display_height
        show_popup_manually = self.display_type == 'popup' and self.popup_launch_type == 'manual'
        lock_next_module = self.is_next_module_locked and self.scorm_progress < constants.MAX_PROGRESS_VALUE
        try:
            player_config = json.loads(self.player_configuration)
        except ValueError:
            player_config = {}

        frag = Fragment()
        frag.add_content(
            MakoTemplate(text=html.format(
                self=self,
                scorm_player_url=scorm_player_url,
                get_url=get_url,
                set_url=set_url,
                get_completion_url=get_completion_url,
                iframe_width=iframe_width,
                iframe_height=iframe_height,
                player_config=player_config,
                show_popup_manually=show_popup_manually,
                scorm_file=course_directory,
                is_next_module_locked=lock_next_module)).render_unicode())

        frag.add_css(self.resource_string("static/css/scormxblock.css"))
        context['block_id'] = self.url_name
        js = self.resource_string("static/js/src/scormxblock.js")
        jsfrag = MakoTemplate(js).render_unicode(**context)
        frag.add_javascript(jsfrag)

        # TODO: this will only work to display staff debug info if 'scormxblock' is one of the
        # categories of blocks that are specified in lms/templates/staff_problem_info.html so this will
        # for now have to be overridden in theme or directly in edx-platform
        # TODO: is there another way to approach this?  key's location.category isn't mutable to spoof 'problem',
        # like setting the name in the entry point to 'problem'.  Doesn't seem like a good idea.  Better to
        # have 'staff debuggable' categories configurable in settings or have an XBlock declare itself staff debuggable
        if SCORM_DISPLAY_STAFF_DEBUG_INFO and not authoring:  # don't show for author preview
            from courseware.access import has_access
            from courseware.courses import get_course_by_id

            course = get_course_by_id(self.xmodule_runtime.course_id)
            dj_user = self.xmodule_runtime._services['user']._django_user
            has_instructor_access = bool(
                has_access(dj_user, 'instructor', course))
            if has_instructor_access:
                disable_staff_debug_info = settings.FEATURES.get(
                    'DISPLAY_DEBUG_INFO_TO_STAFF', True) and False or True
                block = self
                view = 'student_view'
                frag = add_staff_markup(dj_user, has_instructor_access,
                                        disable_staff_debug_info, block, view,
                                        frag, context)

        frag.initialize_js('ScormXBlock_{0}'.format(context['block_id']))
        return frag

    def author_view(self, context=None):
        return self.student_view(context, authoring=True)

    def studio_view(self, context=None):
        html = self.resource_string("static/html/studio.html")
        frag = Fragment()
        file_uploaded_date = get_default_time_display(
            self.file_uploaded_date) if self.file_uploaded_date else ''
        context = {'block': self, 'file_uploaded_date': file_uploaded_date}
        frag.add_content(MakoTemplate(text=html).render_unicode(**context))
        frag.add_css(self.resource_string("static/css/scormxblock.css"))
        frag.add_javascript(self.resource_string("static/js/src/studio.js"))
        frag.add_javascript_url(
            self.runtime.local_resource_url(self,
                                            'public/jquery.fileupload.js'))
        frag.initialize_js('ScormStudioXBlock')
        return frag

    @XBlock.handler
    def upload_status(self, request, suffix=''):
        """
        Scorm package upload to storage status
        """
        upload_percent = ScormPackageUploader.get_upload_percentage(
            self.location.block_id)

        logger.info('Upload percentage is: {}'.format(upload_percent))

        return Response(json_body=json.dumps({"progress": upload_percent}))

    @XBlock.handler
    def file_upload_handler(self, request, suffix=''):
        """
        Handler for scorm file upload
        """
        response = {}
        scorm_uploader = ScormPackageUploader(
            request=request, xblock=self, scorm_storage_location=SCORM_STORAGE)

        try:
            state, data = scorm_uploader.upload()
        except Exception as e:
            logger.error('Scorm package upload error: {}'.format(e.message))
            ScormPackageUploader.clear_percentage_cache(self.location.block_id)
            return Response(json_body=json.dumps({
                'status': 'error',
                'message': e.message
            }))

        if state == UPLOAD_STATE.PROGRESS:
            response = {"files": [{"size": data}]}
        elif state == UPLOAD_STATE.COMPLETE and data:
            ScormPackageUploader.clear_percentage_cache(self.location.block_id)
            self.scorm_file = data
            response = {'status': 'OK'}

        return Response(json_body=json.dumps(response))

    @XBlock.handler
    def studio_submit(self, request, suffix=''):
        self.display_name = request.params['display_name']
        self.description = request.params['description']
        self.weight = request.params['weight']
        self.display_width = request.params['display_width']
        self.display_height = request.params['display_height']
        self.display_type = request.params['display_type']
        self.launch_button_text = request.params['launch_button_text']
        self.popup_launch_type = request.params['popup_launch_type']
        self.scorm_player = request.params['scorm_player']
        self.encoding = request.params['encoding']
        self.auto_completion = request.params['auto_completion']
        self.is_next_module_locked = request.params['is_next_module_locked']
        if request.params['new_scorm_file_uploaded'] == 'true':
            self.scorm_file_name = request.params['scorm_file_name']
            self.file_uploaded_date = datetime.utcnow().replace(
                tzinfo=pytz.utc)

        if request.params['player_configuration']:
            try:
                json.loads(
                    request.params['player_configuration'])  # just validation
                self.player_configuration = request.params[
                    'player_configuration']
            except ValueError as e:
                return Response(json_body=json.dumps({
                    'result':
                    'failure',
                    'error':
                    'Invalid JSON in Player Configuration'.format(e)
                }),
                                content_type='application/json')

        return Response(json_body=json.dumps({'result': 'success'}),
                        content_type='application/json')

    # if player sends SCORM API JSON directly
    @XBlock.json_handler
    def scorm_get_value(self, data, suffix=''):
        name = data.get('name')
        if name == 'cmi.core.lesson_status':
            return {'value': self.lesson_status}
        return {'value': ''}

    # if player sends SCORM API JSON directly
    @XBlock.json_handler
    def scorm_set_value(self, data, suffix=''):
        context = {'result': 'success'}
        name = data.get('name')
        if name == 'cmi.core.lesson_status' and data.get(
                'value') != 'completed':
            self.lesson_status = data.get('value')
            self._publish_grade()
            context.update({"lesson_score": self.lesson_score})
        if name == 'cmi.core.score.raw':
            self._set_lesson_score(data.get('value', 0))
        return context

    def _get_all_scos(self):
        return json.loads(self.raw_scorm_status).get('scos', None)

    def _status_serialize_key(self, key, val):
        """
        update value in JSON serialized raw_scorm_status
        passing a string key and a deserialized object
        """
        status = json.loads(self.raw_scorm_status)
        status[key] = val
        self.raw_scorm_status = json.dumps(status)

    def _scos_set_values(self, key, val, overwrite=False):
        """
        set a value for a key on all scos
        return new full raw scorm data
        """
        scos = self._get_all_scos()
        if scos:
            for sco in scos:
                if not scos[sco].get('key') or (scos[sco].get('key')
                                                and overwrite):
                    scos[sco][key] = val
            self._status_serialize_key('scos', scos)

    def _init_scos(self):
        """
        initialize all SCOs with proper credit and status values in case
        content package does not do this correctly
        """

        # set all scos lesson status to 'not attempted'
        # set credit/no-credit on all scos
        credit = self.weight > 0 and 'credit' or 'no-credit'
        self._scos_set_values('cmi.core.credit', credit)
        self._scos_set_values('cmi.core.lesson_status', 'not attempted', True)
        self.scorm_initialized = True

    @XBlock.handler
    def get_raw_scorm_status(self, request, suffix=''):
        """
        retrieve JSON SCORM API status as stored by SSLA player (or potentially others)
        """
        # TODO: handle errors
        # TODO: this is specific to SSLA player at this point.  evaluate for broader use case
        response = Response(json_body=self.raw_scorm_status,
                            content_type='application/json',
                            charset='UTF-8')
        if self.auto_completion:
            # Mark 100% progress upon launching the scorm content if auto_completion is true
            self._publish_progress(constants.MAX_PROGRESS_VALUE)

        return response

    @XBlock.handler
    def set_raw_scorm_status(self, request, suffix=''):
        """
        store JSON SCORM API status from SSLA player (or potentially others)
        """
        # TODO: this is specific to SSLA player at this point.  evaluate for broader use case
        data = request.POST['data']
        scorm_data = json.loads(data)

        new_status = scorm_data.get('status', 'not attempted')

        if not self.scorm_initialized:
            self._init_scos()

        old_scorm_data = json.loads(self.raw_scorm_status)
        self.raw_scorm_status = data

        self.lesson_status = new_status
        score = scorm_data.get('score', '')
        self._publish_grade(new_status, score)
        self.publish_progress(old_scorm_data, scorm_data)
        self.save()

        # TODO: handle errors
        return Response(json_body=json.dumps(self.raw_scorm_status),
                        content_type='application/json',
                        charset='UTF-8')

    @XBlock.handler
    def get_scorm_completion(self, request, suffix=''):
        completion = {'completion': self.scorm_progress or 0}
        return Response(json_body=json.dumps(completion),
                        content_type='application/json')

    @XBlock.handler
    def proxy_content(self, request, suffix=''):
        storage = default_storage
        contents = ''
        content_type = 'application/octet-stream'
        path_to_file = os.path.join(SCORM_STORAGE, self.location.block_id,
                                    suffix)

        if storage.exists(path_to_file):
            f = storage.open(path_to_file, 'rb')
            contents = f.read()
            ext = os.path.splitext(path_to_file)[1]
            if ext in mimetypes.types_map:
                content_type = mimetypes.types_map[ext]
        else:
            return Response('Did not exist in storage: ' + path_to_file,
                            status=404,
                            content_type='text/html',
                            charset='UTF-8')
        return Response(contents, content_type=content_type)

    def generate_report_data(self, user_state_iterator, limit_responses=None):
        """
        Return a list of student responses to this block in a readable way.
        Arguments:
            user_state_iterator: iterator over UserStateClient objects.
                E.g. the result of user_state_client.iter_all_for_block(block_key)
            limit_responses (int|None): maximum number of responses to include.
                Set to None (default) to include all.
        Returns:
            each call returns a tuple like:
            ("username", {
                           "Question": "What's your favorite color?"
                           "Answer": "Red",
                           "Submissions count": 1
            })
        """

        count = 0
        for user_state in user_state_iterator:
            for report in self._get_user_report(user_state.state):

                if limit_responses is not None and count >= limit_responses:
                    # End the iterator here
                    return

                count += 1
                yield (user_state.username, report)

    def _get_user_report(self, user_state):
        interaction_prefix = "cmi.interactions."
        raw_status = json.loads(user_state['raw_scorm_status'])
        scos = raw_status.get('scos', {})

        for sco in scos.values():
            sco_data = sco.get('data') or {}
            interactions_count = sco_data.get(interaction_prefix + '_count', 0)

            for interaction_index in range(interactions_count):
                current_interaction_prefix = interaction_prefix + str(
                    interaction_index) + "."

                report = {
                    self.ugettext('Question'):
                    sco_data.get(current_interaction_prefix + "description"),
                    self.ugettext('Answer'):
                    sco_data.get(current_interaction_prefix +
                                 "learner_response"),
                    self.ugettext('Submissions count'):
                    interactions_count
                }
                yield report

    def _get_value_from_sco(self, sco, key, default):
        """
        return a set or default value from a key in a SCO
        treat blank string values as empty
        """
        try:
            val = sco[key]
        except (KeyError, ValueError):
            return default
        finally:
            if str(val) == '':
                return default
            else:
                return val

    def _set_lesson_score(self, scos):
        """
        roll up a total lesson score from an average of SCO scores
        """
        # note SCORM 2004+ supports complex weighting of scores from multiple SCOs
        # see http://scorm.com/blog/2009/10/score-rollup-in-scorm-1-2-theres-no-silver-bullet/
        # For now we will weight each SCO equally and take an average
        # TODO: handle more complex weighting when we support SCORM2004+
        total_score = 0
        for sco in scos.keys():
            sco = scos[sco]['data']
            total_score += int(
                self._get_value_from_sco(sco, 'cmi.core.score.raw', 0))
        score_rollup = float(total_score) / float(len(list(scos.keys())))
        self.lesson_score = score_rollup
        return score_rollup

    def _publish_grade(self, status, score):
        """
        publish the grade in the LMS.
        """

        # We must do this regardless of the lesson
        # status to avoid race condition issues where a grade of None might overwrite a
        # grade value for incomplete lesson statuses.

        # translate the internal score as a percentage of block's weight
        # we are assuming here the best practice for SCORM 1.2 of a max score of 100
        # if we weren't dealing with KESDEE publisher's incorrect usage of cmi.core.score.max
        # we could compute based on a real max score
        # in practice, SCOs will almost certainly have a max of 100
        # http://www.ostyn.com/blog/2006/09/scoring-in-scorm.html
        # TODO: handle variable max scores when we support SCORM2004+ or a better KESDEE workaround
        if score != '':
            self.runtime.publish(
                self, 'grade', {
                    'value': (float(score) / float(DEFAULT_SCO_MAX_SCORE)) *
                    self.weight,
                    'max_value': self.weight,
                })

    def publish_progress(self, old_scorm_data, current_scorm_data):
        """
        Update progress % if cmi.progress_measure is emitted (i.e. it exists)
        Else check status and mark 100% completion if course is complete
        """
        progress_measure = self.calculate_progress_measure(current_scorm_data)
        if progress_measure:
            # We do not want the elif to run if progress_measure exits but is invalid
            if self.is_progress_measure_valid(progress_measure,
                                              old_scorm_data):
                self._publish_progress(progress_measure)
        elif current_scorm_data.get('status',
                                    '') in constants.SCORM_COMPLETION_STATUS:
            self._publish_progress(constants.MAX_PROGRESS_VALUE)

    def _publish_progress(self, completion):
        """
        Update completion by calling the completion API
        """
        self.scorm_progress = completion
        self.runtime.publish(self, 'completion', {'completion': completion})

    def calculate_progress_measure(self, scorm_data):
        """
        Returns the averaged progress_measure of all scos in the current scorm content
        :return: progress_measure if found, else 0
        """
        progress_sum = 0
        scos = scorm_data.get('scos', {})
        for sco in scos.values():
            sco_data = sco.get('data', {})
            try:
                progress_sum += float(
                    sco_data.get('cmi.progress_measure', '0.0'))
            except (ValueError, AttributeError):
                pass

        return progress_sum / len(scos) if len(scos) else 0

    def is_progress_measure_valid(self, current_progress_measure,
                                  old_scorm_data):
        """
        - Checks if the current progress (to be updated on the LMS) is greater than
        the previously stored progress
        - This is done to ensure that restarting a scorm course does not resets its
        progress on our LMS
        """
        if old_scorm_data:
            # If old data exists for comparison
            old_progress_measure = self.calculate_progress_measure(
                old_scorm_data)
            if old_progress_measure:
                if current_progress_measure > old_progress_measure:
                    return True
            return False
        else:
            # If nothing to compare, return valid
            return True

    @staticmethod
    def workbench_scenarios():
        """A canned scenario for display in the workbench."""
        return [
            ("ScormXBlock", """<vertical_demo>
                <scormxblock/>
                </vertical_demo>
             """),
        ]
コード例 #18
0
class DoneXBlock(XBlock):
    """
    Show a toggle which lets students mark things as done.
    """

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

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

    has_score = True

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

        return {'state': self.done}

    # Decorate the view in order to support multiple devices e.g. mobile
    # See: https://openedx.atlassian.net/wiki/display/MA/Course+Blocks+API
    # section 'View @supports(multi_device) decorator'
    @XBlock.supports('multi_device')
    def student_view(self, context=None):  # pylint: disable=unused-argument
        """
        The primary view of the DoneXBlock, shown to students
        when viewing courses.
        """
        html_resource = resource_string("static/html/done.html")
        html = html_resource.format(done=self.done, id=uuid.uuid1(0))
        (unchecked_png,
         checked_png) = (self.runtime.local_resource_url(self, x)
                         for x in ('public/check-empty.png',
                                   'public/check-full.png'))

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

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

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

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

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

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

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

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

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

    def max_score(self):
        """The maximum raw score of our problem.
        """
        return 1
コード例 #19
0
class AnimationXBlock(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.
    animation = List(
        default=[],
        scope=Scope.settings,
        help="Animation",
    )

    height = Integer(scope=Scope.settings, help="Height")

    textheight = Integer(scope=Scope.settings, help="Text Height")

    width = Integer(scope=Scope.settings, help="Width")

    position = Integer(scope=Scope.user_state,
                       help="Current position",
                       default=0)

    max_position = Integer(scope=Scope.user_state,
                           help="Maximum position (for progress)",
                           default=0)

    @XBlock.json_handler
    def update_position(self, data, suffix):
        if 'position' in data:
            self.position = data['position']
        if 'max_position' in data:
            self.max_position = data['max_position']
            grade = self.max_position / float(len(self.animation))
            self.runtime.publish(self, 'grade', {
                'value': grade,
                'max_value': 1
            })
        return {"status": "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")

    # TO-DO: change this view to display your data your own way.
    def student_view(self, context=None):
        """
        The primary view of the AnimationXBlock, shown to students
        when viewing courses.
        """
        html = self.resource_string("static/html/animation.html")
        frag = Fragment(
            html.format(height=self.height,
                        textheight=self.textheight,
                        width=self.width,
                        inner_width=self.width - 20,
                        animation=json.dumps(self.animation),
                        position=self.position,
                        max_position=self.max_position))
        #        frag.add_javascript_url("//ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js")
        frag.add_css_url(
            "//ajax.googleapis.com/ajax/libs/jqueryui/1.10.4/themes/smoothness/jquery-ui.css"
        )
        frag.add_css(
            self.resource_string("static/css/jquery.ui.labeledslider.css"))
        frag.add_javascript_url(
            "//ajax.googleapis.com/ajax/libs/jqueryui/1.10.4/jquery-ui.min.js")
        frag.add_javascript(
            self.resource_string("static/js/src/jquery.ui.labeledslider.js"))
        frag.add_css(self.resource_string("static/css/animation.css"))
        frag.add_javascript(self.resource_string("static/js/src/animation.js"))
        frag.initialize_js('AnimationXBlock')
        return frag

    @classmethod
    def parse_xml(cls, node, runtime, keys, id_generator):
        """
        Parse the XML for an HTML block.

        The entire subtree under `node` is re-serialized, and set as the
        content of the XBlock.

        """
        block = runtime.construct_xblock_from_class(cls, keys)
        animation = []

        element = {"desc": ""}  # Dummy; ignored
        for line in node.text.split('\n'):
            line = line.strip()
            if line.startswith("http"):
                element = {"src": line, "desc": ""}
                animation.append(element)
            else:
                element["desc"] = element["desc"] + " " + line

        block.animation = animation
        block.height = node.attrib["height"]
        block.textheight = node.attrib["textheight"]
        block.width = node.attrib["width"]

        return block

    # 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 [
            ("AnimationXBlock", """<vertical_demo>
                <animation width="460" height="384" textheight="100">
http://upload.wikimedia.org/wikipedia/commons/e/e8/Pin_tumbler_no_key.svg
Without a key in the lock, the driver pins (blue) are pushed downwards, preventing the plug (yellow) from rotating.
http://upload.wikimedia.org/wikipedia/commons/5/54/Pin_tumbler_bad_key.svg
When an incorrect key is inserted into the lock, the key pins (red) and driver pins (blue) do not align with the shear line; therefore, it does not allow the plug (yellow) to rotate.
http://upload.wikimedia.org/wikipedia/commons/6/6e/Pin_tumbler_with_key.svg
When the correct key is inserted, the gaps between the key pins (red) and driver pins (blue) align with the edge of the plug (yellow).
http://upload.wikimedia.org/wikipedia/commons/e/e1/Pin_tumbler_unlocked.svg
With the gaps between the pins aligned with the shear line, the plug (yellow) can rotate freely.
                </animation>
                </vertical_demo>
             """),
        ]

    ## Everything below is stolen from https://github.com/edx/edx-ora2/blob/master/apps/openassessment/xblock/lms_mixin.py
    ## It's needed to keep the LMS+Studio happy.
    ## It should be included as a mixin.
    ##
    ## The only LMS functionality we need and use is grading. Cale
    ## believes most of this is unnecessary, but I did not want to do
    ## a binary search for what is and is not necessary, since this is
    ## effectively a TODO.

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

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

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

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

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

    def max_score(self):
        """The maximum raw score of our problem.
        """
        return 1
コード例 #20
0
class DoneXBlock(XBlock):
    """
    Show a toggle which lets students mark things as done.
    """
    done = Boolean(
        scope=Scope.user_state,
        help="Is the student done?",
        default=False
    )

    helpful = Boolean(
        scope=Scope.user_state,
        help="Was the unit helpful?"
    )

    display_type = String(
        scope=Scope.settings,
        help="Toggle student view.",
        default='helpful'
    )

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

    has_score = True

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

        if 'helpful' in data:
            self.helpful = data['helpful']
            if data['helpful']:
                grade = 1
            else:
                grade = 0
            grade_event = {'value': grade, 'max_value': 1}
            self.runtime.publish(self, 'grade', grade_event)
            # This should move to self.runtime.publish, once that pipeline
            # is finished for XBlocks.
            self.runtime.publish(self, "edx.done.toggled", {'done': self.done, 'helpful': self.helpful})

        return {'state': {'done': self.done, 'helpful': self.helpful}}

    @XBlock.handler
    def get_state(self, *args, **kwargs):
        return Response(body=json.dumps({
            'state': self.done,
            'helpful': self.helpful,
        }))

    def student_view(self, context=None):  # pylint: disable=unused-argument
        """
        The primary view of the DoneXBlock, shown to students
        when viewing courses.
        """
        context = {
            'done': self.done,
            'helpful': self.helpful,
            'id': uuid.uuid1(0),
            'display_type': self.display_type
        }
        html = self.render_template('static/html/done.html', context)

        (unchecked_png, checked_png) = (
            self.runtime.local_resource_url(self, x) for x in
            ('public/check-empty.png', 'public/check-full.png')
        )

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

    def studio_view(self, _context=None):  # pylint: disable=unused-argument
        '''
        Minimal view with no configuration options giving some help text.
        '''
        context = {
            'display_type': self.display_type
        }
        html = self.render_template('static/html/studioview.html', context)

        frag = Fragment(html)

        js_str = pkg_resources.resource_string(__name__, "static/js/src/studio_edit.js")
        frag.add_javascript(unicode(js_str))
        frag.initialize_js('StudioEdit')

        return frag

    @XBlock.handler
    def studio_submit(self, request, suffix=''):
        """
        Called when submitting the form in Studio.
        """
        self.display_type = request.POST['display_type']

        return Response(json_body={
            'result': "success"
        })

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


    def load_resource(self, resource_path):
        """
        Gets the content of a resource
        """
        resource_content = pkg_resources.resource_string(__name__, resource_path)
        return unicode(resource_content.decode('utf-8'))


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

        return Template(template_str).render(Context(context))

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

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

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

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

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

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

    def max_score(self):
        """The maximum raw score of our problem.
        """
        return 1
コード例 #21
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

    @XBlock.supports('multi_device') # Mark as mobile-friendly
    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)
コード例 #22
0
class RocketChatXBlock(XBlock, XBlockWithSettingsMixin,
                       StudioEditableXBlockMixin):
    """
    This class allows to embed a chat window inside a unit course
    and set the necessary variables to config the rocketChat enviroment
    """
    display_name = String(display_name=_("Display Name"),
                          scope=Scope.settings,
                          default="Rocket Chat")
    email = String(
        default="",
        scope=Scope.user_state,
        help="Email in rocketChat",
    )
    rocket_chat_role = String(default="user",
                              scope=Scope.user_state,
                              help="The defined role in rocketChat")

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

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

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

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

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

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

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

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

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

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

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

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

    has_score = graded_activity
    team_view = True

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

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

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

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

        if in_studio_runtime:
            return self.author_view(context)

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

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

    def author_view(self, context=None):
        """  Returns author view fragment on Studio """
        # pylint: disable=unused-argument
        self.api_rocket_chat.convert_to_private_channel("general")
        frag = Fragment(u"Studio Runtime RocketChatXBlock")
        frag.initialize_js('RocketChatXBlock')

        return frag

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

    # TO-DO: change this to create the scenarios you'd like to see in the
    # workbench while developing your XBlock.
    @staticmethod
    def workbench_scenarios():
        """A canned scenario for display in the workbench."""
        return [
            ("RocketChatXBlock", """<rocketc/>
             """),
            ("Multiple RocketChatXBlock", """<vertical_demo>
                <rocketc/>
                <rocketc/>
                <rocketc/>
                </vertical_demo>
             """),
        ]

    def _api_rocket_chat(self):
        """
        Creates an ApiRocketChat object
        """
        try:
            user = self.xblock_settings["admin_user"]
            password = self.xblock_settings["admin_pass"]
        except KeyError:
            LOG.exception("The admin's settings has not been found")
            raise
        api = ApiRocketChat(user, password,
                            self.server_data["private_url_service"])

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

        return api

    def _api_teams(self):
        """
        Creates an ApiTeams object
        """
        try:
            client_id = self.xblock_settings["client_id"]
            client_secret = self.xblock_settings["client_secret"]
        except KeyError:
            raise

        server_url = settings.LMS_ROOT_URL

        api = ApiTeams(client_id, client_secret, server_url)

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

        return api

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

    @property
    def user_data(self):
        """
        This method initializes the user's parameters
        """
        runtime = self.xmodule_runtime  # pylint: disable=no-member
        user = runtime.service(self, 'user').get_current_user()
        user_data = {}
        user_data["email"] = user.emails[0]
        user_data["role"] = runtime.get_user_role()
        user_data["course_id"] = runtime.course_id
        user_data["course"] = re.sub('[^A-Za-z0-9]+', '',
                                     runtime.course_id.to_deprecated_string())  # pylint: disable=protected-access
        user_data["username"] = user.opt_attrs['edx-platform.username']
        user_data["anonymous_student_id"] = runtime.anonymous_student_id
        return user_data

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

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

    def get_groups(self):
        """
        This method lists the existing groups
        """
        api = self._api_teams()
        teams = api.get_course_teams(self.runtime.course_id)
        topics = [re.sub(r'\W+', '', team["topic_id"]) for team in teams]

        # the following instructions get all the groups
        # except the groups with the string of some topic in its name
        query = {
            'name': {
                '$regex': '^(?!.*({topics}).*$)'.format(
                    topics='|'.join(topics))
            }
        } if topics else {}
        kwargs = {"query": json.dumps(query)}
        groups = self._api_rocket_chat().get_groups(**kwargs)

        # these instructions get all the groups with the customField "specificTeam" set
        query = {'customFields.specificTeam': {'$regex': '^.*'}}
        kwargs = {'query': json.dumps(query)}
        team_groups = self._api_rocket_chat().get_groups(**kwargs)

        # This instruction adds the string "(Team Group)" if the group is in team_groups
        # pylint: disable=line-too-long
        groups = [
            '{}-{}'.format('(Team Group)', group)
            if group in team_groups else group for group in groups
        ]
        groups.append("")
        return sorted(groups)

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

        user_data = self.user_data

        response = self.login(user_data)
        if response['success']:
            response = response['data']
            user_id = response['userId']
            auth_token = response['authToken']

            response['default_group'] = self._join_user_to_groups(
                user_id, user_data, auth_token)
            self._update_user(user_id, user_data)

            if user_data[
                    "role"] == "instructor" and self.rocket_chat_role != "bot":
                self.api_rocket_chat.change_user_role(user_id, "bot")

            self._grading_discussions(response['default_group'])
            return response
        else:
            return response['errorType']

    def login(self, user_data):
        """
        This method allows to get the user's authToken and id
        or creates a user to login in RocketChat
        """
        api = self.api_rocket_chat

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

        key = hashlib.sha1("{}_{}".format(ROCKET_CHAT_DATA,
                                          user_data["username"])).hexdigest()
        data = cache.get(key)

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

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

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

    def _add_user_to_course_group(self, group_name, user_id):
        """
        This method add the user to the default course channel
        """
        api = self.api_rocket_chat
        rocket_chat_group = api.search_rocket_chat_group(group_name)

        if rocket_chat_group['success']:
            api.add_user_to_group(user_id, rocket_chat_group['group']['_id'])
        else:
            rocket_chat_group = api.create_group(group_name,
                                                 [self.user_data["username"]])

        self.group = api.search_rocket_chat_group(  # pylint: disable=attribute-defined-outside-init
            group_name)

    def _add_user_to_default_group(self, group_name, user_id):
        """
        """
        api = self.api_rocket_chat
        group_info = api.search_rocket_chat_group(group_name)

        if group_info["success"]:
            api.add_user_to_group(user_id, group_info['group']['_id'])
            return True
        return False

    def _add_user_to_team_group(self, user_id, username, course_id,
                                auth_token):
        """
        Add the user to team's group in rocketChat
        """
        self.api_teams = self._api_teams()  # pylint: disable=attribute-defined-outside-init
        team = self._get_team(username, course_id)

        if team is None:
            self._remove_user_from_group(self.team_channel, user_id,
                                         auth_token)
            return False
        topic_id = re.sub(r'\W+', '', team["topic_id"])
        team_name = re.sub(r'\W+', '', team["name"])
        group_name = "-".join(["Team", topic_id, team_name])

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

        self.team_channel = group_name

        response = self._add_user_to_group(user_id, group_name, username)
        LOG.info("Add to team group response: %s", response)
        return response["success"]

    def _get_team(self, username, course_id):
        """
        This method gets the user's team
        """
        team = self.api_teams.get_user_team(course_id, username)
        LOG.info("Get Team response: %s", team)
        if team:
            return team[0]
        return None

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

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

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

            self.ui_is_block = self._add_user_to_default_group(
                default_channel, user_id)
            return default_channel
        else:
            self.ui_is_block = False
            self._add_user_to_course_group(user_data["course"], user_id)
        return None

    def _add_user_to_group(self, user_id, group_name, username):
        group_info = self.api_rocket_chat.search_rocket_chat_group(group_name)

        if group_info["success"]:
            response = self.api_rocket_chat.add_user_to_group(
                user_id, group_info['group']['_id'])
            LOG.info("Add to team group response: %s", response)
            return response

        return self.api_rocket_chat.create_group(group_name, [username])

    def _join_user_to_specific_team_group(self, user_id, user_data,
                                          default_channel):
        self.api_teams = self._api_teams()  # pylint: disable=attribute-defined-outside-init
        team = self._get_team(user_data["username"], user_data["course_id"])
        if team is None:
            self.team_view = False
            return None
        default_channel = self._create_team_group_name(
            team, default_channel.replace("(Team Group)-", ""))
        response = self._add_user_to_group(user_id, default_channel,
                                           user_data["username"])
        self.ui_is_block = response["success"]
        return default_channel

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

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

        return False

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

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

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

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

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

    @XBlock.json_handler
    def create_group(self, data, suffix=""):
        """
        This method allows to create a group
        """
        # pylint: disable=unused-argument
        self.api_rocket_chat = self._api_rocket_chat()  # pylint: disable=attribute-defined-outside-init
        self.api_teams = self._api_teams()  # pylint: disable=attribute-defined-outside-init

        group_name = data["groupName"]
        description = data["description"]
        topic = data["topic"]

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

        group_name = re.sub(r'\W+', '', group_name)

        if data.get("asTeam", False):
            custom_fields = {"customFields": {"specificTeam": group_name}}
            group = self.api_rocket_chat.create_group(group_name,
                                                      **custom_fields)
            if group["success"]:
                self.default_channel = "{}-{}".format('(Team Group)',
                                                      group_name)

        else:
            try:
                course_id = self.xmodule_runtime.course_id  # pylint: disable=no-member
                team = self._get_team(self.user_data["username"], course_id)
                members = list(self.get_team_members(team))
                group = self.api_rocket_chat.create_group(
                    self._create_team_group_name(team, group_name), members)

            except AttributeError:
                group = self.api_rocket_chat.create_group(group_name)
                if group["success"]:
                    self.default_channel = group_name

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

            self.api_rocket_chat.set_group_description(group_id, description)
            self.api_rocket_chat.set_group_topic(group_id, topic)

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

    @staticmethod
    def _create_team_group_name(team, group_name):

        team_topic = re.sub(r'\W+', '', team["topic_id"])
        team_name = re.sub(r'\W+', '', team["name"])
        return "-".join([team_topic, team_name, group_name])

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

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

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

        group = api.search_rocket_chat_group(group_name)
        if group["success"]:
            group = group["group"]
            response = api.kick_user_from_group(user_id, group["_id"])
            return response.get('success', False)
        return False

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

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

        self.api_rocket_chat = self._api_rocket_chat()  # pylint: disable=attribute-defined-outside-init
        username = self.user_data["username"]
        user = self.api_rocket_chat.search_rocket_chat_user(username)
        group_name = data["groupName"]

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

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

        if group_name == self.team_channel or group_name == self.user_data[
                "course"]:
            return {
                "success": False,
                "error": "You Can Not Leave a Main Group"
            }

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

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

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

        course_id = self.xmodule_runtime.course_id  # pylint: disable=no-member
        team = self._get_team(self.user_data["username"], course_id)
        topic = re.sub(r'\W+', '', team["topic_id"])
        team_name = re.sub(r'\W+', '', team["name"])

        regex = "-".join([topic, team_name])
        query = {"name": {"$regex": regex}}
        kwargs = {"query": json.dumps(query)}

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

    def _get_list_groups(self, user_id, auth_token, **kwargs):
        """
        This method allows to get a list of group names
        """
        api = self._api_rocket_chat()
        groups = api.list_all_groups(user_id, auth_token, **kwargs)
        if groups["success"]:
            groups = groups["groups"]
            for group in groups:
                yield group["name"]

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

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

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

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

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

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

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

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

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

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

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

    def max_score(self):
        return self.weight

    @XBlock.handler
    def logout_user(self, request=None, suffix=None):
        """
        This method allows to invalidate the user token
        """
        # pylint: disable=unused-argument
        key = request.GET.get("beacon_rc")
        data = cache.get(key)
        if data:
            api = self._api_rocket_chat()
            user_data = data.get("data")
            login_token = user_data.get("authToken")
            user_id = user_data.get("userId")
            response = api.logout_user(user_id, login_token)
            try:
                response = response.json()
                if response.get("status") == "success":
                    cache.delete(key)
                    return Response(status=202)
            except AttributeError:
                return Response(status=503)

        return Response(status=404)
コード例 #23
0
 def test_none(self):
     self.assertJSONOrSetEquals(None, None)
     self.assertJSONOrSetEquals(None, '')
     self.assertEqual(DateTime().to_json(None), None)
コード例 #24
0
ファイル: essay.py プロジェクト: MasterGowen/Essay-xblock
class Essay(XBlock):
    has_score = True
    icon_class = 'problem'

    display_name = String(
        default='Essay',
        scope=Scope.settings,
        help=u'Имя будет показано над этим блоком и в полосе навигации.')

    points = Integer(display_name='Points count',
                     values={
                         "min": 0,
                         "step": 1
                     },
                     default=10,
                     scope=Scope.settings)

    score = Integer(display_name="Grade score",
                    default=None,
                    values={
                        "min": 0,
                        "step": 1
                    },
                    scope=Scope.user_state)

    comment = String(
        display_name="Instructor comment",
        default='',
        scope=Scope.user_state,
    )

    essay_timestamp = DateTime(display_name="Timestamp",
                               scope=Scope.user_state,
                               default=None)

    def max_score(self):
        return self.points

    def student_view(self, context=None):

        if not self.score_published:
            self.runtime.publish(self, 'grade', {
                'value': self.score,
                'max_value': self.max_score(),
            })
            self.score_published = True

        context = {
            "student_state": json.dumps(self.student_state()),
            "id": self.location.name.replace('.', '_')
        }

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

    def student_state(self):

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

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

    def staff_grading_data(self):
        def get_student_data(module):
            state = json.loads(module.state)
            return {
                'module_id': module.id,
                'username': module.student.username,
                'fullname': module.student.profile.name,
                'timestamp': state.get("essay_timestamp"),
                'score': state.get("score"),
                'comment': state.get("comment", ''),
            }

        query = StudentModule.objects.filter(
            course_id=self.xmodule_runtime.course_id,
            module_state_key=self.location)

        return {
            'assignments': [get_student_data(module) for module in query],
            'max_score': self.max_score(),
        }

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

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

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

            context = {'fields': edit_fields}
            fragment = Fragment()
            fragment.add_content(
                render_template('templates/Essay/edit.html', context))
            fragment.add_javascript(_resource("static/js/studio.js"))
            fragment.initialize_js('Essay')
            return fragment
        except:  # pragma: NO COVER
            log.error("Not Essay", exc_info=True)
            raise
コード例 #25
0
 def test_serialize_error(self):
     with self.assertRaises(TypeError):
         DateTime().to_json('not a datetime')
コード例 #26
0
class StaffGradedAssignmentXBlock2(XBlock):
    """
    This block defines a Staff Graded Assignment.  Students are shown a rubric
    and invited to upload a file which is then graded by staff.
    """
    has_score = True
    icon_class = 'problem'

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

    pass_file = Boolean(display_name=_("pass file"),
                        help=("pass file"),
                        default=True,
                        scope=Scope.settings)

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

    def max_score(self):
        return self.points

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

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

    def get_submission(self, id=None):
        """
        Get student's most recent submission.
        """
        try:
            submissions = submissions_api.get_submissions(
                self.student_submission_id(id))
        except:
            submissions = None

        if submissions:
            # If I understand docs correctly, most recent submission should
            # be first
            return submissions[0]

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

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

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

        fragment = Fragment()

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

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

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

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

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

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

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

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

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

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

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

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

    @XBlock.json_handler
    def save_sga(self, data, suffix=''):
        self.display_name = data.get('display_name', self.display_name)
        self.weight = data.get('weight', self.weight)
        self.pass_file = data.get('pass_file', self.weight)

        # 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

    @XBlock.handler
    def upload_assignment(self, request, suffix=''):
        require(self.upload_allowed())
        answer = {
            "sha1": 'pass_file',
            "filename": '',
            "mimetype": '',
        }
        student_id = self.student_submission_id()
        submissions_api.create_submission(student_id, answer)
        return Response(json_body=self.student_state())

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

    @XBlock.handler
    def staff_upload_annotated(self, request, suffix=''):
        require(self.is_course_staff())
        upload = request.params['annotated']
        module = StudentModule.objects.get(pk=request.params['module_id'])
        state = json.loads(module.state)
        state['annotated_sha1'] = sha1 = _get_sha1(upload.file)
        state['annotated_filename'] = filename = upload.file.name
        state['annotated_mimetype'] = mimetypes.guess_type(upload.file.name)[0]
        state['annotated_timestamp'] = _now().strftime(
            DateTime.DATETIME_FORMAT)
        path = 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()
        return Response(json_body=self.staff_grading_data())

    @XBlock.handler
    def download_assignment(self, request, suffix=''):
        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=''):
        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=''):
        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'])

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

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

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

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

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

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

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

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

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

    def upload_allowed(self):
        return not self.past_due() and self.score is None

    def _file_storage_path(self, sha1, filename):
        path = ('{loc.org}/{loc.course}/{loc.block_type}/{loc.block_id}'
                '/{sha1}{ext}'.format(loc=self.location,
                                      sha1=sha1,
                                      ext=os.path.splitext(filename)[1]))
        return path
コード例 #27
0
ファイル: lms_mixin.py プロジェクト: skim-ks/edx-ora2
class LmsCompatibilityMixin(object):
    """
    Extra fields and methods used by LMS/Studio.
    """
    # Studio the default value for this field to show this XBlock
    # in the list of "Advanced Components"
    display_name = String(default="Peer Assessment",
                          scope=Scope.settings,
                          help="Display name")

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

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

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

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

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

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

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

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

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

        Note that if we have already recorded a score in submissions, this
        method will never be called. So it's perfectly possible for us to have
        10/10 on the progress page and a 12 returning from this method if our
        10/10 score was earned in the past and the problem has changed since
        then.
        """
        return sum(
            max(option["points"] for option in criterion["options"])
            for criterion in self.rubric_criteria)
class GradedDiscussionXBlock(XBlock, StudioEditableXBlockMixin, XBlockWithSettingsMixin):
    """
    GradedDiscussionXBlock Class
    """
    has_score = True

    display_name = String(
        display_name=_("Display Name"),
        help=_("Component Name."),
        scope=Scope.settings,
        default=_("Graded Discussion")
    )

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

    rubric = String(
        display_name=_("Rubric"),
        scope=Scope.settings,
        help=_("A list of criteria to grade the contribution."),
        default="",
        multiline_editor="html",
        resettable_editor=False
    )

    grading_message = String(
        display_name=_("Pre-Grading Message"),
        scope=Scope.settings,
        help=_("The message to show to learners before their score has been graded."),
        default="This discussion is staff graded. Your score will appear here when grading is complete.",
    )

    start_date = DateTime(
        display_name=_("Start Date"),
        default=None,
        scope=Scope.settings,
        help=_("Start date of this assignment.")
    )

    end_date = DateTime(
        display_name=_("End Date"),
        default=None,
        scope=Scope.settings,
        help=_("Due date of this assignment.")
    )

    discussion_topic = String(
        display_name=_("Discussion Topic"),
        default="All topics",
        scope=Scope.settings,
        help=_("Select the topic that you want to use for grading."),
        values_provider=lambda self: self.get_discussion_topics(),
    )

    # Possible editable fields
    editable_fields = (
        "display_name",
        "points",
        "rubric",
        "grading_message",
        "start_date",
        "end_date",
        "discussion_topic",
    )

    @cached_property
    def api_discussion(self):
        """
        Returns an instance of ApiDiscussion
        """
        try:
            client_id = self.get_xblock_settings()["client_id"]
            client_secret = self.get_xblock_settings()["client_secret"]
        except KeyError:
            raise

        return ApiDiscussion(settings.LMS_ROOT_URL, unicode(self.course_id), client_id, client_secret, self.location)

    @cached_property
    def api_teams(self):
        """
        Returns an instance of ApiTeams
        """
        try:
            client_id = self.get_xblock_settings()["client_id"]
            client_secret = self.get_xblock_settings()["client_secret"]
        except KeyError:
            raise

        return ApiTeams(settings.LMS_ROOT_URL, client_id, client_secret, self.location)

    @XBlock.handler
    def enter_grade(self, request, suffix=''):
        """
        """
        require(self.is_course_staff())
        user = get_user_by_username_or_email(request.params.get('user'))

        score = request.params.get('score')
        comment = request.params.get('comment')

        if not score:
            return Response(
                json_body={"error": "Enter a valid grade"},
                status_code=400,
            )

        try:
            score = int(score)
        except ValueError:
            return Response(
                json_body={"error": "Enter a valid grade"},
                status_code=400,
            )

        submission_id = self.get_submission_id(user)

        submission = submissions_api.create_submission(submission_id, {'comment': comment})

        submissions_api.set_score(submission['uuid'], score, self.max_score())

        self.get_or_create_student_module(user, score, comment)

        return Response(json_body={"success": "success"})

    @cached_property
    def contributions(self):

        return self.api_discussion.get_contributions(self.topic_id)

    def get_comment(self):
        """
        """
        submissions = submissions_api.get_submissions(self.submission_id)
        if submissions:
            return submissions[0]['answer']['comment']

    def get_discussion_topics(self):
        """
        """
        topics = self.api_discussion.get_topics_names()
        topics.append("All topics")
        return topics

    def get_or_create_student_module(self, user, score, comment):
        """
        """
        state = {"score": score, "comment": comment}
        student_module, created = StudentModule.objects.get_or_create(
            course_id=self.course_id,
            module_state_key=self.location,
            student=user,
            defaults={
                'state': json.dumps(state),
            }
        )
        return student_module

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

    def get_student_list(self):
        """
        """
        users = CourseEnrollmentManager().users_enrolled_in(self.course_id)
        graded_students = self._get_graded_students()

        return [
            dict(
                username=user.username,
                image_url=self._get_image_url(user),
                last_post=self._get_last_date_on_post(self._get_contributions(user.username)),
                cohort_id=get_cohort_id(user, self.course_id),
                team=self.api_teams.get_user_team(unicode(self.course_id), user.username),
                contributions=json.dumps(self._get_contributions(user.username)),
            )
            for user in users if not user.is_staff and user not in graded_students
        ]

    def get_submission_id(self, user):
        """
        """
        return dict(
            item_id=unicode(self.location),
            item_type='graded_discussion',
            course_id=unicode(self.course_id),
            student_id=anonymous_id_for_user(user, self.course_id)
        )

    def is_course_staff(self):
        """
         Check if user is course staff.
        """
        return getattr(self.xmodule_runtime, "user_is_staff", False)

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

    @XBlock.json_handler
    def get_contributions(self, data, suffix=""):
        """
        """
        require(self.is_course_staff())
        users = data.get("users")
        self._delete_cache(users)
        contributions = {user: json.dumps(self._get_contributions(user)) for user in users}
        return Response(json=contributions)

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

    @cached_property
    def score(self):
        """
        """
        user = get_user_by_username_or_email(self.username)
        return self.get_score(user)

    @cached_property
    def submission_id(self):
        """
        """
        user = get_user_by_username_or_email(self.username)
        return self.get_submission_id(user)

    # TO-DO: change this view to display your data your own way.
    def student_view(self, context=None):
        """
        The primary view of the GradedDiscussionXBlock, shown to students
        when viewing courses.
        """
        frag = Fragment(LOADER.render_django_template("static/html/graded_discussion.html", self._get_context()))
        frag.add_css_url("https://cdnjs.cloudflare.com/ajax/libs/jquery-modal/0.9.1/jquery.modal.min.css")
        frag.add_css(self.resource_string("static/css/graded_discussion.css"))
        frag.add_javascript_url("https://cdnjs.cloudflare.com/ajax/libs/jquery-modal/0.9.1/jquery.modal.min.js")
        frag.add_javascript(self.resource_string("static/js/src/graded_discussion.js"))
        frag.initialize_js('GradedDiscussionXBlock')
        return frag

    @cached_property
    def username(self):
        """
        Returns the username for the currently user
        """
        user = self.runtime.service(self, 'user').get_current_user()
        return user.opt_attrs['edx-platform.username']

    @cached_property
    def topic_id(self):
        """
        """
        if self.discussion_topic == "All topics":
            return None
        return self.api_discussion.get_topic_id(self.discussion_topic)

    def validate_field_data(self, validation, data):
        """
        This method validates the data from studio before it is saved
        """
        start_date = data.start_date
        end_date = data.end_date

        if start_date and end_date and end_date <= start_date:
            validation.add(ValidationMessage(ValidationMessage.ERROR, u"The start date must be before the end date"))

    def _delete_cache(self, users):
        cache.delete(self.location)
        for user in users:
            key = "{}-{}".format(self.location, user)
            cache.delete(key)

        key = "{}-{}".format(self.location, "contributions")
        cache.delete(key)

    def _filter_by_date(self, contributions):
        """
        """
        if self.start_date and self.end_date:
            return [
                contribution
                for contribution in contributions
                if parse(contribution["created_at"]) >= self.start_date and parse(contribution["created_at"]) <= self.end_date
            ]
        elif self.start_date:
            return [
                contribution
                for contribution in contributions
                if parse(contribution["created_at"]) >= self.start_date
            ]
        elif self.end_date:
            return [
                contribution
                for contribution in contributions
                if parse(contribution["created_at"]) <= self.end_date
            ]
        return contributions

    def _get_context(self):
        """
        """
        if self.is_course_staff():
            return dict(
                user_is_staff=True,
                rubric=self.rubric,
                users=self.get_student_list(),
                reload_url=self.runtime.local_resource_url(self, 'public/img/reload-icon.png'),
                cohorts=get_cohort_names(get_course_by_id(self.course_id)),
                teams=self.api_teams.get_course_teams(unicode(self.course_id)),
                grading_message=self.grading_message,
            )

        comment = self.get_comment()
        return dict(
            user_is_staff=False,
            grading_message=comment if comment and self.score else self.grading_message,
            score=self.score,
            max_score=self.points
        )

    def _get_contributions(self, username):
        """
        This returns the contributions for a given username
        """
        key = "{}-{}".format(self.location, username)
        contributions = cache.get(key)
        if contributions:
            return contributions

        contributions = [contribution for contribution in self.contributions if contribution["author"] == username]

        contributions = self._filter_by_date(contributions)

        cache.set(key, contributions)

        return contributions

    def _get_graded_students(self):
        """
        """
        students = StudentItem.objects.filter(
            course_id=self.course_id,
            item_id=unicode(self.location)
        )
        result = []
        for student in students:
            user = user_by_anonymous_id(student.student_id)
            if self.get_score(user):
                result.append(user)
        return result

    def _get_image_url(self, user):
        """
        """
        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

    def _get_last_date_on_post(self, contributions):
        """
        """
        contributions.sort(key=lambda item: item["created_at"])
        try:
            return contributions[0].get("created_at")
        except IndexError:
            return ""

    def _get_topic_id(self, name):
        """
        """
        return self.api_discussion.get_topic_id(name)

    # 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 [
            ("GradedDiscussionXBlock",
             """<graded_discussion/>
             """),
            ("Multiple GradedDiscussionXBlock",
             """<vertical_demo>
                <graded_discussion/>
                <graded_discussion/>
                <graded_discussion/>
                </vertical_demo>
             """),
        ]
コード例 #29
0
class VideoFields(object):
    """Fields for `VideoModule` and `VideoDescriptor`."""
    display_name = String(
        help=_("The name students see. This name appears in the course ribbon and as a header for the video."),
        display_name=_("Component Display Name"),
        default="Video",
        scope=Scope.settings
    )

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

    # `source` is deprecated field and should not be used in future.
    # `download_video` is used instead.
    source = String(
        help=_("The external URL to download the video."),
        display_name=_("Download Video"),
        scope=Scope.settings,
        default=""
    )
    download_video = Boolean(
        help=_("Allow students to download versions of this video in different formats if they cannot use the edX video player or do not have access to YouTube. You must add at least one non-YouTube URL in the Video File URLs field."),  # pylint: disable=line-too-long
        display_name=_("Video Download Allowed"),
        scope=Scope.settings,
        default=False
    )
    html5_sources = List(
        help=_("The URL or URLs where you've posted non-YouTube versions of the video. Each URL must end in .mpeg, .mp4, .ogg, or .webm and cannot be a YouTube URL. (For browser compatibility, we strongly recommend .mp4 and .webm format.) Students will be able to view the first listed video that's compatible with the student's computer. To allow students to download these videos, set Video Download Allowed to True."),  # pylint: disable=line-too-long
        display_name=_("Video File URLs"),
        scope=Scope.settings,
    )
    track = String(
        help=_("By default, students can download an .srt or .txt transcript when you set Download Transcript Allowed to True. If you want to provide a downloadable transcript in a different format, we recommend that you upload a handout by using the Upload a Handout field. If this isn't possible, you can post a transcript file on the Files & Uploads page or on the Internet, and then add the URL for the transcript here. Students see a link to download that transcript below the video."),  # pylint: disable=line-too-long
        display_name=_("Downloadable Transcript URL"),
        scope=Scope.settings,
        default=''
    )
    download_track = Boolean(
        help=_("Allow students to download the timed transcript. A link to download the file appears below the video. By default, the transcript is an .srt or .txt file. If you want to provide the transcript for download in a different format, upload a file by using the Upload Handout field."),  # pylint: disable=line-too-long
        display_name=_("Download Transcript Allowed"),
        scope=Scope.settings,
        default=False
    )
    sub = String(
        help=_("The default transcript for the video, from the Default Timed Transcript field on the Basic tab. This transcript should be in English. You don't have to change this setting."),  # pylint: disable=line-too-long
        display_name=_("Default Timed Transcript"),
        scope=Scope.settings,
        default=""
    )
    show_captions = Boolean(
        help=_("Specify whether the transcripts appear with the video by default."),
        display_name=_("Show Transcript"),
        scope=Scope.settings,
        default=True
    )
    # Data format: {'de': 'german_translation', 'uk': 'ukrainian_translation'}
    transcripts = Dict(
        help=_("Add transcripts in different languages. Click below to specify a language and upload an .srt transcript file for that language."),  # pylint: disable=line-too-long
        display_name=_("Transcript Languages"),
        scope=Scope.settings,
        default={}
    )
    transcript_language = String(
        help=_("Preferred language for transcript."),
        display_name=_("Preferred language for transcript"),
        scope=Scope.preferences,
        default="en"
    )
    transcript_download_format = String(
        help=_("Transcript file format to download by user."),
        scope=Scope.preferences,
        values=[
            # Translators: This is a type of file used for captioning in the video player.
            {"display_name": _("SubRip (.srt) file"), "value": "srt"},
            {"display_name": _("Text (.txt) file"), "value": "txt"}
        ],
        default='srt',
    )
    speed = Float(
        help=_("The last speed that the user specified for the video."),
        scope=Scope.user_state
    )
    global_speed = Float(
        help=_("The default speed for the video."),
        scope=Scope.preferences,
        default=1.0
    )
    youtube_is_available = Boolean(
        help=_("Specify whether YouTube is available for the user."),
        scope=Scope.user_info,
        default=True
    )
    handout = String(
        help=_("Upload a handout to accompany this video. Students can download the handout by clicking Download Handout under the video."),  # pylint: disable=line-too-long
        display_name=_("Upload Handout"),
        scope=Scope.settings,
    )
    only_on_web = Boolean(
        help=_(
            "Specify whether access to this video is limited to browsers only, or if it can be "
            "accessed from other applications including mobile apps."
        ),
        display_name=_("Video Available on Web Only"),
        scope=Scope.settings,
        default=False
    )
    edx_video_id = String(
        help=_("If you were assigned a Video ID by edX for the video to play in this component, enter the ID here. In this case, do not enter values in the Default Video URL, the Video File URLs, and the YouTube ID fields. If you were not assigned a Video ID, enter values in those other fields and ignore this field."),  # pylint: disable=line-too-long
        display_name=_("Video ID"),
        scope=Scope.settings,
        default="",
    )
    bumper_last_view_date = DateTime(
        display_name=_("Date of the last view of the bumper"),
        scope=Scope.preferences,
    )
    bumper_do_not_show_again = Boolean(
        display_name=_("Do not show bumper again"),
        scope=Scope.preferences,
        default=False,
    )
コード例 #30
0
ファイル: poll.py プロジェクト: kursitet/xblock-poll
class PollBase(XBlock, ResourceMixin, PublishEventMixin):
    """
    Base class for Poll-like XBlocks.
    """
    event_namespace = 'xblock.pollbase'
    private_results = Boolean(default=False,
                              help=u"Показывать ли студенту результаты.")
    max_submissions = Integer(
        default=1,
        help=
        u"Максимальное допустимое количество отправок ответов одним студентом."
    )
    submissions_count = Integer(
        default=0,
        help=u"Сколько раз студент отправил свои ответы.",
        scope=Scope.user_state)
    feedback = String(
        default='',
        help=
        u"Текст, показываемый студенту, после того, как он отправил свои ответы."
    )

    due = DateTime(
        default=None,
        help=
        u"Время и дата окончания приема ответов - часовой пояс UTC, формат записи ISO-8601."
    )

    has_score = True
    weight = 1.0

    def max_score(self):
        return self.weight

    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.
        """
        try:
            if self.due and arrow.now() > arrow.get(self.due):
                return False
        except:
            pass
        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):
        # Mihara: I'm not sure why, but this whole logic plain doesn't work. Where did they run this?
        # I'm falling back to what I know does work...
        if HAS_EDX_ACCESS:
            if self.xmodule_runtime.get_user_role() in ['staff', 'instructor']:
                return True
            else:
                # Check if user is member of a group that is explicitly granted
                # permission to view the results through django configuration.
                if HAS_API_MANAGER:
                    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()
        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(
                u'Параметр "Максимальное Количество Отправок" отсутствует или не является целым числом.'
            )

        # 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(
                u'Параметр "Скрыть результаты" не может быть False если "Максимальное Количество Отправок" не равно 1.'
            )
        return max_submissions