To determine whether or not we need to revoke any program certificates, we first run get_revokable_program_uuids. This calls get_certified_programs, which calls get_credentials, which uses the OpenEdx Core utility method get_api_data. get_api_data makes the API call inside a try block, does raise_for_status also inside the try block, and then catches the exception, logs it, and returns an empty result to the caller. This means that on a failure to call the credentials API, get_credentials can’t tell the difference between a failure to hit the API (because credentials is, as it sometimes is during a notify_programs run, overloaded), or a learner with no program certificates. In this particular case, this is absolute failure, incorrect behavior. * Adds a new flag, `raise_on_error` which will make `get_api_data` log the exception and then re-raise the HTTPError, defaulting to false in order to avoid changing the behavior on any other callers * Also: my editor reformatted all of the touched files to our modern code standards, and it seemed appropriate to let it do that. * Also: added type hints in some cases, because they helped me write the code and debug. Our test suite definitely reports mypy results on type errors so we are verifying that hints are correct. FIXES: APER-3146
186 lines
6.2 KiB
Python
186 lines
6.2 KiB
Python
"""Helper functions for working with Credentials."""
|
|
import logging
|
|
from typing import Dict, List
|
|
from urllib.parse import urljoin
|
|
|
|
import requests
|
|
from django.conf import settings
|
|
from django.contrib.auth import get_user_model
|
|
from edx_rest_api_client.auth import SuppliedJwtAuth
|
|
|
|
from openedx.core.djangoapps.credentials.models import CredentialsApiConfig
|
|
from openedx.core.djangoapps.oauth_dispatch.jwt import create_jwt_for_user
|
|
from openedx.core.lib.edx_api_utils import get_api_data
|
|
|
|
log = logging.getLogger(__name__)
|
|
User = get_user_model()
|
|
|
|
|
|
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 Program UUIDs without dashes so we remove them here
|
|
stripped_program_uuid = program_uuid.replace("-", "")
|
|
return urljoin(base_url, f"programs/{stripped_program_uuid}")
|
|
|
|
return base_url
|
|
|
|
|
|
def get_credentials_api_client(user):
|
|
"""
|
|
Returns an authenticated Credentials API client.
|
|
|
|
Arguments:
|
|
user (User): The user to authenticate as when requesting credentials.
|
|
"""
|
|
scopes = ["email", "profile", "user_id"]
|
|
jwt = create_jwt_for_user(user, scopes=scopes)
|
|
|
|
client = requests.Session()
|
|
client.auth = SuppliedJwtAuth(jwt)
|
|
return client
|
|
|
|
|
|
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
|
|
|
|
|
|
def get_credentials(
|
|
user: User,
|
|
program_uuid: str = None,
|
|
credential_type: str = None,
|
|
raise_on_error: bool = False,
|
|
) -> List[Dict]:
|
|
"""
|
|
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)
|
|
raise_on_error (bool): Reraise errors back to the caller, instead if returning empty results.
|
|
|
|
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,
|
|
raise_on_error=raise_on_error,
|
|
)
|
|
|
|
|
|
def get_courses_completion_status(username, course_run_ids):
|
|
"""
|
|
Given the username and course run ids, checks for course completion status
|
|
Arguments:
|
|
username (User): Username of the user whose credentials are being requested.
|
|
course_run_ids(List): list of course run ids for which we need to check the completion status
|
|
Returns:
|
|
list of course_run_ids for which user has completed the course
|
|
Boolean: True if an exception occurred while calling the api, False otherwise
|
|
"""
|
|
credential_configuration = CredentialsApiConfig.current()
|
|
if not credential_configuration.enabled:
|
|
log.warning("%s configuration is disabled.", credential_configuration.API_NAME)
|
|
return [], False
|
|
|
|
completion_status_url = (
|
|
f"{settings.CREDENTIALS_INTERNAL_SERVICE_URL}/api"
|
|
"/credentials/v1/learner_cert_status/"
|
|
)
|
|
try:
|
|
api_client = get_credentials_api_client(
|
|
User.objects.get(username=settings.CREDENTIALS_SERVICE_USERNAME)
|
|
)
|
|
api_response = api_client.post(
|
|
completion_status_url,
|
|
json={
|
|
"username": username,
|
|
"course_runs": course_run_ids,
|
|
},
|
|
)
|
|
api_response.raise_for_status()
|
|
course_completion_response = api_response.json()
|
|
except Exception as exc: # pylint: disable=broad-except
|
|
log.exception(
|
|
"An unexpected error occurred while reqeusting course completion statuses "
|
|
"for user [%s] for course_run_ids [%s] with exc [%s]:",
|
|
username,
|
|
course_run_ids,
|
|
exc,
|
|
)
|
|
return [], True
|
|
log.info(
|
|
"Course completion status response for user [%s] for course_run_ids [%s] is [%s]",
|
|
username,
|
|
course_run_ids,
|
|
course_completion_response,
|
|
)
|
|
# Yes, This is course_credentials_data. The key is named status but
|
|
# it contains all the courses data from credentials.
|
|
course_credentials_data = course_completion_response.get("status", [])
|
|
if course_credentials_data is not None:
|
|
filtered_records = [
|
|
course_data["course_run"]["key"]
|
|
for course_data in course_credentials_data
|
|
if course_data["course_run"]["key"] in course_run_ids
|
|
and course_data["status"] == settings.CREDENTIALS_COURSE_COMPLETION_STATE
|
|
]
|
|
return filtered_records, False
|
|
return [], False
|