Пример #1
0
def get_user_course_response_task(users, course_str, depth, callback_url):
    """
    Get a list of users grades' for a course
    """
    user_grades = {}
    grades_schema = {}
    course_key = CourseKey.from_string(str(course_str))
    course = courses.get_course(course_key)
    for user in users:
        course_grade = CourseGradeFactory().update(user, course)
        if depth == "all":
            grades_schema = get_user_grades(user.id, course_str)
        else:
            grades_schema = "Showing course grade summary, specify depth=all in query params."
        user_grades[user.username] = {
            'name': "{} {}".format(user.first_name, user.last_name),
            'email': user.email,
            'start_date': course.start,
            'end_date':
            course.end if not None else "This course has no end date.",
            'all_grades': grades_schema,
            "passed": course_grade.passed,
            "percent": course_grade.percent
        }

    #requests.post(str(callback_url), data=user_grades)
    return user_grades
Пример #2
0
    def send_xapi_statements(self, lrs_configuration, days):
        """
        Send xAPI analytics data of the enterprise learners to the given LRS.

        Arguments:
            lrs_configuration (XAPILRSConfiguration): Configuration object containing LRS configurations
                of the LRS where to send xAPI  learner analytics.
            days (int): Include course enrollment of this number of days.
        """
        persistent_course_grades = self.get_course_completions(
            lrs_configuration.enterprise_customer, days)
        users = self.prefetch_users(persistent_course_grades)
        course_overviews = self.prefetch_courses(persistent_course_grades)

        for persistent_course_grade in persistent_course_grades:
            try:
                user = users.get(persistent_course_grade.user_id)
                course_overview = course_overviews.get(
                    persistent_course_grade.course_id)
                course_grade = CourseGradeFactory().read(
                    user, course_key=persistent_course_grade.course_id)
                send_course_completion_statement(lrs_configuration, user,
                                                 course_overview, course_grade)
            except ClientError:
                LOGGER.exception(
                    'Client error while sending course completion to xAPI for'
                    ' enterprise customer {enterprise_customer}.'.format(
                        enterprise_customer=lrs_configuration.
                        enterprise_customer.name))
Пример #3
0
def get_course_grades(user, course):
    """
    Gets course grades for a given student
    """
    grades = CourseGradeFactory().read(user, course)

    return grades
Пример #4
0
 def test_cert_already_generated(self):
     with mock.patch(
             'lms.djangoapps.certificates.signals.generate_certificate.apply_async',
             return_value=None
     ) as mock_generate_certificate_apply_async:
         grade_factory = CourseGradeFactory()
         # Create the certificate
         GeneratedCertificate.eligible_certificates.create(
             user=self.user,
             course_id=self.course.id,
             status=CertificateStatuses.downloadable
         )
         # Certs are not re-fired after passing
         with mock_passing_grade():
             grade_factory.update(self.user, self.course)
             mock_generate_certificate_apply_async.assert_not_called()
Пример #5
0
def get_course_info_for_application_review(courses, user):
    """
    Get courses info for application review page.

    Args:
        courses (list): List of courses
        user (User): User object

    Returns:
        dict: Contains course name and percent grade | status
    """
    courses_info = {}
    for course in courses:
        if not CourseEnrollment.is_enrolled(user, course.id):
            courses_info[course.display_name] = NOT_STARTED
            continue

        grade = CourseGradeFactory().read(user, course_key=course.id)
        if grade.passed:
            courses_info[
                course.
                display_name] = f'{convert_float_point_to_percentage(grade.percent)}%'
            continue

        courses_info[course.display_name] = IN_PROGRESS
    return courses_info
Пример #6
0
    def check_state(self,
                    user,
                    descriptor,
                    expected_score,
                    expected_max_score,
                    expected_attempts=1):
        """
        Check that the StudentModule state contains the expected values.

        The student module is found for the test course, given the `username` and problem `descriptor`.

        Values checked include the number of attempts, the score, and the max score for a problem.
        """
        module = self.get_student_module(user.username, descriptor)
        self.assertEqual(module.grade, expected_score)
        self.assertEqual(module.max_grade, expected_max_score)
        state = json.loads(module.state)
        attempts = state['attempts']
        self.assertEqual(attempts, expected_attempts)
        if attempts > 0:
            self.assertIn('correct_map', state)
            self.assertIn('student_answers', state)
            self.assertGreater(len(state['correct_map']), 0)
            self.assertGreater(len(state['student_answers']), 0)

        # assume only one problem in the subsection and the grades
        # are in sync.
        expected_subsection_grade = expected_score

        course_grade = CourseGradeFactory().read(user, self.course)
        self.assertEquals(
            course_grade.graded_subsections_by_format['Homework'][
                self.problem_section.location].graded_total.earned,
            expected_subsection_grade,
        )
Пример #7
0
def get_grade_book_page(request, course, course_key):
    """
    Get student records per page along with page information i.e current page, total pages and
    offset information.
    """
    # Unsanitized offset
    current_offset = request.GET.get('offset', 0)
    enrolled_students = User.objects.filter(
        courseenrollment__course_id=course_key,
        courseenrollment__is_active=1
    ).order_by('username').select_related("profile")

    total_students = enrolled_students.count()
    page = calculate_page_info(current_offset, total_students)
    offset = page["offset"]
    total_pages = page["total_pages"]

    if total_pages > 1:
        # Apply limit on queryset only if total number of students are greater then MAX_STUDENTS_PER_PAGE_GRADE_BOOK.
        enrolled_students = enrolled_students[offset: offset + MAX_STUDENTS_PER_PAGE_GRADE_BOOK]

    with modulestore().bulk_operations(course.location.course_key):
        student_info = [
            {
                'username': student.username,
                'id': student.id,
                'email': student.email,
                'grade_summary': CourseGradeFactory().create(student, course).summary
            }
            for student in enrolled_students
        ]
    return student_info, page
Пример #8
0
    def __init__(self, site, user, enrollments=None, uuid=None):
        self.site = site
        self.user = user

        self.enrollments = enrollments or list(CourseEnrollment.enrollments_for_user(self.user))
        self.enrollments.sort(key=lambda e: e.created, reverse=True)

        self.enrolled_run_modes = {}
        self.course_run_ids = []
        for enrollment in self.enrollments:
            # enrollment.course_id is really a CourseKey (╯ಠ_ಠ)╯︵ ┻━┻
            enrollment_id = unicode(enrollment.course_id)
            mode = enrollment.mode
            if mode == CourseMode.NO_ID_PROFESSIONAL_MODE:
                mode = CourseMode.PROFESSIONAL
            self.enrolled_run_modes[enrollment_id] = mode
            # We can't use dict.keys() for this because the course run ids need to be ordered
            self.course_run_ids.append(enrollment_id)

        self.course_grade_factory = CourseGradeFactory()

        if uuid:
            self.programs = [get_programs(self.site, uuid=uuid)]
        else:
            self.programs = attach_program_detail_url(get_programs(self.site))
