Beispiel #1
0
    def test_get_completed_courses(self, mock_get_certs_for_user):
        """
        Ensure the function correctly calls to and handles results from the
        certificates API
        """
        student = UserFactory(username='******')
        mock_get_certs_for_user.return_value = [
            self.make_cert_result(status='downloadable',
                                  type='verified',
                                  course_key='downloadable-course'),
            self.make_cert_result(status='generating',
                                  type='professional',
                                  course_key='generating-course'),
            self.make_cert_result(status='unknown',
                                  type='honor',
                                  course_key='unknown-course'),
        ]

        result = utils.get_completed_courses(student)
        self.assertEqual(mock_get_certs_for_user.call_args[0],
                         (student.username, ))
        self.assertEqual(result, [
            {
                'course_id': 'downloadable-course',
                'mode': 'verified'
            },
            {
                'course_id': 'generating-course',
                'mode': 'professional'
            },
        ])
    def test_get_completed_courses(self, mock_get_certs_for_user):
        """
        Ensure the function correctly calls to and handles results from the
        certificates API
        """
        student = UserFactory(username='******')
        mock_get_certs_for_user.return_value = [
            self.make_cert_result(status='downloadable', type='verified', course_key='downloadable-course'),
            self.make_cert_result(status='generating', type='professional', course_key='generating-course'),
            self.make_cert_result(status='unknown', type='honor', course_key='unknown-course'),
        ]

        result = utils.get_completed_courses(student)
        self.assertEqual(mock_get_certs_for_user.call_args[0], (student.username, ))
        self.assertEqual(result, [
            {'course_id': 'downloadable-course', 'mode': 'verified'},
            {'course_id': 'generating-course', 'mode': 'professional'},
        ])
Beispiel #3
0
    def test_get_completed_courses(self, mock_get_certs_for_user):
        """
        Ensure the function correctly calls to and handles results from the
        certificates API
        """
        student = UserFactory(username="******")
        mock_get_certs_for_user.return_value = [
            self.make_cert_result(status="downloadable", type="verified", course_key="downloadable-course"),
            self.make_cert_result(status="generating", type="professional", course_key="generating-course"),
            self.make_cert_result(status="unknown", type="honor", course_key="unknown-course"),
        ]

        result = utils.get_completed_courses(student)
        self.assertEqual(mock_get_certs_for_user.call_args[0], (student.username,))
        self.assertEqual(
            result,
            [
                {"course_id": "downloadable-course", "mode": "verified"},
                {"course_id": "generating-course", "mode": "professional"},
            ],
        )
