Exemple #1
0
def grade_updated(**kwargs):
    """
    Emits the appropriate grade-related event after checking for which
    event-transaction is active.

    Emits a problem.submitted event only if there is no current event
    transaction type, i.e. we have not reached this point in the code via
    an outer event type (such as problem.rescored or score_overridden).
    """
    root_type = get_event_transaction_type()

    if not root_type:
        root_id = get_event_transaction_id()
        if not root_id:
            root_id = create_new_event_transaction_id()
        set_event_transaction_type(PROBLEM_SUBMITTED_EVENT_TYPE)
        tracker.emit(
            unicode(PROBLEM_SUBMITTED_EVENT_TYPE),
            {
                'user_id': unicode(kwargs['user_id']),
                'course_id': unicode(kwargs['course_id']),
                'problem_id': unicode(kwargs['usage_id']),
                'event_transaction_id': unicode(root_id),
                'event_transaction_type': unicode(PROBLEM_SUBMITTED_EVENT_TYPE),
                'weighted_earned': kwargs.get('weighted_earned'),
                'weighted_possible': kwargs.get('weighted_possible'),
            }
        )

    elif root_type in [GRADES_RESCORE_EVENT_TYPE, GRADES_OVERRIDE_EVENT_TYPE]:
        current_user = get_current_user()
        instructor_id = getattr(current_user, 'id', None)
        tracker.emit(
            unicode(root_type),
            {
                'course_id': unicode(kwargs['course_id']),
                'user_id': unicode(kwargs['user_id']),
                'problem_id': unicode(kwargs['usage_id']),
                'new_weighted_earned': kwargs.get('weighted_earned'),
                'new_weighted_possible': kwargs.get('weighted_possible'),
                'only_if_higher': kwargs.get('only_if_higher'),
                'instructor_id': unicode(instructor_id),
                'event_transaction_id': unicode(get_event_transaction_id()),
                'event_transaction_type': unicode(root_type),
            }
        )

    elif root_type in [SUBSECTION_OVERRIDE_EVENT_TYPE]:
        tracker.emit(
            unicode(root_type),
            {
                'course_id': unicode(kwargs['course_id']),
                'user_id': unicode(kwargs['user_id']),
                'problem_id': unicode(kwargs['usage_id']),
                'only_if_higher': kwargs.get('only_if_higher'),
                'override_deleted': kwargs.get('score_deleted', False),
                'event_transaction_id': unicode(get_event_transaction_id()),
                'event_transaction_type': unicode(root_type),
            }
        )
Exemple #2
0
def enqueue_subsection_update(sender, **kwargs):  # pylint: disable=unused-argument
    """
    Handles the PROBLEM_WEIGHTED_SCORE_CHANGED signal by
    enqueueing a subsection update operation to occur asynchronously.
    """
    _emit_problem_submitted_event(kwargs)
    result = recalculate_subsection_grade_v3.apply_async(
        kwargs=dict(
            user_id=kwargs['user_id'],
            anonymous_user_id=kwargs.get('anonymous_user_id'),
            course_id=kwargs['course_id'],
            usage_id=kwargs['usage_id'],
            only_if_higher=kwargs.get('only_if_higher'),
            expected_modified_time=to_timestamp(kwargs['modified']),
            score_deleted=kwargs.get('score_deleted', False),
            event_transaction_id=unicode(get_event_transaction_id()),
            event_transaction_type=unicode(get_event_transaction_type()),
            score_db_table=kwargs['score_db_table'],
        ),
        countdown=RECALCULATE_GRADE_DELAY,
    )
    log.info(
        u'Grades: Request async calculation of subsection grades with args: {}. Task [{}]'.format(
            ', '.join('{}:{}'.format(arg, kwargs[arg]) for arg in sorted(kwargs)),
            getattr(result, 'id', 'N/A'),
        )
    )