Пример #9
0
    def _get_single_user_grade(self, request, course_key):
        """
        Returns a grade response for the user object corresponding to the request's 'username' parameter,
        or the current request.user if no 'username' was provided.
        Args:
            request (Request): django request object to check for username or request.user object
            course_key (CourseLocator): The course to retrieve user grades for.

        Returns:
            A serializable list of grade responses
        """
        if 'username' in request.GET:
            username = request.GET.get('username')
        else:
            username = request.user.username

        grade_user = USER_MODEL.objects.get(username=username)

        if not enrollment_data.get_course_enrollment(username,
                                                     str(course_key)):
            raise CourseEnrollment.DoesNotExist

        course_grade = CourseGradeFactory().read(grade_user,
                                                 course_key=course_key)
        return Response(
            [self._make_grade_response(grade_user, course_key, course_grade)])
Пример #10
0
    def test_grade_changed(self, mock_send_grade_if_interesting):
        user = UserFactory()
        course = XModuleCourseFactory()

        self.assertFalse(mock_send_grade_if_interesting.called)
        CourseGradeFactory().update(user, course=course)
        self.assertTrue(mock_send_grade_if_interesting.called)
Пример #11
0
 def test_cert_already_generated(self):
     with mock.patch(
             'lms.djangoapps.certificates.signals.generate_certificate.apply_async',
             return_value=None
     ) as mock_generate_certificate_apply_async:
         grade_factory = CourseGradeFactory()
         # Create the certificate
         GeneratedCertificate.eligible_certificates.create(
             user=self.user,
             course_id=self.course.id,
             status=CertificateStatuses.downloadable
         )
         # Certs are not re-fired after passing
         with mock_passing_grade():
             grade_factory.update(self.user, self.course)
             mock_generate_certificate_apply_async.assert_not_called()
Пример #12
0
    def get(self, request, course_id):
        """
        Returns a gradebook entry/entries (i.e. both course and subsection-level grade data)
        for all users enrolled in a course, or a single user enrolled in a course
        if a `username` parameter is provided.

        Args:
            request: A Django request object.
            course_id: A string representation of a CourseKey object.
        """
        username = request.GET.get('username')
        course_key = get_course_key(request, course_id)
        course = get_course_with_access(request.user, 'staff', course_key, depth=None)

        if username:
            with self._get_user_or_raise(request, course_id) as grade_user:
                course_grade = CourseGradeFactory().read(grade_user, course)

            entry = self._gradebook_entry(grade_user, course, course_grade)
            serializer = StudentGradebookEntrySerializer(entry)
            return Response(serializer.data)
        else:
            # list gradebook data for all course enrollees
            entries = []
            for user, course_grade, exc in self._iter_user_grades(course_key):
                if not exc:
                    entries.append(self._gradebook_entry(user, course, course_grade))
            serializer = StudentGradebookEntrySerializer(entries, many=True)
            return self.get_paginated_response(serializer.data)
Пример #13
0
    def test_grade_changed(self, mock_send_grade_if_interesting):
        user = UserFactory()
        course = XModuleCourseFactory()

        assert not mock_send_grade_if_interesting.called
        CourseGradeFactory().update(user, course=course)
        assert mock_send_grade_if_interesting.called
Пример #14
0
    def get(self, request, course_id):
        """
        Gets a course progress status.

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

        Return:
            A JSON serialized representation of the requesting user's current grade status.
        """

        course = self._get_course(course_id, request.user, 'load')
        if isinstance(course, Response):
            # Returns a 404 if course_id is invalid, or request.user is not enrolled in the course
            return course

        grade_user = self._get_effective_user(request, course)
        if isinstance(grade_user, Response):
            # Returns a 403 if the request.user can't access grades for the requested user,
            # or a 404 if the requested user does not exist.
            return grade_user

        prep_course_for_grading(course, request)
        course_grade = CourseGradeFactory().create(grade_user, course)
        return Response([{
            'username': grade_user.username,
            'course_key': course_id,
            'passed': course_grade.passed,
            'percent': course_grade.percent,
            'letter_grade': course_grade.letter_grade,
        }])
    def _rows_for_users(self, context, users):
        """
        Returns a list of rows for the given users for this report.
        """
        with modulestore().bulk_operations(context.course_id):
            bulk_context = _CourseGradeBulkContext(context, users)

            success_rows, error_rows = [], []
            for user, course_grade, error in CourseGradeFactory().iter(
                    users,
                    course=context.course,
                    collected_block_structure=context.course_structure,
                    course_key=context.course_id,
            ):
                if not course_grade:
                    # An empty gradeset means we failed to grade a student.
                    error_rows.append(
                        [user.id, user.username,
                         text_type(error)])
                else:
                    success_rows.append(
                        [user.id, user.email, user.username] +
                        self._user_grades(course_grade, context) +
                        self._user_cohort_group_names(user, context) +
                        self._user_experiment_group_names(user, context) +
                        self._user_team_names(user, bulk_context.teams) +
                        self._user_verification_mode(
                            user, context, bulk_context.enrollments) +
                        self._user_certificate_info(
                            user, context, course_grade, bulk_context.certs) +
                        [_user_enrollment_status(user, context.course_id)])
            return success_rows, error_rows
Пример #16
0
 def setUp(self):
     super(TestGradeUcursosExportView, self).setUp()
     self.grade_factory = CourseGradeFactory()
     with patch('student.models.cc.User.save'):
         # staff user
         self.client_instructor = Client()
         self.client_student = Client()
         self.client_anonymous = Client()
         self.user_instructor = UserFactory(
             username='******',
             password='******',
             email='*****@*****.**',
             is_staff=True)
         role = CourseInstructorRole(self.course.id)
         role.add_users(self.user_instructor)
         self.client_instructor.login(
             username='******', password='******')
         self.student = UserFactory(
             username='******',
             password='******',
             email='*****@*****.**')
         self.student_2 = UserFactory(
             username='******',
             password='******',
             email='*****@*****.**')
         # Enroll the student in the course
         CourseEnrollmentFactory(
             user=self.student, course_id=self.course.id, mode='honor')
         CourseEnrollmentFactory(
             user=self.student_2, course_id=self.course.id, mode='honor')
         self.client_student.login(
             username='******', password='******')
