diff --git a/lms/envs/common.py b/lms/envs/common.py index 52cb04d3e5a8..16f4ae1ebb84 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -4477,6 +4477,7 @@ def _make_locale_paths(settings): # pylint: disable=missing-function-docstring CREDENTIALS_SERVICE_USERNAME = 'credentials_service_user' CREDENTIALS_GENERATION_ROUTING_KEY = DEFAULT_PRIORITY_QUEUE +CREDENTIALS_COURSE_COMPLETION_STATE = 'awarded' # Queue to use for award program certificates PROGRAM_CERTIFICATES_ROUTING_KEY = 'edx.lms.core.default' diff --git a/openedx/core/djangoapps/credentials/tests/factories.py b/openedx/core/djangoapps/credentials/tests/factories.py index be2af5e79b0c..2bcd759dac29 100644 --- a/openedx/core/djangoapps/credentials/tests/factories.py +++ b/openedx/core/djangoapps/credentials/tests/factories.py @@ -25,3 +25,15 @@ class UserCredential(DictFactoryBase): uuid = factory.Faker('uuid4') certificate_url = factory.Faker('url') credential = ProgramCredential() + + +class UserCredentialsCourseRunStatus(DictFactoryBase): + course_uuid = str(factory.Faker('uuid4')) + course_run = { + "uuid": str(factory.Faker('uuid4')), + "key": factory.LazyFunction(generate_course_run_key) + } + status = 'awarded' + type = 'verified' + certificate_available_date = factory.Faker('date') + grade = None diff --git a/openedx/core/djangoapps/credentials/tests/test_utils.py b/openedx/core/djangoapps/credentials/tests/test_utils.py index 4ab15091ffcb..2e24dba1a536 100644 --- a/openedx/core/djangoapps/credentials/tests/test_utils.py +++ b/openedx/core/djangoapps/credentials/tests/test_utils.py @@ -2,13 +2,21 @@ import uuid from unittest import mock +from django.conf import settings +from requests import Response +from requests.exceptions import HTTPError + +from common.djangoapps.student.tests.factories import UserFactory from openedx.core.djangoapps.credentials.models import CredentialsApiConfig from openedx.core.djangoapps.credentials.tests import factories from openedx.core.djangoapps.credentials.tests.mixins import CredentialsApiConfigMixin -from openedx.core.djangoapps.credentials.utils import get_credentials, get_credentials_records_url +from openedx.core.djangoapps.credentials.utils import ( + get_courses_completion_status, + get_credentials, + get_credentials_records_url +) from openedx.core.djangoapps.oauth_dispatch.tests.factories import ApplicationFactory from openedx.core.djangolib.testing.utils import CacheIsolationTestCase, skip_unless_lms -from common.djangoapps.student.tests.factories import UserFactory UTILS_MODULE = 'openedx.core.djangoapps.credentials.utils' @@ -98,3 +106,31 @@ def test_get_credentials_records_url(self): result = get_credentials_records_url("abcdefgh-ijkl-mnop-qrst-uvwxyz123456") assert result == "https://credentials.example.com/records/programs/abcdefghijklmnopqrstuvwxyz123456" + + @mock.patch('requests.Response.raise_for_status') + @mock.patch('requests.Response.json') + @mock.patch(UTILS_MODULE + '.get_credentials_api_client') + def test_get_courses_completion_status(self, mock_get_api_client, mock_json, mock_raise): + """ + Test to verify the functionality of get_courses_completion_status + """ + UserFactory.create(username=settings.CREDENTIALS_SERVICE_USERNAME) + course_statuses = factories.UserCredentialsCourseRunStatus.create_batch(3) + response_data = [course_status['course_run']['key'] for course_status in course_statuses] + mock_raise.return_value = None + mock_json.return_value = {'lms_user_id': self.user.id, + 'status': course_statuses, + 'username': self.user.username} + mock_get_api_client.return_value.post.return_value = Response() + course_run_keys = [course_status['course_run']['key'] for course_status in course_statuses] + api_response, is_exception = get_courses_completion_status(self.user.id, course_run_keys) + assert api_response == response_data + assert is_exception is False + + @mock.patch('requests.Response.raise_for_status') + def test_get_courses_completion_status_api_error(self, mock_raise): + mock_raise.return_value = HTTPError('An Error occured') + UserFactory.create(username=settings.CREDENTIALS_SERVICE_USERNAME) + api_response, is_exception = get_courses_completion_status(self.user.id, ['fake1', 'fake2', 'fake3']) + assert api_response == [] + assert is_exception is True diff --git a/openedx/core/djangoapps/credentials/utils.py b/openedx/core/djangoapps/credentials/utils.py index 107555e04dd3..bdc4a0fc3f2f 100644 --- a/openedx/core/djangoapps/credentials/utils.py +++ b/openedx/core/djangoapps/credentials/utils.py @@ -1,12 +1,19 @@ """Helper functions for working with Credentials.""" +import logging +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 urllib.parse import urljoin 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): """ @@ -101,3 +108,52 @@ def get_credentials(user, program_uuid=None, credential_type=None): querystring=querystring, cache_key=cache_key ) + + +def get_courses_completion_status(lms_user_id, course_run_ids): + """ + Given the lms_user_id and course run ids, checks for course completion status + Arguments: + lms_user_id (User): The user to authenticate as when requesting credentials. + 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 + + base_api_url = get_credentials_api_base_url() + completion_status_url = f'{base_api_url}/api/credentials/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, + data={ + 'lms_user_id': lms_user_id, + '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 lms_user_id [%s] for course_run_ids [%s] with exc [%s]:", + lms_user_id, + course_run_ids, + exc + ) + return [], True + # 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