Skip to content

Commit

Permalink
Merge pull request #2008 from openedx/asaeed/ENT-8252
Browse files Browse the repository at this point in the history
[ENT-8252] feat: added caching for fetching degreed course id
  • Loading branch information
justEhmadSaeed authored Jan 31, 2024
2 parents 1750a65 + 8a37f2e commit 765f473
Show file tree
Hide file tree
Showing 4 changed files with 48 additions and 6 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ Change Log
Unreleased
----------

[4.11.2]
---------
* feat: added caching for fetching degreed course id

[4.11.1]
---------
* Added management command to fix `LearnerDataTransmissionAudit` table records.
Expand Down
2 changes: 1 addition & 1 deletion enterprise/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
Your project description goes here.
"""

__version__ = "4.11.1"
__version__ = "4.11.2"
19 changes: 16 additions & 3 deletions integrated_channels/degreed2/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from http import HTTPStatus

import requests
from edx_django_utils.cache import TieredCache, get_cache_key
from six.moves.urllib.parse import urljoin

from django.apps import apps
Expand Down Expand Up @@ -182,9 +183,17 @@ def delete_course_completion(self, user_id, payload):

def fetch_degreed_course_id(self, external_id):
"""
Fetch the 'id' of a course from Degreed2, given the external-id as a search param
'external-id' is the edX course key
Fetch the 'id' of a course from cache first and if not found then send a request to Degreed2,
given the external-id as a search param 'external-id' is the edX course key.
"""
cache_key = get_cache_key(
resource='degreed2_course_id',
resource_id=external_id,
)
cached_course_id = TieredCache.get_cached_response(cache_key)
if cached_course_id.is_found:
LOGGER.info(self.make_log_msg(external_id, f'Found cached course id: {cached_course_id.value}'))
return cached_course_id.value
# QueryDict converts + to space
params = QueryDict(f"filter[external_id]={external_id.replace('+','%2B')}")
course_search_url = f'{self.get_courses_url()}?{params.urlencode(safe="[]")}'
Expand All @@ -201,7 +210,11 @@ def fetch_degreed_course_id(self, external_id):
)
response_json = json.loads(response_body)
if response_json['data']:
return response_json['data'][0]['id']
# cache the course id with a 1 day expiration
response_course_id = response_json['data'][0]['id']
expires_in = 60 * 60 * 24 # 1 day
TieredCache.set_all_tiers(cache_key, response_course_id, expires_in)
return response_course_id
raise ClientError(
f'Degreed2: Attempted to find degreed course id but failed, external id was {external_id}'
f', Response from Degreed was {response_body}')
Expand Down
29 changes: 27 additions & 2 deletions tests/test_integrated_channels/test_degreed2/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,29 @@ def test_delete_course_completion(self):
degreed_api_client = Degreed2APIClient(enterprise_config)
degreed_api_client.delete_course_completion(None, None)

@mock.patch('integrated_channels.degreed2.client.Degreed2APIClient._get')
def test_fetch_degreed_course_id_cache(self, mock_get_request):
"""
``fetch_degreed_course_id`` should fetch data from the API only if the cache is empty.
"""
enterprise_config = factories.Degreed2EnterpriseCustomerConfigurationFactory()
degreed_api_client = Degreed2APIClient(enterprise_config)
mock_get_request.return_value = (
200, '{"data": [{"id": "degreed_course_id"}]}'
)
degreed_external_course_id_1 = 'course_id_1'
degreed_external_course_id_2 = 'course_id_2'

degreed_api_client.fetch_degreed_course_id(degreed_external_course_id_1)
degreed_api_client.fetch_degreed_course_id(degreed_external_course_id_2)
assert mock_get_request.call_count == 2

# The second call for the same course id should return the degreed_course_id from the cache
mock_get_request.reset_mock()
degreed_api_client.fetch_degreed_course_id(degreed_external_course_id_1)
degreed_api_client.fetch_degreed_course_id(degreed_external_course_id_2)
assert mock_get_request.call_count == 0

@responses.activate
@pytest.mark.django_db
@mock.patch('enterprise.api_client.client.JwtBuilder', mock.Mock())
Expand Down Expand Up @@ -239,6 +262,7 @@ def test_create_content_metadata_success(self):
json={"skill_names": ["Supply Chain", "Supply Chain Management"]},
status=200,
)
# The second call for the same course id should return the degreed_course_id from the cache
responses.add(
responses.GET,
course_url + "?filter%5Bexternal_id%5D=key",
Expand All @@ -253,7 +277,7 @@ def test_create_content_metadata_success(self):
)

status_code, response_body = degreed_api_client.create_content_metadata(create_course_payload())
assert len(responses.calls) == 5
assert len(responses.calls) == 4
assert responses.calls[0].request.url == oauth_url
assert responses.calls[1].request.url == course_url
assert status_code == 200
Expand Down Expand Up @@ -298,6 +322,7 @@ def test_create_content_metadata_retry_success(self):
json={"skill_names": ["Supply Chain", "Supply Chain Management"]},
status=200,
)
# The second call for the same course id should return the degreed_course_id from the cache
responses.add(
responses.GET,
course_url + "?filter%5Bexternal_id%5D=key",
Expand All @@ -311,7 +336,7 @@ def test_create_content_metadata_retry_success(self):
status=200,
)
status_code, response_body = degreed_api_client.create_content_metadata(create_course_payload())
assert len(responses.calls) == 6
assert len(responses.calls) == 5
assert responses.calls[0].request.url == oauth_url
assert responses.calls[1].request.url == course_url
assert responses.calls[2].request.url == course_url
Expand Down

0 comments on commit 765f473

Please sign in to comment.