Пример #17
0
    def _iter_user_grades(self,
                          course_key,
                          course_enrollment_filter=None,
                          related_models=None):
        """
        Args:
            course_key (CourseLocator): The course to retrieve grades for.
            course_enrollment_filter: Optional dictionary of keyword arguments to pass
            to `CourseEnrollment.filter()`.
            related_models: Optional list of related models to join to the CourseEnrollment table.

        Returns:
            An iterator of CourseGrade objects for users enrolled in the given course.
        """
        filter_kwargs = {
            'course_id': course_key,
            'is_active': True,
        }
        filter_kwargs.update(course_enrollment_filter or {})
        enrollments_in_course = CourseEnrollment.objects.filter(
            **filter_kwargs)
        if related_models:
            enrollments_in_course = enrollments_in_course.select_related(
                *related_models)

        paged_enrollments = self.paginate_queryset(enrollments_in_course)
        users = (enrollment.user for enrollment in paged_enrollments)
        grades = CourseGradeFactory().iter(users, course_key=course_key)

        for user, course_grade, exc in grades:
            yield user, course_grade, exc
Пример #18
0
    def test_cert_already_generated_unverified(self):
        with override_waffle_switch(AUTO_CERTIFICATE_GENERATION, active=True):
            GeneratedCertificateFactory(
                user=self.user,
                course_id=self.course.id,
                status=CertificateStatuses.unverified
            )

            with mock.patch(
                'lms.djangoapps.certificates.signals.generate_certificate_task',
                return_value=None
            ) as mock_cert_task:
                grade_factory = CourseGradeFactory()
                with mock_passing_grade():
                    grade_factory.update(self.user, self.course)
                    mock_cert_task.assert_called_with(self.user, self.course_key)
Пример #19
0
 def test_user_has_passing_grade(self):
     CourseEnrollment.enroll(self.user, self.course.id)
     self.course._grading_policy['GRADE_CUTOFFS']['Pass'] = 0  # pylint: disable=protected-access
     self.update_course(self.course, self.user.id)
     CourseGradeFactory().update(self.user, self.course)
     response = self.client.get(self.url)
     assert response.status_code == 200
     assert response.data['user_has_passing_grade'] is True
Пример #20
0
def send_composite_outcome(user_id, course_id, assignment_id, version):
    """
    Calculate and transmit the score for a composite module (such as a
    vertical).

    A composite module may contain multiple problems, so we need to
    calculate the total points earned and possible for all child problems. This
    requires calculating the scores for the whole course, which is an expensive
    operation.

    Callers should be aware that the score calculation code accesses the latest
    scores from the database. This can lead to a race condition between a view
    that updates a user's score and the calculation of the grade. If the Celery
    task attempts to read the score from the database before the view exits (and
    its transaction is committed), it will see a stale value. Care should be
    taken that this task is not triggered until the view exits.

    The GradedAssignment model has a version_number field that is incremented
    whenever the score is updated. It is used by this method for two purposes.
    First, it allows the task to exit if it detects that it has been superseded
    by another task that will transmit the score for the same assignment.
    Second, it prevents a race condition where two tasks calculate different
    scores for a single assignment, and may potentially update the campus LMS
    in the wrong order.
    """
    assignment = GradedAssignment.objects.get(id=assignment_id)
    if version != assignment.version_number:
        log.info(
            "Score passback for GradedAssignment %s skipped. More recent score available.",
            assignment.id
        )
        return
    course_key = CourseKey.from_string(course_id)
    mapped_usage_key = assignment.usage_key.map_into_course(course_key)
    user = User.objects.get(id=user_id)
    course = modulestore().get_course(course_key, depth=0)
    course_grade = CourseGradeFactory().read(user, course)
    earned, possible = course_grade.score_for_module(mapped_usage_key)
    if possible == 0:
        weighted_score = 0
    else:
        weighted_score = float(earned) / float(possible)

    assignment = GradedAssignment.objects.get(id=assignment_id)
    if assignment.version_number == version:
        outcomes.send_score_update(assignment, weighted_score)
Пример #21
0
def _get_course_grade_passed(user, course_id):
    """
        Get 'passed' (Boolean representing whether the course has been
                passed according to the course's grading policy.)
    """
    course_key = CourseKey.from_string(course_id)
    course_grade = CourseGradeFactory().read(user, course_key=course_key)
    return course_grade.passed
Пример #22
0
def send_composite_outcome(user_id, course_id, assignment_id, version):
    """
    Calculate and transmit the score for a composite module (such as a
    vertical).

    A composite module may contain multiple problems, so we need to
    calculate the total points earned and possible for all child problems. This
    requires calculating the scores for the whole course, which is an expensive
    operation.

    Callers should be aware that the score calculation code accesses the latest
    scores from the database. This can lead to a race condition between a view
    that updates a user's score and the calculation of the grade. If the Celery
    task attempts to read the score from the database before the view exits (and
    its transaction is committed), it will see a stale value. Care should be
    taken that this task is not triggered until the view exits.

    The GradedAssignment model has a version_number field that is incremented
    whenever the score is updated. It is used by this method for two purposes.
    First, it allows the task to exit if it detects that it has been superseded
    by another task that will transmit the score for the same assignment.
    Second, it prevents a race condition where two tasks calculate different
    scores for a single assignment, and may potentially update the campus LMS
    in the wrong order.
    """
    assignment = GradedAssignment.objects.get(id=assignment_id)
    if version != assignment.version_number:
        log.info(
            "Score passback for GradedAssignment %s skipped. More recent score available.",
            assignment.id)
        return
    course_key = CourseKey.from_string(course_id)
    mapped_usage_key = assignment.usage_key.map_into_course(course_key)
    user = User.objects.get(id=user_id)
    course = modulestore().get_course(course_key, depth=0)
    course_grade = CourseGradeFactory().create(user, course)
    earned, possible = course_grade.score_for_module(mapped_usage_key)
    if possible == 0:
        weighted_score = 0
    else:
        weighted_score = float(earned) / float(possible)

    assignment = GradedAssignment.objects.get(id=assignment_id)
    if assignment.version_number == version:
        outcomes.send_score_update(assignment, weighted_score)
Пример #23
0
    def get(self, request, course_id):
        """
        Returns a gradebook entry/entries (i.e. both course and subsection-level grade data)
        for all users enrolled in a course, or a single user enrolled in a course
        if a `username` parameter is provided.

        Args:
            request: A Django request object.
            course_id: A string representation of a CourseKey object.
        """
        course_key = get_course_key(request, course_id)
        course = get_course_with_access(request.user, 'staff', course_key, depth=None)

        if request.GET.get('username'):
            with self._get_user_or_raise(request, course_key) as grade_user:
                course_grade = CourseGradeFactory().read(grade_user, course)

            entry = self._gradebook_entry(grade_user, course, course_grade)
            serializer = StudentGradebookEntrySerializer(entry)
            return Response(serializer.data)
        else:
            filter_kwargs = {}
            related_models = []
            if request.GET.get('username_contains'):
                filter_kwargs['user__username__icontains'] = request.GET.get('username_contains')
                related_models.append('user')
            if request.GET.get('cohort_id'):
                cohort = cohorts.get_cohort_by_id(course_key, request.GET.get('cohort_id'))
                if cohort:
                    filter_kwargs['user__in'] = cohort.users.all()
                else:
                    filter_kwargs['user__in'] = []
            if request.GET.get('enrollment_mode'):
                filter_kwargs['mode'] = request.GET.get('enrollment_mode')

            entries = []
            users = self._paginate_users(course_key, filter_kwargs, related_models)

            with bulk_gradebook_view_context(course_key, users):
                for user, course_grade, exc in CourseGradeFactory().iter(users, course_key=course_key):
                    if not exc:
                        entries.append(self._gradebook_entry(user, course, course_grade))

            serializer = StudentGradebookEntrySerializer(entries, many=True)
            return self.get_paginated_response(serializer.data)