Exemple #3
0
def _emit_problem_submitted_event(kwargs):
    """
    Emits a problem submitted event only if
    there is no current event transaction type,
    i.e. we have not reached this point in the
    code via a rescore or student state deletion.
    """
    root_type = get_event_transaction_type()

    if not root_type:
        root_id = get_event_transaction_id()
        if not root_id:
            root_id = create_new_event_transaction_id()
        set_event_transaction_type(PROBLEM_SUBMITTED_EVENT_TYPE)
        tracker.emit(
            unicode(PROBLEM_SUBMITTED_EVENT_TYPE),
            {
                'user_id': unicode(kwargs['user_id']),
                'course_id': unicode(kwargs['course_id']),
                'problem_id': unicode(kwargs['usage_id']),
                'event_transaction_id': unicode(root_id),
                'event_transaction_type': unicode(PROBLEM_SUBMITTED_EVENT_TYPE),
                'weighted_earned': kwargs.get('weighted_earned'),
                'weighted_possible': kwargs.get('weighted_possible'),
            }
        )
Exemple #4
0
def _retry_recalculate_subsection_grade(
        user_id,
        course_id,
        usage_id,
        only_if_higher,
        expected_modified_time,
        score_deleted,
        exc=None,
):
    """
    Calls retry for the recalculate_subsection_grade task with the
    given inputs.
    """
    recalculate_subsection_grade_v2.retry(
        kwargs=dict(
            user_id=user_id,
            course_id=course_id,
            usage_id=usage_id,
            only_if_higher=only_if_higher,
            expected_modified_time=expected_modified_time,
            score_deleted=score_deleted,
            event_transaction_id=unicode(get_event_transaction_id()),
            event_transaction_type=unicode(get_event_transaction_type()),
        ),
        exc=exc,
    )
Exemple #5
0
def subsection_grade_calculated(subsection_grade):
    """
    Emits an edx.grades.subsection.grade_calculated event
    with data from the passed subsection_grade.
    """
    event_name = SUBSECTION_GRADE_CALCULATED
    context = contexts.course_context_from_course_id(subsection_grade.course_id)
    # TODO (AN-6134): remove this context manager
    with tracker.get_tracker().context(event_name, context):
        tracker.emit(
            event_name,
            {
                'user_id': unicode(subsection_grade.user_id),
                'course_id': unicode(subsection_grade.course_id),
                'block_id': unicode(subsection_grade.usage_key),
                'course_version': unicode(subsection_grade.course_version),
                'weighted_total_earned': subsection_grade.earned_all,
                'weighted_total_possible': subsection_grade.possible_all,
                'weighted_graded_earned': subsection_grade.earned_graded,
                'weighted_graded_possible': subsection_grade.possible_graded,
                'first_attempted': unicode(subsection_grade.first_attempted),
                'subtree_edited_timestamp': unicode(subsection_grade.subtree_edited_timestamp),
                'event_transaction_id': unicode(get_event_transaction_id()),
                'event_transaction_type': unicode(get_event_transaction_type()),
                'visible_blocks_hash': unicode(subsection_grade.visible_blocks_id),
            }
        )
Exemple #6
0
 def _emit_grade_calculated_event(grade):
     """
     Emits an edx.grades.subsection.grade_calculated event
     with data from the passed grade.
     """
     # TODO: remove this context manager after completion of AN-6134
     event_name = u'edx.grades.subsection.grade_calculated'
     context = contexts.course_context_from_course_id(grade.course_id)
     with tracker.get_tracker().context(event_name, context):
         tracker.emit(
             event_name,
             {
                 'user_id': unicode(grade.user_id),
                 'course_id': unicode(grade.course_id),
                 'block_id': unicode(grade.usage_key),
                 'course_version': unicode(grade.course_version),
                 'weighted_total_earned': grade.earned_all,
                 'weighted_total_possible': grade.possible_all,
                 'weighted_graded_earned': grade.earned_graded,
                 'weighted_graded_possible': grade.possible_graded,
                 'first_attempted': unicode(grade.first_attempted),
                 'subtree_edited_timestamp': unicode(grade.subtree_edited_timestamp),
                 'event_transaction_id': unicode(get_event_transaction_id()),
                 'event_transaction_type': unicode(get_event_transaction_type()),
                 'visible_blocks_hash': unicode(grade.visible_blocks_id),
             }
         )
