Example #1
0
    def create_credentials_config(self, **kwargs):
        """ Creates a new CredentialsApiConfig with DEFAULTS, updated with any
        provided overrides.
        """
        fields = dict(self.CREDENTIALS_DEFAULTS, **kwargs)
        CredentialsApiConfig(**fields).save()

        return CredentialsApiConfig.current()
Example #2
0
def get_credentials_api_base_url(org=None):
    """
    Returns a credentials API base URL.

    Arguments:
        org (str): Optional organization to look up the site config for, rather than the current request
    """
    if org is None:
        url = CredentialsApiConfig.current(
        ).internal_api_url  # by current request
    else:
        url = CredentialsApiConfig.get_internal_api_url_for_org(org)  # by org

    return url
Example #3
0
def get_credentials_api_client(user, org=None):
    """
    Returns an authenticated Credentials API client.

    Arguments:
        user (User): The user to authenticate as when requesting credentials.
        org (str): Optional organization to look up the site config for, rather than the current request

    """
    jwt = create_jwt_for_user(user)

    if org is None:
        url = CredentialsApiConfig.current().internal_api_url  # by current request
    else:
        url = CredentialsApiConfig.get_internal_api_url_for_org(org)  # by org
    return EdxRestApiClient(url, jwt=jwt)
Example #4
0
def update_certificate_visible_date_on_course_update(
        self, course_key, certificate_available_date):
    """
    This task is designed to be called whenever a course is updated with
    certificate_available_date so that visible_date is updated on credential
    service as well.

    It will get all users within the course that have a certificate and call
    the credentials API to update all these certificates visible_date value
    to keep certificates in sync on both sides.

    If this function is moved, make sure to update it's entry in
    EXPLICIT_QUEUES in the settings files so it runs in the correct queue.

    Arguments:
        course_key (str): The course identifier
        certificate_available_date (str): The date to update the certificate availablity date to. It's a string
            representation of a datetime object because task parameters must be JSON-able.

    Returns:
        None

    """
    countdown = 2**self.request.retries
    # If the credentials config model is 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 CredentialsApiConfig.current().is_learner_issuance_enabled:
        error_msg = (
            "Task update_certificate_visible_date_on_course_update cannot be executed when credentials issuance is "
            "disabled in API config")
        LOGGER.info(error_msg)
        exception = MaxRetriesExceededError(
            f"Failed to update certificate availability date for course {course_key}. Reason: {error_msg}"
        )
        raise self.retry(exc=exception,
                         countdown=countdown,
                         max_retries=MAX_RETRIES)
    # update the course certificate with the new certificate available date if:
    # - The course is not self paced
    # - The certificates_display_behavior is not "end_with_date"
    course_overview = CourseOverview.get_from_id(course_key)
    if (course_overview.self_paced is False
            and course_overview.certificates_display_behavior
            == CertificatesDisplayBehaviors.END_WITH_DATE):
        update_credentials_course_certificate_configuration_available_date.delay(
            str(course_key), certificate_available_date)
    users_with_certificates_in_course = GeneratedCertificate.eligible_available_certificates.filter(
        course_id=course_key).values_list('user__username', flat=True)

    LOGGER.info(
        "Task update_certificate_visible_date_on_course_update resending course certificates "
        f"for {len(users_with_certificates_in_course)} users in course {course_key}."
    )
    for user in users_with_certificates_in_course:
        award_course_certificate.delay(
            user,
            str(course_key),
            certificate_available_date=certificate_available_date)
Example #5
0
def handle_course_cert_date_change(sender, course_key, **kwargs):  # lint-amnesty, pylint: disable=unused-argument
    """
    If course is updated and the certificate_available_date is changed,
    schedule a celery task to update visible_date for all certificates
    within course.

    Args:
        course_key:
            refers to the course whose certificate_available_date was updated.

    Returns:
        None

    """
    # Import here instead of top of file since this module gets imported before
    # the credentials app is loaded, resulting in a Django deprecation warning.
    from openedx.core.djangoapps.credentials.models import CredentialsApiConfig

    # Avoid scheduling new tasks if certification is disabled.
    if not CredentialsApiConfig.current().is_learner_issuance_enabled:
        return

    # schedule background task to process
    LOGGER.info(
        'handling COURSE_CERT_DATE_CHANGE for course %s',
        course_key,
    )
    # import here, because signal is registered at startup, but items in tasks are not yet loaded
    from openedx.core.djangoapps.programs.tasks import update_certificate_visible_date_on_course_update
    update_certificate_visible_date_on_course_update.delay(course_key)
Example #6
0
def update_certificate_visible_date_on_course_update(self, course_key):
    """
    This task is designed to be called whenever a course is updated with
    certificate_available_date so that visible_date is updated on credential
    service as well.

    It will get all users within the course that have a certificate and call
    the credentials API to update all these certificates visible_date value
    to keep certificates in sync on both sides.

    Args:
        course_key (str): The course identifier

    Returns:
        None

    """
    countdown = 2**self.request.retries
    # If the credentials config model is 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 CredentialsApiConfig.current().is_learner_issuance_enabled:
        LOGGER.info(
            'Task update_certificate_visible_date_on_course_update cannot be executed when credentials issuance is '
            'disabled in API config', )
        raise self.retry(countdown=countdown, max_retries=MAX_RETRIES)

    users_with_certificates_in_course = GeneratedCertificate.eligible_available_certificates.filter(
        course_id=course_key).values_list('user__username', flat=True)

    for user in users_with_certificates_in_course:
        award_course_certificate.delay(user, str(course_key))
Example #7
0
def get_user_program_credentials(user):
    """Given a user, get the list of all program credentials earned and returns
    list of dictionaries containing related programs data.

    Arguments:
        user (User): The user object for getting programs credentials.

    Returns:
        list, containing programs dictionaries.
    """
    programs_credentials_data = []
    credential_configuration = CredentialsApiConfig.current()
    if not credential_configuration.is_learner_issuance_enabled:
        log.debug('Display of certificates for programs is disabled.')
        return programs_credentials_data

    credentials = get_user_credentials(user)
    if not credentials:
        log.info('No credential earned by the given user.')
        return programs_credentials_data

    programs_credentials = []
    for credential in credentials:
        try:
            if 'program_uuid' in credential['credential']:
                programs_credentials.append(credential)
        except KeyError:
            log.exception('Invalid credential structure: %r', credential)

    if programs_credentials:
        programs_credentials_data = get_programs_for_credentials(programs_credentials)

    return programs_credentials_data
Example #8
0
def get_credentials(user, program_uuid=None):
    """
    Given a user, get credentials earned from the credentials service.

    Arguments:
        user (User): The user to authenticate as when requesting credentials.

    Keyword Arguments:
        program_uuid (str): UUID of the program whose credential to retrieve.

    Returns:
        list of dict, representing credentials returned by the Credentials
        service.
    """
    credential_configuration = CredentialsApiConfig.current()

    querystring = {'username': user.username, 'status': 'awarded'}

    if program_uuid:
        querystring['program_uuid'] = program_uuid

    # Bypass caching for staff users, who may be generating credentials and
    # want to see them displayed immediately.
    use_cache = credential_configuration.is_cache_enabled and not user.is_staff
    cache_key = credential_configuration.CACHE_KEY + '.' + user.username if use_cache else None
    api = get_credentials_api_client(user)

    return get_edx_api_data(
        credential_configuration, 'credentials', api=api, querystring=querystring, cache_key=cache_key
    )
Example #9
0
def get_credentials_api_client(user):
    """ Returns an authenticated Credentials API client. """

    scopes = ['email', 'profile']
    expires_in = settings.OAUTH_ID_TOKEN_EXPIRATION
    jwt = JwtBuilder(user).build_token(scopes, expires_in)
    return EdxRestApiClient(CredentialsApiConfig.current().internal_api_url, jwt=jwt)
Example #10
0
    def mock_credentials_api(self, user, data=None, status_code=200, reset_url=True, is_next_page=False):
        """Utility for mocking out Credentials API URLs."""
        self.assertTrue(httpretty.is_enabled(), msg='httpretty must be enabled to mock Credentials API calls.')
        internal_api_url = CredentialsApiConfig.current().internal_api_url.strip('/')

        url = internal_api_url + '/user_credentials/?username='******'/user_credentials/?page=2&username='******'next'] = next_page_url
            next_page_body = json.dumps(self.CREDENTIALS_NEXT_API_RESPONSE)
            httpretty.register_uri(
                httpretty.GET, next_page_url, body=body, content_type='application/json', status=status_code
            )
            httpretty.register_uri(
                httpretty.GET, url, body=next_page_body, content_type='application/json', status=status_code
            )
        else:
            httpretty.register_uri(
                httpretty.GET, url, body=body, content_type='application/json', status=status_code
            )