Пример #24
0
 def _add_entrance_exam_to_context(self, courseware_context):
     """
     Adds entrance exam related information to the given context.
     """
     if course_has_entrance_exam(self.course) and getattr(self.chapter, 'is_entrance_exam', False):
         courseware_context['entrance_exam_passed'] = user_has_passed_entrance_exam(self.effective_user, self.course)
         courseware_context['entrance_exam_current_score'] = get_entrance_exam_score_ratio(
             CourseGradeFactory().create(self.effective_user, self.course),
             get_entrance_exam_usage_key(self.course),
         )
 def user_course_passed(self):
     from lms.djangoapps.grades.course_grade_factory import CourseGradeFactory
     from django.contrib.auth.models import User
     try:
         user = User.objects.get(id=self.scope_ids.user_id)
         response = CourseGradeFactory().read(user,
                                              course_key=self.course_id)
         return response.passed
     except User.DoesNotExist:
         return False
Пример #26
0
    def get_grade_data(self, user, course_key, grade_cutoffs):
        """
        Collects and formats the grades data for a particular user and course.

        Args:
            user (User)
            course_key (CourseKey)
            grade_cutoffs: # TODO: LEARNER-3854: Complete docstring if implementing Learner Analytics.
        """
        course_grade = CourseGradeFactory().read(user, course_key=course_key)
        grades = []
        total_earned = 0
        total_possible = 0
        # answered_percent seems to be unused and it does not take into account assignment type weightings
        answered_percent = None

        chapter_grades = course_grade.chapter_grades.values()

        for chapter in chapter_grades:
            # Note: this code exists on the progress page. We should be able to remove it going forward.
            if not chapter['display_name'] == "hidden":
                for subsection_grade in chapter['sections']:
                    log.info(subsection_grade.display_name)
                    possible = subsection_grade.graded_total.possible
                    earned = subsection_grade.graded_total.earned
                    passing_grade = math.ceil(possible * grade_cutoffs['Pass'])
                    grades.append({
                        'assignment_type':
                        subsection_grade.format,
                        'total_earned':
                        earned,
                        'total_possible':
                        possible,
                        'passing_grade':
                        passing_grade,
                        'display_name':
                        subsection_grade.display_name,
                        'location':
                        unicode(subsection_grade.location),
                        'assigment_url':
                        reverse('jump_to_id',
                                kwargs={
                                    'course_id':
                                    unicode(course_key),
                                    'module_id':
                                    unicode(subsection_grade.location),
                                })
                    })
                    if earned > 0:
                        total_earned += earned
                        total_possible += possible

        if total_possible > 0:
            answered_percent = float(total_earned) / total_possible
        return (grades, answered_percent, course_grade.percent)
Пример #27
0
    def send_xapi_statements(self, lrs_configuration, days):
        """
        Send xAPI analytics data of the enterprise learners to the given LRS.

        Arguments:
            lrs_configuration (XAPILRSConfiguration): Configuration object containing LRS configurations
                of the LRS where to send xAPI  learner analytics.
            days (int): Include course enrollment of this number of days.
        """
        persistent_course_grades = self.get_course_completions(
            lrs_configuration.enterprise_customer, days)
        users = self.prefetch_users(persistent_course_grades)
        course_overviews = self.prefetch_courses(persistent_course_grades)

        for persistent_course_grade in persistent_course_grades:
            error_message = None
            user = users.get(persistent_course_grade.user_id)
            course_overview = course_overviews.get(
                persistent_course_grade.course_id)
            course_grade = CourseGradeFactory().read(
                user, course_key=persistent_course_grade.course_id)
            xapi_transmission_queryset = XAPILearnerDataTransmissionAudit.objects.filter(
                user=user, course_id=persistent_course_grade.course_id)
            if not xapi_transmission_queryset.exists():
                LOGGER.warning(
                    'XAPILearnerDataTransmissionAudit object does not exist for user: {username}, course: '
                    '{course_id} so skipping the course completion statement to xapi.'
                )
                continue
            try:
                send_course_completion_statement(lrs_configuration, user,
                                                 course_overview, course_grade)
            except ClientError:
                error_message = 'Client error while sending course completion to xAPI for ' \
                                'enterprise customer: {enterprise_customer}, user: {username} ' \
                                'and course: {course_id}'.format(
                                    enterprise_customer=lrs_configuration.enterprise_customer.name,
                                    username=user.username if user else '',
                                    course_id=persistent_course_grade.course_id
                                )
                LOGGER.exception(error_message)
                status = 500
            else:
                LOGGER.info(
                    'Successfully sent course completion to xAPI for user: {username} for course: {course_id}'
                    .format(username=user.username if user else '',
                            course_id=persistent_course_grade.course_id))
                status = 200
            fields = {'status': status, 'error_message': error_message}
            if status == 200:
                fields.update({
                    'grade': course_grade.percent,
                    'timestamp': course_grade.passed_timestamp
                })
            xapi_transmission_queryset.update(**fields)
Пример #28
0
def get_course_grades_for_users(user_ids, course_id):
    """
    Returns course grades for the given users and course.

    Args:
        user_ids (list): List of user ids.
        course_id (CourseKeyString): Course id.

    Returns:
        dict: Dictionary containing user and grades information.
    """
    grades = PersistentCourseGrade.objects.filter(
        user_id__in=user_ids, course_id=course_id
    ).only('user_id', 'percent_grade', 'passed_timestamp')

    grades_dict = {
        grade.user_id: {
            'grade_percentage': f'{convert_float_point_to_percentage(grade.percent_grade)}%',
            'passed_timestamp': grade.passed_timestamp
        }
        for grade in grades
    }

    if len(user_ids) != len(grades):
        course_grade_factory = CourseGradeFactory()

        user_ids_without_grades = [user_id for user_id in user_ids if user_id not in grades_dict.keys()]
        users = User.objects.filter(id__in=user_ids_without_grades)

        for user in users:
            if grades_dict.get(user.id, None):
                continue

            course_grade = course_grade_factory.read(
                user=user, course_key=course_id
            )
            grades_dict[user.id] = {
                'grade_percentage': f'{convert_float_point_to_percentage(course_grade.percent)}%',
                'passed_timestamp': now() if course_grade.passed else None,
            }

    return grades_dict