Exemple #7
0
    def _create_override(self, request_user, subsection_grade_model, **override_data):
        """
        Helper method to create a `PersistentSubsectionGradeOverride` object
        and send a `SUBSECTION_OVERRIDE_CHANGED` signal.
        """
        override = PersistentSubsectionGradeOverride.update_or_create_override(
            requesting_user=request_user,
            subsection_grade_model=subsection_grade_model,
            feature=grades_constants.GradeOverrideFeatureEnum.gradebook,
            **override_data
        )

        set_event_transaction_type(grades_events.SUBSECTION_GRADE_CALCULATED)
        create_new_event_transaction_id()

        recalculate_subsection_grade_v3.apply(
            kwargs=dict(
                user_id=subsection_grade_model.user_id,
                anonymous_user_id=None,
                course_id=text_type(subsection_grade_model.course_id),
                usage_id=text_type(subsection_grade_model.usage_key),
                only_if_higher=False,
                expected_modified_time=to_timestamp(override.modified),
                score_deleted=False,
                event_transaction_id=six.text_type(get_event_transaction_id()),
                event_transaction_type=six.text_type(get_event_transaction_type()),
                score_db_table=grades_constants.ScoreDatabaseTableEnum.overrides,
                force_update_subsections=True,
            )
        )
        # Emit events to let our tracking system to know we updated subsection grade
        grades_events.subsection_grade_calculated(subsection_grade_model)
        return override
Exemple #8
0
def _emit_event(kwargs):
    """
    Emits a problem submitted event only if there is no current event
    transaction type, i.e. we have not reached this point in the code via a
    rescore or student state deletion.

    If the event transaction type has already been set and the transacation is
    a rescore, emits a problem rescored event.
    """
    root_type = get_event_transaction_type()

    if not root_type:
        root_id = get_event_transaction_id()
        if not root_id:
            root_id = create_new_event_transaction_id()
        set_event_transaction_type(PROBLEM_SUBMITTED_EVENT_TYPE)
        tracker.emit(
            unicode(PROBLEM_SUBMITTED_EVENT_TYPE),
            {
                'user_id': unicode(kwargs['user_id']),
                'course_id': unicode(kwargs['course_id']),
                'problem_id': unicode(kwargs['usage_id']),
                'event_transaction_id': unicode(root_id),
                'event_transaction_type': unicode(PROBLEM_SUBMITTED_EVENT_TYPE),
                'weighted_earned': kwargs.get('weighted_earned'),
                'weighted_possible': kwargs.get('weighted_possible'),
            }
        )

    if root_type in [GRADES_RESCORE_EVENT_TYPE, GRADES_OVERRIDE_EVENT_TYPE]:
        current_user = get_current_user()
        instructor_id = getattr(current_user, 'id', None)
        tracker.emit(
            unicode(GRADES_RESCORE_EVENT_TYPE),
            {
                'course_id': unicode(kwargs['course_id']),
                'user_id': unicode(kwargs['user_id']),
                'problem_id': unicode(kwargs['usage_id']),
                'new_weighted_earned': kwargs.get('weighted_earned'),
                'new_weighted_possible': kwargs.get('weighted_possible'),
                'only_if_higher': kwargs.get('only_if_higher'),
                'instructor_id': unicode(instructor_id),
                'event_transaction_id': unicode(get_event_transaction_id()),
                'event_transaction_type': unicode(root_type),
            }
        )
