Skip to content

Commit

Permalink
feat: add course_status utility (openedx#32545)
Browse files Browse the repository at this point in the history
* feat: add course_status utility
  • Loading branch information
aht007 committed Jul 7, 2023
1 parent 74e3bb9 commit ee87173
Show file tree
Hide file tree
Showing 4 changed files with 108 additions and 3 deletions.
1 change: 1 addition & 0 deletions lms/envs/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
12 changes: 12 additions & 0 deletions openedx/core/djangoapps/credentials/tests/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
40 changes: 38 additions & 2 deletions openedx/core/djangoapps/credentials/tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -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
58 changes: 57 additions & 1 deletion openedx/core/djangoapps/credentials/utils.py
Original file line number Diff line number Diff line change
@@ -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):
"""
Expand Down Expand Up @@ -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

0 comments on commit ee87173

Please sign in to comment.