Пример #29
0
def user_course_passed(user, course_key):
    """
       Get if user passed course with percert
    """
    response = CourseGradeFactory().read(user, course_key=course_key)
    if response is None:
        logger.error(
            'CorfoGenerateCode - Error to get CourseGradeFactory().read(...), user: {}, course: {}'
            .format(user, str(course_key)))
        return None, None
    return response.passed, response.percent
Пример #30
0
 def test_cert_failure(self, status):
     if CertificateStatuses.is_passing_status(status):
         expected_status = CertificateStatuses.notpassing
     else:
         expected_status = status
     GeneratedCertificate.eligible_certificates.create(
         user=self.user, course_id=self.course.id, status=status)
     CourseGradeFactory().update(self.user, self.course)
     cert = GeneratedCertificate.certificate_for_student(
         self.user, self.course.id)
     self.assertEqual(cert.status, expected_status)
Пример #31
0
 def test_cert_generation_on_passing_instructor_paced(self):
     with mock.patch(
         'lms.djangoapps.certificates.signals.generate_certificate.apply_async',
         return_value=None
     ) as mock_generate_certificate_apply_async:
         with waffle.waffle().override(waffle.AUTO_CERTIFICATE_GENERATION, active=True):
             grade_factory = CourseGradeFactory()
             # Not passing
             grade_factory.update(self.user, self.ip_course)
             mock_generate_certificate_apply_async.assert_not_called()
             # Certs fired after passing
             with mock_passing_grade():
                 grade_factory.update(self.user, self.ip_course)
                 mock_generate_certificate_apply_async.assert_called_with(
                     countdown=CERTIFICATE_DELAY_SECONDS,
                     kwargs={
                         'student': unicode(self.user.id),
                         'course_key': unicode(self.ip_course.id),
                     }
                 )
Пример #32
0
 def setUp(self):
     super(LearnerTrackChangeCertsTest, self).setUp()  # lint-amnesty, pylint: disable=super-with-arguments
     self.course_one = CourseFactory.create(self_paced=True)
     self.user_one = UserFactory.create()
     self.enrollment_one = CourseEnrollmentFactory(
         user=self.user_one,
         course_id=self.course_one.id,
         is_active=True,
         mode='verified',
     )
     self.user_two = UserFactory.create()
     self.course_two = CourseFactory.create(self_paced=False)
     self.enrollment_two = CourseEnrollmentFactory(
         user=self.user_two,
         course_id=self.course_two.id,
         is_active=True,
         mode='verified')
     with mock_passing_grade():
         grade_factory = CourseGradeFactory()
         grade_factory.update(self.user_one, self.course_one)
         grade_factory.update(self.user_two, self.course_two)
Пример #33
0
    def test_failing_grade_allowlist(self):
        # User who is not on the allowlist
        GeneratedCertificateFactory(user=self.user,
                                    course_id=self.course.id,
                                    status=CertificateStatuses.downloadable)
        CourseGradeFactory().update(self.user, self.course)
        cert = GeneratedCertificate.certificate_for_student(
            self.user, self.course.id)
        self.assertEqual(cert.status, CertificateStatuses.notpassing)

        # User who is on the allowlist
        u = UserFactory.create()
        c = CourseFactory()
        course_key = c.id  # pylint: disable=no-member
        CertificateWhitelistFactory(user=u, course_id=course_key)
        GeneratedCertificateFactory(user=u,
                                    course_id=course_key,
                                    status=CertificateStatuses.downloadable)
        CourseGradeFactory().update(u, c)
        cert = GeneratedCertificate.certificate_for_student(u, course_key)
        self.assertEqual(cert.status, CertificateStatuses.downloadable)
Пример #34
0
    def assert_course_grade(self, user, expected_percent):
        """
        Verifies the given user's course grade is the expected percentage.
        Also verifies the user's grade information contains values for
        all problems in the course, whether or not they are currently
        gated.
        """
        course_grade = CourseGradeFactory().create(user, self.course)
        for prob in [self.gating_prob1, self.gated_prob2, self.prob3]:
            self.assertIn(prob.location, course_grade.problem_scores)

        self.assertEquals(course_grade.percent, expected_percent)
Пример #35
0
 def setUp(self):
     super(LearnerTrackChangeCertsTest, self).setUp()
     self.course_one = CourseFactory.create(self_paced=True)
     self.user_one = UserFactory.create()
     self.enrollment_one = CourseEnrollmentFactory(
         user=self.user_one,
         course_id=self.course_one.id,
         is_active=True,
         mode='verified',
     )
     self.user_two = UserFactory.create()
     self.course_two = CourseFactory.create(self_paced=False)
     self.enrollment_two = CourseEnrollmentFactory(
         user=self.user_two,
         course_id=self.course_two.id,
         is_active=True,
         mode='verified'
     )
     with mock_passing_grade():
         grade_factory = CourseGradeFactory()
         grade_factory.update(self.user_one, self.course_one)
         grade_factory.update(self.user_two, self.course_two)
Пример #36
0
def _listen_for_id_verification_status_changed(sender, user, **kwargs):  # pylint: disable=unused-argument
    """
    Catches a track change signal, determines user status,
    calls fire_ungenerated_certificate_task for passing grades
    """
    if not auto_certificate_generation_enabled():
        return

    user_enrollments = CourseEnrollment.enrollments_for_user(user=user)
    grade_factory = CourseGradeFactory()
    expected_verification_status, _ = SoftwareSecurePhotoVerification.user_status(user)
    for enrollment in user_enrollments:
        if grade_factory.read(user=user, course=enrollment.course_overview).passed:
            if fire_ungenerated_certificate_task(user, enrollment.course_id, expected_verification_status):
                message = (
                    u'Certificate generation task initiated for {user} : {course} via track change ' +
                    u'with verification status of {status}'
                )
                log.info(message.format(
                    user=user.id,
                    course=enrollment.course_id,
                    status=expected_verification_status
                ))