Example #11
0
def get_credentials_api_client(user, org=None):
    """
    Returns an authenticated Credentials API client.

    Arguments:
        user (User): The user to authenticate as when requesting credentials.
        org (str): Optional organization to look up the site config for, rather than the current request

    """
    jwt = create_jwt_for_user(user)

    if org is None:
        url = CredentialsApiConfig.current().internal_api_url  # by current request
    else:
        url = CredentialsApiConfig.get_internal_api_url_for_org(org)  # by org
    return EdxRestApiClient(url, jwt=jwt)
Example #12
0
def get_credentials(user, program_uuid=None):
    """
    Given a user, get credentials earned from the credentials service.

    Arguments:
        user (User): The user to authenticate as when requesting credentials.

    Keyword Arguments:
        program_uuid (str): UUID of the program whose credential to retrieve.

    Returns:
        list of dict, representing credentials returned by the Credentials
        service.
    """
    credential_configuration = CredentialsApiConfig.current()

    querystring = {'username': user.username, 'status': 'awarded'}

    if program_uuid:
        querystring['program_uuid'] = program_uuid

    # Bypass caching for staff users, who may be generating credentials and
    # want to see them displayed immediately.
    use_cache = credential_configuration.is_cache_enabled and not user.is_staff
    cache_key = credential_configuration.CACHE_KEY + '.' + user.username if use_cache else None

    return get_edx_api_data(
        credential_configuration, user, 'credentials', querystring=querystring, cache_key=cache_key
    )
def get_user_program_credentials(user):
    """Given a user, get the list of all program credentials earned and returns
    list of dictionaries containing related programs data.

    Arguments:
        user (User): The user object for getting programs credentials.

    Returns:
        list, containing programs dictionaries.
    """
    programs_credentials_data = []
    credential_configuration = CredentialsApiConfig.current()
    if not credential_configuration.is_learner_issuance_enabled:
        log.debug('Display of certificates for programs is disabled.')
        return programs_credentials_data

    credentials = get_user_credentials(user)
    if not credentials:
        log.info('No credential earned by the given user.')
        return programs_credentials_data

    programs_credentials = []
    for credential in credentials:
        try:
            if 'program_id' in credential['credential'] and credential[
                    'status'] == 'awarded':
                programs_credentials.append(credential)
        except KeyError:
            log.exception('Invalid credential structure: %r', credential)

    if programs_credentials:
        programs_credentials_data = get_programs_for_credentials(
            user, programs_credentials)

    return programs_credentials_data
Example #14
0
def get_credentials_api_client(user):
    """ Returns an authenticated Credentials API client. """

    scopes = ['email', 'profile']
    expires_in = settings.OAUTH_ID_TOKEN_EXPIRATION
    jwt = JwtBuilder(user).build_token(scopes, expires_in)
    return EdxRestApiClient(CredentialsApiConfig.current().internal_api_url, jwt=jwt)
Example #15
0
    def create_credentials_config(self, **kwargs):
        """ Creates a new CredentialsApiConfig with DEFAULTS, updated with any
        provided overrides.
        """
        fields = dict(self.CREDENTIALS_DEFAULTS, **kwargs)
        CredentialsApiConfig(**fields).save()

        return CredentialsApiConfig.current()
Example #16
0
def get_credentials_api_client(user, org=None):
    """
    Returns an authenticated Credentials API client.

    Arguments:
        user (User): The user to authenticate as when requesting credentials.
        org (str): Optional organization to look up the site config for, rather than the current request

    """

    scopes = ['email', 'profile']
    expires_in = settings.OAUTH_ID_TOKEN_EXPIRATION
    jwt = JwtBuilder(user).build_token(scopes, expires_in)

    if org is None:
        url = CredentialsApiConfig.current().internal_api_url  # by current request
    else:
        url = CredentialsApiConfig.get_internal_api_url_for_org(org)  # by org
    return EdxRestApiClient(url, jwt=jwt)
def get_credentials_api_client(user, org=None):
    """
    Returns an authenticated Credentials API client.

    Arguments:
        user (User): The user to authenticate as when requesting credentials.
        org (str): Optional organization to look up the site config for, rather than the current request

    """

    scopes = ['email', 'profile']
    expires_in = settings.OAUTH_ID_TOKEN_EXPIRATION
    jwt = JwtBuilder(user).build_token(scopes, expires_in)

    if org is None:
        url = CredentialsApiConfig.current().internal_api_url  # by current request
    else:
        url = CredentialsApiConfig.get_internal_api_url_for_org(org)  # by org
    return EdxRestApiClient(url, jwt=jwt)
Example #18
0
def get_credentials_records_url(program_uuid=None):
    """
    Returns a URL for a given records page (or general records list if given no UUID).
    May return None if this feature is disabled.
    """
    base_url = CredentialsApiConfig.current().public_records_url
    if base_url is None:
        return None
    if program_uuid:
        return base_url + 'programs/{}/'.format(program_uuid)
    return base_url
Example #19
0
    def mock_credentials_api(self, data):
        """Helper for mocking out Credentials API URLs."""
        self.assertTrue(httpretty.is_enabled(), msg='httpretty must be enabled to mock Credentials API calls.')

        url = '{base}/user_credentials/?username={username}'.format(
            base=CredentialsApiConfig.current().internal_api_url.strip('/'),
            username=self.user.username
        )
        body = json.dumps({'results': data})

        httpretty.register_uri(httpretty.GET, url, body=body, content_type='application/json')
Example #20
0
def get_credentials_records_url(program_uuid=None):
    """
    Returns a URL for a given records page (or general records list if given no UUID).
    May return None if this feature is disabled.
    """
    base_url = CredentialsApiConfig.current().public_records_url
    if base_url is None:
        return None
    if program_uuid:
        return base_url + 'programs/{}/'.format(program_uuid)
    return base_url
    def mock_credentials_api(self, data):
        """Helper for mocking out Credentials API URLs."""
        self.assertTrue(httpretty.is_enabled(), msg='httpretty must be enabled to mock Credentials API calls.')

        url = '{base}/user_credentials/?username={username}'.format(
            base=CredentialsApiConfig.current().internal_api_url.strip('/'),
            username=self.user.username
        )
        body = json.dumps({'results': data})

        httpretty.register_uri(httpretty.GET, url, body=body, content_type='application/json')
def get_credentials_records_url(program_uuid=None):
    """
    Returns a URL for a given records page (or general records list if given no UUID).
    May return None if this feature is disabled.

    Arguments:
        program_uuid (str): Optional program uuid to link for a program records URL
    """
    base_url = CredentialsApiConfig.current().public_records_url
    if base_url is None:
        return None
    if program_uuid:
        # Credentials expects the uuid without dashes so we are converting here
        return base_url + 'programs/{}/'.format(program_uuid.replace('-', ''))
    return base_url
Example #23
0
def get_credentials_records_url(program_uuid=None):
    """
    Returns a URL for a given records page (or general records list if given no UUID).
    May return None if this feature is disabled.

    Arguments:
        program_uuid (str): Optional program uuid to link for a program records URL
    """
    base_url = CredentialsApiConfig.current().public_records_url
    if base_url is None:
        return None
    if program_uuid:
        # Credentials expects the uuid without dashes so we are converting here
        return base_url + 'programs/{}/'.format(program_uuid.replace('-', ''))
    return base_url
Example #24
0
def handle_course_cert_changed(sender, user, course_key, mode, status,
                               **kwargs):  # pylint: disable=unused-argument
    """
        If a learner is awarded a course certificate,
        schedule a celery task to process that course certificate

        Args:
            sender:
                class of the object instance that sent this signal
            user:
                django.contrib.auth.User - the user to whom a cert was awarded
            course_key:
                refers to the course run for which the cert was awarded
            mode:
                mode / certificate type, e.g. "verified"
            status:
                "downloadable"

        Returns:
            None

        """
    # Import here instead of top of file since this module gets imported before
    # the credentials app is loaded, resulting in a Django deprecation warning.
    from openedx.core.djangoapps.credentials.models import CredentialsApiConfig

    # Avoid scheduling new tasks if certification is disabled.
    if not CredentialsApiConfig.current().is_learner_issuance_enabled:
        return

    # Avoid scheduling new tasks if learner records are disabled for this site (right now, course certs are only
    # used for learner records -- when that changes, we can remove this bit and always send course certs).
    if not helpers.get_value_for_org(course_key.org, 'ENABLE_LEARNER_RECORDS',
                                     True):
        return

    # schedule background task to process
    LOGGER.debug(
        'handling COURSE_CERT_CHANGED: username=%s, course_key=%s, mode=%s, status=%s',
        user,
        course_key,
        mode,
        status,
    )
    # import here, because signal is registered at startup, but items in tasks are not yet able to be loaded
    from openedx.core.djangoapps.programs.tasks.v1.tasks import award_course_certificate
    award_course_certificate.delay(user.username, str(course_key))