Exemple #9
0
    def set_up_course(self, enable_persistent_grades=True, create_multiple_subsections=False):
        """
        Configures the course for this test.
        """
        # pylint: disable=attribute-defined-outside-init,no-member
        self.course = CourseFactory.create(
            org='edx',
            name='course',
            run='run',
        )
        if not enable_persistent_grades:
            PersistentGradesEnabledFlag.objects.create(enabled=False)

        self.chapter = ItemFactory.create(parent=self.course, category="chapter", display_name="Chapter")
        self.sequential = ItemFactory.create(parent=self.chapter, category='sequential', display_name="Sequential1")
        self.problem = ItemFactory.create(parent=self.sequential, category='problem', display_name='Problem')

        if create_multiple_subsections:
            seq2 = ItemFactory.create(parent=self.chapter, category='sequential')
            ItemFactory.create(parent=seq2, category='problem')

        self.frozen_now_datetime = datetime.now().replace(tzinfo=pytz.UTC)
        self.frozen_now_timestamp = to_timestamp(self.frozen_now_datetime)

        self.problem_weighted_score_changed_kwargs = OrderedDict([
            ('weighted_earned', 1.0),
            ('weighted_possible', 2.0),
            ('user_id', self.user.id),
            ('anonymous_user_id', 5),
            ('course_id', unicode(self.course.id)),
            ('usage_id', unicode(self.problem.location)),
            ('only_if_higher', None),
            ('modified', self.frozen_now_datetime),
            ('score_db_table', ScoreDatabaseTableEnum.courseware_student_module),
        ])

        create_new_event_transaction_id()

        self.recalculate_subsection_grade_kwargs = OrderedDict([
            ('user_id', self.user.id),
            ('course_id', unicode(self.course.id)),
            ('usage_id', unicode(self.problem.location)),
            ('anonymous_user_id', 5),
            ('only_if_higher', None),
            ('expected_modified_time', self.frozen_now_timestamp),
            ('score_deleted', False),
            ('event_transaction_id', unicode(get_event_transaction_id())),
            ('event_transaction_type', u'edx.grades.problem.submitted'),
            ('score_db_table', ScoreDatabaseTableEnum.courseware_student_module),
        ])

        # this call caches the anonymous id on the user object, saving 4 queries in all happy path tests
        _ = anonymous_id_for_user(self.user, self.course.id)
def handle_grading_policy_changed(sender, **kwargs):
    # pylint: disable=unused-argument
    """
    Receives signal and kicks off celery task to recalculate grades
    """
    kwargs = {
        'course_key': unicode(kwargs.get('course_key')),
        'event_transaction_id': unicode(get_event_transaction_id()),
        'event_transaction_type': unicode(get_event_transaction_type()),
    }
    result = compute_all_grades_for_course.apply_async(kwargs=kwargs)
    log.info("Grades: Created {task_name}[{task_id}] with arguments {kwargs}".format(
        task_name=compute_all_grades_for_course.name,
        task_id=result.task_id,
        kwargs=kwargs,
    ))
    def _run_command_and_check_output(self, task_mock, score_db_table, include_anonymous_id=False):
        self.command.handle(modified_start='2016-08-25 16:42', modified_end='2018-08-25 16:44')
        kwargs = {
            "user_id": "ID",
            "course_id": u'x/y/z',
            "usage_id": u'abc',
            "only_if_higher": False,
            "expected_modified_time": to_timestamp(utc.localize(datetime.strptime('2016-08-23 16:43', DATE_FORMAT))),
            "score_deleted": False,
            "event_transaction_id": unicode(get_event_transaction_id()),
            "event_transaction_type": u'edx.grades.problem.submitted',
            "score_db_table": score_db_table,
        }

        if include_anonymous_id:
            kwargs['anonymous_user_id'] = 'anonymousID'

        task_mock.apply_async.assert_called_with(kwargs=kwargs)
 def _assert_tracker_emitted_event(self, tracker_mock, grade):
     """
     Helper function to ensure that the mocked event tracker
     was called with the expected info based on the passed grade.
     """
     tracker_mock.emit.assert_called_with(
         u'edx.grades.course.grade_calculated',
         {
             'user_id': unicode(grade.user_id),
             'course_id': unicode(grade.course_id),
             'course_version': unicode(grade.course_version),
             'percent_grade': grade.percent_grade,
             'letter_grade': unicode(grade.letter_grade),
             'course_edited_timestamp': unicode(grade.course_edited_timestamp),
             'event_transaction_id': unicode(get_event_transaction_id()),
             'event_transaction_type': unicode(get_event_transaction_type()),
             'grading_policy_hash': unicode(grade.grading_policy_hash),
         }
     )
Exemple #13
0
def enqueue_subsection_update(sender, **kwargs):  # pylint: disable=unused-argument
    """
    Handles the PROBLEM_WEIGHTED_SCORE_CHANGED or SUBSECTION_OVERRIDE_CHANGED signals by
    enqueueing a subsection update operation to occur asynchronously.
    """
    events.grade_updated(**kwargs)
    recalculate_subsection_grade_v3.apply_async(
        kwargs=dict(
            user_id=kwargs['user_id'],
            anonymous_user_id=kwargs.get('anonymous_user_id'),
            course_id=kwargs['course_id'],
            usage_id=kwargs['usage_id'],
            only_if_higher=kwargs.get('only_if_higher'),
            expected_modified_time=to_timestamp(kwargs['modified']),
            score_deleted=kwargs.get('score_deleted', False),
            event_transaction_id=unicode(get_event_transaction_id()),
            event_transaction_type=unicode(get_event_transaction_type()),
            score_db_table=kwargs['score_db_table'],
        ),
        countdown=RECALCULATE_GRADE_DELAY_SECONDS,
    )