Пример #37
0
class ProgramProgressMeter(object):
    """Utility for gauging a user's progress towards program completion.

    Arguments:
        user (User): The user for which to find programs.

    Keyword Arguments:
        enrollments (list): List of the user's enrollments.
        uuid (str): UUID identifying a specific program. If provided, the meter
            will only inspect this one program, not all programs the user may be
            engaged with.
    """
    def __init__(self, site, user, enrollments=None, uuid=None, mobile_only=False):
        self.site = site
        self.user = user
        self.mobile_only = mobile_only

        self.enrollments = enrollments or list(CourseEnrollment.enrollments_for_user(self.user))
        self.enrollments.sort(key=lambda e: e.created, reverse=True)

        self.enrolled_run_modes = {}
        self.course_run_ids = []
        for enrollment in self.enrollments:
            # enrollment.course_id is really a CourseKey (╯ಠ_ಠ)╯︵ ┻━┻
            enrollment_id = unicode(enrollment.course_id)
            mode = enrollment.mode
            if mode == CourseMode.NO_ID_PROFESSIONAL_MODE:
                mode = CourseMode.PROFESSIONAL
            self.enrolled_run_modes[enrollment_id] = mode
            # We can't use dict.keys() for this because the course run ids need to be ordered
            self.course_run_ids.append(enrollment_id)

        self.entitlements = list(CourseEntitlement.unexpired_entitlements_for_user(self.user))
        self.course_uuids = [str(entitlement.course_uuid) for entitlement in self.entitlements]

        self.course_grade_factory = CourseGradeFactory()

        if uuid:
            self.programs = [get_programs(uuid=uuid)]
        else:
            self.programs = attach_program_detail_url(get_programs(self.site), self.mobile_only)

    def invert_programs(self):
        """Intersect programs and enrollments.

        Builds a dictionary of program dict lists keyed by course run ID and by course UUID.
        The resulting dictionary is suitable in applications where programs must be
        filtered by the course runs or courses they contain (e.g., the student dashboard).

        Returns:
            defaultdict, programs keyed by course run ID
        """
        inverted_programs = defaultdict(list)

        for program in self.programs:
            for course in program['courses']:
                course_uuid = course['uuid']
                if course_uuid in self.course_uuids:
                    program_list = inverted_programs[course_uuid]
                    if program not in program_list:
                        program_list.append(program)
                for course_run in course['course_runs']:
                    course_run_id = course_run['key']
                    if course_run_id in self.course_run_ids:
                        program_list = inverted_programs[course_run_id]
                        if program not in program_list:
                            program_list.append(program)

        # Sort programs by title for consistent presentation.
        for program_list in inverted_programs.itervalues():
            program_list.sort(key=lambda p: p['title'])

        return inverted_programs

    @cached_property
    def engaged_programs(self):
        """Derive a list of programs in which the given user is engaged.

        Returns:
            list of program dicts, ordered by most recent enrollment
        """
        inverted_programs = self.invert_programs()

        programs = []
        # Remember that these course run ids are derived from a list of
        # enrollments sorted from most recent to least recent. Iterating
        # over the values in inverted_programs alone won't yield a program
        # ordering consistent with the user's enrollments.
        for course_run_id in self.course_run_ids:
            for program in inverted_programs[course_run_id]:
                # Dicts aren't a hashable type, so we can't use a set. Sets also
                # aren't ordered, which is important here.
                if program not in programs:
                    programs.append(program)

        for course_uuid in self.course_uuids:
            for program in inverted_programs[course_uuid]:
                if program not in programs:
                    programs.append(program)

        return programs

    def _is_course_in_progress(self, now, course):
        """Check if course qualifies as in progress as part of the program.

        A course is considered to be in progress if a user is enrolled in a run
        of the correct mode or a run of the correct mode is still available for enrollment.

        Arguments:
            now (datetime): datetime for now
            course (dict): Containing nested course runs.

        Returns:
            bool, indicating whether the course is in progress.
        """
        enrolled_runs = [run for run in course['course_runs'] if run['key'] in self.course_run_ids]

        # Check if the user is enrolled in a required run and mode/seat.
        runs_with_required_mode = [
            run for run in enrolled_runs
            if run['type'] == self.enrolled_run_modes[run['key']]
        ]

        if runs_with_required_mode:
            not_failed_runs = [run for run in runs_with_required_mode if run not in self.failed_course_runs]
            if not_failed_runs:
                return True

        # Check if seats required for course completion are still available.
        upgrade_deadlines = []
        for run in enrolled_runs:
            for seat in run['seats']:
                if seat['type'] == run['type'] and run['type'] != self.enrolled_run_modes[run['key']]:
                    upgrade_deadlines.append(seat['upgrade_deadline'])

        # An upgrade deadline of None means the course is always upgradeable.
        return any(not deadline or deadline and parse(deadline) > now for deadline in upgrade_deadlines)

    def progress(self, programs=None, count_only=True):
        """Gauge a user's progress towards program completion.

        Keyword Arguments:
            programs (list): Specific list of programs to check the user's progress
                against. If left unspecified, self.engaged_programs will be used.

            count_only (bool): Whether or not to return counts of completed, in
                progress, and unstarted courses instead of serialized representations
                of the courses.

        Returns:
            list of dict, each containing information about a user's progress
                towards completing a program.
        """
        now = datetime.datetime.now(utc)

        progress = []
        programs = programs or self.engaged_programs
        for program in programs:
            program_copy = deepcopy(program)
            completed, in_progress, not_started = [], [], []

            for course in program_copy['courses']:
                active_entitlement = CourseEntitlement.get_entitlement_if_active(
                    user=self.user,
                    course_uuid=course['uuid']
                )
                if self._is_course_complete(course):
                    completed.append(course)
                elif self._is_course_enrolled(course) or active_entitlement:
                    # Show all currently enrolled courses and active entitlements as in progress
                    if active_entitlement:
                        course['course_runs'] = get_fulfillable_course_runs_for_entitlement(
                            active_entitlement,
                            course['course_runs']
                        )
                        course['user_entitlement'] = active_entitlement.to_dict()
                        course['enroll_url'] = reverse(
                            'entitlements_api:v1:enrollments',
                            args=[str(active_entitlement.uuid)]
                        )
                        in_progress.append(course)
                    else:
                        course_in_progress = self._is_course_in_progress(now, course)
                        if course_in_progress:
                            in_progress.append(course)
                        else:
                            course['expired'] = not course_in_progress
                            not_started.append(course)
                else:
                    not_started.append(course)

            grades = {}
            for run in self.course_run_ids:
                grade = self.course_grade_factory.read(self.user, course_key=CourseKey.from_string(run))
                grades[run] = grade.percent

            progress.append({
                'uuid': program_copy['uuid'],
                'completed': len(completed) if count_only else completed,
                'in_progress': len(in_progress) if count_only else in_progress,
                'not_started': len(not_started) if count_only else not_started,
                'grades': grades,
            })

        return progress

    @property
    def completed_programs_with_available_dates(self):
        """
        Calculate the available date for completed programs based on course runs.

        Returns a dict of {uuid_string: available_datetime}
        """
        # Query for all user certs up front, for performance reasons (rather than querying per course run).
        user_certificates = GeneratedCertificate.eligible_available_certificates.filter(user=self.user)
        certificates_by_run = {cert.course_id: cert for cert in user_certificates}

        completed = {}
        for program in self.programs:
            available_date = self._available_date_for_program(program, certificates_by_run)
            if available_date:
                completed[program['uuid']] = available_date
        return completed

    def _available_date_for_program(self, program_data, certificates):
        """
        Calculate the available date for the program based on the courses within it.

        Arguments:
            program_data (dict): nested courses and course runs
            certificates (dict): course run key -> certificate mapping

        Returns a datetime object or None if the program is not complete.
        """
        program_available_date = None
        for course in program_data['courses']:
            earliest_course_run_date = None

            for course_run in course['course_runs']:
                key = CourseKey.from_string(course_run['key'])

                # Get a certificate if one exists
                certificate = certificates.get(key)
                if certificate is None:
                    continue

                # Modes must match (see _is_course_complete() comments for why)
                course_run_mode = self._course_run_mode_translation(course_run['type'])
                certificate_mode = self._certificate_mode_translation(certificate.mode)
                modes_match = course_run_mode == certificate_mode

                # Grab the available date and keep it if it's the earliest one for this catalog course.
                if modes_match and certificate_api.is_passing_status(certificate.status):
                    course_overview = CourseOverview.get_from_id(key)
                    available_date = available_date_for_certificate(course_overview, certificate)
                    earliest_course_run_date = min(filter(None, [available_date, earliest_course_run_date]))

            # If we're missing a cert for a course, the program isn't completed and we should just bail now
            if earliest_course_run_date is None:
                return None

            # Keep the catalog course date if it's the latest one
            program_available_date = max(filter(None, [earliest_course_run_date, program_available_date]))

        return program_available_date

    def _course_run_mode_translation(self, course_run_mode):
        """
        Returns a canonical mode for a course run (whose data is coming from the program cache).
        This mode must match the certificate mode to be counted as complete.
        """
        mappings = {
            # Runs of type 'credit' are counted as 'verified' since verified
            # certificates are earned when credit runs are completed. LEARNER-1274
            # tracks a cleaner way to do this using the discovery service's
            # applicable_seat_types field.
            CourseMode.CREDIT_MODE: CourseMode.VERIFIED,
        }
        return mappings.get(course_run_mode, course_run_mode)

    def _certificate_mode_translation(self, certificate_mode):
        """
        Returns a canonical mode for a certificate (whose data is coming from the database).
        This mode must match the course run mode to be counted as complete.
        """
        mappings = {
            # Treat "no-id-professional" certificates as "professional" certificates
            CourseMode.NO_ID_PROFESSIONAL_MODE: CourseMode.PROFESSIONAL,
        }
        return mappings.get(certificate_mode, certificate_mode)

    def _is_course_complete(self, course):
        """Check if a user has completed a course.

        A course is completed if the user has earned a certificate for any of
        the nested course runs.

        Arguments:
            course (dict): Containing nested course runs.

        Returns:
            bool, indicating whether the course is complete.
        """

        def reshape(course_run):
            """
            Modify the structure of a course run dict to facilitate comparison
            with course run certificates.
            """
            return {
                'course_run_id': course_run['key'],
                # A course run's type is assumed to indicate which mode must be
                # completed in order for the run to count towards program completion.
                # This supports the same flexible program construction allowed by the
                # old programs service (e.g., completion of an old honor-only run may
                # count towards completion of a course in a program). This may change
                # in the future to make use of the more rigid set of "applicable seat
                # types" associated with each program type in the catalog.
                'type': self._course_run_mode_translation(course_run['type']),
            }

        return any(reshape(course_run) in self.completed_course_runs for course_run in course['course_runs'])

    @cached_property
    def completed_course_runs(self):
        """
        Determine which course runs have been completed by the user.

        Returns:
            list of dicts, each representing a course run certificate
        """
        return self.course_runs_with_state['completed']

    @cached_property
    def failed_course_runs(self):
        """
        Determine which course runs have been failed by the user.

        Returns:
            list of dicts, each a course run ID
        """
        return [run['course_run_id'] for run in self.course_runs_with_state['failed']]

    @cached_property
    def course_runs_with_state(self):
        """
        Determine which course runs have been completed and failed by the user.

        Returns:
            dict with a list of completed and failed runs
        """
        course_run_certificates = certificate_api.get_certificates_for_user(self.user.username)

        completed_runs, failed_runs = [], []
        for certificate in course_run_certificates:
            course_data = {
                'course_run_id': unicode(certificate['course_key']),
                'type': self._certificate_mode_translation(certificate['type']),
            }

            if certificate_api.is_passing_status(certificate['status']):
                completed_runs.append(course_data)
            else:
                failed_runs.append(course_data)

        return {'completed': completed_runs, 'failed': failed_runs}

    def _is_course_enrolled(self, course):
        """Check if a user is enrolled in a course.

        A user is considered to be enrolled in a course if
        they're enrolled in any of the nested course runs.

        Arguments:
            course (dict): Containing nested course runs.

        Returns:
            bool, indicating whether the course is in progress.
        """
        return any(course_run['key'] in self.course_run_ids for course_run in course['course_runs'])
