Beispiel #1
0
    def mark_blocks_completed(block, user, course_key):
        """
        Walk course tree, marking block completion.
        Mark 'most recent completed block as 'resume_block'

        """
        last_completed_child_position = BlockCompletion.get_latest_block_completed(user, course_key)

        if last_completed_child_position:
            # Mutex w/ NOT 'course_block_completions'
            recurse_mark_complete(
                course_block_completions=BlockCompletion.get_course_completions(user, course_key),
                latest_completion=last_completed_child_position,
                block=block
            )
Beispiel #2
0
def get_subsection_completion_percentage(subsection_usage_key, user):
    """
    Computes completion percentage for a subsection in a given course for a user
    Arguments:
        subsection_usage_key: key of subsection
        user: The user whose completion percentage needs to be computed
    Returns:
        User's completion percentage for given subsection
    """
    subsection_completion_percentage = 0.0
    try:
        subsection_structure = get_course_blocks(user, subsection_usage_key)
        if any(subsection_structure):
            completable_blocks = [
                block for block in subsection_structure
                if block.block_type not in ['chapter', 'sequential', 'vertical']
            ]
            if not completable_blocks:
                return 0
            subsection_completion_total = 0
            course_block_completions = BlockCompletion.get_course_completions(user, subsection_usage_key.course_key)
            for block in completable_blocks:
                if course_block_completions.get(block):
                    subsection_completion_total += course_block_completions.get(block)
            subsection_completion_percentage = min(
                100 * (subsection_completion_total / float(len(completable_blocks))), 100
            )

    except ItemNotFoundError as err:
        log.warning("Could not find course_block for subsection=%s error=%s", subsection_usage_key, err)

    return subsection_completion_percentage
Beispiel #3
0
def get_subsection_completion_percentage(subsection_usage_key, user):
    """
    Computes completion percentage for a subsection in a given course for a user
    Arguments:
        subsection_usage_key: key of subsection
        user: The user whose completion percentage needs to be computed
    Returns:
        User's completion percentage for given subsection
    """
    subsection_completion_percentage = 0.0
    try:
        subsection_structure = get_course_blocks(user, subsection_usage_key)
        if any(subsection_structure):
            completable_blocks = []
            for block in subsection_structure:
                completion_mode = subsection_structure.get_xblock_field(
                    block, 'completion_mode')

                #  always exclude html blocks (in addition to EXCLUDED blocks) for gating calculations
                #  See https://openedx.atlassian.net/browse/WL-1798
                if completion_mode not in (CompletionMode.AGGREGATOR, CompletionMode.EXCLUDED) \
                        and not block.block_type == 'html':
                    completable_blocks.append(block)

            if not completable_blocks:
                return 100
            subsection_completion_total = 0
            course_key = subsection_usage_key.course_key
            course_block_completions = BlockCompletion.get_learning_context_completions(
                user, course_key)
            for block in completable_blocks:
                if course_block_completions.get(block):
                    subsection_completion_total += course_block_completions.get(
                        block)
            subsection_completion_percentage = min(
                100 *
                (subsection_completion_total / float(len(completable_blocks))),
                100)

    except ItemNotFoundError as err:
        log.warning(u"Could not find course_block for subsection=%s error=%s",
                    subsection_usage_key, err)

    return subsection_completion_percentage
Beispiel #4
0
def get_subsection_completion_percentage(subsection_usage_key, user):
    """
    Computes completion percentage for a subsection in a given course for a user
    Arguments:
        subsection_usage_key: key of subsection
        user: The user whose completion percentage needs to be computed
    Returns:
        User's completion percentage for given subsection
    """
    subsection_completion_percentage = 0.0
    try:
        subsection_structure = get_course_blocks(user, subsection_usage_key)
        if any(subsection_structure):
            completable_blocks = []
            for block in subsection_structure:
                completion_mode = subsection_structure.get_xblock_field(
                    block, 'completion_mode')

                if completion_mode not in (CompletionMode.AGGREGATOR,
                                           CompletionMode.EXCLUDED):
                    completable_blocks.append(block)

            if not completable_blocks:
                return 0
            subsection_completion_total = 0
            course_block_completions = BlockCompletion.get_course_completions(
                user, subsection_usage_key.course_key)
            for block in completable_blocks:
                if course_block_completions.get(block):
                    subsection_completion_total += course_block_completions.get(
                        block)
            subsection_completion_percentage = min(
                100 *
                (subsection_completion_total / float(len(completable_blocks))),
                100)

    except ItemNotFoundError as err:
        log.warning("Could not find course_block for subsection=%s error=%s",
                    subsection_usage_key, err)

    return subsection_completion_percentage