Exemple #14
0
def course_grade_calculated(course_grade):
    """
    Emits an edx.grades.course.grade_calculated event
    with data from the passed course_grade.
    """
    event_name = COURSE_GRADE_CALCULATED
    context = contexts.course_context_from_course_id(course_grade.course_id)
    # TODO (AN-6134): remove this context manager
    with tracker.get_tracker().context(event_name, context):
        tracker.emit(
            event_name,
            {
                'user_id': unicode(course_grade.user_id),
                'course_id': unicode(course_grade.course_id),
                'course_version': unicode(course_grade.course_version),
                'percent_grade': course_grade.percent_grade,
                'letter_grade': unicode(course_grade.letter_grade),
                'course_edited_timestamp': unicode(course_grade.course_edited_timestamp),
                'event_transaction_id': unicode(get_event_transaction_id()),
                'event_transaction_type': unicode(get_event_transaction_type()),
                'grading_policy_hash': unicode(course_grade.grading_policy_hash),
            }
        )
 def _assert_tracker_emitted_event(self, tracker_mock, grade):
     """
     Helper function to ensure that the mocked event tracker
     was called with the expected info based on the passed grade.
     """
     tracker_mock.emit.assert_called_with(
         u'edx.grades.subsection.grade_calculated',
         {
             'user_id': unicode(grade.user_id),
             'course_id': unicode(grade.course_id),
             'block_id': unicode(grade.usage_key),
             'course_version': unicode(grade.course_version),
             'weighted_total_earned': grade.earned_all,
             'weighted_total_possible': grade.possible_all,
             'weighted_graded_earned': grade.earned_graded,
             'weighted_graded_possible': grade.possible_graded,
             'first_attempted': unicode(grade.first_attempted),
             'subtree_edited_timestamp': unicode(grade.subtree_edited_timestamp),
             'event_transaction_id': unicode(get_event_transaction_id()),
             'event_transaction_type': unicode(get_event_transaction_type()),
             'visible_blocks_hash': unicode(grade.visible_blocks_id),
         }
     )
Exemple #16
0
 def _emit_grade_calculated_event(grade):
     """
     Emits an edx.grades.course.grade_calculated event
     with data from the passed grade.
     """
     # TODO: remove this context manager after completion of AN-6134
     event_name = u'edx.grades.course.grade_calculated'
     context = contexts.course_context_from_course_id(grade.course_id)
     with tracker.get_tracker().context(event_name, context):
         tracker.emit(
             event_name,
             {
                 'user_id': unicode(grade.user_id),
                 'course_id': unicode(grade.course_id),
                 'course_version': unicode(grade.course_version),
                 'percent_grade': grade.percent_grade,
                 'letter_grade': unicode(grade.letter_grade),
                 'course_edited_timestamp': unicode(grade.course_edited_timestamp),
                 'event_transaction_id': unicode(get_event_transaction_id()),
                 'event_transaction_type': unicode(get_event_transaction_type()),
                 'grading_policy_hash': unicode(grade.grading_policy_hash),
             }
         )