Beispiel #4
0
def award_program_certificates(self, username):
    """
    This task is designed to be called whenever a student's completion status
    changes with respect to one or more courses (primarily, when a course
    certificate is awarded).

    It will consult with a variety of APIs to determine whether or not the
    specified user should be awarded a certificate in one or more programs, and
    use the credentials service to create said certificates if so.

    This task may also be invoked independently of any course completion status
    change - for example, to backpopulate missing program credentials for a
    student.

    Args:
        username:
            The username of the student

    Returns:
        None

    """
    LOGGER.info('Running task award_program_certificates for username %s', username)

    config = ProgramsApiConfig.current()
    countdown = 2 ** self.request.retries

    # If either programs or credentials config models are disabled for this
    # feature, it may indicate a condition where processing of such tasks
    # has been temporarily disabled.  Since this is a recoverable situation,
    # mark this task for retry instead of failing it altogether.
    if not config.is_certification_enabled:
        LOGGER.warning(
            'Task award_program_certificates cannot be executed when program certification is disabled in API config',
        )
        raise self.retry(countdown=countdown, max_retries=config.max_retries)

    if not CredentialsApiConfig.current().is_learner_issuance_enabled:
        LOGGER.warning(
            'Task award_program_certificates cannot be executed when credentials issuance is disabled in API config',
        )
        raise self.retry(countdown=countdown, max_retries=config.max_retries)

    try:
        try:
            student = User.objects.get(username=username)
        except User.DoesNotExist:
            LOGGER.exception('Task award_program_certificates was called with invalid username %s', username)
            # Don't retry for this case - just conclude the task.
            return

        # Fetch the set of all course runs for which the user has earned a
        # certificate.
        course_certs = get_completed_courses(student)
        if not course_certs:
            # Highly unlikely, since at present the only trigger for this task
            # is the earning of a new course certificate.  However, it could be
            # that the transaction in which a course certificate was awarded
            # was subsequently rolled back, which could lead to an empty result
            # here, so we'll at least log that this happened before exiting.
            #
            # If this task is ever updated to support revocation of program
            # certs, this branch should be removed, since it could make sense
            # in that case to call this task for a user without any (valid)
            # course certs.
            LOGGER.warning('Task award_program_certificates was called for user %s with no completed courses', username)
            return

        # Invoke the Programs API completion check endpoint to identify any
        # programs that are satisfied by these course completions.
        programs_client = get_api_client(config, student)
        program_ids = get_completed_programs(programs_client, course_certs)
        if not program_ids:
            # Again, no reason to continue beyond this point unless/until this
            # task gets updated to support revocation of program certs.
            return

        # Determine which program certificates the user has already been
        # awarded, if any.
        existing_program_ids = get_awarded_certificate_programs(student)

    except Exception as exc:  # pylint: disable=broad-except
        LOGGER.exception('Failed to determine program certificates to be awarded for user %s', username)
        raise self.retry(exc=exc, countdown=countdown, max_retries=config.max_retries)

    # For each completed program for which the student doesn't already have a
    # certificate, award one now.
    #
    # This logic is important, because we will retry the whole task if awarding any particular program cert fails.
    #
    # N.B. the list is sorted to facilitate deterministic ordering, e.g. for tests.
    new_program_ids = sorted(list(set(program_ids) - set(existing_program_ids)))
    if new_program_ids:
        try:
            credentials_client = get_api_client(
                CredentialsApiConfig.current(),
                User.objects.get(username=settings.CREDENTIALS_SERVICE_USERNAME)  # pylint: disable=no-member
            )
        except Exception as exc:  # pylint: disable=broad-except
            LOGGER.exception('Failed to create a credentials API client to award program certificates')
            # Retry because a misconfiguration could be fixed
            raise self.retry(exc=exc, countdown=countdown, max_retries=config.max_retries)

        retry = False
        for program_id in new_program_ids:
            try:
                award_program_certificate(credentials_client, username, program_id)
                LOGGER.info('Awarded certificate for program %s to user %s', program_id, username)
            except Exception:  # pylint: disable=broad-except
                # keep trying to award other certs, but retry the whole task to fix any missing entries
                LOGGER.exception('Failed to award certificate for program %s to user %s', program_id, username)
                retry = True

        if retry:
            # N.B. This logic assumes that this task is idempotent
            LOGGER.info('Retrying task to award failed certificates to user %s', username)
            raise self.retry(countdown=countdown, max_retries=config.max_retries)

    LOGGER.info('Successfully completed the task award_program_certificates for username %s', username)