Beispiel #5
0
def get_subsection_completion_percentage(subsection_usage_key, user):
    """
    Computes completion percentage for a subsection in a given course for a user
    Arguments:
        subsection_usage_key: key of subsection
        user: The user whose completion percentage needs to be computed
    Returns:
        User's completion percentage for given subsection
    """
    subsection_completion_percentage = 0.0
    try:
        subsection_structure = get_course_blocks(user, subsection_usage_key)
        if any(subsection_structure):
            completable_blocks = []
            for block in subsection_structure:
                completion_mode = subsection_structure.get_xblock_field(
                    block, 'completion_mode'
                )

                #  always exclude html blocks (in addition to EXCLUDED blocks) for gating calculations
                #  See https://openedx.atlassian.net/browse/WL-1798
                if completion_mode not in (CompletionMode.AGGREGATOR, CompletionMode.EXCLUDED) \
                        and not block.block_type == 'html':
                    completable_blocks.append(block)

            if not completable_blocks:
                return 100
            subsection_completion_total = 0
            course_block_completions = BlockCompletion.get_course_completions(user, subsection_usage_key.course_key)
            for block in completable_blocks:
                if course_block_completions.get(block):
                    subsection_completion_total += course_block_completions.get(block)
            subsection_completion_percentage = min(
                100 * (subsection_completion_total / float(len(completable_blocks))), 100
            )

    except ItemNotFoundError as err:
        log.warning(u"Could not find course_block for subsection=%s error=%s", subsection_usage_key, err)

    return subsection_completion_percentage
Beispiel #6
0
    def test_migrated(self):
        source = self._create_user(enrolled=self.course)
        target = self._create_user()

        self._create_user_progress(source)

        with patch('openedx.core.djangoapps.user_api.completion.tasks.update_user_gradebook') as update_user_gradebook:
            outcome = _migrate_progress(self.course_id, source.email, target.email)

        course_key = str(self.course.id)
        update_user_gradebook.assert_has_calls([
            call(course_key, source.id), call(course_key, target.id)
        ])
        self.assertEqual(outcome, OUTCOME_MIGRATED)

        # Check that all user's progress transferred to another user
        assert CourseEnrollment.objects.filter(user=target, course=self.course.id).exists()
        assert BlockCompletion.user_learning_context_completion_queryset(user=target, context_key=self.course.id).exists()
        assert StudentItem.objects.filter(
            course_id=self.course.id, student_id=anonymous_id_for_user(target, self.course.id)
        ).exists()
        assert StudentModule.objects.filter(student=target, course_id=self.course.id).exists()
Beispiel #7
0
def get_subsection_completion_percentage(subsection_usage_key, user):
    """
    Computes completion percentage for a subsection in a given course for a user
    Arguments:
        subsection_usage_key: key of subsection
        user: The user whose completion percentage needs to be computed
    Returns:
        User's completion percentage for given subsection
    """
    subsection_completion_percentage = 0.0
    try:
        subsection_structure = get_course_blocks(user, subsection_usage_key)
        if any(subsection_structure):
            completable_blocks = []
            for block in subsection_structure:
                completion_mode = subsection_structure.get_xblock_field(
                    block, 'completion_mode'
                )

                if completion_mode not in (CompletionMode.AGGREGATOR, CompletionMode.EXCLUDED):
                    completable_blocks.append(block)

            if not completable_blocks:
                return 0
            subsection_completion_total = 0
            course_block_completions = BlockCompletion.get_course_completions(user, subsection_usage_key.course_key)
            for block in completable_blocks:
                if course_block_completions.get(block):
                    subsection_completion_total += course_block_completions.get(block)
            subsection_completion_percentage = min(
                100 * (subsection_completion_total / float(len(completable_blocks))), 100
            )

    except ItemNotFoundError as err:
        log.warning("Could not find course_block for subsection=%s error=%s", subsection_usage_key, err)

    return subsection_completion_percentage