Exemple #17
0
    def _create_override(self, request_user, subsection_grade_model, **override_data):
        """
        Helper method to create a `PersistentSubsectionGradeOverride` object
        and send a `SUBSECTION_OVERRIDE_CHANGED` signal.
        """
        override, _ = PersistentSubsectionGradeOverride.objects.update_or_create(
            grade=subsection_grade_model,
            defaults=self._clean_override_data(override_data),
        )

        _ = PersistentSubsectionGradeOverrideHistory.objects.create(
            override_id=override.id,
            user=request_user,
            feature=PersistentSubsectionGradeOverrideHistory.GRADEBOOK,
            action=PersistentSubsectionGradeOverrideHistory.CREATE_OR_UPDATE,
        )

        set_event_transaction_type(SUBSECTION_GRADE_CALCULATED)
        create_new_event_transaction_id()

        recalculate_subsection_grade_v3.apply(
            kwargs=dict(
                user_id=subsection_grade_model.user_id,
                anonymous_user_id=None,
                course_id=text_type(subsection_grade_model.course_id),
                usage_id=text_type(subsection_grade_model.usage_key),
                only_if_higher=False,
                expected_modified_time=to_timestamp(override.modified),
                score_deleted=False,
                event_transaction_id=unicode(get_event_transaction_id()),
                event_transaction_type=unicode(get_event_transaction_type()),
                score_db_table=ScoreDatabaseTableEnum.overrides,
                force_update_subsections=True,
            )
        )
        # Emit events to let our tracking system to know we updated subsection grade
        subsection_grade_calculated(subsection_grade_model)
def grade_updated(**kwargs):
    """
    Emits the appropriate grade-related event after checking for which
    event-transaction is active.

    Emits a problem.submitted event only if there is no current event
    transaction type, i.e. we have not reached this point in the code via
    an outer event type (such as problem.rescored or score_overridden).
    """
    root_type = get_event_transaction_type()

    if not root_type:
        root_id = get_event_transaction_id()
        if not root_id:
            root_id = create_new_event_transaction_id()
        set_event_transaction_type(PROBLEM_SUBMITTED_EVENT_TYPE)
        tracker.emit(
            six.text_type(PROBLEM_SUBMITTED_EVENT_TYPE), {
                'user_id':
                six.text_type(kwargs['user_id']),
                'course_id':
                six.text_type(kwargs['course_id']),
                'problem_id':
                six.text_type(kwargs['usage_id']),
                'event_transaction_id':
                six.text_type(root_id),
                'event_transaction_type':
                six.text_type(PROBLEM_SUBMITTED_EVENT_TYPE),
                'weighted_earned':
                kwargs.get('weighted_earned'),
                'weighted_possible':
                kwargs.get('weighted_possible'),
            })

    elif root_type in [GRADES_RESCORE_EVENT_TYPE, GRADES_OVERRIDE_EVENT_TYPE]:
        current_user = get_current_user()
        instructor_id = getattr(current_user, 'id', None)
        tracker.emit(
            six.text_type(root_type), {
                'course_id': six.text_type(kwargs['course_id']),
                'user_id': six.text_type(kwargs['user_id']),
                'problem_id': six.text_type(kwargs['usage_id']),
                'new_weighted_earned': kwargs.get('weighted_earned'),
                'new_weighted_possible': kwargs.get('weighted_possible'),
                'only_if_higher': kwargs.get('only_if_higher'),
                'instructor_id': six.text_type(instructor_id),
                'event_transaction_id': six.text_type(
                    get_event_transaction_id()),
                'event_transaction_type': six.text_type(root_type),
            })

    elif root_type in [SUBSECTION_OVERRIDE_EVENT_TYPE]:
        tracker.emit(
            six.text_type(root_type), {
                'course_id': six.text_type(kwargs['course_id']),
                'user_id': six.text_type(kwargs['user_id']),
                'problem_id': six.text_type(kwargs['usage_id']),
                'only_if_higher': kwargs.get('only_if_higher'),
                'override_deleted': kwargs.get('score_deleted', False),
                'event_transaction_id': six.text_type(
                    get_event_transaction_id()),
                'event_transaction_type': six.text_type(root_type),
            })