Example #25
0
def handle_course_cert_changed(sender, user, course_key, mode, status, **kwargs):  # pylint: disable=unused-argument
    """
        If a learner is awarded a course certificate,
        schedule a celery task to process that course certificate

        Args:
            sender:
                class of the object instance that sent this signal
            user:
                django.contrib.auth.User - the user to whom a cert was awarded
            course_key:
                refers to the course run for which the cert was awarded
            mode:
                mode / certificate type, e.g. "verified"
            status:
                "downloadable"

        Returns:
            None

        """
    # Import here instead of top of file since this module gets imported before
    # the credentials app is loaded, resulting in a Django deprecation warning.
    from openedx.core.djangoapps.credentials.models import CredentialsApiConfig

    # Avoid scheduling new tasks if certification is disabled.
    if not CredentialsApiConfig.current().is_learner_issuance_enabled:
        return

    # Avoid scheduling new tasks if learner records are disabled for this site (right now, course certs are only
    # used for learner records -- when that changes, we can remove this bit and always send course certs).
    if not helpers.get_value_for_org(course_key.org, 'ENABLE_LEARNER_RECORDS', True):
        return

    # schedule background task to process
    LOGGER.debug(
        'handling COURSE_CERT_CHANGED: username=%s, course_key=%s, mode=%s, status=%s',
        user,
        course_key,
        mode,
        status,
    )
    # import here, because signal is registered at startup, but items in tasks are not yet able to be loaded
    from openedx.core.djangoapps.programs.tasks.v1.tasks import award_course_certificate
    award_course_certificate.delay(user.username, str(course_key))
Example #26
0
def get_credentials(user, program_uuid=None, credential_type=None):
    """
    Given a user, get credentials earned from the credentials service.

    Arguments:
        user (User): The user to authenticate as when requesting credentials.

    Keyword Arguments:
        program_uuid (str): UUID of the program whose credential to retrieve.
        credential_type (str): Which type of credentials to return (course-run or program)

    Returns:
        list of dict, representing credentials returned by the Credentials
        service.
    """
    credential_configuration = CredentialsApiConfig.current()

    querystring = {
        'username': user.username,
        'status': 'awarded',
        'only_visible': 'True'
    }

    if program_uuid:
        querystring['program_uuid'] = program_uuid

    if credential_type:
        querystring['type'] = credential_type

    # Bypass caching for staff users, who may be generating credentials and
    # want to see them displayed immediately.
    use_cache = credential_configuration.is_cache_enabled and not user.is_staff
    cache_key = f'{credential_configuration.CACHE_KEY}.{user.username}' if use_cache else None
    if cache_key and program_uuid:
        cache_key = f'{cache_key}.{program_uuid}'

    api_client = get_credentials_api_client(user)
    base_api_url = get_credentials_api_base_url()

    return get_api_data(credential_configuration,
                        'credentials',
                        api_client=api_client,
                        base_api_url=base_api_url,
                        querystring=querystring,
                        cache_key=cache_key)
Example #27
0
def get_user_credentials(user):
    """Given a user, get credentials earned from the Credentials service.
    Arguments:
        user (User): The user to authenticate as when requesting credentials.
    Returns:
        list of dict, representing credentials returned by the Credentials
        service.
    """
    credential_configuration = CredentialsApiConfig.current()
    user_query = {'status': 'awarded', 'username': user.username}
    # Bypass caching for staff users, who may be generating credentials and
    # want to see them displayed immediately.
    use_cache = credential_configuration.is_cache_enabled and not user.is_staff
    cache_key = credential_configuration.CACHE_KEY + '.' + user.username if use_cache else None

    credentials = get_edx_api_data(
        credential_configuration, user, 'credentials', querystring=user_query, cache_key=cache_key
    )
    return credentials
Example #28
0
    def mock_credentials_api(self,
                             user,
                             data=None,
                             status_code=200,
                             reset_url=True,
                             is_next_page=False):
        """Utility for mocking out Credentials API URLs."""
        self.assertTrue(
            httpretty.is_enabled(),
            msg='httpretty must be enabled to mock Credentials API calls.')
        internal_api_url = CredentialsApiConfig.current(
        ).internal_api_url.strip('/')

        url = internal_api_url + '/user_credentials/?username='******'/user_credentials/?page=2&username='******'next'] = next_page_url
            next_page_body = json.dumps(self.CREDENTIALS_NEXT_API_RESPONSE)
            httpretty.register_uri(httpretty.GET,
                                   next_page_url,
                                   body=body,
                                   content_type='application/json',
                                   status=status_code)
            httpretty.register_uri(httpretty.GET,
                                   url,
                                   body=next_page_body,
                                   content_type='application/json',
                                   status=status_code)
        else:
            httpretty.register_uri(httpretty.GET,
                                   url,
                                   body=body,
                                   content_type='application/json',
                                   status=status_code)
def handle_course_cert_awarded(sender, user, course_key, mode, status,
                               **kwargs):  # pylint: disable=unused-argument
    """
    If programs is enabled and a learner is awarded a course certificate,
    schedule a celery task to process any programs certificates for which
    the learner may now be eligible.

    Args:
        sender:
            class of the object instance that sent this signal
        user:
            django.contrib.auth.User - the user to whom a cert was awarded
        course_key:
            refers to the course run for which the cert was awarded
        mode:
            mode / certificate type, e.g. "verified"
        status:
            either "downloadable" or "generating"

    Returns:
        None

    """
    # Import here instead of top of file since this module gets imported before
    # the credentials app is loaded, resulting in a Django deprecation warning.
    from openedx.core.djangoapps.credentials.models import CredentialsApiConfig

    # Avoid scheduling new tasks if certification is disabled.
    if not CredentialsApiConfig.current().is_learner_issuance_enabled:
        return

    # schedule background task to process
    LOGGER.debug(
        'handling COURSE_CERT_AWARDED: username=%s, course_key=%s, mode=%s, status=%s',
        user,
        course_key,
        mode,
        status,
    )
    # import here, because signal is registered at startup, but items in tasks are not yet able to be loaded
    from openedx.core.djangoapps.programs.tasks import award_program_certificates
    award_program_certificates.delay(user.username)
Example #30
0
def send_grade_if_interesting(user, course_run_key, mode, status, letter_grade, percent_grade):
    """ Checks if grade is interesting to Credentials and schedules a Celery task if so. """

    # Avoid scheduling new tasks if certification is disabled. (Grades are a part of the records/cert story)
    if not CredentialsApiConfig.current().is_learner_issuance_enabled:
        return

    # Avoid scheduling new tasks if learner records are disabled for this site.
    if not helpers.get_value_for_org(course_run_key.org, 'ENABLE_LEARNER_RECORDS', True):
        return

    # Grab mode/status if we don't have them in hand
    if mode is None or status is None:
        try:
            cert = GeneratedCertificate.objects.get(user=user, course_id=course_run_key)  # pylint: disable=no-member
            mode = cert.mode
            status = cert.status
        except GeneratedCertificate.DoesNotExist:
            # We only care about grades for which there is a certificate.
            return

    # Don't worry about whether it's available as well as awarded. Just awarded is good enough to record a verified
    # attempt at a course. We want even the grades that didn't pass the class because Credentials wants to know about
    # those too.
    if mode not in INTERESTING_MODES or status not in INTERESTING_STATUSES:
        return

    # If the course isn't in any program, don't bother telling Credentials about it. When Credentials grows support
    # for course records as well as program records, we'll need to open this up.
    if not is_course_run_in_a_program(course_run_key):
        return

    # Grab grades if we don't have them in hand
    if letter_grade is None or percent_grade is None:
        grade = CourseGradeFactory().read(user, course_key=course_run_key, create_if_needed=False)
        if grade is None:
            return
        letter_grade = grade.letter_grade
        percent_grade = grade.percent

    send_grade_to_credentials.delay(user.username, str(course_run_key), True, letter_grade, percent_grade)
Example #31
0
def handle_course_cert_awarded(sender, user, course_key, mode, status, **kwargs):  # pylint: disable=unused-argument
    """
    If programs is enabled and a learner is awarded a course certificate,
    schedule a celery task to process any programs certificates for which
    the learner may now be eligible.

    Args:
        sender:
            class of the object instance that sent this signal
        user:
            django.contrib.auth.User - the user to whom a cert was awarded
        course_key:
            refers to the course run for which the cert was awarded
        mode:
            mode / certificate type, e.g. "verified"
        status:
            either "downloadable" or "generating"

    Returns:
        None

    """
    # Import here instead of top of file since this module gets imported before
    # the credentials app is loaded, resulting in a Django deprecation warning.
    from openedx.core.djangoapps.credentials.models import CredentialsApiConfig

    # Avoid scheduling new tasks if certification is disabled.
    if not CredentialsApiConfig.current().is_learner_issuance_enabled:
        return

    # schedule background task to process
    LOGGER.debug(
        'handling COURSE_CERT_AWARDED: username=%s, course_key=%s, mode=%s, status=%s',
        user,
        course_key,
        mode,
        status,
    )
    # import here, because signal is registered at startup, but items in tasks are not yet able to be loaded
    from openedx.core.djangoapps.programs.tasks.v1.tasks import award_program_certificates
    award_program_certificates.delay(user.username)