Beispiel #5
0
def award_program_certificates(self, username):
    """
    This task is designed to be called whenever a student's completion status
    changes with respect to one or more courses (primarily, when a course
    certificate is awarded).

    It will consult with a variety of APIs to determine whether or not the
    specified user should be awarded a certificate in one or more programs, and
    use the credentials service to create said certificates if so.

    This task may also be invoked independently of any course completion status
    change - for example, to backpopulate missing program credentials for a
    student.

    Args:
        username:
            The username of the student

    Returns:
        None

    """
    LOGGER.info('Running task award_program_certificates for username %s',
                username)

    config = ProgramsApiConfig.current()
    countdown = 2**self.request.retries

    # If either programs or credentials config models are disabled for this
    # feature, it may indicate a condition where processing of such tasks
    # has been temporarily disabled.  Since this is a recoverable situation,
    # mark this task for retry instead of failing it altogether.
    if not config.is_certification_enabled:
        LOGGER.warning(
            'Task award_program_certificates cannot be executed when program certification is disabled in API config',
        )
        raise self.retry(countdown=countdown, max_retries=config.max_retries)

    if not CredentialsApiConfig.current().is_learner_issuance_enabled:
        LOGGER.warning(
            'Task award_program_certificates cannot be executed when credentials issuance is disabled in API config',
        )
        raise self.retry(countdown=countdown, max_retries=config.max_retries)

    try:
        try:
            student = User.objects.get(username=username)
        except User.DoesNotExist:
            LOGGER.exception(
                'Task award_program_certificates was called with invalid username %s',
                username)
            # Don't retry for this case - just conclude the task.
            return

        # Fetch the set of all course runs for which the user has earned a
        # certificate.
        course_certs = get_completed_courses(student)
        if not course_certs:
            # Highly unlikely, since at present the only trigger for this task
            # is the earning of a new course certificate.  However, it could be
            # that the transaction in which a course certificate was awarded
            # was subsequently rolled back, which could lead to an empty result
            # here, so we'll at least log that this happened before exiting.
            #
            # If this task is ever updated to support revocation of program
            # certs, this branch should be removed, since it could make sense
            # in that case to call this task for a user without any (valid)
            # course certs.
            LOGGER.warning(
                'Task award_program_certificates was called for user %s with no completed courses',
                username)
            return

        # Invoke the Programs API completion check endpoint to identify any
        # programs that are satisfied by these course completions.
        programs_client = get_api_client(config, student)
        program_ids = get_completed_programs(programs_client, course_certs)
        if not program_ids:
            # Again, no reason to continue beyond this point unless/until this
            # task gets updated to support revocation of program certs.
            LOGGER.info(
                'Task award_program_certificates was called for user %s with no completed programs',
                username)
            return

        # Determine which program certificates the user has already been
        # awarded, if any.
        existing_program_ids = get_awarded_certificate_programs(student)

    except Exception as exc:  # pylint: disable=broad-except
        LOGGER.exception(
            'Failed to determine program certificates to be awarded for user %s',
            username)
        raise self.retry(exc=exc,
                         countdown=countdown,
                         max_retries=config.max_retries)

    # For each completed program for which the student doesn't already have a
    # certificate, award one now.
    #
    # This logic is important, because we will retry the whole task if awarding any particular program cert fails.
    #
    # N.B. the list is sorted to facilitate deterministic ordering, e.g. for tests.
    new_program_ids = sorted(
        list(set(program_ids) - set(existing_program_ids)))
    if new_program_ids:
        try:
            credentials_client = get_api_client(
                CredentialsApiConfig.current(),
                User.objects.get(
                    username=settings.CREDENTIALS_SERVICE_USERNAME)  # pylint: disable=no-member
            )
        except Exception as exc:  # pylint: disable=broad-except
            LOGGER.exception(
                'Failed to create a credentials API client to award program certificates'
            )
            # Retry because a misconfiguration could be fixed
            raise self.retry(exc=exc,
                             countdown=countdown,
                             max_retries=config.max_retries)

        retry = False
        for program_id in new_program_ids:
            try:
                award_program_certificate(credentials_client, username,
                                          program_id)
                LOGGER.info('Awarded certificate for program %s to user %s',
                            program_id, username)
            except Exception:  # pylint: disable=broad-except
                # keep trying to award other certs, but retry the whole task to fix any missing entries
                LOGGER.exception(
                    'Failed to award certificate for program %s to user %s',
                    program_id, username)
                retry = True

        if retry:
            # N.B. This logic assumes that this task is idempotent
            LOGGER.info(
                'Retrying task to award failed certificates to user %s',
                username)
            raise self.retry(countdown=countdown,
                             max_retries=config.max_retries)
    else:
        LOGGER.info('User %s is not eligible for any new program certificates',
                    username)

    LOGGER.info(
        'Successfully completed the task award_program_certificates for username %s',
        username)