Exemple #19
0
def reset_student_attempts(course_id,
                           student,
                           module_state_key,
                           requesting_user,
                           delete_module=False):
    """
    Reset student attempts for a problem. Optionally deletes all student state for the specified problem.

    In the previous instructor dashboard it was possible to modify/delete
    modules that were not problems. That has been disabled for safety.

    `student` is a User
    `problem_to_reset` is the name of a problem e.g. 'L2Node1'.
    To build the module_state_key 'problem/' and course information will be appended to `problem_to_reset`.

    Raises:
        ValueError: `problem_state` is invalid JSON.
        StudentModule.DoesNotExist: could not load the student module.
        submissions.SubmissionError: unexpected error occurred while resetting the score in the submissions API.

    """
    user_id = anonymous_id_for_user(student, course_id)
    requesting_user_id = anonymous_id_for_user(requesting_user, course_id)
    submission_cleared = False
    try:
        # A block may have children. Clear state on children first.
        block = modulestore().get_item(module_state_key)
        if block.has_children:
            for child in block.children:
                try:
                    reset_student_attempts(course_id,
                                           student,
                                           child,
                                           requesting_user,
                                           delete_module=delete_module)
                except StudentModule.DoesNotExist:
                    # If a particular child doesn't have any state, no big deal, as long as the parent does.
                    pass
        if delete_module:
            # Some blocks (openassessment) use StudentModule data as a key for internal submission data.
            # Inform these blocks of the reset and allow them to handle their data.
            clear_student_state = getattr(block, "clear_student_state", None)
            if callable(clear_student_state):
                with disconnect_submissions_signal_receiver(score_set):
                    clear_student_state(user_id=user_id,
                                        course_id=unicode(course_id),
                                        item_id=unicode(module_state_key),
                                        requesting_user_id=requesting_user_id)
                submission_cleared = True
    except ItemNotFoundError:
        block = None
        log.warning(
            "Could not find %s in modulestore when attempting to reset attempts.",
            module_state_key)

    # Reset the student's score in the submissions API, if xblock.clear_student_state has not done so already.
    # We need to do this before retrieving the `StudentModule` model, because a score may exist with no student module.

    # TODO: Should the LMS know about sub_api and call this reset, or should it generically call it on all of its
    # xblock services as well?  See JIRA ARCH-26.
    if delete_module and not submission_cleared:
        sub_api.reset_score(
            user_id,
            text_type(course_id),
            text_type(module_state_key),
        )

    module_to_reset = StudentModule.objects.get(
        student_id=student.id,
        course_id=course_id,
        module_state_key=module_state_key)

    if delete_module:
        module_to_reset.delete()
        create_new_event_transaction_id()
        set_event_transaction_type(STATE_DELETED_EVENT_TYPE)
        tracker.emit(
            unicode(STATE_DELETED_EVENT_TYPE), {
                'user_id': unicode(student.id),
                'course_id': unicode(course_id),
                'problem_id': unicode(module_state_key),
                'instructor_id': unicode(requesting_user.id),
                'event_transaction_id': unicode(get_event_transaction_id()),
                'event_transaction_type': unicode(STATE_DELETED_EVENT_TYPE),
            })
        if not submission_cleared:
            _fire_score_changed_for_block(
                course_id,
                student,
                block,
                module_state_key,
            )
    else:
        _reset_module_attempts(module_to_reset)