Example #32
0
def send_grade_if_interesting(user, course_run_key, mode, status, letter_grade, percent_grade):
    """ Checks if grade is interesting to Credentials and schedules a Celery task if so. """

    # Avoid scheduling new tasks if certification is disabled. (Grades are a part of the records/cert story)
    if not CredentialsApiConfig.current().is_learner_issuance_enabled:
        return

    # Avoid scheduling new tasks if learner records are disabled for this site.
    if not helpers.get_value_for_org(course_run_key.org, 'ENABLE_LEARNER_RECORDS', True):
        return

    # Grab mode/status if we don't have them in hand
    if mode is None or status is None:
        try:
            cert = GeneratedCertificate.objects.get(user=user, course_id=course_run_key)  # pylint: disable=no-member
            mode = cert.mode
            status = cert.status
        except GeneratedCertificate.DoesNotExist:
            # We only care about grades for which there is a certificate.
            return

    # Don't worry about whether it's available as well as awarded. Just awarded is good enough to record a verified
    # attempt at a course. We want even the grades that didn't pass the class because Credentials wants to know about
    # those too.
    if mode not in INTERESTING_MODES or status not in INTERESTING_STATUSES:
        return

    # If the course isn't in any program, don't bother telling Credentials about it. When Credentials grows support
    # for course records as well as program records, we'll need to open this up.
    if not is_course_run_in_a_program(course_run_key):
        return

    # Grab grades if we don't have them in hand
    if letter_grade is None or percent_grade is None:
        grade = CourseGradeFactory().read(user, course_key=course_run_key, create_if_needed=False)
        if grade is None:
            return
        letter_grade = grade.letter_grade
        percent_grade = grade.percent

    send_grade_to_credentials.delay(user.username, str(course_run_key), True, letter_grade, percent_grade)
Example #33
0
def handle_course_cert_revoked(sender, user, course_key, mode, status,
                               **kwargs):  # pylint: disable=unused-argument
    """
    If programs is enabled and a learner's course certificate is revoked,
    schedule a celery task to revoke any related program certificates.

    Args:
        sender:
            class of the object instance that sent this signal
        user:
            django.contrib.auth.User - the user for which a cert was revoked
        course_key:
            refers to the course run for which the cert was revoked
        mode:
            mode / certificate type, e.g. "verified"
        status:
            revoked

    Returns:
        None

    """
    # Import here instead of top of file since this module gets imported before
    # the credentials app is loaded, resulting in a Django deprecation warning.
    from openedx.core.djangoapps.credentials.models import CredentialsApiConfig

    # Avoid scheduling new tasks if certification is disabled.
    if not CredentialsApiConfig.current().is_learner_issuance_enabled:
        return

    # schedule background task to process
    LOGGER.info(
        f"handling COURSE_CERT_REVOKED: user={user.id}, course_key={course_key}, mode={mode}, status={status}"
    )
    # import here, because signal is registered at startup, but items in tasks are not yet able to be loaded
    from openedx.core.djangoapps.programs.tasks import revoke_program_certificates
    revoke_program_certificates.delay(user.username, str(course_key))
Example #34
0
def get_credentials(user, program_uuid=None, credential_type=None):
    """
    Given a user, get credentials earned from the credentials service.

    Arguments:
        user (User): The user to authenticate as when requesting credentials.

    Keyword Arguments:
        program_uuid (str): UUID of the program whose credential to retrieve.
        credential_type (str): Which type of credentials to return (course-run or program)

    Returns:
        list of dict, representing credentials returned by the Credentials
        service.
    """
    credential_configuration = CredentialsApiConfig.current()

    querystring = {'username': user.username, 'status': 'awarded', 'only_visible': 'True'}

    if program_uuid:
        querystring['program_uuid'] = program_uuid

    if credential_type:
        querystring['type'] = credential_type

    # Bypass caching for staff users, who may be generating credentials and
    # want to see them displayed immediately.
    use_cache = credential_configuration.is_cache_enabled and not user.is_staff
    cache_key = '{}.{}'.format(credential_configuration.CACHE_KEY, user.username) if use_cache else None
    if cache_key and program_uuid:
        cache_key = '{}.{}'.format(cache_key, program_uuid)
    api = get_credentials_api_client(user)

    return get_edx_api_data(
        credential_configuration, 'credentials', api=api, querystring=querystring, cache_key=cache_key
    )