Beispiel #8
0
def get_subsection_completion_percentage(subsection_usage_key, user):
    """
    Computes completion percentage for a subsection in a given course for a user
    Arguments:
        subsection_usage_key: key of subsection
        user: The user whose completion percentage needs to be computed
    Returns:
        User's completion percentage for given subsection
    """
    subsection_completion_percentage = 0.0
    try:
        subsection_structure = get_course_blocks(user, subsection_usage_key)
        if any(subsection_structure):
            completable_blocks = [
                block for block in subsection_structure if block.block_type
                not in ['chapter', 'sequential', 'vertical']
            ]
            if not completable_blocks:
                return 0
            subsection_completion_total = 0
            course_block_completions = BlockCompletion.get_course_completions(
                user, subsection_usage_key.course_key)
            for block in completable_blocks:
                if course_block_completions.get(block):
                    subsection_completion_total += course_block_completions.get(
                        block)
            subsection_completion_percentage = min(
                100 *
                (subsection_completion_total / float(len(completable_blocks))),
                100)

    except ItemNotFoundError as err:
        log.warning("Could not find course_block for subsection=%s error=%s",
                    subsection_usage_key, err)

    return subsection_completion_percentage
Beispiel #9
0
def retrieve_last_sitewide_block_completed(username):
    """
    Completion utility
    From a string 'username' or object User retrieve
    the last course block marked as 'completed' and construct a URL

    :param username: str(username) or obj(User)
    :return: block_lms_url

    """
    if not completion_waffle.waffle().is_enabled(
            completion_waffle.ENABLE_COMPLETION_TRACKING):
        return

    if not isinstance(username, User):
        userobj = User.objects.get(username=username)
    else:
        userobj = username
    latest_completions_by_course = BlockCompletion.latest_blocks_completed_all_courses(
        userobj)

    known_site_configs = [
        other_site_config.get_value('course_org_filter')
        for other_site_config in SiteConfiguration.objects.all()
        if other_site_config.get_value('course_org_filter')
    ]

    current_site_configuration = get_config_value_from_site_or_settings(
        name='course_org_filter', site=get_current_site())

    # courses.edx.org has no 'course_org_filter'
    # however the courses within DO, but those entries are not found in
    # known_site_configs, which are White Label sites
    # This is necessary because the WL sites and courses.edx.org
    # have the same AWS RDS mySQL instance
    candidate_course = None
    candidate_block_key = None
    latest_date = None
    # Go through dict, find latest
    for course, [modified_date,
                 block_key] in latest_completions_by_course.items():
        if not current_site_configuration:
            # This is a edx.org
            if course.org in known_site_configs:
                continue
            if not latest_date or modified_date > latest_date:
                candidate_course = course
                candidate_block_key = block_key
                latest_date = modified_date

        else:
            # This is a White Label site, and we should find candidates from the same site
            if course.org not in current_site_configuration:
                # Not the same White Label, or a edx.org course
                continue
            if not latest_date or modified_date > latest_date:
                candidate_course = course
                candidate_block_key = block_key
                latest_date = modified_date

    if not candidate_course:
        return

    lms_root = SiteConfiguration.get_value_for_org(candidate_course.org,
                                                   "LMS_ROOT_URL",
                                                   settings.LMS_ROOT_URL)

    try:
        item = modulestore().get_item(candidate_block_key, depth=1)
    except ItemNotFoundError:
        item = None

    if not (lms_root and item):
        return

    return u"{lms_root}/courses/{course_key}/jump_to/{location}".format(
        lms_root=lms_root,
        course_key=text_type(item.location.course_key),
        location=text_type(item.location),
    )