Пример #38
0
class ProgramProgressMeter(object):
    """Utility for gauging a user's progress towards program completion.

    Arguments:
        user (User): The user for which to find programs.

    Keyword Arguments:
        enrollments (list): List of the user's enrollments.
        uuid (str): UUID identifying a specific program. If provided, the meter
            will only inspect this one program, not all programs the user may be
            engaged with.
    """
    def __init__(self, site, user, enrollments=None, uuid=None):
        self.site = site
        self.user = user

        self.enrollments = enrollments or list(CourseEnrollment.enrollments_for_user(self.user))
        self.enrollments.sort(key=lambda e: e.created, reverse=True)

        self.enrolled_run_modes = {}
        self.course_run_ids = []
        for enrollment in self.enrollments:
            # enrollment.course_id is really a CourseKey (╯ಠ_ಠ)╯︵ ┻━┻
            enrollment_id = unicode(enrollment.course_id)
            mode = enrollment.mode
            if mode == CourseMode.NO_ID_PROFESSIONAL_MODE:
                mode = CourseMode.PROFESSIONAL
            self.enrolled_run_modes[enrollment_id] = mode
            # We can't use dict.keys() for this because the course run ids need to be ordered
            self.course_run_ids.append(enrollment_id)

        self.course_grade_factory = CourseGradeFactory()

        if uuid:
            self.programs = [get_programs(self.site, uuid=uuid)]
        else:
            self.programs = attach_program_detail_url(get_programs(self.site))

    def invert_programs(self):
        """Intersect programs and enrollments.

        Builds a dictionary of program dict lists keyed by course run ID. The
        resulting dictionary is suitable in applications where programs must be
        filtered by the course runs they contain (e.g., the student dashboard).

        Returns:
            defaultdict, programs keyed by course run ID
        """
        inverted_programs = defaultdict(list)

        for program in self.programs:
            for course in program['courses']:
                for course_run in course['course_runs']:
                    course_run_id = course_run['key']
                    if course_run_id in self.course_run_ids:
                        program_list = inverted_programs[course_run_id]
                        if program not in program_list:
                            program_list.append(program)

        # Sort programs by title for consistent presentation.
        for program_list in inverted_programs.itervalues():
            program_list.sort(key=lambda p: p['title'])

        return inverted_programs

    @cached_property
    def engaged_programs(self):
        """Derive a list of programs in which the given user is engaged.

        Returns:
            list of program dicts, ordered by most recent enrollment
        """
        inverted_programs = self.invert_programs()

        programs = []
        # Remember that these course run ids are derived from a list of
        # enrollments sorted from most recent to least recent. Iterating
        # over the values in inverted_programs alone won't yield a program
        # ordering consistent with the user's enrollments.
        for course_run_id in self.course_run_ids:
            for program in inverted_programs[course_run_id]:
                # Dicts aren't a hashable type, so we can't use a set. Sets also
                # aren't ordered, which is important here.
                if program not in programs:
                    programs.append(program)

        return programs

    def _is_course_in_progress(self, now, course):
        """Check if course qualifies as in progress as part of the program.

        A course is considered to be in progress if a user is enrolled in a run
        of the correct mode or a run of the correct mode is still available for enrollment.

        Arguments:
            now (datetime): datetime for now
            course (dict): Containing nested course runs.

        Returns:
            bool, indicating whether the course is in progress.
        """
        enrolled_runs = [run for run in course['course_runs'] if run['key'] in self.course_run_ids]

        # Check if the user is enrolled in a required run and mode/seat.
        runs_with_required_mode = [
            run for run in enrolled_runs
            if run['type'] == self.enrolled_run_modes[run['key']]
        ]

        if runs_with_required_mode:
            not_failed_runs = [run for run in runs_with_required_mode if run not in self.failed_course_runs]
            if not_failed_runs:
                return True

        # Check if seats required for course completion are still available.
        upgrade_deadlines = []
        for run in enrolled_runs:
            for seat in run['seats']:
                if seat['type'] == run['type'] and run['type'] != self.enrolled_run_modes[run['key']]:
                    upgrade_deadlines.append(seat['upgrade_deadline'])

        # An upgrade deadline of None means the course is always upgradeable.
        return any(not deadline or deadline and parse(deadline) > now for deadline in upgrade_deadlines)

    def progress(self, programs=None, count_only=True):
        """Gauge a user's progress towards program completion.

        Keyword Arguments:
            programs (list): Specific list of programs to check the user's progress
                against. If left unspecified, self.engaged_programs will be used.

            count_only (bool): Whether or not to return counts of completed, in
                progress, and unstarted courses instead of serialized representations
                of the courses.

        Returns:
            list of dict, each containing information about a user's progress
                towards completing a program.
        """
        now = datetime.datetime.now(utc)

        progress = []
        programs = programs or self.engaged_programs
        for program in programs:
            program_copy = deepcopy(program)
            completed, in_progress, not_started = [], [], []

            for course in program_copy['courses']:
                if self._is_course_complete(course):
                    completed.append(course)
                elif self._is_course_enrolled(course):
                    course_in_progress = self._is_course_in_progress(now, course)
                    if course_in_progress:
                        in_progress.append(course)
                    else:
                        course['expired'] = not course_in_progress
                        not_started.append(course)
                else:
                    not_started.append(course)

            grades = {}
            for run in self.course_run_ids:
                grade = self.course_grade_factory.read(self.user, course_key=CourseKey.from_string(run))
                grades[run] = grade.percent

            progress.append({
                'uuid': program_copy['uuid'],
                'completed': len(completed) if count_only else completed,
                'in_progress': len(in_progress) if count_only else in_progress,
                'not_started': len(not_started) if count_only else not_started,
                'grades': grades,
            })

        return progress

    @property
    def completed_programs(self):
        """Identify programs completed by the student.

        Returns:
            list of UUIDs, each identifying a completed program.
        """
        return [program['uuid'] for program in self.programs if self._is_program_complete(program)]

    def _is_program_complete(self, program):
        """Check if a user has completed a program.

        A program is completed if the user has completed all nested courses.

        Arguments:
            program (dict): Representing the program whose completion to assess.

        Returns:
            bool, indicating whether the program is complete.
        """
        return all(self._is_course_complete(course) for course in program['courses']) \
            and len(program['courses']) > 0

    def _is_course_complete(self, course):
        """Check if a user has completed a course.

        A course is completed if the user has earned a certificate for any of
        the nested course runs.

        Arguments:
            course (dict): Containing nested course runs.

        Returns:
            bool, indicating whether the course is complete.
        """

        def reshape(course_run):
            """
            Modify the structure of a course run dict to facilitate comparison
            with course run certificates.
            """
            return {
                'course_run_id': course_run['key'],
                # A course run's type is assumed to indicate which mode must be
                # completed in order for the run to count towards program completion.
                # This supports the same flexible program construction allowed by the
                # old programs service (e.g., completion of an old honor-only run may
                # count towards completion of a course in a program). This may change
                # in the future to make use of the more rigid set of "applicable seat
                # types" associated with each program type in the catalog.

                # Runs of type 'credit' are counted as 'verified' since verified
                # certificates are earned when credit runs are completed. LEARNER-1274
                # tracks a cleaner way to do this using the discovery service's
                # applicable_seat_types field.
                'type': 'verified' if course_run['type'] == 'credit' else course_run['type'],
            }

        return any(reshape(course_run) in self.completed_course_runs for course_run in course['course_runs'])

    @cached_property
    def completed_course_runs(self):
        """
        Determine which course runs have been completed by the user.

        Returns:
            list of dicts, each representing a course run certificate
        """
        return self.course_runs_with_state['completed']

    @cached_property
    def failed_course_runs(self):
        """
        Determine which course runs have been failed by the user.

        Returns:
            list of dicts, each a course run ID
        """
        return [run['course_run_id'] for run in self.course_runs_with_state['failed']]

    @cached_property
    def course_runs_with_state(self):
        """
        Determine which course runs have been completed and failed by the user.

        Returns:
            dict with a list of completed and failed runs
        """
        course_run_certificates = certificate_api.get_certificates_for_user(self.user.username)

        completed_runs, failed_runs = [], []
        for certificate in course_run_certificates:
            certificate_type = certificate['type']

            # Treat "no-id-professional" certificates as "professional" certificates
            if certificate_type == CourseMode.NO_ID_PROFESSIONAL_MODE:
                certificate_type = CourseMode.PROFESSIONAL

            course_data = {
                'course_run_id': unicode(certificate['course_key']),
                'type': certificate_type,
            }

            if certificate_api.is_passing_status(certificate['status']):
                completed_runs.append(course_data)
            else:
                failed_runs.append(course_data)

        return {'completed': completed_runs, 'failed': failed_runs}

    def _is_course_enrolled(self, course):
        """Check if a user is enrolled in a course.

        A user is considered to be enrolled in a course if
        they're enrolled in any of the nested course runs.

        Arguments:
            course (dict): Containing nested course runs.

        Returns:
            bool, indicating whether the course is in progress.
        """
        return any(course_run['key'] in self.course_run_ids for course_run in course['course_runs'])