Example #35
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 (str): The username of the student

    Returns:
        None

    """
    LOGGER.info(u'Running task award_program_certificates for username %s', username)
    programs_without_certificates = configuration_helpers.get_value('programs_without_certificates', [])
    if programs_without_certificates:
        if str(programs_without_certificates[0]).lower() == "all":
            # this check will prevent unnecessary logging for partners without program certificates
            return

    countdown = 2 ** self.request.retries
    # If the credentials config model is 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 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=MAX_RETRIES)

    try:
        try:
            student = User.objects.get(username=username)
        except User.DoesNotExist:
            LOGGER.exception(u'Task award_program_certificates was called with invalid username %s', username)
            # Don't retry for this case - just conclude the task.
            return
        completed_programs = {}
        for site in Site.objects.all():
            completed_programs.update(get_completed_programs(site, student))
        if not completed_programs:
            # No reason to continue beyond this point unless/until this
            # task gets updated to support revocation of program certs.
            LOGGER.info(u'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_uuids = get_certified_programs(student)

        # we will skip all the programs which have already been awarded and we want to skip the programs
        # which are exit in site configuration in 'programs_without_certificates' list.
        awarded_and_skipped_program_uuids = list(set(existing_program_uuids + list(programs_without_certificates)))

    except Exception as exc:
        LOGGER.exception(u'Failed to determine program certificates to be awarded for user %s', username)
        raise self.retry(exc=exc, countdown=countdown, max_retries=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_uuids = sorted(list(set(completed_programs.keys()) - set(awarded_and_skipped_program_uuids)))
    if new_program_uuids:
        try:
            credentials_client = get_credentials_api_client(
                User.objects.get(username=settings.CREDENTIALS_SERVICE_USERNAME),
            )
        except Exception as exc:
            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=MAX_RETRIES)

        failed_program_certificate_award_attempts = []
        for program_uuid in new_program_uuids:
            visible_date = completed_programs[program_uuid]
            try:
                award_program_certificate(credentials_client, username, program_uuid, visible_date)
                LOGGER.info(u'Awarded certificate for program %s to user %s', program_uuid, username)
            except exceptions.HttpNotFoundError:
                LOGGER.exception(
                    u"""Certificate for program {uuid} could not be found. Unable to award certificate to user
                    {username}. The program might not be configured.""".format(uuid=program_uuid, username=username)
                )
            except exceptions.HttpClientError as exc:
                # Grab the status code from the client error, because our API
                # client handles all 4XX errors the same way. In the future,
                # we may want to fork slumber, add 429 handling, and use that
                # in edx_rest_api_client.
                if exc.response.status_code == 429:  # pylint: disable=no-member
                    rate_limit_countdown = 60
                    LOGGER.info(
                        u"""Rate limited. Retrying task to award certificates to user {username} in {countdown}
                        seconds""".format(username=username, countdown=rate_limit_countdown)
                    )
                    # Retry after 60 seconds, when we should be in a new throttling window
                    raise self.retry(exc=exc, countdown=rate_limit_countdown, max_retries=MAX_RETRIES)
                else:
                    LOGGER.exception(
                        u"""Unable to award certificate to user {username} for program {uuid}. The program might not be
                        configured.""".format(username=username, uuid=program_uuid)
                    )
            except Exception:  # pylint: disable=broad-except
                # keep trying to award other certs, but retry the whole task to fix any missing entries
                LOGGER.warning(u'Failed to award certificate for program {uuid} to user {username}.'.format(
                    uuid=program_uuid, username=username))
                failed_program_certificate_award_attempts.append(program_uuid)

        if failed_program_certificate_award_attempts:
            # N.B. This logic assumes that this task is idempotent
            LOGGER.info(u'Retrying task to award failed certificates to user %s', username)
            # The error message may change on each reattempt but will never be raised until
            # the max number of retries have been exceeded. It is unlikely that this list
            # will change by the time it reaches its maximimum number of attempts.
            exception = MaxRetriesExceededError(
                u"Failed to award certificate for user {} for programs {}".format(
                    username, failed_program_certificate_award_attempts))
            raise self.retry(
                exc=exception,
                countdown=countdown,
                max_retries=MAX_RETRIES)
    else:
        LOGGER.info(u'User %s is not eligible for any new program certificates', username)

    LOGGER.info(u'Successfully completed the task award_program_certificates for username %s', username)
Example #36
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)
Example #37
0
def award_course_certificate(self, username, course_run_key, certificate_available_date=None):
    """
    This task is designed to be called whenever a student GeneratedCertificate is updated.
    It can be called independently for a username and a course_run, but is invoked on each GeneratedCertificate.save.

    Arguments:
        username (str): The user to award the Credentials course cert to
        course_run_key (str): The course run key to award the certificate for
        certificate_available_date (str): A string representation of the datetime for when to make the certificate
            available to the user. If not provided, it will calculate the date.

    """
    def _retry_with_custom_exception(username, course_run_key, reason, countdown):
        exception = MaxRetriesExceededError(
            f"Failed to award course certificate for user {username} for course {course_run_key}. Reason: {reason}"
        )
        return self.retry(
            exc=exception,
            countdown=countdown,
            max_retries=MAX_RETRIES
        )

    LOGGER.info(f"Running task award_course_certificate for username {username}")

    countdown = 2 ** self.request.retries

    # If the credentials config model is 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 CredentialsApiConfig.current().is_learner_issuance_enabled:
        error_msg = (
            "Task award_course_certificate cannot be executed when credentials issuance is disabled in API config"
        )
        LOGGER.warning(error_msg)
        raise _retry_with_custom_exception(
            username=username,
            course_run_key=course_run_key,
            reason=error_msg,
            countdown=countdown
        )

    try:
        course_key = CourseKey.from_string(course_run_key)
        try:
            user = User.objects.get(username=username)
        except User.DoesNotExist:
            LOGGER.exception(f"Task award_course_certificate was called with invalid username {username}")
            # Don't retry for this case - just conclude the task.
            return
        # Get the cert for the course key and username if it's both passing and available in professional/verified
        try:
            certificate = GeneratedCertificate.eligible_certificates.get(
                user=user.id,
                course_id=course_key
            )
        except GeneratedCertificate.DoesNotExist:
            LOGGER.exception(
                "Task award_course_certificate was called without Certificate found "
                f"for {course_key} to user {username}"
            )
            return
        if certificate.mode in CourseMode.CERTIFICATE_RELEVANT_MODES:
            try:
                course_overview = CourseOverview.get_from_id(course_key)
            except (CourseOverview.DoesNotExist, IOError):
                LOGGER.exception(
                    f"Task award_course_certificate was called without course overview data for course {course_key}"
                )
                return
            credentials_client = get_credentials_api_client(User.objects.get(
                username=settings.CREDENTIALS_SERVICE_USERNAME),
                org=course_key.org,
            )

            # Date is being passed via JSON and is encoded in the EMCA date time string format. The rest of the code
            # expects a datetime.
            if certificate_available_date:
                certificate_available_date = datetime.strptime(certificate_available_date, VISIBLE_DATE_FORMAT)

            # Even in the cases where this task is called with a certificate_available_date, we still need to retrieve
            # the course overview because it's required to determine if we should use the certificate_available_date or
            # the certs modified date
            visible_date = available_date_for_certificate(
                course_overview,
                certificate,
                certificate_available_date=certificate_available_date
            )
            LOGGER.info(
                "Task award_course_certificate will award certificate for course "
                f"{course_key} with a visible date of {visible_date}"
            )
            post_course_certificate(credentials_client, username, certificate, visible_date)

            LOGGER.info(f"Awarded certificate for course {course_key} to user {username}")
    except Exception as exc:
        error_msg = f"Failed to determine course certificates to be awarded for user {username}."
        LOGGER.exception(error_msg)
        raise _retry_with_custom_exception(
            username=username,
            course_run_key=course_run_key,
            reason=error_msg,
            countdown=countdown
        ) from exc
Example #38
0
def revoke_program_certificates(self, username, course_key):
    """
    This task is designed to be called whenever a student's course certificate is
    revoked.

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

    Args:
        username (str): The username of the student
        course_key (str|CourseKey): The course identifier

    Returns:
        None

    """
    def _retry_with_custom_exception(username, course_key, reason, countdown):
        exception = MaxRetriesExceededError(
            f"Failed to revoke program certificate for user {username} for course {course_key}. Reason: {reason}"
        )
        return self.retry(
            exc=exception,
            countdown=countdown,
            max_retries=MAX_RETRIES
        )

    countdown = 2 ** self.request.retries
    # If the credentials config model is 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 CredentialsApiConfig.current().is_learner_issuance_enabled:
        error_msg = (
            "Task revoke_program_certificates cannot be executed when credentials issuance is disabled in API config"
        )
        LOGGER.warning(error_msg)
        raise _retry_with_custom_exception(
            username=username,
            course_key=course_key,
            reason=error_msg,
            countdown=countdown
        )

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

    try:
        inverted_programs = get_inverted_programs(student)
        course_specific_programs = inverted_programs.get(str(course_key))
        if not course_specific_programs:
            # No reason to continue beyond this point
            LOGGER.info(
                f"Task revoke_program_certificates was called for user {username} "
                f"and course {course_key} with no engaged programs"
            )
            return

        # Determine which program certificates the user has already been awarded, if any.
        program_uuids_to_revoke = get_revokable_program_uuids(course_specific_programs, student)
    except Exception as exc:
        error_msg = (
            f"Failed to determine program certificates to be revoked for user {username} "
            f"with course {course_key}"
        )
        LOGGER.exception(error_msg)
        raise _retry_with_custom_exception(
            username=username,
            course_key=course_key,
            reason=error_msg,
            countdown=countdown
        ) from exc

    if program_uuids_to_revoke:
        try:
            credentials_client = get_credentials_api_client(
                User.objects.get(username=settings.CREDENTIALS_SERVICE_USERNAME),
            )
        except Exception as exc:
            error_msg = "Failed to create a credentials API client to revoke program certificates"
            LOGGER.exception(error_msg)
            # Retry because a misconfiguration could be fixed
            raise _retry_with_custom_exception(username, course_key, reason=exc, countdown=countdown) from exc

        failed_program_certificate_revoke_attempts = []
        for program_uuid in program_uuids_to_revoke:
            try:
                revoke_program_certificate(credentials_client, username, program_uuid)
                LOGGER.info(f"Revoked certificate for program {program_uuid} for user {username}")
            except exceptions.HttpNotFoundError:
                LOGGER.exception(
                    f"Certificate for program {program_uuid} could not be found. "
                    f"Unable to revoke certificate for user {username}"
                )
            except exceptions.HttpClientError as exc:
                # Grab the status code from the client error, because our API
                # client handles all 4XX errors the same way. In the future,
                # we may want to fork slumber, add 429 handling, and use that
                # in edx_rest_api_client.
                if exc.response.status_code == 429:  # pylint: disable=no-member, no-else-raise
                    rate_limit_countdown = 60
                    error_msg = (
                        "Rate limited. "
                        f"Retrying task to revoke certificates for user {username} in {rate_limit_countdown} seconds"
                    )
                    LOGGER.info(error_msg)
                    # Retry after 60 seconds, when we should be in a new throttling window
                    raise _retry_with_custom_exception(
                        username,
                        course_key,
                        reason=error_msg,
                        countdown=rate_limit_countdown
                    ) from exc
                else:
                    LOGGER.exception(f"Unable to revoke certificate for user {username} for program {program_uuid}.")
            except Exception:  # pylint: disable=broad-except
                # keep trying to revoke other certs, but retry the whole task to fix any missing entries
                LOGGER.warning(f"Failed to revoke certificate for program {program_uuid} of user {username}.")
                failed_program_certificate_revoke_attempts.append(program_uuid)

        if failed_program_certificate_revoke_attempts:
            # N.B. This logic assumes that this task is idempotent
            LOGGER.info(f"Retrying task to revoke failed certificates to user {username}")
            # The error message may change on each reattempt but will never be raised until
            # the max number of retries have been exceeded. It is unlikely that this list
            # will change by the time it reaches its maximimum number of attempts.
            error_msg = (
                f"Failed to revoke certificate for user {username} "
                f"for programs {failed_program_certificate_revoke_attempts}"
            )
            raise _retry_with_custom_exception(
                username,
                course_key,
                reason=error_msg,
                countdown=countdown
            )

    else:
        LOGGER.info(f"There is no program certificates for user {username} to revoke")
    LOGGER.info(f"Successfully completed the task revoke_program_certificates for username {username}")
Example #39
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)
Example #40
0
def award_program_certificates(self, username):  # lint-amnesty, pylint: disable=too-many-statements
    """
    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 (str): The username of the student

    Returns:
        None

    """
    def _retry_with_custom_exception(username, reason, countdown):
        exception = MaxRetriesExceededError(
            f"Failed to award program certificate for user {username}. Reason: {reason}"
        )
        return self.retry(
            exc=exception,
            countdown=countdown,
            max_retries=MAX_RETRIES
        )

    LOGGER.info(f"Running task award_program_certificates for username {username}")
    programs_without_certificates = configuration_helpers.get_value('programs_without_certificates', [])
    if programs_without_certificates:
        if str(programs_without_certificates[0]).lower() == "all":
            # this check will prevent unnecessary logging for partners without program certificates
            return

    countdown = 2 ** self.request.retries
    # If the credentials config model is 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 CredentialsApiConfig.current().is_learner_issuance_enabled:
        error_msg = (
            "Task award_program_certificates cannot be executed when credentials issuance is disabled in API config"
        )
        LOGGER.warning(error_msg)
        raise _retry_with_custom_exception(username=username, reason=error_msg, countdown=countdown)

    try:
        try:
            student = User.objects.get(username=username)
        except User.DoesNotExist:
            LOGGER.exception(f"Task award_program_certificates was called with invalid username {username}")
            # Don't retry for this case - just conclude the task.
            return
        completed_programs = {}
        for site in Site.objects.all():
            completed_programs.update(get_completed_programs(site, student))
        if not completed_programs:
            # No reason to continue beyond this point unless/until this
            # task gets updated to support revocation of program certs.
            LOGGER.info(f"Task award_program_certificates was called for user {username} with no completed programs")
            return

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

        # we will skip all the programs which have already been awarded and we want to skip the programs
        # which are exit in site configuration in 'programs_without_certificates' list.
        awarded_and_skipped_program_uuids = list(set(existing_program_uuids + list(programs_without_certificates)))

    except Exception as exc:
        error_msg = f"Failed to determine program certificates to be awarded for user {username}. {exc}"
        LOGGER.exception(error_msg)
        raise _retry_with_custom_exception(username=username, reason=error_msg, countdown=countdown) from exc

    # 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_uuids = sorted(list(set(completed_programs.keys()) - set(awarded_and_skipped_program_uuids)))
    if new_program_uuids:
        try:
            credentials_client = get_credentials_api_client(
                User.objects.get(username=settings.CREDENTIALS_SERVICE_USERNAME),
            )
        except Exception as exc:
            error_msg = "Failed to create a credentials API client to award program certificates"
            LOGGER.exception(error_msg)
            # Retry because a misconfiguration could be fixed
            raise _retry_with_custom_exception(username=username, reason=error_msg, countdown=countdown) from exc

        failed_program_certificate_award_attempts = []
        for program_uuid in new_program_uuids:
            visible_date = completed_programs[program_uuid]
            try:
                LOGGER.info(f"Visible date for user {username} : program {program_uuid} is {visible_date}")
                award_program_certificate(credentials_client, username, program_uuid, visible_date)
                LOGGER.info(f"Awarded certificate for program {program_uuid} to user {username}")
            except exceptions.HttpNotFoundError:
                LOGGER.exception(
                    f"Certificate for program {program_uuid} could not be found. " +
                    f"Unable to award certificate to user {username}. The program might not be configured."
                )
            except exceptions.HttpClientError as exc:
                # Grab the status code from the client error, because our API
                # client handles all 4XX errors the same way. In the future,
                # we may want to fork slumber, add 429 handling, and use that
                # in edx_rest_api_client.
                if exc.response.status_code == 429:  # lint-amnesty, pylint: disable=no-else-raise, no-member
                    rate_limit_countdown = 60
                    error_msg = (
                        f"Rate limited. "
                        f"Retrying task to award certificates to user {username} in {rate_limit_countdown} seconds"
                    )
                    LOGGER.info(error_msg)
                    # Retry after 60 seconds, when we should be in a new throttling window
                    raise _retry_with_custom_exception(
                        username=username,
                        reason=error_msg,
                        countdown=rate_limit_countdown
                    ) from exc
                else:
                    LOGGER.exception(
                        f"Unable to award certificate to user {username} for program {program_uuid}. "
                        "The program might not be configured."
                    )
            except Exception as exc:  # pylint: disable=broad-except
                # keep trying to award other certs, but retry the whole task to fix any missing entries
                LOGGER.exception(f"Failed to award certificate for program {program_uuid} to user {username}.")
                failed_program_certificate_award_attempts.append(program_uuid)

        if failed_program_certificate_award_attempts:
            # N.B. This logic assumes that this task is idempotent
            LOGGER.info(f"Retrying task to award failed certificates to user {username}")
            # The error message may change on each reattempt but will never be raised until
            # the max number of retries have been exceeded. It is unlikely that this list
            # will change by the time it reaches its maximimum number of attempts.
            error_msg = (
                f"Failed to award certificate for user {username} "
                f"for programs {failed_program_certificate_award_attempts}"
            )
            raise _retry_with_custom_exception(username=username, reason=error_msg, countdown=countdown)
    else:
        LOGGER.info(f"User {username} is not eligible for any new program certificates")

    LOGGER.info(f"Successfully completed the task award_program_certificates for username {username}")
def award_course_certificate(self, username, course_run_key):
    """
    This task is designed to be called whenever a student GeneratedCertificate is updated.
    It can be called independently for a username and a course_run, but is invoked on each GeneratedCertificate.save.
    """
    LOGGER.info('Running task award_course_certificate for username %s',
                username)

    countdown = 2**self.request.retries

    # If the credentials config model is 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 CredentialsApiConfig.current().is_learner_issuance_enabled:
        LOGGER.warning(
            'Task award_course_certificate cannot be executed when credentials issuance is disabled in API config',
        )
        raise self.retry(countdown=countdown, max_retries=MAX_RETRIES)

    try:
        course_key = CourseKey.from_string(course_run_key)
        try:
            user = User.objects.get(username=username)
        except User.DoesNotExist:
            LOGGER.exception(
                'Task award_course_certificate was called with invalid username %s',
                username)
            # Don't retry for this case - just conclude the task.
            return
        # Get the cert for the course key and username if it's both passing and available in professional/verified
        try:
            certificate = GeneratedCertificate.eligible_certificates.get(
                user=user.id, course_id=course_key)
        except GeneratedCertificate.DoesNotExist:
            LOGGER.exception(
                'Task award_course_certificate was called without Certificate found for %s to user %s',
                course_key, username)
            return
        if certificate.mode in CourseMode.CREDIT_ELIGIBLE_MODES + CourseMode.CREDIT_MODES:
            try:
                course_overview = CourseOverview.get_from_id(course_key)
            except (CourseOverview.DoesNotExist, IOError):
                LOGGER.exception(
                    'Task award_course_certificate was called without course overview data for course %s',
                    course_key)
                return
            credentials_client = get_credentials_api_client(
                User.objects.get(
                    username=settings.CREDENTIALS_SERVICE_USERNAME),
                org=course_key.org,
            )
            # FIXME This may result in visible dates that do not update alongside the Course Overview if that changes
            # This is a known limitation of this implementation and was chosen to reduce the amount of replication,
            # endpoints, celery tasks, and jenkins jobs that needed to be written for this functionality
            visible_date = available_date_for_certificate(
                course_overview, certificate)
            post_course_certificate(credentials_client, username, certificate,
                                    visible_date)

            LOGGER.info('Awarded certificate for course %s to user %s',
                        course_key, username)
    except Exception as exc:
        LOGGER.exception(
            'Failed to determine course certificates to be awarded for user %s',
            username)
        raise self.retry(exc=exc, countdown=countdown, max_retries=MAX_RETRIES)
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 (str): The username of the student

    Returns:
        None

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

    countdown = 2**self.request.retries

    # If the credentials config model is 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 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=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
        program_uuids = []
        for site in Site.objects.all():
            program_uuids.extend(get_completed_programs(site, student))
        if not program_uuids:
            # 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_uuids = get_certified_programs(student)

    except Exception as exc:
        LOGGER.exception(
            'Failed to determine program certificates to be awarded for user %s',
            username)
        raise self.retry(exc=exc, countdown=countdown, max_retries=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_uuids = sorted(
        list(set(program_uuids) - set(existing_program_uuids)))
    if new_program_uuids:
        try:
            credentials_client = get_credentials_api_client(
                User.objects.get(
                    username=settings.CREDENTIALS_SERVICE_USERNAME), )
        except Exception as exc:
            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=MAX_RETRIES)

        failed_program_certificate_award_attempts = []
        for program_uuid in new_program_uuids:
            try:
                award_program_certificate(credentials_client, username,
                                          program_uuid)
                LOGGER.info('Awarded certificate for program %s to user %s',
                            program_uuid, username)
            except exceptions.HttpNotFoundError:
                LOGGER.exception(
                    """Certificate for program {uuid} could not be found. Unable to award certificate to user
                    {username}. The program might not be configured.""".format(
                        uuid=program_uuid, username=username))
            except exceptions.HttpClientError as exc:
                # Grab the status code from the client error, because our API
                # client handles all 4XX errors the same way. In the future,
                # we may want to fork slumber, add 429 handling, and use that
                # in edx_rest_api_client.
                if exc.response.status_code == 429:  # pylint: disable=no-member
                    rate_limit_countdown = 60
                    LOGGER.info(
                        """Rate limited. Retrying task to award certificates to user {username} in {countdown}
                        seconds""".format(username=username,
                                          countdown=rate_limit_countdown))
                    # Retry after 60 seconds, when we should be in a new throttling window
                    raise self.retry(exc=exc,
                                     countdown=rate_limit_countdown,
                                     max_retries=MAX_RETRIES)
                else:
                    LOGGER.exception(
                        """Unable to award certificate to user {username} for program {uuid}. The program might not be
                        configured.""".format(username=username,
                                              uuid=program_uuid))
            except Exception:  # pylint: disable=broad-except
                # keep trying to award other certs, but retry the whole task to fix any missing entries
                LOGGER.warning(
                    'Failed to award certificate for program {uuid} to user {username}.'
                    .format(uuid=program_uuid, username=username))
                failed_program_certificate_award_attempts.append(program_uuid)

        if failed_program_certificate_award_attempts:
            # N.B. This logic assumes that this task is idempotent
            LOGGER.info(
                'Retrying task to award failed certificates to user %s',
                username)
            # The error message may change on each reattempt but will never be raised until
            # the max number of retries have been exceeded. It is unlikely that this list
            # will change by the time it reaches its maximimum number of attempts.
            exception = MaxRetriesExceededError(
                "Failed to award certificate for user {} for programs {}".
                format(username, failed_program_certificate_award_attempts))
            raise self.retry(exc=exception,
                             countdown=countdown,
                             max_retries=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)
Example #43
0
def handle_course_cert_changed(sender, user, course_key, mode, status, **kwargs):
    """
        If a learner is awarded a course certificate,
        schedule a celery task to process that course certificate

        Args:
            sender:
                class of the object instance that sent this signal
            user:
                django.contrib.auth.User - the user to whom a cert was awarded
            course_key:
                refers to the course run for which the cert was awarded
            mode:
                mode / certificate type, e.g. "verified"
            status:
                "downloadable"

        Returns:
            None

        """
    # Import here instead of top of file since this module gets imported before
    # the credentials app is loaded, resulting in a Django deprecation warning.
    from openedx.core.djangoapps.credentials.models import CredentialsApiConfig

    verbose = kwargs.get('verbose', False)
    if verbose:
        msg = "Starting handle_course_cert_changed with params: "\
            "sender [{sender}], "\
            "user [{username}], "\
            "course_key [{course_key}], "\
            "mode [{mode}], "\
            "status [{status}], "\
            "kwargs [{kw}]"\
            .format(
                sender=sender,
                username=getattr(user, 'username', None),
                course_key=str(course_key),
                mode=mode,
                status=status,
                kw=kwargs
            )

        LOGGER.info(msg)

    # Avoid scheduling new tasks if certification is disabled.
    if not CredentialsApiConfig.current().is_learner_issuance_enabled:
        if verbose:
            LOGGER.info("Skipping send cert: is_learner_issuance_enabled False")
        return

    # Avoid scheduling new tasks if learner records are disabled for this site (right now, course certs are only
    # used for learner records -- when that changes, we can remove this bit and always send course certs).
    if not helpers.get_value_for_org(course_key.org, 'ENABLE_LEARNER_RECORDS', True):
        if verbose:
            LOGGER.info(
                "Skipping send cert: ENABLE_LEARNER_RECORDS False for org [{org}]".format(
                    org=course_key.org
                )
            )
        return

    # schedule background task to process
    LOGGER.debug(
        'handling COURSE_CERT_CHANGED: username=%s, course_key=%s, mode=%s, status=%s',
        user,
        course_key,
        mode,
        status,
    )
    # import here, because signal is registered at startup, but items in tasks are not yet able to be loaded
    from openedx.core.djangoapps.programs.tasks.v1.tasks import award_course_certificate
    award_course_certificate.delay(user.username, str(course_key))
Example #44
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 (str): The username of the student

    Returns:
        None

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

    countdown = 2**self.request.retries

    # If the credentials config model is 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 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=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
        program_uuids = []
        for site in Site.objects.all():
            program_uuids.extend(get_completed_programs(site, student))
        if not program_uuids:
            # 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_uuids = get_certified_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=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_uuids = sorted(
        list(set(program_uuids) - set(existing_program_uuids)))
    if new_program_uuids:
        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=MAX_RETRIES)

        retry = False
        for program_uuid in new_program_uuids:
            try:
                award_program_certificate(credentials_client, username,
                                          program_uuid)
                LOGGER.info('Awarded certificate for program %s to user %s',
                            program_uuid, username)
            except exceptions.HttpNotFoundError:
                LOGGER.exception(
                    'Certificate for program %s not configured, unable to award certificate to %s',
                    program_uuid, 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_uuid, 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=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)
Example #45
0
def send_grade_if_interesting(user, course_run_key, mode, status, letter_grade, percent_grade, verbose=False):
    """ Checks if grade is interesting to Credentials and schedules a Celery task if so. """

    if verbose:
        msg = u"Starting send_grade_if_interesting with params: "\
            u"user [{username}], "\
            u"course_run_key [{key}], "\
            u"mode [{mode}], "\
            u"status [{status}], "\
            u"letter_grade [{letter_grade}], "\
            u"percent_grade [{percent_grade}], "\
            u"verbose [{verbose}]"\
            .format(
                username=getattr(user, 'username', None),
                key=str(course_run_key),
                mode=mode,
                status=status,
                letter_grade=letter_grade,
                percent_grade=percent_grade,
                verbose=verbose
            )
        log.info(msg)
    # Avoid scheduling new tasks if certification is disabled. (Grades are a part of the records/cert story)
    if not CredentialsApiConfig.current().is_learner_issuance_enabled:
        if verbose:
            log.info("Skipping send grade: is_learner_issuance_enabled False")
        return

    # Avoid scheduling new tasks if learner records are disabled for this site.
    if not helpers.get_value_for_org(course_run_key.org, 'ENABLE_LEARNER_RECORDS', True):
        if verbose:
            log.info(
                u"Skipping send grade: ENABLE_LEARNER_RECORDS False for org [{org}]".format(
                    org=course_run_key.org
                )
            )
        return

    # Grab mode/status if we don't have them in hand
    if mode is None or status is None:
        try:
            cert = GeneratedCertificate.objects.get(user=user, course_id=course_run_key)  # pylint: disable=no-member
            mode = cert.mode
            status = cert.status
        except GeneratedCertificate.DoesNotExist:
            # We only care about grades for which there is a certificate.
            if verbose:
                log.info(
                    u"Skipping send grade: no cert for user [{username}] & course_id [{course_id}]".format(
                        username=getattr(user, 'username', None),
                        course_id=str(course_run_key)
                    )
                )
            return

    # Don't worry about whether it's available as well as awarded. Just awarded is good enough to record a verified
    # attempt at a course. We want even the grades that didn't pass the class because Credentials wants to know about
    # those too.
    if mode not in INTERESTING_MODES or status not in INTERESTING_STATUSES:
        if verbose:
            log.info(
                u"Skipping send grade: mode/status uninteresting for mode [{mode}] & status [{status}]".format(
                    mode=mode,
                    status=status
                )
            )
        return

    # If the course isn't in any program, don't bother telling Credentials about it. When Credentials grows support
    # for course records as well as program records, we'll need to open this up.
    if not is_course_run_in_a_program(course_run_key):
        if verbose:
            log.info(
                u"Skipping send grade: course run not in a program. [{course_id}]".format(course_id=str(course_run_key))
            )
        return

    # Grab grades if we don't have them in hand
    if letter_grade is None or percent_grade is None:
        grade = CourseGradeFactory().read(user, course_key=course_run_key, create_if_needed=False)
        if grade is None:
            if verbose:
                log.info(
                    u"Skipping send grade: No grade found for user [{username}] & course_id [{course_id}]".format(
                        username=getattr(user, 'username', None),
                        course_id=str(course_run_key)
                    )
                )
            return
        letter_grade = grade.letter_grade
        percent_grade = grade.percent

    send_grade_to_credentials.delay(user.username, str(course_run_key), True, letter_grade, percent_grade)
Example #46
0
        # awarded, if any.
        existing_program_ids = get_awarded_certificate_programs(student)

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

    # For each completed program for which the student doesn't already have a
    # certificate, award one now.
    #
    # 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, 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)

        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.
                LOGGER.exception('Failed to award certificate for program %s to user %s', program_id, username)
Example #47
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)

    # If either programs or credentials config models are disabled for this
    # feature, this task should not have been invoked in the first place, and
    # an error somewhere is likely (though a race condition is also possible).
    # In either case, the task should not be executed nor should it be retried.
    if not ProgramsApiConfig.current().is_certification_enabled:
        LOGGER.warning(
            'Task award_program_certificates cannot be executed when program certification is disabled in API config',
        )
        return

    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',
        )
        return

    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(ProgramsApiConfig.current(), 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, exc:  # pylint: disable=broad-except
        LOGGER.exception('Failed to determine program certificates to be awarded for user %s', username)
        raise self.retry(exc=exc)
Example #48
0
def award_course_certificate(self, username, course_run_key):
    """
    This task is designed to be called whenever a student GeneratedCertificate is updated.
    It can be called independently for a username and a course_run, but is invoked on each GeneratedCertificate.save.
    """
    LOGGER.info(u'Running task award_course_certificate for username %s', username)

    countdown = 2 ** self.request.retries

    # If the credentials config model is 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 CredentialsApiConfig.current().is_learner_issuance_enabled:
        LOGGER.warning(
            'Task award_course_certificate cannot be executed when credentials issuance is disabled in API config',
        )
        raise self.retry(countdown=countdown, max_retries=MAX_RETRIES)

    try:
        course_key = CourseKey.from_string(course_run_key)
        try:
            user = User.objects.get(username=username)
        except User.DoesNotExist:
            LOGGER.exception(u'Task award_course_certificate was called with invalid username %s', username)
            # Don't retry for this case - just conclude the task.
            return
        # Get the cert for the course key and username if it's both passing and available in professional/verified
        try:
            certificate = GeneratedCertificate.eligible_certificates.get(
                user=user.id,
                course_id=course_key
            )
        except GeneratedCertificate.DoesNotExist:
            LOGGER.exception(
                u'Task award_course_certificate was called without Certificate found for %s to user %s',
                course_key,
                username
            )
            return
        if certificate.mode in CourseMode.CERTIFICATE_RELEVANT_MODES:
            try:
                course_overview = CourseOverview.get_from_id(course_key)
            except (CourseOverview.DoesNotExist, IOError):
                LOGGER.exception(
                    u'Task award_course_certificate was called without course overview data for course %s',
                    course_key
                )
                return
            credentials_client = get_credentials_api_client(User.objects.get(
                username=settings.CREDENTIALS_SERVICE_USERNAME),
                org=course_key.org,
            )
            # FIXME This may result in visible dates that do not update alongside the Course Overview if that changes
            # This is a known limitation of this implementation and was chosen to reduce the amount of replication,
            # endpoints, celery tasks, and jenkins jobs that needed to be written for this functionality
            visible_date = available_date_for_certificate(course_overview, certificate)
            post_course_certificate(credentials_client, username, certificate, visible_date)

            LOGGER.info(u'Awarded certificate for course %s to user %s', course_key, username)
    except Exception as exc:
        LOGGER.exception(u'Failed to determine course certificates to be awarded for user %s', username)
        raise self.retry(exc=exc, countdown=countdown, max_retries=MAX_RETRIES)
Example #49
0
def send_grade_if_interesting(user,
                              course_run_key,
                              mode,
                              status,
                              letter_grade,
                              percent_grade,
                              verbose=False):
    """ Checks if grade is interesting to Credentials and schedules a Celery task if so. """

    if verbose:
        msg = u"Starting send_grade_if_interesting with params: "\
            "user [{username}], "\
            "course_run_key [{key}], "\
            "mode [{mode}], "\
            "status [{status}], "\
            "letter_grade [{letter_grade}], "\
            "percent_grade [{percent_grade}], "\
            "verbose [{verbose}]"\
            .format(
                username=getattr(user, 'username', None),
                key=str(course_run_key),
                mode=mode,
                status=status,
                letter_grade=letter_grade,
                percent_grade=percent_grade,
                verbose=verbose
            )
        log.info(msg)
    # Avoid scheduling new tasks if certification is disabled. (Grades are a part of the records/cert story)
    if not CredentialsApiConfig.current().is_learner_issuance_enabled:
        if verbose:
            log.info("Skipping send grade: is_learner_issuance_enabled False")
        return

    # Avoid scheduling new tasks if learner records are disabled for this site.
    if not helpers.get_value_for_org(course_run_key.org,
                                     'ENABLE_LEARNER_RECORDS', True):
        if verbose:
            log.info(
                u"Skipping send grade: ENABLE_LEARNER_RECORDS False for org [{org}]"
                .format(org=course_run_key.org))
        return

    # Grab mode/status if we don't have them in hand
    if mode is None or status is None:
        try:
            cert = GeneratedCertificate.objects.get(user=user,
                                                    course_id=course_run_key)  # pylint: disable=no-member
            mode = cert.mode
            status = cert.status
        except GeneratedCertificate.DoesNotExist:
            # We only care about grades for which there is a certificate.
            if verbose:
                log.info(
                    u"Skipping send grade: no cert for user [{username}] & course_id [{course_id}]"
                    .format(username=getattr(user, 'username', None),
                            course_id=str(course_run_key)))
            return

    # Don't worry about whether it's available as well as awarded. Just awarded is good enough to record a verified
    # attempt at a course. We want even the grades that didn't pass the class because Credentials wants to know about
    # those too.
    if mode not in INTERESTING_MODES or status not in INTERESTING_STATUSES:
        if verbose:
            log.info(
                u"Skipping send grade: mode/status uninteresting for mode [{mode}] & status [{status}]"
                .format(mode=mode, status=status))
        return

    # If the course isn't in any program, don't bother telling Credentials about it. When Credentials grows support
    # for course records as well as program records, we'll need to open this up.
    if not is_course_run_in_a_program(course_run_key):
        if verbose:
            log.info(
                u"Skipping send grade: course run not in a program. [{course_id}]"
                .format(course_id=str(course_run_key)))
        return

    # Grab grades if we don't have them in hand
    if letter_grade is None or percent_grade is None:
        grade = CourseGradeFactory().read(user,
                                          course_key=course_run_key,
                                          create_if_needed=False)
        if grade is None:
            if verbose:
                log.info(
                    u"Skipping send grade: No grade found for user [{username}] & course_id [{course_id}]"
                    .format(username=getattr(user, 'username', None),
                            course_id=str(course_run_key)))
            return
        letter_grade = grade.letter_grade
        percent_grade = grade.percent

    send_grade_to_credentials.delay(user.username, str(course_run_key), True,
                                    letter_grade, percent_grade)
Example #50
0
def handle_course_cert_changed(sender, user, course_key, mode, status, **kwargs):
    """
        If a learner is awarded a course certificate,
        schedule a celery task to process that course certificate

        Args:
            sender:
                class of the object instance that sent this signal
            user:
                django.contrib.auth.User - the user to whom a cert was awarded
            course_key:
                refers to the course run for which the cert was awarded
            mode:
                mode / certificate type, e.g. "verified"
            status:
                "downloadable"

        Returns:
            None

        """
    # Import here instead of top of file since this module gets imported before
    # the credentials app is loaded, resulting in a Django deprecation warning.
    from openedx.core.djangoapps.credentials.models import CredentialsApiConfig

    verbose = kwargs.get('verbose', False)
    if verbose:
        msg = u"Starting handle_course_cert_changed with params: "\
            u"sender [{sender}], "\
            u"user [{username}], "\
            u"course_key [{course_key}], "\
            u"mode [{mode}], "\
            u"status [{status}], "\
            u"kwargs [{kw}]"\
            .format(
                sender=sender,
                username=getattr(user, 'username', None),
                course_key=str(course_key),
                mode=mode,
                status=status,
                kw=kwargs
            )

        LOGGER.info(msg)

    # Avoid scheduling new tasks if certification is disabled.
    if not CredentialsApiConfig.current().is_learner_issuance_enabled:
        if verbose:
            LOGGER.info("Skipping send cert: is_learner_issuance_enabled False")
        return

    # Avoid scheduling new tasks if learner records are disabled for this site (right now, course certs are only
    # used for learner records -- when that changes, we can remove this bit and always send course certs).
    if not is_learner_records_enabled_for_org(course_key.org):
        if verbose:
            LOGGER.info(
                u"Skipping send cert: ENABLE_LEARNER_RECORDS False for org [{org}]".format(
                    org=course_key.org
                )
            )
        return

    # schedule background task to process
    LOGGER.debug(
        u'handling COURSE_CERT_CHANGED: username=%s, course_key=%s, mode=%s, status=%s',
        user,
        course_key,
        mode,
        status,
    )
    # import here, because signal is registered at startup, but items in tasks are not yet able to be loaded
    from openedx.core.djangoapps.programs.tasks import award_course_certificate
    award_course_certificate.delay(user.username, str(course_key))
Example #51
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)

    countdown = 2 ** self.request.retries

    # If the credentials config model is 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 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=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

        program_uuids = get_completed_programs(student)
        if not program_uuids:
            # 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_uuids = get_certified_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=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_uuids = sorted(list(set(program_uuids) - set(existing_program_uuids)))
    if new_program_uuids:
        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=MAX_RETRIES)

        retry = False
        for program_uuid in new_program_uuids:
            try:
                award_program_certificate(credentials_client, username, program_uuid)
                LOGGER.info('Awarded certificate for program %s to user %s', program_uuid, username)
            except exceptions.HttpNotFoundError:
                LOGGER.exception(
                    'Certificate for program %s not configured, unable to award certificate to %s',
                    program_uuid, 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_uuid, 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=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)