Beispiel #10
0
def retrieve_last_sitewide_block_completed(user):
    """
    Completion utility
    From a string 'username' or object User retrieve
    the last course block marked as 'completed' and construct a URL

    :param user: obj(User)
    :return: block_lms_url

    """
    if not completion_waffle.waffle().is_enabled(completion_waffle.ENABLE_COMPLETION_TRACKING):
        return

    latest_completions_by_course = BlockCompletion.latest_blocks_completed_all_courses(user)

    known_site_configs = [
        other_site_config.get_value('course_org_filter') for other_site_config in SiteConfiguration.objects.all()
        if other_site_config.get_value('course_org_filter')
    ]

    current_site_configuration = get_config_value_from_site_or_settings(
        name='course_org_filter',
        site=get_current_site()
    )

    # courses.edx.org has no 'course_org_filter'
    # however the courses within DO, but those entries are not found in
    # known_site_configs, which are White Label sites
    # This is necessary because the WL sites and courses.edx.org
    # have the same AWS RDS mySQL instance
    candidate_course = None
    candidate_block_key = None
    latest_date = None
    # Go through dict, find latest
    for course, [modified_date, block_key] in latest_completions_by_course.items():
        if not current_site_configuration:
            # This is a edx.org
            if course.org in known_site_configs:
                continue
            if not latest_date or modified_date > latest_date:
                candidate_course = course
                candidate_block_key = block_key
                latest_date = modified_date

        else:
            # This is a White Label site, and we should find candidates from the same site
            if course.org not in current_site_configuration:
                # Not the same White Label, or a edx.org course
                continue
            if not latest_date or modified_date > latest_date:
                candidate_course = course
                candidate_block_key = block_key
                latest_date = modified_date

    if not candidate_course:
        return

    lms_root = SiteConfiguration.get_value_for_org(candidate_course.org, "LMS_ROOT_URL", settings.LMS_ROOT_URL)

    try:
        item = modulestore().get_item(candidate_block_key, depth=1)
    except ItemNotFoundError:
        item = None

    if not (lms_root and item):
        return

    return u"{lms_root}/courses/{course_key}/jump_to/{location}".format(
        lms_root=lms_root,
        course_key=text_type(item.location.course_key),
        location=text_type(item.location),
    )
Beispiel #11
0
def _migrate_progress(course, source, target):
    """
    Task that migrates progress from one user to another
    """
    log.info('Started progress migration from "%s" to "%s" for "%s" course', source, target, course)

    try:
        course_key = CourseKey.from_string(course)
    except InvalidKeyError:
        log.warning('Migration failed. Invalid course key: %s', course)
        return OUTCOME_COURSE_KEY_INVALID

    try:
        get_course(course_key)
    except ValueError:
        log.warning('Migration failed. Course not found:: %s', course_key)
        return OUTCOME_COURSE_NOT_FOUND

    try:
        source = get_user_model().objects.get(email=source)
    except ObjectDoesNotExist:
        log.warning('Migration failed. Source user with such email not found: %s', source)
        return OUTCOME_SOURCE_NOT_FOUND

    try:
        enrollment = CourseEnrollment.objects.select_for_update().get(user=source, course=course_key)
    except ObjectDoesNotExist:
        log.warning(
            'Migration failed. Source user with email "%s" not enrolled in "%s" course', source.email, course_key
        )
        return OUTCOME_SOURCE_NOT_ENROLLED

    try:
        target = get_user_model().objects.get(email=target)
    except ObjectDoesNotExist:
        log.warning('Migration failed. Target user with such email not found: %s', target)
        return OUTCOME_TARGET_NOT_FOUND

    try:
        assert not BlockCompletion.user_learning_context_completion_queryset(
            user=target, context_key=course_key
        ).exists()
        anonymous_ids = AnonymousUserId.objects.filter(user=target, course_id=course_key).values('anonymous_user_id')
        assert not StudentItem.objects.filter(course_id=course_key, student_id__in=anonymous_ids).exists()
    except AssertionError:
        log.warning(
            'Migration failed. Target user with email "%s" already enrolled in "%s" course and progress is present.', target.email, course_key
        )
        return OUTCOME_TARGET_ALREADY_ENROLLED

    # Fetch completions for source user
    completions = BlockCompletion.user_learning_context_completion_queryset(
        user=source, context_key=course_key
    ).select_for_update()

    # Fetch edx-submissions data for source user
    anonymous_ids = AnonymousUserId.objects.filter(user=source, course_id=course_key).values('anonymous_user_id')
    submissions = StudentItem.objects.select_for_update().filter(course_id=course_key, student_id__in=anonymous_ids)

    # Fetch StudentModule table data for source user
    student_states = StudentModule.objects.select_for_update().filter(student=source, course_id=course_key)

    # Actually migrate completions and progress
    try:
        # Modify enrollment
        try:
            target_enrollment = CourseEnrollment.objects.select_for_update().get(user=target, course=course_key)
        except ObjectDoesNotExist:
            enrollment.user = target
        else:
            enrollment.is_active = False
            target_enrollment.is_active = True
            target_enrollment.save()
        finally:
            enrollment.save()

        # Migrate completions for user
        for completion in completions:
            completion.user = target
            completion.save()

        # Migrate edx-submissions
        for submission in submissions:
            submission.student_id = anonymous_id_for_user(target, course_key)
            submission.save()

        # Migrate StudentModule
        for state in student_states:
            state.student = target
            state.save()

        log.info('Removing stale aggregators for source user.')
        Aggregator.objects.filter(user=source, course_key=course_key).delete()

    except Exception:
        log.exception("Unexpected error while migrating user progress.")
        return OUTCOME_FAILED_MIGRATION

    log.info('Updating gradebook for %s user.', source.email)
    update_user_gradebook(course, source.id)
    log.info('Updating gradebook for %s user.', target.email)
    update_user_gradebook(course, target.id)

    log.info(
        'User progress in "%s" course successfully migrated from "%s" to "%s"', course_key, source.email, target.email
    )
    return OUTCOME_MIGRATED