Exemple #20
0
def reset_student_attempts(course_id, student, module_state_key, requesting_user, delete_module=False):
    """
    Reset student attempts for a problem. Optionally deletes all student state for the specified problem.

    In the previous instructor dashboard it was possible to modify/delete
    modules that were not problems. That has been disabled for safety.

    `student` is a User
    `problem_to_reset` is the name of a problem e.g. 'L2Node1'.
    To build the module_state_key 'problem/' and course information will be appended to `problem_to_reset`.

    Raises:
        ValueError: `problem_state` is invalid JSON.
        StudentModule.DoesNotExist: could not load the student module.
        submissions.SubmissionError: unexpected error occurred while resetting the score in the submissions API.

    """
    user_id = anonymous_id_for_user(student, course_id)
    requesting_user_id = anonymous_id_for_user(requesting_user, course_id)
    submission_cleared = False
    try:
        # A block may have children. Clear state on children first.
        block = modulestore().get_item(module_state_key)
        if block.has_children:
            for child in block.children:
                try:
                    reset_student_attempts(course_id, student, child, requesting_user, delete_module=delete_module)
                except StudentModule.DoesNotExist:
                    # If a particular child doesn't have any state, no big deal, as long as the parent does.
                    pass
        if delete_module:
            # Some blocks (openassessment) use StudentModule data as a key for internal submission data.
            # Inform these blocks of the reset and allow them to handle their data.
            clear_student_state = getattr(block, "clear_student_state", None)
            if callable(clear_student_state):
                with disconnect_submissions_signal_receiver(score_set):
                    clear_student_state(
                        user_id=user_id,
                        course_id=unicode(course_id),
                        item_id=unicode(module_state_key),
                        requesting_user_id=requesting_user_id
                    )
                submission_cleared = True
    except ItemNotFoundError:
        block = None
        log.warning("Could not find %s in modulestore when attempting to reset attempts.", module_state_key)

    # Reset the student's score in the submissions API, if xblock.clear_student_state has not done so already.
    # We need to do this before retrieving the `StudentModule` model, because a score may exist with no student module.

    # TODO: Should the LMS know about sub_api and call this reset, or should it generically call it on all of its
    # xblock services as well?  See JIRA ARCH-26.
    if delete_module and not submission_cleared:
        sub_api.reset_score(
            user_id,
            course_id.to_deprecated_string(),
            module_state_key.to_deprecated_string(),
        )

    module_to_reset = StudentModule.objects.get(
        student_id=student.id,
        course_id=course_id,
        module_state_key=module_state_key
    )

    if delete_module:
        module_to_reset.delete()
        create_new_event_transaction_id()
        grade_update_root_type = 'edx.grades.problem.state_deleted'
        set_event_transaction_type(grade_update_root_type)
        tracker.emit(
            unicode(grade_update_root_type),
            {
                'user_id': unicode(student.id),
                'course_id': unicode(course_id),
                'problem_id': unicode(module_state_key),
                'instructor_id': unicode(requesting_user.id),
                'event_transaction_id': unicode(get_event_transaction_id()),
                'event_transaction_type': unicode(grade_update_root_type),
            }
        )
        if not submission_cleared:
            _fire_score_changed_for_block(
                course_id,
                student,
                block,
                module_state_key,
            )
    else:
        _reset_module_attempts(module_to_reset)
Exemple #21
0
    def set_up_course(self,
                      enable_persistent_grades=True,
                      create_multiple_subsections=False):
        """
        Configures the course for this test.
        """
        # pylint: disable=attribute-defined-outside-init,no-member
        self.course = CourseFactory.create(
            org='edx',
            name='course',
            run='run',
        )
        if not enable_persistent_grades:
            PersistentGradesEnabledFlag.objects.create(enabled=False)

        self.chapter = ItemFactory.create(parent=self.course,
                                          category="chapter",
                                          display_name="Chapter")
        self.sequential = ItemFactory.create(parent=self.chapter,
                                             category='sequential',
                                             display_name="Sequential1")
        self.problem = ItemFactory.create(parent=self.sequential,
                                          category='problem',
                                          display_name='Problem')

        if create_multiple_subsections:
            seq2 = ItemFactory.create(parent=self.chapter,
                                      category='sequential')
            ItemFactory.create(parent=seq2, category='problem')

        self.frozen_now_datetime = datetime.now().replace(tzinfo=pytz.UTC)
        self.frozen_now_timestamp = to_timestamp(self.frozen_now_datetime)

        self.problem_weighted_score_changed_kwargs = OrderedDict([
            ('weighted_earned', 1.0),
            ('weighted_possible', 2.0),
            ('user_id', self.user.id),
            ('anonymous_user_id', 5),
            ('course_id', unicode(self.course.id)),
            ('usage_id', unicode(self.problem.location)),
            ('only_if_higher', None),
            ('modified', self.frozen_now_datetime),
            ('score_db_table',
             ScoreDatabaseTableEnum.courseware_student_module),
        ])

        create_new_event_transaction_id()

        self.recalculate_subsection_grade_kwargs = OrderedDict([
            ('user_id', self.user.id),
            ('course_id', unicode(self.course.id)),
            ('usage_id', unicode(self.problem.location)),
            ('anonymous_user_id', 5),
            ('only_if_higher', None),
            ('expected_modified_time', self.frozen_now_timestamp),
            ('score_deleted', False),
            ('event_transaction_id', unicode(get_event_transaction_id())),
            ('event_transaction_type', u'edx.grades.problem.submitted'),
            ('score_db_table',
             ScoreDatabaseTableEnum.courseware_student_module),
        ])

        # this call caches the anonymous id on the user object, saving 4 queries in all happy path tests
        _ = anonymous_id_for_user(self.user, self.course.id)