Beispiel #12
0
    def get(self, request, username, course_id):
        """
        Gets a progress information.

        Args:
            request (Request): Django request object.
            username (string): URI element specifying the user's username.
            course_id (string): URI element specifying the course location.

        Return:
            A JSON serialized representation of the certificate.
        """

        def aggregate_progress(course_completions, all_blocks, block_id):
            """
            Recursively get the progress for a units (vertical),
            given list of all blocks.
            Parameters:
                course_completions: a dictionary of completion values by block IDs
                all_blocks: a dictionary of the block structure for a subsection
                block_id: an ID of a block for which to get completion
            """
            block = all_blocks.get(block_id)
            child_ids = block.get('children', [])
            if block.get('type', None) == 'vertical':
                self.units_progress_list.append([block_id, 0, 0])

            if not child_ids and (block.get('type', None) in block_xblocks_types_filter):
                self.units_progress_list[-1][1] += 1
                self.units_progress_list[-1][2] += course_completions.get(block.serializer.instance, 0)

            for child_id in child_ids:
                aggregate_progress(course_completions, all_blocks, child_id)

        def calculate_progress():
            """
            Calculate course progress from units progress
            """
            number_of_units = len(self.units_progress_list)
            if number_of_units == 0:
                return float(0.0)
            else:
                cumulative_sum = 0
                for unit_progress in self.units_progress_list:
                    if unit_progress[1] == 0:
                        number_of_units -= 1
                    else:
                        cumulative_sum += unit_progress[2]/unit_progress[1]
                return round(cumulative_sum/number_of_units, 3)

        course_object_id = CourseKey.from_string(course_id)
        self.check_object_permissions(self.request, courses.get_course_by_id(course_object_id))
        course_usage_key = modulestore().make_course_usage_key(course_object_id)

        try:
            user_id = User.objects.get(username=username).id
        except User.DoesNotExist:
            return Response(
                status=404,
                data={'error_code': u'Not found.'}
            )

        block_navigation_types_filter = [
            'course',
            'chapter',
            'sequential',
            'vertical',
        ]

        block_xblocks_types_filter = [
            'html',
            'problem',
            'video',
            'drag-and-drop-v2',
            'poll',
            'videojs',
            'embedded_answers',
            'inline-dropdown',
            'openassessment',
            'audioplayer',
        ]

        block_types_filter = block_navigation_types_filter + block_xblocks_types_filter

        try:
            blocks = get_blocks(request, course_usage_key, nav_depth=3, requested_fields=[
                'children', 'type',
            ],
                block_types_filter=block_types_filter
            )
        except ItemNotFoundError:
            return Response(
                status=404,
                data={'error_code': u'Not found.'}
            )

        course_completions = BlockCompletion.get_course_completions(user_id, course_object_id)
        aggregate_progress(course_completions, blocks['blocks'], blocks['root'])
        calculated_progress = calculate_progress()
        response_dict = {"username": username,
                         "course_id": course_id,
                         "completion_value": calculated_progress}

        return Response(response_dict, status=status.HTTP_200_OK)